From 01dca29af043f57ffce3bc273ce3db80827a161f Mon Sep 17 00:00:00 2001 From: loheagn Date: Sat, 19 Mar 2022 20:32:55 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E4=B8=BA=E4=BA=92=E8=AF=84?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=B3=E8=AF=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/cloudapi_v2.yaml | 166 ++++++++++++++++++ src/main/kotlin/cn/edu/buaa/scs/auth/User.kt | 12 ++ .../models/CreatePeerAppealRequest.kt | 24 +++ .../models/PatchPeerAppealRequest.kt | 26 +++ .../controller/models/PeerAppealResponse.kt | 40 +++++ .../cn/edu/buaa/scs/model/PeerAppeal.kt | 37 ++++ src/main/kotlin/cn/edu/buaa/scs/route/Peer.kt | 64 +++++++ .../kotlin/cn/edu/buaa/scs/service/Peer.kt | 96 ++++++++++ 8 files changed, 465 insertions(+) create mode 100644 src/main/kotlin/cn/edu/buaa/scs/controller/models/CreatePeerAppealRequest.kt create mode 100644 src/main/kotlin/cn/edu/buaa/scs/controller/models/PatchPeerAppealRequest.kt create mode 100644 src/main/kotlin/cn/edu/buaa/scs/controller/models/PeerAppealResponse.kt create mode 100644 src/main/kotlin/cn/edu/buaa/scs/model/PeerAppeal.kt diff --git a/openapi/cloudapi_v2.yaml b/openapi/cloudapi_v2.yaml index d907f3fe..1e849852 100644 --- a/openapi/cloudapi_v2.yaml +++ b/openapi/cloudapi_v2.yaml @@ -646,6 +646,111 @@ paths: name: expId required: true parameters: [] + /peerAppeal: + post: + summary: 提起申诉 + operationId: post-peerAppeal + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PeerAppealResponse' + tags: + - 互评 + description: 学生提起申诉 + security: + - Authorization: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePeerAppealRequest' + '/peerAppeal/{peerAppealId}': + parameters: + - schema: + type: integer + format: int32 + name: peerAppealId + in: path + required: true + get: + summary: 查看一项互评申诉 + tags: + - 互评 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PeerAppealResponse' + operationId: get-peerAppeal-peerAppealId + security: + - Authorization: [] + description: 查看一项互评申诉 + delete: + summary: 删除一项互评申诉 + operationId: delete-peerAppeal-peerAppealId + responses: + '200': + description: OK + security: + - Authorization: [] + tags: + - 互评 + description: 删除一项互评申诉(学生主动撤销) + patch: + summary: 修改互评申诉 + operationId: patch-peerAppeal-peerAppealId + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PeerAppealResponse' + tags: + - 互评 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchPeerAppealRequest' + description: 两种应用场景:学生修改申诉的内容;教师或助教处理申诉 + security: + - Authorization: [] + /peerAppeals: + get: + summary: 获取互评申诉列表 + tags: + - 互评 + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PeerAppealResponse' + operationId: get-peerAppeals + description: 获取互评申诉列表 + security: + - Authorization: [] + parameters: + - schema: + type: integer + format: int32 + in: query + name: expId + required: true + - schema: + type: integer + in: query + name: studentId + description: 如果调用者为学生,那么即使设置了该字段,也会被忽略 components: schemas: AssignmentResponse: @@ -1335,6 +1440,67 @@ components: required: - id - peerInfoList + PeerAppealResponse: + title: PeerAppealResponse + type: object + properties: + id: + type: integer + format: int32 + expId: + type: integer + format: int32 + studentId: + type: string + content: + type: string + appealedAt: + type: integer + format: int64 + processor: + $ref: '#/components/schemas/SimpleUser' + processContent: + type: string + processStatus: + type: integer + description: |- + 0: 未处理 + 1: 批准 + 2: 驳回 + format: int32 + processedAt: + type: integer + format: int64 + required: + - id + - expId + - studentId + - content + - appealedAt + - processStatus + CreatePeerAppealRequest: + title: CreatePeerAppealRequest + type: object + properties: + content: + type: string + expId: + type: integer + format: int32 + required: + - content + - expId + PatchPeerAppealRequest: + title: PatchPeerAppealRequest + type: object + properties: + content: + type: string + processStatus: + type: integer + format: int32 + processContent: + type: string securitySchemes: Authorization: type: apiKey diff --git a/src/main/kotlin/cn/edu/buaa/scs/auth/User.kt b/src/main/kotlin/cn/edu/buaa/scs/auth/User.kt index 33329a8c..6971c17d 100644 --- a/src/main/kotlin/cn/edu/buaa/scs/auth/User.kt +++ b/src/main/kotlin/cn/edu/buaa/scs/auth/User.kt @@ -34,6 +34,10 @@ fun User.authRead(entity: IEntity): Boolean { FileType.ExperimentResource -> authRead(Experiment.id(entity.involvedId)) } + is PeerAppeal -> + entity.studentId == this.id + || authWrite(Experiment.id(entity.expId)) + else -> throw BadRequestException("unsupported auth entity: $entity") } } @@ -68,6 +72,10 @@ fun User.authWrite(entity: IEntity): Boolean { FileType.ExperimentResource -> authWrite(Experiment.id(entity.involvedId)) } + is PeerAppeal -> + entity.studentId == this.id + || authWrite(Experiment.id(entity.expId)) + else -> throw BadRequestException("unsupported auth entity: $entity") } } @@ -105,6 +113,10 @@ fun User.authAdmin(entity: IEntity): Boolean { FileType.ExperimentResource -> authWrite(Experiment.id(entity.involvedId)) } + is PeerAppeal -> + entity.studentId == this.id + || authWrite(Experiment.id(entity.expId)) + else -> throw BadRequestException("unsupported auth entity: $entity") } } diff --git a/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreatePeerAppealRequest.kt b/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreatePeerAppealRequest.kt new file mode 100644 index 00000000..224cd451 --- /dev/null +++ b/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreatePeerAppealRequest.kt @@ -0,0 +1,24 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + + +/** + * + * @param content + * @param expId + */ +data class CreatePeerAppealRequest( + val content: kotlin.String, + val expId: kotlin.Int +) + diff --git a/src/main/kotlin/cn/edu/buaa/scs/controller/models/PatchPeerAppealRequest.kt b/src/main/kotlin/cn/edu/buaa/scs/controller/models/PatchPeerAppealRequest.kt new file mode 100644 index 00000000..c61f7c71 --- /dev/null +++ b/src/main/kotlin/cn/edu/buaa/scs/controller/models/PatchPeerAppealRequest.kt @@ -0,0 +1,26 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + + +/** + * + * @param content + * @param processStatus + * @param processContent + */ +data class PatchPeerAppealRequest( + val content: kotlin.String? = null, + val processStatus: kotlin.Int? = null, + val processContent: kotlin.String? = null +) + diff --git a/src/main/kotlin/cn/edu/buaa/scs/controller/models/PeerAppealResponse.kt b/src/main/kotlin/cn/edu/buaa/scs/controller/models/PeerAppealResponse.kt new file mode 100644 index 00000000..9c41f0b3 --- /dev/null +++ b/src/main/kotlin/cn/edu/buaa/scs/controller/models/PeerAppealResponse.kt @@ -0,0 +1,40 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + +import cn.edu.buaa.scs.controller.models.SimpleUser + +/** + * + * @param id + * @param expId + * @param studentId + * @param content + * @param appealedAt + * @param processStatus 0: 未处理 1: 批准 2: 驳回 + * @param processor + * @param processContent + * @param processedAt + */ +data class PeerAppealResponse( + val id: kotlin.Int, + val expId: kotlin.Int, + val studentId: kotlin.String, + val content: kotlin.String, + val appealedAt: kotlin.Long, + /* 0: 未处理 1: 批准 2: 驳回 */ + val processStatus: kotlin.Int, + val processor: SimpleUser? = null, + val processContent: kotlin.String? = null, + val processedAt: kotlin.Long? = null +) + diff --git a/src/main/kotlin/cn/edu/buaa/scs/model/PeerAppeal.kt b/src/main/kotlin/cn/edu/buaa/scs/model/PeerAppeal.kt new file mode 100644 index 00000000..61c4e5d2 --- /dev/null +++ b/src/main/kotlin/cn/edu/buaa/scs/model/PeerAppeal.kt @@ -0,0 +1,37 @@ +package cn.edu.buaa.scs.model + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.* + +interface PeerAppeal : Entity, IEntity { + companion object : Entity.Factory() + + var id: Int + var expId: Int + var studentId: String + var content: String + var appealedAt: Long + var processorId: String? + var processorName: String? + var processContent: String? + var processStatus: Int + var processedAt: Long? +} + +object PeerAppeals : Table("peer_appeal") { + val id = int("id").primaryKey().bindTo { it.id } + val expId = int("exp_id").bindTo { it.expId } + val studentId = varchar("student_id").bindTo { it.studentId } + val content = text("content").bindTo { it.content } + val appealedAt = long("appealed_at").bindTo { it.appealedAt } + val processorId = varchar("processor_id").bindTo { it.processorId } + val processorName = varchar("processor_name").bindTo { it.processorName } + val processContent = text("process_content").bindTo { it.processContent } + val processStatus = int("process_status").bindTo { it.processStatus } + val processAt = long("processed_at").bindTo { it.processedAt } +} + +val Database.peerAppeals + get() = this.sequenceOf(PeerAppeals) \ No newline at end of file diff --git a/src/main/kotlin/cn/edu/buaa/scs/route/Peer.kt b/src/main/kotlin/cn/edu/buaa/scs/route/Peer.kt index eb4aff62..7d401fa5 100644 --- a/src/main/kotlin/cn/edu/buaa/scs/route/Peer.kt +++ b/src/main/kotlin/cn/edu/buaa/scs/route/Peer.kt @@ -3,6 +3,7 @@ package cn.edu.buaa.scs.route import cn.edu.buaa.scs.controller.models.* import cn.edu.buaa.scs.error.BadRequestException import cn.edu.buaa.scs.model.Assignment +import cn.edu.buaa.scs.model.PeerAppeal import cn.edu.buaa.scs.model.PeerStandard import cn.edu.buaa.scs.model.PeerTask import cn.edu.buaa.scs.service.PeerService @@ -44,6 +45,55 @@ fun Route.peerRoute() { ) } } + route("/peerAppeal") { + post { + val req = call.receive() + call.peer.createAppeal(req.expId, req.content).let { + call.respond(convertPeerAppeal(it)) + } + } + + route("/{peerAppealId}") { + fun ApplicationCall.getPeerAppealIdFromPath(): Int { + return parameters["peerAppealId"]?.toInt() + ?: throw BadRequestException("invalid peerAppealId") + } + get { + call.respond( + convertPeerAppeal( + call.peer.getAppeal( + call.getPeerAppealIdFromPath() + ) + ) + ) + } + + patch { + val req = call.receive() + call.peer.updateAppeal( + call.getPeerAppealIdFromPath(), + req + ).let { + call.respond(convertPeerAppeal(it)) + } + } + + delete { + call.peer.deleteAppeal(call.getPeerAppealIdFromPath()) + call.respond("OK") + } + } + } + route("/peerAppeals") { + get { + val expId = call.request.queryParameters["expId"]?.toInt() + ?: throw BadRequestException("invalid expId") + val studentId = call.request.queryParameters["studentId"] + call.respond( + call.peer.getAllAppeal(expId, studentId).map { convertPeerAppeal(it) } + ) + } + } } internal fun convertAssignmentWithStandardScore( @@ -108,4 +158,18 @@ internal fun convertAssignmentPeerAssessmentResult(result: PeerService.PeerAsses finalScore = result.finalScore, peerInfoList = result.peerInfoList.map { convertAssessmentInfo(it) } ) +} + +internal fun convertPeerAppeal(appeal: PeerAppeal): PeerAppealResponse { + return PeerAppealResponse( + id = appeal.id, + studentId = appeal.studentId, + expId = appeal.expId, + content = appeal.content, + appealedAt = appeal.appealedAt, + processor = appeal.processorId?.let { SimpleUser(it, appeal.processorName!!) }, + processStatus = appeal.processStatus, + processContent = appeal.processContent, + processedAt = appeal.processedAt, + ) } \ No newline at end of file diff --git a/src/main/kotlin/cn/edu/buaa/scs/service/Peer.kt b/src/main/kotlin/cn/edu/buaa/scs/service/Peer.kt index caa25e54..782f9afc 100644 --- a/src/main/kotlin/cn/edu/buaa/scs/service/Peer.kt +++ b/src/main/kotlin/cn/edu/buaa/scs/service/Peer.kt @@ -1,8 +1,13 @@ package cn.edu.buaa.scs.service +import cn.edu.buaa.scs.auth.assertAdmin import cn.edu.buaa.scs.auth.assertRead import cn.edu.buaa.scs.auth.authWrite +import cn.edu.buaa.scs.controller.models.PatchPeerAppealRequest +import cn.edu.buaa.scs.error.AuthorizationException import cn.edu.buaa.scs.error.BadRequestException +import cn.edu.buaa.scs.error.BusinessException +import cn.edu.buaa.scs.error.NotFoundException import cn.edu.buaa.scs.model.* import cn.edu.buaa.scs.storage.mysql import cn.edu.buaa.scs.utils.getOrPut @@ -14,6 +19,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import org.ktorm.dsl.* import org.ktorm.entity.* +import org.ktorm.schema.ColumnDeclaring import java.util.concurrent.ConcurrentHashMap val ApplicationCall.peer get() = PeerService.getSvc(this) { PeerService(this) } @@ -292,4 +298,94 @@ class PeerService(val call: ApplicationCall) : IService { val score = originScore * (1.0 + rate) return if (score > 100) 100.0 else if (score < 0) 0.0 else score } + + fun createAppeal(expId: Int, content: String): PeerAppeal { + val peerAppeal = PeerAppeal { + this.expId = expId + this.studentId = call.userId() + this.content = content + this.appealedAt = System.currentTimeMillis() + this.processStatus = 0 + } + call.user().assertAdmin(peerAppeal) + mysql.peerAppeals.add(peerAppeal) + return peerAppeal + } + + fun getAllAppeal(expId: Int, studentId: String?): List { + val course = Experiment.id(expId).course + val isAdmin = call.user().isCourseAdmin(course) + val stuId = if (!isAdmin) call.userId() else studentId + val condition: (PeerAppeals) -> ColumnDeclaring = stuId?.let { id -> + { it.expId.eq(expId) and it.studentId.eq(id) } + } ?: { it.expId.eq(expId) } + return mysql.peerAppeals.filter(condition).toList().sortedBy { it.appealedAt } + } + + fun getAppeal(id: Int): PeerAppeal { + val appeal = mysql.peerAppeals.find { it.id.eq(id) } + ?: throw NotFoundException("找不到该申诉") + call.user().assertRead(appeal) + return appeal + } + + fun updateAppeal(appealId: Int, req: PatchPeerAppealRequest): PeerAppeal { + val appeal = mysql.peerAppeals.find { it.id.eq(appealId) } + ?: throw NotFoundException("找不到该申诉") + return if (req.content != null) { + patchAppeal(appeal, req.content) + } else { + processAppeal( + appeal, + req.processStatus ?: throw BadRequestException("invalid processStatus"), + req.processContent + ) + } + } + + /** + * 修改申诉的内容 + */ + private fun patchAppeal(appeal: PeerAppeal, content: String): PeerAppeal { + if (appeal.studentId != call.userId() && !call.user().isAdmin()) { + throw AuthorizationException("您没有权限修改该申诉") + } + appeal.content = content + mysql.peerAppeals.update(appeal) + return appeal + } + + /** + * 处理申诉 + */ + private fun processAppeal(appeal: PeerAppeal, processStatus: Int, processContent: String?): PeerAppeal { + val experiment = Experiment.id(appeal.expId) + if (!call.user().isCourseAdmin(experiment.course)) { + throw AuthorizationException("您没有权限处理该申诉") + } + appeal.processStatus = processStatus + appeal.processContent = processContent ?: "" + appeal.processedAt = System.currentTimeMillis() + appeal.processorId = call.userId() + appeal.processorName = call.user().name + mysql.peerAppeals.update(appeal) + return appeal + } + + /** + * 删除申诉 + */ + fun deleteAppeal(appealId: Int) { + val appeal = mysql.peerAppeals.find { it.id.eq(appealId) } + ?: throw NotFoundException("找不到该申诉") + if (appeal.studentId != call.userId() && !call.user().isAdmin()) { + throw AuthorizationException("您没有权限删除该申诉") + } + mysql.delete(PeerAppeals) { it.id.eq(appealId) } + } +} + +fun PeerAppeal.Companion.id(pid: Int): PeerAppeal { + return mysql.peerAppeals.find { it.id eq pid } + ?: throw BusinessException("find peerAppeal($pid) from database error") } \ No newline at end of file