@@ -14,7 +14,7 @@ import {
1414 ContestScoreboardHiddenError , FileLimitExceededError , FileUploadError ,
1515 InvalidTokenError , NotAssignedError , NotFoundError , PermissionError , ValidationError ,
1616} from '../error' ;
17- import { ScoreboardConfig , Tdoc } from '../interface' ;
17+ import { FileInfo , ScoreboardConfig , Tdoc } from '../interface' ;
1818import { PERM , PRIV , STATUS } from '../model/builtin' ;
1919import * as contest from '../model/contest' ;
2020import * as discussion from '../model/discussion' ;
@@ -25,7 +25,6 @@ import problem from '../model/problem';
2525import record from '../model/record' ;
2626import ScheduleModel from '../model/schedule' ;
2727import storage from '../model/storage' ;
28- import * as system from '../model/system' ;
2928import user from '../model/user' ;
3029import {
3130 Handler , param , post , Type , Types ,
@@ -156,13 +155,13 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
156155 tdoc : this . tdoc ,
157156 tsdoc : this . tsdocAsPublic ( ) ,
158157 udict,
159- files : sortFiles ( this . tdoc . files || [ ] ) ,
160- urlForFile : ( filename : string ) => this . url ( 'contest_file_download' , { tid, filename } ) ,
158+ files : ( this . tsdoc ?. attend && ! contest . isNotStarted ( this . tdoc ) ) ? sortFiles ( this . tdoc . privateFiles || [ ] ) : [ ] ,
159+ urlForFile : ( filename : string ) => this . url ( 'contest_file_download' , { tid, filename, type : 'private' } ) ,
161160 } ;
162161 if ( this . request . json ) return ;
163162 this . response . body . tdoc . content = this . response . body . tdoc . content
164- . replace ( / \( f i l e : \/ \/ / g, `(./${ this . tdoc . docId } /file/` )
165- . replace ( / = " f i l e : \/ \/ / g, `="./${ this . tdoc . docId } /file/` ) ;
163+ . replace ( / \( f i l e : \/ \/ / g, `(./${ this . tdoc . docId } /file/public/ ` )
164+ . replace ( / = " f i l e : \/ \/ / g, `="./${ this . tdoc . docId } /file/public/ ` ) ;
166165 }
167166
168167 @param ( 'tid' , Types . ObjectId )
@@ -312,9 +311,9 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler {
312311 await contest . addClarification ( domainId , tid , this . user . _id , content , this . request . ip , subject ) ;
313312 if ( ! this . user . own ( this . tdoc ) ) {
314313 await message . send ( 1 , this . tdoc . maintainer . concat ( this . tdoc . owner ) , JSON . stringify ( {
315- message : 'Contest {0} has a new clarification about {1}, please go to contest management to reply.' ,
314+ message : 'Contest {0} has a new clarification about {1}, please go to contest clarifications page to reply.' ,
316315 params : [ this . tdoc . title , subject > 0 ? `#${ this . tdoc . pids . indexOf ( subject ) + 1 } ` : 'the contest' ] ,
317- url : this . url ( 'contest_manage ' , { tid } ) ,
316+ url : this . url ( 'contest_clarification ' , { tid } ) ,
318317 } ) , message . FLAG_I18N | message . FLAG_UNREAD ) ;
319318 }
320319 this . back ( ) ;
@@ -354,6 +353,8 @@ export class ContestEditHandler extends Handler {
354353 pids : tid ? this . tdoc . pids . join ( ',' ) : '' ,
355354 beginAt,
356355 page_name : tid ? 'contest_edit' : 'contest_create' ,
356+ files : tid ? this . tdoc . files : [ ] ,
357+ urlForFile : ( filename : string ) => this . url ( 'contest_file_download' , { tid, filename, type : 'public' } ) ,
357358 } ;
358359 }
359360
@@ -439,7 +440,11 @@ export class ContestEditHandler extends Handler {
439440 ScheduleModel . deleteMany ( {
440441 type : 'schedule' , subType : 'contest' , domainId, tid,
441442 } ) ,
442- storage . del ( this . tdoc . files ?. map ( ( i ) => `contest/${ domainId } /${ tid } /${ i . name } ` ) || [ ] , this . user . _id ) ,
443+ storage . del (
444+ ( this . tdoc . files ?. map ( ( i ) => `contest/${ domainId } /${ tid } /public/${ i . name } ` ) || [ ] )
445+ . concat ( this . tdoc . privateFiles ?. map ( ( i ) => `contest/${ domainId } /${ tid } /private/${ i . name } ` ) || [ ] ) ,
446+ this . user . _id ,
447+ ) ,
443448 ] ) ) ;
444449 this . response . redirect = this . url ( 'contest_main' ) ;
445450 }
@@ -502,22 +507,95 @@ export class ContestCodeHandler extends Handler {
502507export class ContestManagementHandler extends ContestManagementBaseHandler {
503508 @param ( 'tid' , Types . ObjectId )
504509 async get ( domainId : string , tid : ObjectId ) {
505- const tcdocs = await contest . getMultiClarification ( domainId , tid ) ;
506510 this . response . body = {
507511 tdoc : this . tdoc ,
508512 tsdoc : this . tsdoc ,
509513 owner_udoc : await user . getById ( domainId , this . tdoc . owner ) ,
510514 pdict : await problem . getList ( domainId , this . tdoc . pids , true , true , [ ...problem . PROJECTION_CONTEST_LIST , 'tag' ] ) ,
511515 files : sortFiles ( this . tdoc . files || [ ] ) ,
516+ privateFiles : sortFiles ( this . tdoc . privateFiles || [ ] ) ,
517+ urlForFile : ( filename : string , type : string ) => this . url ( 'contest_file_download' , { tid, filename, type } ) ,
518+ } ;
519+ this . response . pjax = [
520+ [ 'partials/files.html' , { filetype : 'public' } ] ,
521+ [ 'partials/files.html' , {
522+ files : this . response . body . privateFiles ,
523+ filetype : 'private' ,
524+ } ] ,
525+ ] ;
526+ this . response . template = 'contest_manage.html' ;
527+ }
528+
529+ @param ( 'tid' , Types . ObjectId )
530+ @post ( 'filename' , Types . Filename , true )
531+ @post ( 'type' , Types . Range ( [ 'private' , 'public' ] ) , true )
532+ async postUploadFile ( domainId : string , tid : ObjectId , filename : string , type : 'private' | 'public' = 'private' ) {
533+ const allFiles = [ ...( this . tdoc . files || [ ] ) , ...( this . tdoc . privateFiles || [ ] ) ] ;
534+ if ( allFiles . length >= this . ctx . setting . get ( 'limit.contest_files' ) ) {
535+ throw new FileLimitExceededError ( 'count' ) ;
536+ }
537+ const file = this . request . files ?. file ;
538+ if ( ! file ) throw new ValidationError ( 'file' ) ;
539+ if ( Math . sum ( allFiles . map ( ( i ) => i . size ) ) + file . size >= this . ctx . setting . get ( 'limit.contest_files_size' ) ) {
540+ throw new FileLimitExceededError ( 'size' ) ;
541+ }
542+ filename ||= file . originalFilename || randomstring ( 16 ) ;
543+ const target = `contest/${ domainId } /${ tid } /${ type } /${ filename } ` ;
544+ await storage . put ( target , file . filepath , this . user . _id ) ;
545+ const meta = await storage . getMeta ( target ) ;
546+ const payload = { _id : filename , name : filename , ...pick ( meta , [ 'size' , 'lastModified' , 'etag' ] ) } ;
547+ if ( ! meta ) throw new FileUploadError ( ) ;
548+ const updateList = ( files : FileInfo [ ] , newFile : FileInfo ) => ( files || [ ] ) . filter ( ( i ) => i . _id !== newFile . _id ) . concat ( newFile ) ;
549+ await contest . edit ( domainId , tid , {
550+ files : type === 'private' ? this . tdoc . files : updateList ( this . tdoc . files , payload ) ,
551+ privateFiles : type === 'private' ? updateList ( this . tdoc . privateFiles , payload ) : this . tdoc . privateFiles ,
552+ } ) ;
553+ this . back ( ) ;
554+ }
555+
556+ @param ( 'tid' , Types . ObjectId )
557+ @post ( 'files' , Types . ArrayOf ( Types . Filename ) )
558+ @post ( 'type' , Types . Range ( [ 'public' , 'private' ] ) , true )
559+ async postDeleteFiles ( domainId : string , tid : ObjectId , files : string [ ] , type = 'private' ) {
560+ await Promise . all ( [
561+ storage . del ( files . map ( ( t ) => `contest/${ domainId } /${ tid } /${ type } /${ t } ` ) , this . user . _id ) ,
562+ contest . edit ( domainId , tid , type === 'private'
563+ ? { privateFiles : this . tdoc . privateFiles ?. filter ( ( i ) => ! files . includes ( i . name ) ) }
564+ : { files : this . tdoc . files ?. filter ( ( i ) => ! files . includes ( i . name ) ) } ,
565+ ) ,
566+ ] ) ;
567+ this . back ( ) ;
568+ }
569+
570+ @param ( 'pid' , Types . PositiveInt )
571+ @param ( 'score' , Types . PositiveInt )
572+ async postSetScore ( domainId : string , pid : number , score : number ) {
573+ if ( ! this . tdoc . pids . includes ( pid ) ) throw new ValidationError ( 'pid' ) ;
574+ this . tdoc . score ||= { } ;
575+ this . tdoc . score [ pid ] = score ;
576+ await contest . edit ( domainId , this . tdoc . docId , { score : this . tdoc . score } ) ;
577+ await contest . recalcStatus ( domainId , this . tdoc . docId ) ;
578+ this . back ( ) ;
579+ }
580+ }
581+
582+ class ContestClarificationHandler extends ContestManagementBaseHandler {
583+ @param ( 'tid' , Types . ObjectId )
584+ async get ( domainId : string , tid : ObjectId ) {
585+ const tcdocs = await contest . getMultiClarification ( domainId , tid ) ;
586+ this . response . body = {
587+ tdoc : this . tdoc ,
588+ tsdoc : this . tsdoc ,
589+ owner_udoc : await user . getById ( domainId , this . tdoc . owner ) ,
590+ pdict : await problem . getList ( domainId , this . tdoc . pids , true , true , [ ...problem . PROJECTION_CONTEST_LIST , 'tag' ] ) ,
591+ tcdocs,
512592 udict : await user . getListForRender (
513593 domainId , tcdocs . map ( ( i ) => i . owner ) ,
514594 this . user . hasPerm ( PERM . PERM_VIEW_USER_PRIVATE_INFO ) ,
515595 ) ,
516- tcdocs,
517- urlForFile : ( filename : string ) => this . url ( 'contest_file_download' , { tid, filename } ) ,
518596 } ;
519- this . response . pjax = 'partials/files .html' ;
520- this . response . template = 'contest_manage .html' ;
597+ this . response . pjax = 'partials/contest_clarification .html' ;
598+ this . response . template = 'contest_clarification .html' ;
521599 }
522600
523601 @param ( 'tid' , Types . ObjectId )
@@ -550,57 +628,21 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
550628 }
551629 this . back ( ) ;
552630 }
553-
554- @param ( 'tid' , Types . ObjectId )
555- @post ( 'filename' , Types . Filename , true )
556- async postUploadFile ( domainId : string , tid : ObjectId , filename : string ) {
557- if ( ( this . tdoc . files ?. length || 0 ) >= system . get ( 'limit.contest_files' ) ) {
558- throw new FileLimitExceededError ( 'count' ) ;
559- }
560- const file = this . request . files ?. file ;
561- if ( ! file ) throw new ValidationError ( 'file' ) ;
562- const size = Math . sum ( ( this . tdoc . files || [ ] ) . map ( ( i ) => i . size ) ) + file . size ;
563- if ( size >= system . get ( 'limit.contest_files_size' ) ) {
564- throw new FileLimitExceededError ( 'size' ) ;
565- }
566- filename ||= file . originalFilename || randomstring ( 16 ) ;
567- await storage . put ( `contest/${ domainId } /${ tid } /${ filename } ` , file . filepath , this . user . _id ) ;
568- const meta = await storage . getMeta ( `contest/${ domainId } /${ tid } /${ filename } ` ) ;
569- const payload = { _id : filename , name : filename , ...pick ( meta , [ 'size' , 'lastModified' , 'etag' ] ) } ;
570- if ( ! meta ) throw new FileUploadError ( ) ;
571- await contest . edit ( domainId , tid , { files : [ ...( this . tdoc . files || [ ] ) , payload ] } ) ;
572- this . back ( ) ;
573- }
574-
575- @param ( 'tid' , Types . ObjectId )
576- @post ( 'files' , Types . ArrayOf ( Types . Filename ) )
577- async postDeleteFiles ( domainId : string , tid : ObjectId , files : string [ ] ) {
578- await Promise . all ( [
579- storage . del ( files . map ( ( t ) => `contest/${ domainId } /${ tid } /${ t } ` ) , this . user . _id ) ,
580- contest . edit ( domainId , tid , { files : this . tdoc . files . filter ( ( i ) => ! files . includes ( i . name ) ) } ) ,
581- ] ) ;
582- this . back ( ) ;
583- }
584-
585- @param ( 'pid' , Types . PositiveInt )
586- @param ( 'score' , Types . PositiveInt )
587- async postSetScore ( domainId : string , pid : number , score : number ) {
588- if ( ! this . tdoc . pids . includes ( pid ) ) throw new ValidationError ( 'pid' ) ;
589- this . tdoc . score ||= { } ;
590- this . tdoc . score [ pid ] = score ;
591- await contest . edit ( domainId , this . tdoc . docId , { score : this . tdoc . score } ) ;
592- await contest . recalcStatus ( domainId , this . tdoc . docId ) ;
593- this . back ( ) ;
594- }
595631}
596632
597633export class ContestFileDownloadHandler extends ContestDetailBaseHandler {
598634 @param ( 'tid' , Types . ObjectId )
599635 @param ( 'filename' , Types . Filename )
600636 @param ( 'noDisposition' , Types . Boolean )
601- async get ( domainId : string , tid : ObjectId , filename : string , noDisposition = false ) {
637+ @param ( 'type' , Types . Range ( [ 'public' , 'private' ] ) , true )
638+ async get ( domainId : string , tid : ObjectId , filename : string , noDisposition = false , type = 'private' ) {
639+ if ( type === 'private' && ! this . user . own ( this . tdoc ) ) {
640+ if ( ! this . tsdoc ?. attend ) throw new ContestNotAttendedError ( domainId , tid ) ;
641+ if ( ! contest . isOngoing ( this . tdoc ) && ! contest . isDone ( this . tdoc ) ) throw new ContestNotLiveError ( domainId , tid ) ;
642+ if ( ! this . tsdoc . startAt ) await contest . setStatus ( domainId , tid , this . user . _id , { startAt : new Date ( ) } ) ;
643+ }
602644 this . response . addHeader ( 'Cache-Control' , 'public' ) ;
603- const target = `contest/${ domainId } /${ tid } /${ filename } ` ;
645+ const target = `contest/${ domainId } /${ tid } /${ type } / ${ filename } ` ;
604646 const file = await storage . getMeta ( target ) ;
605647 await oplog . log ( this , 'download.file.contest' , {
606648 target,
@@ -805,8 +847,9 @@ export async function apply(ctx: Context) {
805847 ctx . Route ( 'contest_edit' , '/contest/:tid/edit' , ContestEditHandler , PERM . PERM_VIEW_CONTEST ) ;
806848 ctx . Route ( 'contest_print' , '/contest/:tid/print' , ContestPrintHandler , PERM . PERM_VIEW_CONTEST ) ;
807849 ctx . Route ( 'contest_manage' , '/contest/:tid/management' , ContestManagementHandler ) ;
850+ ctx . Route ( 'contest_clarification' , '/contest/:tid/clarification' , ContestClarificationHandler ) ;
808851 ctx . Route ( 'contest_code' , '/contest/:tid/code' , ContestCodeHandler , PERM . PERM_VIEW_CONTEST ) ;
809- ctx . Route ( 'contest_file_download' , '/contest/:tid/file/:filename' , ContestFileDownloadHandler , PERM . PERM_VIEW_CONTEST ) ;
852+ ctx . Route ( 'contest_file_download' , '/contest/:tid/file/:type/: filename' , ContestFileDownloadHandler , PERM . PERM_VIEW_CONTEST ) ;
810853 ctx . Route ( 'contest_user' , '/contest/:tid/user' , ContestUserHandler , PERM . PERM_VIEW_CONTEST ) ;
811854 ctx . Route ( 'contest_balloon' , '/contest/:tid/balloon' , ContestBalloonHandler , PERM . PERM_VIEW_CONTEST ) ;
812855 ctx . worker . addHandler ( 'contest' , async ( doc ) => {
0 commit comments