Improved status servlet.

svn: r830
This commit is contained in:
Eli Barzilay 2005-09-10 18:54:04 +00:00
parent abe6514c2c
commit 8b5832a24d
3 changed files with 152 additions and 143 deletions

View File

@ -257,11 +257,11 @@ sub-directories:
"users.ss" file can be updated by the server with new users. It
can always be updated by the server to change passwords.
The username "solution" is special. It is used by the HTTPS status
server. Independent of the 'user-regexp and 'username-case-sensitive?
configration items, usernames are not allowed to contain characters
that are illegal in Windows pathnames, they cannot end in spaces
or periods, and no to user ids can have the same case folding.
Username that begin with "solution" are special. They are used by
the HTTPS status server. Independent of the 'user-regexp and
'username-case-sensitive? configration items, usernames are not
allowed to contain characters that are illegal in Windows
pathnames, they cannot end or begin in spaces or periods.
* "active/" --- sub-directory for active assignments. A list of
active assignments is sent to a client tool when a student clicks
@ -357,12 +357,13 @@ sub-directories:
* "[in]active/<assignment>/<user>/grade" (optional) --- <user>'s grade
for <assignment>, to be reported by the HTTPS status web server.
* "[in]active/<assignment>/solution/<file>" --- the solution to the
* "[in]active/<assignment>/solution*" --- the solution to the
assignment, made available by the status server to any user who
logs in. Normally, <file> is the only file in the directory
"<assignment>/solution/"; if there are multiple files in the
directory, only one named "<assignment>sol.scm" is made available
as the solution.
logs in. The solution can be either a file or a directory with a
name that begins with "solution". In the first case, the status
web server will have a "Solution" link to the file, and in the
second case, all files in the "solution*" directory will be listed
and accessible.
The server can be run within either MzScheme or MrEd, but "utils.ss"
requires MrEd (which means that `checker' modules will likely require

View File

@ -221,7 +221,7 @@
(error 'handin
"bad submission: ~a has an existing submission (~a)"
d dir)))
(regexp-split #rx"[+]" (path->string dir))))
(regexp-split #rx" *[+] *" (path->string dir))))
(directory-list))
(make-directory dirname))
(parameterize ([current-directory dirname])
@ -296,8 +296,8 @@
(error 'handin "username must not be a Windows special file name"))
(when (regexp-match #rx"^[ .]|[ .]$" username)
(error 'handin "username must not begin or end with a space or period"))
(when (string=? "solution" username)
(error 'handin "the username \"solution\" is reserved"))
(when (regexp-match #rx"^solution" username)
(error 'handin "the username prefix \"solution\" is reserved"))
(when (string=? "checker.ss" username)
(error 'handin "the username \"checker.ss\" is reserved"))
(unless (regexp-match ID-REGEXP id)

View File

@ -2,6 +2,7 @@
(module status mzscheme
(require (lib "file.ss")
(lib "list.ss")
(lib "string.ss")
(lib "date.ss")
(lib "unitsig.ss")
(lib "servlet-sig.ss" "web-server")
@ -9,24 +10,25 @@
(lib "md5.ss" "handin-server"))
(provide status-servlet)
(define TOPICS-PER-PAGE 25)
(define handin-dir (or (getenv "HANDIN_SERVER_DIR") (current-directory)))
(define active-dir (build-path handin-dir "active"))
(define handin-dir (or (getenv "HANDIN_SERVER_DIR") (current-directory)))
(define active-dir (build-path handin-dir "active"))
(define inactive-dir (build-path handin-dir "inactive"))
(define active/inactive-dirs (list active-dir inactive-dir))
(define master-password
(define master-password
(with-handlers ([exn:fail? (lambda (x) #f)])
(cadr (assq 'master-password
(cadr (assq 'master-password
(with-input-from-file (build-path handin-dir "config.ss")
read)))))
(define get-user-data
(let ([users-file (build-path handin-dir "users.ss")])
(lambda (user)
(get-preference (string->symbol user) (lambda () #f) #f users-file))))
(define (clean-str s)
(regexp-replace
" *$"
(regexp-replace "^ *" s "")
""))
(regexp-replace #rx" *$" (regexp-replace #rx"^ *" s "") ""))
(define (update-status status tag val)
(let loop ([status status])
@ -37,53 +39,63 @@
(define (get-status status tag default)
(let ([a (assq tag status)])
(if a
(cdr a)
default)))
(if a (cdr a) default)))
(define (make-page title . body)
`(html (head (title ,title))
(body ([bgcolor "white"])
(p ((align "center"))
,title)
,@body)))
(body ([bgcolor "white"]) (p ((align "center")) ,title) ,@body)))
(define status-servlet
(unit/sig ()
(import servlet^)
(define handin-prefix-re
;; a regexp that turns a full path to a handin-dir relative path
(regexp
(string-append
"^" (regexp-quote
(regexp-replace
#rx"/?$"
(if (path? handin-dir) (path->string handin-dir) handin-dir)
"/")))))
(define (make-k k tag)
(format "~a~atag=~a" k
(if (regexp-match #rx"^[^#]*[?]" k)
"&"
"?")
tag))
(let ([tag (if (path? tag) (path->string tag) tag)])
(format "~a~atag=~a" k (if (regexp-match #rx"^[^#]*[?]" k) "&" "?")
(regexp-replace handin-prefix-re tag ""))))
(define (select-k request)
(let ([a (assq 'tag (request-bindings request))])
(and a (cdr a))))
(define (link-tag k tag label)
`(a ((href ,(make-k k tag)))
,label))
(define (find-latest dir)
(let ([zero (build-path dir "SUCCESS-0")])
(if (directory-exists? zero)
zero
(build-path dir "SUCCESS-1"))))
;; `look-for' can be a username as a string (will find "bar+foo" for
;; "foo"), or a regexp that should match the whole directory name (used
;; with "^solution" below)
(define (find-hi-entry hi look-for)
(define (find-submission top)
(let ([dir (build-path top hi)])
(and (directory-exists? dir)
(ormap
(lambda (d)
(let ([d (path->string d)])
(and (cond [(string? look-for)
(member look-for
(regexp-split #rx" *[+] *" d))]
[(regexp? look-for) (regexp-match look-for d)]
[else (error 'find-hi-entry
"internal error: ~e" look-for)])
(build-path dir d))))
(directory-list dir)))))
(ormap find-submission active/inactive-dirs))
(define (handin-link k user hi)
(let* ([dir (find-latest
(build-path handin-dir
(if (directory-exists? (build-path handin-dir "active" hi))
"active"
"inactive")
hi
user))]
[l (with-handlers ([exn:fail? (lambda (x) null)])
(parameterize ([current-directory dir])
(filter (lambda (f)
(and (file-exists? f) (not (equal? (path->string f) "grade"))))
(directory-list))))])
(let* ([dir (find-hi-entry hi user)]
[l (and dir (with-handlers ([exn:fail? (lambda (x) null)])
(parameterize ([current-directory dir])
(quicksort
(filter (lambda (f)
(and (not (equal? f "grade"))
(file-exists? f)))
(map path->string (directory-list)))
string<?))))])
(if (pair? l)
(cdr
(apply
@ -92,9 +104,8 @@
(lambda (i) `((br) ,i))
(map (lambda (f)
(let ([hi (build-path dir f)])
`(font
()
(a ((href ,(make-k k hi))) ,(path->string f))
`(font ()
(a ((href ,(make-k k hi))) ,f)
" ("
,(date->string
(seconds->date
@ -105,40 +116,36 @@
(list (format "No handins accepted so far for user ~s, assignment ~s" user hi)))))
(define (solution-link k hi)
(let* ([soln-dir (build-path handin-dir
(if (directory-exists? (build-path handin-dir "active" hi))
"active"
"inactive")
hi
"solution")]
[soln (let ([f (build-path soln-dir (format "~asol.scm" hi))])
(if (or (file-exists? f)
(not (directory-exists? soln-dir)))
f
(let ([l (directory-list soln-dir)])
(if (= 1 (length l))
(build-path soln-dir (car l))
f))))])
(if (file-exists? soln)
`((a ((href ,(make-k k soln)))
"Solution: " ,(let-values ([(base name dir?) (split-path soln)])
(path->string name))))
`((i "Solution not available")))))
(let ([soln (find-hi-entry hi #rx"^solution")]
[none `((i "---"))])
(cond [(not soln) none]
[(file-exists? soln)
`((a ((href ,(make-k k soln))) "Solution"))]
[(directory-exists? soln)
(parameterize ([current-directory soln])
(let ([files (mergesort (map path->string
(filter file-exists?
(directory-list)))
string<?)])
(if (null? files)
none
(apply append
(map (lambda (f)
`((a ((href ,(make-k k (build-path soln f))))
(tt ,f))
(br)))
files)))))]
[else none])))
(define (handin-grade user hi)
(let* ([dir (build-path handin-dir
(if (directory-exists? (build-path handin-dir "active" hi))
"active"
"inactive")
hi
user)]
[grade (let ([filename (build-path dir "grade")])
(and (file-exists? filename)
(with-input-from-file filename
(lambda () (read-string (file-size filename))))))])
(if grade
grade
"no grade so far")))
(let* ([dir (find-hi-entry hi user)]
[grade (and dir
(let ([filename (build-path dir "grade")])
(and (file-exists? filename)
(with-input-from-file filename
(lambda ()
(read-string (file-size filename)))))))])
(or grade "--")))
(define (one-status-page status for-handin)
(let ([user (get-status status 'user (lambda () "???"))])
@ -157,14 +164,14 @@
(all-status-page status)
(download status tag))))))
(define re:base "^([a-zA-Z]*)([0-9]+)")
(define re:base #rx"^([a-zA-Z]*)([0-9]+)")
(define (all-status-page status)
(let ([l (quicksort
(map path->string
(append (directory-list (build-path handin-dir "active"))
(append (directory-list active-dir)
(with-handlers ([exn:fail? (lambda (x) null)])
(directory-list (build-path handin-dir "inactive")))))
(directory-list inactive-dir))))
(lambda (a b)
(let ([am (regexp-match re:base a)]
[bm (regexp-match re:base b)])
@ -176,63 +183,67 @@
(let ([next
(send/suspend
(lambda (k)
(define (header text)
`(td ((bgcolor "#f0f0f0")) (big (strong ,text))))
(make-page
(format "All Handins for ~a" user)
`(table
((bgcolor "#ddddff"))
`(table ([bgcolor "#ddddff"] [cellpadding "6"] [align "center"])
(tr () ,@(map header '(nbsp "Files" "Grade" "Solution")))
,@(map (lambda (hi)
`(tr (td ((bgcolor "white")) ,hi)
(td ((bgcolor "white")) ,@(handin-link k user hi))
(td ((bgcolor "white")) ,(handin-grade user hi))
(td ((bgcolor "white")) ,@(solution-link k hi))))
`(tr ([valign "top"])
,(header hi)
(td ([bgcolor "white"]) ,@(handin-link k user hi))
(td ([bgcolor "white"] (align "right")) ,(handin-grade user hi))
(td ([bgcolor "white"]) ,@(solution-link k hi))))
l)))))])
(let ([tag (select-k next)])
(download status tag)))))
(define (download status tag)
;; Make sure the user is allowed to read the requested file:
(define (check path elts)
(let loop ([path path] [elts (reverse elts)])
(let*-values ([(base name dir?) (split-path path)]
[(name) (path->string name)]
[(check) (and (pair? elts) (car elts))])
(if (null? elts)
;; must be rooted in active/inactive (why build-path instead of
;; using `path'? -- because path will have a trailing slash)
(member (build-path base name) active/inactive-dirs)
(and (cond [(eq? '* check) #t]
[(regexp? check) (regexp-match check name)]
[(string? check)
(or (equal? name check)
(member check
(regexp-split #rx" *[+] *" name)))]
[else #f])
(loop base (cdr elts)))))))
(define file (build-path handin-dir tag))
(with-handlers ([exn:fail?
(lambda (exn)
(make-page "Error" "Illegal file access: "
(exn-message exn)))])
(let ([who (get-status status 'user (lambda () "???"))])
(let-values ([(base name dir?) (split-path tag)])
;; Any file name is ok...
(unless (path? name) (error "bad"))
(let-values ([(base name dir?) (split-path base)])
;; Directory must be SUCCESS-0 or SUCCESS-1
(unless (or (string=? (path->string name) "SUCCESS-0")
(string=? (path->string name) "SUCCESS-1"))
(error "bad"))
(let-values ([(base name dir?) (split-path base)])
;; Directory must be user or "solution"
(unless (or (string=? (path->string name) who)
(string=? (path->string name) "solution"))
(error "bad"))
;; Any dir name is ok...
(let-values ([(base name dir?) (split-path base)])
(unless (path? name) (error "bad"))
;; Base must be active or inactive
(let-values ([(base name dir?) (split-path base)])
(unless (or (string=? (path->string name) "active")
(string=? (path->string name) "inactive"))
(error "bad"))
;; No more to path
(unless (equal? (build-path base 'same) (build-path handin-dir 'same))
(error "bad"))))))))
(make-page "Error" "Illegal file access"))])
(let ([who (get-status status 'user (lambda () "???"))])
;; Make sure the user is allowed to read the requested file:
(or (check file `(* ,who *))
(check file `(* #rx"^solution"))
(check file `(* #rx"^solution" *))
(error "Boom!")))
;; Return the downloaded file
(let ([data (with-input-from-file tag
(lambda ()
(read-bytes (file-size tag))))])
(make-response/full 200 "Okay"
(current-seconds)
#"application/data"
(let* ([data (with-input-from-file file
(lambda () (read-bytes (file-size file))))]
[html? (regexp-match #rx"[.]html?$" (string-foldcase tag))]
[wxme? (regexp-match #rx#"^WXME" data)])
(make-response/full 200 "Okay" (current-seconds)
(cond [html? #"text/html"]
[wxme? #"application/data"]
[else #"text/plain"])
`((Content-length . ,(number->string (bytes-length data)))
(Content-disposition
.
,(format "attachment; filename=~s"
(let-values ([(base name dir?) (split-path tag)])
(path->string name)))))
,@(if wxme?
`((Content-Disposition
.
,(format "attachment; filename=~s"
(let-values ([(base name dir?) (split-path file)])
(path->string name)))))
'()))
(list data)))))
(define (status-page status for-handin)
@ -263,10 +274,7 @@
(input ([type "submit"] [name "post"] [value "Login"]))))))))])
(let ([user (clean-str (cdr (assq 'user (request-bindings request))))]
[passwd (cdr (assq 'passwd (request-bindings request)))])
(let ([user-data (get-preference (string->symbol user)
(lambda () #f)
#f
(build-path handin-dir "users.ss"))])
(let ([user-data (get-user-data user)])
(cond
[(and user-data
(string? passwd)