* Main change: added active-dirs' and
inactive-dirs' as configuration options
instead of moving directories. * Don't show solutions in active assignments, also check that when downloading files. * Improved (no `suffix:') error messages for errors that users should eventually see. svn: r5331
This commit is contained in:
parent
f6047d1bff
commit
812997204f
|
@ -51,9 +51,10 @@ Quick Start for a Test Drive:
|
|||
"Chester Tester"
|
||||
"123")))
|
||||
|
||||
5. Make an "active" subdirectory in your new directory.
|
||||
5. Make an "test" subdirectory in your new directory.
|
||||
|
||||
6. Make a "test" subdirectory in "active".
|
||||
6. Create a file "config.ss" with the following content:
|
||||
((active-dirs ("test")))
|
||||
|
||||
7. In your new directory, run
|
||||
mred -mvqM handin-server
|
||||
|
@ -65,7 +66,7 @@ Quick Start for a Test Drive:
|
|||
9. Start DrScheme, click "Handin" to run the client, submit with
|
||||
username "tester" and password "pw".
|
||||
|
||||
The submitted file will be .../active/test/tester/handin.scm.
|
||||
The submitted file will be .../test/tester/handin.scm.
|
||||
|
||||
10. Check the status of your submission by pointing a web browser at
|
||||
https://localhost:7980/servlets/status.ss
|
||||
|
@ -164,13 +165,22 @@ sub-directories:
|
|||
students with the handin client, "private-key.pem" is kept
|
||||
private.
|
||||
|
||||
* "config.ss" (optional) --- configuration options. The file format
|
||||
is
|
||||
* "config.ss" --- configuration options. The file format is
|
||||
|
||||
((<key> <val>) ...)
|
||||
|
||||
The following keys can be used (without the preceding quote):
|
||||
|
||||
'active-dirs : a list of directories that are active
|
||||
submissions, relative to the current directory or absolute;
|
||||
the last path element for each of these (and 'inactive-dirs
|
||||
below) should be unique, and is used to identify the
|
||||
submission (for example, in the client's submission dialog
|
||||
and in the status servlet)
|
||||
|
||||
'inactive-dirs : a list of inactive submission directories (see
|
||||
above for details)
|
||||
|
||||
'port-number : the port for the main handin server; the default
|
||||
is 7979
|
||||
|
||||
|
@ -299,7 +309,8 @@ sub-directories:
|
|||
don't save a copy that has inconsistent options: it is best to
|
||||
create a new configuration file and move it over the old one, or
|
||||
use an editor that does so and not save until the new contents is
|
||||
ready.)
|
||||
ready.) This is most useful for closing & openning submission
|
||||
directories.
|
||||
|
||||
* "users.ss" (created if not present if a user is added) --- keeps
|
||||
the list of user accounts, along with the associated password
|
||||
|
@ -362,67 +373,81 @@ sub-directories:
|
|||
'plaintext symbol, which will be used without encryption. This
|
||||
may be useful for manually resetting a forgotten passwords.
|
||||
|
||||
* "active/" --- sub-directory for active assignments. A list of
|
||||
active assignments is sent to a client tool when a student clicks
|
||||
"Handin", based on the contents of this directory. The student
|
||||
then selects from the list. The assignments are ordered in the
|
||||
student's menu using `string<?', and the first assignment is the
|
||||
default selection.
|
||||
* "log" (or any other name that the 'log-file configuration option
|
||||
specifies (if any), created if not present, appended otherwise)
|
||||
--- records connections and actions, where each entry is of the
|
||||
form
|
||||
(id time-str msg-str)
|
||||
[<id>|<time>] <msg>
|
||||
where `<id>' is an integer representing the connection (numbered
|
||||
consecutively from 1 when the server starts), "-" for a message
|
||||
without a connection, and "wN" for a message from the status
|
||||
servlet.
|
||||
|
||||
Within each directory, the student id is used for a sub-directory
|
||||
name. Within each student sub-directory are directories for handin
|
||||
attempts and successes. If a directory "ATTEMPT" exists, it
|
||||
contains the most recent (unsuccessful) handin attempt.
|
||||
Directories "SUCCESS-n" (where n counts from 0) contain successful
|
||||
handins; the lowest numbered such directory represents the latest
|
||||
handin.
|
||||
* Active and inactive assignment directories (which you can put in a
|
||||
nested directory for convenience, or specify a different absolute
|
||||
directory), as specified by the configuration file using the
|
||||
`active-dirs' and `inactive-dirs'. A list of active assignment
|
||||
directories (the last path element in each specified path is used
|
||||
as a label) is sent to the client tool when a student clicks
|
||||
"Handin". The assignment labels are ordered in the student's menu
|
||||
using `string<?', and the first assignment is the default
|
||||
selection.
|
||||
|
||||
A cleanup process in the server copies successful submission to
|
||||
Within each assignment directory, the student id is used for a
|
||||
sub-directory name. Within each student sub-directory are
|
||||
directories for handin attempts and successes. If a directory
|
||||
"ATTEMPT" exists, it contains the most recent (unsuccessful or
|
||||
currently-in-submission) handin attempt. Directories "SUCCESS-n"
|
||||
(where n counts from 0) contain successful handins; the lowest
|
||||
numbered such directory represents the latest handin.
|
||||
|
||||
A cleanup process in the server copies successful submissions to
|
||||
the student directory -- one level up from the corresponding
|
||||
"SUCCESS-n" directory. This is done only for files and
|
||||
directories that are newer in "SUCCESS-n" than in the submission
|
||||
root, other files and directories are left intact. If external
|
||||
tools add new content to the student directory (eg, a "grade"
|
||||
file, as described below) it will stay there. If the machine
|
||||
file, as described below) it will stay there. If the machine
|
||||
crashes or the server is stopped, the cleanup process might not
|
||||
finish. When the server is started, it automatically runs the
|
||||
finish. When the server is started, it automatically runs the
|
||||
cleanup process for each student directory.
|
||||
|
||||
Within a student directory, a file "handin.scm" (or some other
|
||||
name if `default-file-name' is set) contains the actual
|
||||
submission. A `checker' procedure can change this default file
|
||||
Within a student directory, a "handin.scm" file (or some other
|
||||
name if the `default-file-name' option is set) contains the actual
|
||||
submission. A `checker' procedure can change this default file
|
||||
name, and it can create additional files in an "ATTEMPT" directory
|
||||
(to be copied by the cleanup process); see below on "checker.ss"
|
||||
for more details.
|
||||
(to be copied by the cleanup process); see below for more details
|
||||
on "checker.ss".
|
||||
|
||||
For submissions from a normal DrScheme frame, a submission file
|
||||
contains a copy of the student's definitions and interactions
|
||||
windows. The file is in a binary format (to support non-text
|
||||
windows. The file is in a binary format (to support non-text
|
||||
code), and opening the file directly in DrScheme shows the
|
||||
definitions part. To get both the definitions and interactions
|
||||
definitions part. To get both the definitions and interactions
|
||||
parts, the file can be parsed with `unpack-submission' from
|
||||
"utils.ss" (see below).
|
||||
|
||||
To submit an assignment as a group, students use a concatenation
|
||||
of usernames separated by "+" and any number of spaces (e.g.,
|
||||
"user1+user2"). The same syntax ("user1+user2") is used for the
|
||||
directory for shared submissions, and the usernames are always
|
||||
sorted so that the directory name is deterministic. Multiple
|
||||
directory for shared submissions, where the usernames are always
|
||||
sorted so that directory names are deterministic. Multiple
|
||||
submissions for a particular user in different groups will be
|
||||
rejected.
|
||||
|
||||
* "inactive/" --- sub-directory for inactive assignments, used by
|
||||
the HTTPS status web server.
|
||||
Inactive assignment directories are used by the the HTTPS status
|
||||
web server.
|
||||
|
||||
* "active/<assignment>/checker.ss" (optional) --- a module that
|
||||
exports a `checker' function. This function receives two strings.
|
||||
The first is a username list and the second is the submission as a
|
||||
byte string. (See also `unpack-submission', etc. from "util.ss",
|
||||
below.) To reject the submission, the `checker' function can
|
||||
raise an exception; the exception message will be relayed back to
|
||||
the student. The module is loaded when the current directory is
|
||||
the main server directory, so it can read information from
|
||||
"config.ss".
|
||||
* "<active-dir>/<assignment>/checker.ss" (optional) --- a module
|
||||
that exports a `checker' function. This function receives two
|
||||
strings. The first is a username list and the second is the
|
||||
submission as a byte string. (See also `unpack-submission',
|
||||
etc. from "util.ss", below.) To reject the submission, the
|
||||
`checker' function can raise an exception; the exception message
|
||||
will be relayed back to the student. The module is loaded when
|
||||
the current directory is the main server directory, so it can read
|
||||
information from "config.ss".
|
||||
|
||||
The first argument is a list of usernames with at least one
|
||||
username, and more than one if this is a joint submission (where
|
||||
|
@ -430,16 +455,16 @@ sub-directories:
|
|||
by "+").
|
||||
|
||||
The `checker' function is called with the current directory as
|
||||
"active/<assignment>/<username(s)>/ATTEMPT", and the submission is
|
||||
"<active-assignment>/<username(s)>/ATTEMPT", and the submission is
|
||||
saved in the file "handin", and the timeout clock is reset to the
|
||||
value of the 'session-timeout configuration. The checker function
|
||||
can change "handin", and it can create additional files in this
|
||||
directory. (Extra files in the current directory will be
|
||||
preserved as it is later renamed to "SUCCESS-0", and copied to the
|
||||
submission's root ("active/<assignment>/<user/s>/"), etc.) To
|
||||
hide generated files from the HTTPS status web server interface,
|
||||
put the files in a subdirectory, which is preserved but hidden
|
||||
from the status interface.
|
||||
submission's root ("<active-assignment>/<username(s)>/"), etc.)
|
||||
To hide generated files from the HTTPS status web server
|
||||
interface, put the files in a subdirectory, it is preserved but
|
||||
hidden from the status interface.
|
||||
|
||||
The checker should return a string, such as "handin.scm", to use
|
||||
in naming the submission file, or #f to indicate that he file
|
||||
|
@ -469,31 +494,22 @@ sub-directories:
|
|||
To specify only pre/post-checker, use #f for the one you want to
|
||||
omit.
|
||||
|
||||
* "log" (or any other name that the 'log-file configuration option
|
||||
specifies (if any), created if not present, appended otherwise)
|
||||
--- records connections and actions, where each entry is of the
|
||||
form
|
||||
(id time-str msg-str)
|
||||
[<id>|<time>] <msg>
|
||||
where `<id>' is an integer representing the connection (numbered
|
||||
consecutively from 1 when the server starts), "-" for a message
|
||||
without a connection, and "wN" for a message from the status
|
||||
servlet.
|
||||
* "<[in]active-assignment>/<user(s)>/<filename>" (if submitted) ---
|
||||
the most recent submission for <[in]active-assignment> by
|
||||
<user(s)> where <filename> was returned by the checker (or the
|
||||
value of the `default-file-name' configuration option if there's
|
||||
no checker). If the submission is from multiple users, then
|
||||
"<user(s)>" is actually "<user1>+<user2>" etc. Also, if the
|
||||
cleanup process was interrupted (by a machine failure, etc.), the
|
||||
submission may actually be in "SUCCESS-n" as described above, but
|
||||
will move up when the server performs a cleanup (or when
|
||||
restarted).
|
||||
|
||||
* "[in]active/<assignment>/<user>/<filename>" (if submitted) --- the
|
||||
most recent submission for <assignment> by <user> where <filename>
|
||||
was returned by the checker (or the value of the
|
||||
`default-file-name' configuration option if there's no checker).
|
||||
If the submission is from multiple users, then "<user>" is
|
||||
actually "<user1>+<user2>" etc. Also, if the cleanup process was
|
||||
interrupted (by a machine failure, etc.), the submission may
|
||||
actually be in "SUCCESS-n" as described above.
|
||||
* "<[in]active-assignment>/<user(s)>/grade" (optional) ---
|
||||
<user(s)>'s grade for <assignment>, to be reported by the HTTPS
|
||||
status web server
|
||||
|
||||
* "[in]active/<assignment>/<user>/grade" (optional) --- <user>'s
|
||||
grade for <assignment>, to be reported by the HTTPS status web
|
||||
server
|
||||
|
||||
* "[in]active/<assignment>/solution*" --- 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. The solution can be either a file or a directory with a
|
||||
name that begins with "solution". In the first case, the status
|
||||
|
@ -510,7 +526,9 @@ related crashes.
|
|||
The server currently provides no mechanism for a graceful shutdown,
|
||||
but terminating the server is no worse than a network outage. (In
|
||||
particular, no data should be lost.) To reconfigure the server (e.g.,
|
||||
to change the set of active assignments), stop it and restart it.
|
||||
to change a checker module), stop it and restart it. (Changing the
|
||||
configuration file is detected, and options are reloaded, so no
|
||||
restart is needed for that.)
|
||||
|
||||
The client and server are designed to be robust against network
|
||||
problems and timeouts. The client-side tool always provides a
|
||||
|
@ -691,7 +709,8 @@ The _utils.ss_ module provides utilities helpful in implementing
|
|||
Produces a line in the server log file, using the given format
|
||||
string and arguments. All this actually does, is arrange to print
|
||||
the line fast (to avoid mixing lines from different threads) to the
|
||||
error port, and flush it.
|
||||
error port, and flush it. (The log port will prefix all lines with
|
||||
a time stamp and connection identifier.)
|
||||
|
||||
> (timeout-control msg)
|
||||
Control the timeout for this session. The timeout is initialized by
|
||||
|
@ -848,13 +867,12 @@ Keywords for configuring `check:':
|
|||
`current-value-printer' (see above).
|
||||
|
||||
* :coverage? -- collect coverage information when evaluating the
|
||||
submission. This will
|
||||
cause an error if some input is not covered. This check happens
|
||||
after checker tests are run, but the information is collected and
|
||||
stored before, so checker tests do not change the result. Also, you
|
||||
can use the `!all-covered' procedure in the checker before other
|
||||
tests, if you want that feedback earlier.
|
||||
Does not work with non-textual submissions.
|
||||
submission. This will cause an error if some input is not covered.
|
||||
This check happens after checker tests are run, but the information
|
||||
is collected and stored before, so checker tests do not change the
|
||||
result. Also, you can use the `!all-covered' procedure in the
|
||||
checker before other tests, if you want that feedback earlier. Does
|
||||
not work with non-textual submissions.
|
||||
|
||||
Within the body of `check:', `users' and `submission' will be bound to
|
||||
the checker arguments -- a (sorted) list of usernames and the
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
|
||||
(provide submission-dir)
|
||||
(define submission-dir-re
|
||||
(regexp (string-append "[/\\]active[/\\]([^/\\]+)[/\\](?:[^/\\]+)"
|
||||
"[/\\](?:SUCCESS-[0-9]+|ATTEMPT)[/\\]?$")))
|
||||
(regexp (string-append "[/\\]([^/\\]+)[/\\](?:[^/\\]+)[/\\]"
|
||||
"(?:SUCCESS-[0-9]+|ATTEMPT)[/\\]?$")))
|
||||
(define (submission-dir)
|
||||
(let ([m (regexp-match submission-dir-re
|
||||
(path->string (current-directory)))])
|
||||
|
@ -165,10 +165,9 @@
|
|||
(read-bytes-line (current-input-port) 'any)))])
|
||||
(unless (eof-object? line)
|
||||
(let* ([line (regexp-replace #rx#"[ \t]+$" line #"")]
|
||||
[line (if (and untabify?
|
||||
(regexp-match-positions #rx"\t" line))
|
||||
[line (if (and untabify? (regexp-match? #rx"\t" line))
|
||||
(untabify line) line)])
|
||||
(when (and bad-re (regexp-match bad-re line))
|
||||
(when (and bad-re (regexp-match? bad-re line))
|
||||
(error* "You cannot use \"~a\" in ~a!~a"
|
||||
(if (regexp? bad-re) (object-name bad-re) bad-re)
|
||||
(currently-processed-file-name)
|
||||
|
@ -285,14 +284,14 @@
|
|||
(let* ([files (read-multifile (open-input-bytes submission))]
|
||||
[names (map car files)])
|
||||
(cond [(ormap (lambda (f)
|
||||
(and (regexp-match #rx"^[.]|[/\\ ]" (car f)) (car f)))
|
||||
(and (regexp-match? #rx"^[.]|[/\\ ]" (car f)) (car f)))
|
||||
files)
|
||||
=> (lambda (file) (error* "bad filename: ~e" file))])
|
||||
(cond [(procedure? names-checker) (names-checker names)]
|
||||
[(or (regexp? names-checker)
|
||||
(string? names-checker) (bytes? names-checker))
|
||||
(cond [(ormap (lambda (n)
|
||||
(and (not (regexp-match names-checker n)) n))
|
||||
(and (not (regexp-match? names-checker n)) n))
|
||||
names)
|
||||
=> (lambda (file) (error* "bad filename: ~e" file))])]
|
||||
[(and (list? names-checker) (andmap string? names-checker))
|
||||
|
@ -376,7 +375,7 @@
|
|||
(syntax-case stx ()
|
||||
[(key val x ...)
|
||||
(and (identifier? #'key)
|
||||
(regexp-match #rx"^:" (symbol->string (syntax-e #'key))))
|
||||
(regexp-match? #rx"^:" (symbol->string (syntax-e #'key))))
|
||||
(loop #'(x ...) (cons (list (syntax-e #'key) #'key #'val) keyvals))]
|
||||
[(body ...)
|
||||
(with-syntax
|
||||
|
@ -530,8 +529,8 @@
|
|||
[(not (string? user-error-message))
|
||||
(error*
|
||||
"badly configured user-error-message")]
|
||||
[(regexp-match #rx"~[aesvAESV]"
|
||||
user-error-message)
|
||||
[(regexp-match? #rx"~[aesvAESV]"
|
||||
user-error-message)
|
||||
(error* user-error-message m)]
|
||||
[else
|
||||
(error* "~a" user-error-message)])))])
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
|
||||
(install-logger-port)
|
||||
|
||||
;; errors to the user: no need for a "foo: " prefix
|
||||
(define (error* fmt . args)
|
||||
(error (apply format fmt args)))
|
||||
|
||||
(define (write+flush port . xs)
|
||||
(for-each (lambda (x) (write x port) (newline port)) xs)
|
||||
(flush-output port))
|
||||
|
@ -45,9 +49,8 @@
|
|||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(define ATTEMPT-DIR "ATTEMPT")
|
||||
(define (success-dir n) (format "SUCCESS-~a" n))
|
||||
|
||||
(define (success-dir n)
|
||||
(format "SUCCESS-~a" n))
|
||||
(define (make-success-dir-available n)
|
||||
(let ([name (success-dir n)])
|
||||
(when (directory-exists? name)
|
||||
|
@ -111,37 +114,29 @@
|
|||
|
||||
(define (cleanup-all-submissions)
|
||||
(log-line "Cleaning up all submission directories")
|
||||
(for-each (lambda (top)
|
||||
(when (directory-exists? top)
|
||||
(parameterize ([current-directory top])
|
||||
(for-each (lambda (pset)
|
||||
(when (directory-exists? pset) ; filter non-dirs
|
||||
(parameterize ([current-directory pset])
|
||||
(for-each (lambda (sub)
|
||||
(when (directory-exists? sub)
|
||||
(cleanup-submission sub)))
|
||||
(directory-list)))))
|
||||
(for-each (lambda (pset)
|
||||
(when (directory-exists? pset) ; just in case
|
||||
(parameterize ([current-directory pset])
|
||||
(for-each (lambda (sub)
|
||||
(when (directory-exists? sub) ; filter non-dirs
|
||||
(cleanup-submission sub)))
|
||||
(directory-list)))))
|
||||
'("active" "inactive")))
|
||||
(get-conf 'all-dirs)))
|
||||
|
||||
;; On startup, we scan all submissions, then repeat at random intervals (only
|
||||
;; if clients connected in that time), and check often for changes in the
|
||||
;; active/inactive directories and run a cleanup if there was a change
|
||||
(define connection-num 0)
|
||||
(thread (lambda ()
|
||||
(define last-active/inactive #f)
|
||||
(define last-all-dirs #f)
|
||||
(define last-connection-num #f)
|
||||
(let loop ()
|
||||
(let loop ([n (+ 20 (random 20))]) ; 10-20 minute delay
|
||||
(when (>= n 0)
|
||||
(let ([new (map (lambda (x)
|
||||
(if (directory-exists? x)
|
||||
(directory-list x)
|
||||
null))
|
||||
'("active" "inactive"))])
|
||||
(if (equal? new last-active/inactive)
|
||||
(let ([new (get-conf 'all-dirs)])
|
||||
(if (equal? new last-all-dirs)
|
||||
(begin (sleep 30) (loop (sub1 n)))
|
||||
(begin (set! last-active/inactive new)
|
||||
(begin (set! last-all-dirs new)
|
||||
(set! last-connection-num #f))))))
|
||||
(unless (equal? last-connection-num connection-num)
|
||||
(cleanup-all-submissions)
|
||||
|
@ -154,27 +149,28 @@
|
|||
(with-output-to-file part
|
||||
(lambda () (display s))))
|
||||
|
||||
(define (users->dirname users)
|
||||
(apply string-append (car users)
|
||||
(map (lambda (u) (string-append "+" u)) (cdr users))))
|
||||
|
||||
(define (accept-specific-submission data r r-safe w)
|
||||
;; Note: users are always sorted
|
||||
(define users (a-ref data 'usernames))
|
||||
(define assignments (a-ref data 'assignments))
|
||||
(define assignment (a-ref data 'assignment))
|
||||
(define dirname
|
||||
(apply string-append (car users)
|
||||
(map (lambda (u) (string-append "+" u)) (cdr users))))
|
||||
(define dirname (users->dirname users))
|
||||
(define len #f)
|
||||
(unless (member assignment assignments)
|
||||
(error 'handin "not an active assignment: ~a" assignment))
|
||||
(error* "not an active assignment: ~a" assignment))
|
||||
(log-line "assignment for ~a: ~a" users assignment)
|
||||
(write+flush w 'ok)
|
||||
(set! len (read r-safe))
|
||||
(unless (and (number? len) (integer? len) (positive? len))
|
||||
(error 'handin "bad length: ~s" len))
|
||||
(error* "bad length: ~s" len))
|
||||
(unless (len . < . (get-conf 'max-upload))
|
||||
(error 'handin
|
||||
"max handin file size is ~s bytes, file to handin is too big (~s bytes)"
|
||||
(get-conf 'max-upload) len))
|
||||
(parameterize ([current-directory (build-path "active" assignment)])
|
||||
(error* "max handin file size is ~s bytes, file to handin is too big (~s bytes)"
|
||||
(get-conf 'max-upload) len))
|
||||
(parameterize ([current-directory (assignment<->dir assignment)])
|
||||
(wait-for-lock dirname
|
||||
(let ([dir (build-path (current-directory) dirname)])
|
||||
(lambda () (cleanup-submission dir))))
|
||||
|
@ -183,11 +179,11 @@
|
|||
(for-each wait-for-lock users))
|
||||
(write+flush w 'go)
|
||||
(unless (regexp-match #rx"[$]" r-safe)
|
||||
(error 'handin "did not find start-of-content marker"))
|
||||
(error* "did not find start-of-content marker"))
|
||||
(let ([s (read-bytes len r)])
|
||||
(unless (and (bytes? s) (= (bytes-length s) len))
|
||||
(error 'handin "error uploading (got ~e, expected ~s bytes)"
|
||||
(if (bytes? s) (bytes-length s) s) len))
|
||||
(error* "error uploading (got ~e, expected ~s bytes)"
|
||||
(if (bytes? s) (bytes-length s) s) len))
|
||||
;; we have a submission, need to create a directory if needed, make
|
||||
;; sure that no users submitted work with someone else
|
||||
(unless (directory-exists? dirname)
|
||||
|
@ -196,21 +192,20 @@
|
|||
(for-each
|
||||
(lambda (d)
|
||||
(when (member d users)
|
||||
(error 'handin
|
||||
"bad submission: ~a has an existing submission (~a)"
|
||||
d dir)))
|
||||
(error* "bad submission: ~a has an existing submission (~a)"
|
||||
d dir)))
|
||||
(regexp-split #rx" *[+] *" (path->string dir))))
|
||||
(directory-list))
|
||||
(make-directory dirname))
|
||||
(parameterize ([current-directory dirname]
|
||||
[current-messenger
|
||||
(case-lambda
|
||||
[(msg) (write+flush w 'message msg)]
|
||||
[(msg styles)
|
||||
(if (eq? 'final styles)
|
||||
(write+flush w 'message-final msg)
|
||||
(begin (write+flush w 'message-box msg styles)
|
||||
(read (make-limited-input-port r 50))))])])
|
||||
[(msg) (write+flush w 'message msg)]
|
||||
[(msg styles)
|
||||
(if (eq? 'final styles)
|
||||
(write+flush w 'message-final msg)
|
||||
(begin (write+flush w 'message-box msg styles)
|
||||
(read (make-limited-input-port r 50))))])])
|
||||
;; Clear out old ATTEMPT, if any, and make a new one:
|
||||
(when (directory-exists? ATTEMPT-DIR)
|
||||
(delete-directory/files ATTEMPT-DIR))
|
||||
|
@ -227,18 +222,18 @@
|
|||
[(procedure? checker*) (values #f checker* #f)]
|
||||
[(and (list? checker*) (= 3 (length checker*)))
|
||||
(apply values checker*)]
|
||||
[else (error 'handin-configuration
|
||||
"bad checker value: ~e" checker*)]))
|
||||
[else (error* "bad checker value: ~e" checker*)]))
|
||||
(when pre
|
||||
(let ([dir (current-directory)])
|
||||
(with-handlers
|
||||
([void (lambda (e)
|
||||
(parameterize ([current-directory dir])
|
||||
(unless (ormap
|
||||
(lambda (d)
|
||||
(and (directory-exists? d)
|
||||
(regexp-match SUCCESS-RE d)))
|
||||
(map path->string (directory-list)))
|
||||
(unless (ormap (lambda (d)
|
||||
(and (directory-exists? d)
|
||||
(regexp-match
|
||||
SUCCESS-RE
|
||||
(path->string d))))
|
||||
(directory-list))
|
||||
(parameterize ([current-directory ".."])
|
||||
(when (directory-exists? dirname)
|
||||
(delete-directory/files dirname)))))
|
||||
|
@ -265,23 +260,21 @@
|
|||
(when post
|
||||
(parameterize ([current-directory (success-dir 0)])
|
||||
(post users s))))
|
||||
(error 'handin "upload not confirmed: ~s" v)))))))))
|
||||
(error* "upload not confirmed: ~s" v)))))))))
|
||||
|
||||
(define (retrieve-specific-submission data w)
|
||||
;; Note: users are always sorted
|
||||
(define users (a-ref data 'usernames))
|
||||
(define assignments (a-ref data 'assignments))
|
||||
(define assignment (a-ref data 'assignment))
|
||||
(define dirname
|
||||
(apply string-append (car users)
|
||||
(map (lambda (u) (string-append "+" u)) (cdr users))))
|
||||
(define submission-dir (build-path "active" assignment dirname))
|
||||
(define dirname (users->dirname users))
|
||||
(define submission-dir (build-path (assignment<->dir assignment) dirname))
|
||||
(unless (member assignment assignments)
|
||||
(error 'handin "not an active assignment: ~a" assignment))
|
||||
(error* "not an active assignment: ~a" assignment))
|
||||
(unless (directory-exists? submission-dir)
|
||||
(error 'handin "no ~a submission directory for ~a" assignment users))
|
||||
(error* "no ~a submission directory for ~a" assignment users))
|
||||
(log-line "retrieving assignment for ~a: ~a" users assignment)
|
||||
(parameterize ([current-directory (build-path "active" assignment dirname)])
|
||||
(parameterize ([current-directory submission-dir])
|
||||
(define magics '(#"WXME" #"<<<MULTI-SUBMISSION-FILE>>>"))
|
||||
(define mlen (apply max (map bytes-length magics)))
|
||||
(define file
|
||||
|
@ -307,7 +300,7 @@
|
|||
(display "$" w)
|
||||
(display (with-input-from-file file (lambda () (read-bytes len))) w)
|
||||
(flush-output w))
|
||||
(error 'handin "no ~a submission file found for ~a" assignment users))))
|
||||
(error* "no ~a submission file found for ~a" assignment users))))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
|
@ -321,7 +314,7 @@
|
|||
(put-preferences
|
||||
(list (string->symbol username)) (list data)
|
||||
(lambda (f)
|
||||
(error 'handin "user database busy; please try again, and alert the adminstrator if problems persist"))
|
||||
(error* "user database busy; please try again, and alert the adminstrator if problems persist"))
|
||||
"users.ss"))
|
||||
orig-custodian))
|
||||
|
||||
|
@ -333,10 +326,9 @@
|
|||
[(list? field-re) (member value field-re)]
|
||||
[(not field-re) #t]
|
||||
[(eq? field-re '-) #t] ; -> hidden field, no check
|
||||
[else (error 'handin "bad spec: field-regexp is ~e"
|
||||
field-re)])
|
||||
(error 'handin "bad ~a: \"~a\"~a" field-name value
|
||||
(if field-desc (format "; need ~a" field-desc) ""))))
|
||||
[else (error* "bad spec: field-regexp is ~e" field-re)])
|
||||
(error* "bad ~a: \"~a\"~a" field-name value
|
||||
(if field-desc (format "; need ~a" field-desc) ""))))
|
||||
|
||||
;; Utility for the next two functions: reconstruct a full list of
|
||||
;; extra-fields from user-fields, using "" for hidden fields
|
||||
|
@ -354,24 +346,24 @@
|
|||
(define user-fields (a-ref data 'user-fields))
|
||||
(define extra-fields (add-hidden-to-user-fields user-fields))
|
||||
(unless (get-conf 'allow-new-users)
|
||||
(error 'handin "new users not allowed: ~a" username))
|
||||
(error* "new users not allowed: ~a" username))
|
||||
(check-field username (get-conf 'user-regexp) "username"
|
||||
(get-conf 'user-desc))
|
||||
;; Since we're going to use the username in paths, and + to split names:
|
||||
(when (regexp-match #rx"[+/\\:|\"<>]" username)
|
||||
(error 'handin "username must not contain one of the following: + / \\ : | \" < >"))
|
||||
(error* "username must not contain these characters: + / \\ : | \" < >"))
|
||||
(when (regexp-match
|
||||
#rx"^((nul)|(con)|(prn)|(aux)|(clock[$])|(com[1-9])|(lpt[1-9]))[.]?"
|
||||
(string-foldcase username))
|
||||
(error 'handin "username must not be a Windows special file name"))
|
||||
(error* "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"))
|
||||
(error* "username must not begin or end with a space or period"))
|
||||
(when (regexp-match #rx"^solution" username)
|
||||
(error 'handin "the username prefix \"solution\" is reserved"))
|
||||
(error* "the username prefix \"solution\" is reserved"))
|
||||
(when (string=? "checker.ss" username)
|
||||
(error 'handin "the username \"checker.ss\" is reserved"))
|
||||
(error* "the username \"checker.ss\" is reserved"))
|
||||
(when (get-user-data username)
|
||||
(error 'handin "username already exists: `~a'" username))
|
||||
(error* "username already exists: `~a'" username))
|
||||
(for-each (lambda (str info)
|
||||
(check-field str (cadr info) (car info) (caddr info)))
|
||||
extra-fields (get-conf 'extra-fields))
|
||||
|
@ -386,17 +378,16 @@
|
|||
(define user-fields (a-ref data 'user-fields))
|
||||
(define extra-fields (add-hidden-to-user-fields user-fields))
|
||||
(unless (= 1 (length usernames))
|
||||
(error 'handin "cannot change a password for multiple users: ~a"
|
||||
usernames))
|
||||
(error* "cannot change a password for multiple users: ~a" usernames))
|
||||
;; the new data is the same as the old one for every empty string (includes
|
||||
;; hidden fields)
|
||||
(let ([new-data (map (lambda (old new) (if (equal? "" new) old new))
|
||||
(car user-datas) (cons passwd extra-fields))])
|
||||
(unless (or (get-conf 'allow-change-info)
|
||||
(equal? (cdr new-data) (cdar user-datas)))
|
||||
(error 'handin "changing information not allowed: ~a" (car usernames)))
|
||||
(error* "changing information not allowed: ~a" (car usernames)))
|
||||
(when (equal? new-data (car user-datas))
|
||||
(error 'handin "no fields changed: ~a" (car usernames)))
|
||||
(error* "no fields changed: ~a" (car usernames)))
|
||||
(for-each (lambda (str info)
|
||||
(check-field str (cadr info) (car info) (caddr info)))
|
||||
(cdr new-data) (get-conf 'extra-fields))
|
||||
|
@ -407,7 +398,7 @@
|
|||
(define (get-user-info data)
|
||||
(define usernames (a-ref data 'usernames))
|
||||
(unless (= 1 (length usernames))
|
||||
(error 'handin "cannot get user-info for multiple users: ~a" usernames))
|
||||
(error* "cannot get user-info for multiple users: ~a" usernames))
|
||||
;; filter out hidden fields
|
||||
(let ([all-data (cdar (a-ref data 'user-datas))])
|
||||
(filter values (map (lambda (d f)
|
||||
|
@ -426,7 +417,7 @@
|
|||
(define (good? passwd)
|
||||
(define (bad-password msg)
|
||||
(log-line "ERROR: ~a -- ~s" msg passwd)
|
||||
(error 'handin "bad password in user database"))
|
||||
(error* "bad password in user database"))
|
||||
(cond [(string? passwd) (equal? md5 passwd)]
|
||||
[(and (list? passwd) (= 2 (length passwd))
|
||||
(symbol? (car passwd)) (string? (cadr passwd)))
|
||||
|
@ -513,8 +504,8 @@
|
|||
(cons (get-conf 'master-password)
|
||||
(map car user-datas)))))
|
||||
(log-line "failed login: ~a" (a-ref data 'username/s))
|
||||
(error 'handin "bad username or password for ~a"
|
||||
(a-ref data 'username/s)))
|
||||
(error* "bad username or password for ~a"
|
||||
(a-ref data 'username/s)))
|
||||
(log-line "login: ~a" usernames))
|
||||
(case msg
|
||||
[(change-user-info) (change-user-info data)]
|
||||
|
@ -525,7 +516,7 @@
|
|||
(write+flush w 'ok)) ; final confirmation for *all* actions
|
||||
|
||||
(define (assignment-list)
|
||||
(sort (map path->string (directory-list "active")) string<?))
|
||||
(map assignment<->dir (get-conf 'active-dirs)))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
(unless (and raw-config
|
||||
(< (- (current-inexact-milliseconds) last-poll) poll-freq))
|
||||
(set! last-poll (current-inexact-milliseconds))
|
||||
(printf "polling...\n")
|
||||
(let ([filetime (file-or-directory-modify-seconds config-file)])
|
||||
(unless (and filetime (equal? filetime last-filetime))
|
||||
(set! last-filetime filetime)
|
||||
|
@ -28,7 +27,6 @@
|
|||
(error 'get-conf
|
||||
"could not read conf (~a)"
|
||||
config-file))])
|
||||
(printf "reading...\n")
|
||||
(with-input-from-file config-file read)))
|
||||
(set! config-cache (make-hash-table)))))
|
||||
(hash-table-get config-cache key
|
||||
|
@ -46,9 +44,12 @@
|
|||
(define (rx s) (if (regexp? s) s (regexp s)))
|
||||
(define (path p) (path->complete-path p server-dir))
|
||||
(define (path/false p) (and p (path p)))
|
||||
(define (path-list l) (map path l))
|
||||
|
||||
(define (config-default+translate which)
|
||||
(case which
|
||||
[(active-dirs) (values '() path-list )]
|
||||
[(inactive-dirs) (values '() path-list )]
|
||||
[(port-number) (values 7979 id )]
|
||||
[(https-port-number) (values (add1 (get-conf 'port-number)) id )]
|
||||
[(session-timeout) (values 300 id )]
|
||||
|
@ -73,10 +74,32 @@
|
|||
"a valid email address"))
|
||||
id)]
|
||||
;; computed from the above (mark by translate = #f)
|
||||
[(all-dirs)
|
||||
(values (append (get-conf 'active-dirs) (get-conf 'inactive-dirs)) #f)]
|
||||
[(names-dirs) ; see below
|
||||
(values (paths->map (get-conf 'all-dirs)) #f)]
|
||||
[(user-fields)
|
||||
(values (filter (lambda (f) (not (eq? '- (cadr f))))
|
||||
(get-conf 'extra-fields))
|
||||
#f)]
|
||||
[else (error 'get-conf "unknown configuration entry: ~s" which)]))
|
||||
|
||||
;; This is used below to map names to submission directory paths and back
|
||||
;; returns a (list-of (either (list name path) (list path name)))
|
||||
(define (paths->map dirs)
|
||||
(define (path->name dir)
|
||||
(unless (directory-exists? dir)
|
||||
(error 'get-conf
|
||||
"directory entry for an inexistent directory: ~e" dir))
|
||||
(let-values ([(_1 name _2) (split-path dir)])
|
||||
(bytes->string/locale (path-element->bytes name))))
|
||||
(let ([names (map path->name dirs)])
|
||||
(append (map list names dirs) (map list dirs names))))
|
||||
|
||||
;; Translates an assignment name to a directory path or back
|
||||
(provide assignment<->dir)
|
||||
(define (assignment<->dir a/d)
|
||||
(cond [(assoc a/d (get-conf 'names-dirs)) => cadr]
|
||||
[else (error 'assignment<->dir "internal error: ~e" a/d)]))
|
||||
|
||||
)
|
||||
|
|
|
@ -10,10 +10,6 @@
|
|||
(lib "logger.ss" "handin-server" "private")
|
||||
(lib "config.ss" "handin-server" "private"))
|
||||
|
||||
(define active-dir (build-path server-dir "active"))
|
||||
(define inactive-dir (build-path server-dir "inactive"))
|
||||
(define active/inactive-dirs (list active-dir inactive-dir))
|
||||
|
||||
(define get-user-data
|
||||
(let ([users-file (build-path server-dir "users.ss")])
|
||||
(lambda (user)
|
||||
|
@ -40,7 +36,7 @@
|
|||
"/")))))
|
||||
|
||||
(define (make-k k tag)
|
||||
(format "~a~atag=~a" k (if (regexp-match #rx"^[^#]*[?]" k) "&" "?")
|
||||
(format "~a~atag=~a" k (if (regexp-match? #rx"^[^#]*[?]" k) "&" "?")
|
||||
(uri-encode (regexp-replace handin-prefix-re
|
||||
(if (path? tag) (path->string tag) tag)
|
||||
""))))
|
||||
|
@ -51,24 +47,22 @@
|
|||
;; `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 (find-handin-entry hi look-for)
|
||||
(let ([dir (assignment<->dir 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-handin-entry
|
||||
"internal error: ~e" look-for)])
|
||||
(build-path dir d))))
|
||||
(directory-list dir)))))
|
||||
|
||||
(define (handin-link k user hi)
|
||||
(let* ([dir (find-hi-entry hi user)]
|
||||
(let* ([dir (find-handin-entry hi user)]
|
||||
[l (and dir (with-handlers ([exn:fail? (lambda (x) null)])
|
||||
(parameterize ([current-directory dir])
|
||||
(sort (filter (lambda (f)
|
||||
|
@ -94,7 +88,8 @@
|
|||
user hi)))))
|
||||
|
||||
(define (solution-link k hi)
|
||||
(let ([soln (find-hi-entry hi #rx"^solution")]
|
||||
(let ([soln (and (member (assignment<->dir hi) (get-conf 'inactive-dirs))
|
||||
(find-handin-entry hi #rx"^solution"))]
|
||||
[none `((i "---"))])
|
||||
(cond [(not soln) none]
|
||||
[(file-exists? soln)
|
||||
|
@ -115,7 +110,7 @@
|
|||
[else none])))
|
||||
|
||||
(define (handin-grade user hi)
|
||||
(let* ([dir (find-hi-entry hi user)]
|
||||
(let* ([dir (find-handin-entry hi user)]
|
||||
[grade (and dir
|
||||
(let ([filename (build-path dir "grade")])
|
||||
(and (file-exists? filename)
|
||||
|
@ -140,51 +135,42 @@
|
|||
|
||||
(define re:base #rx"^([a-zA-Z]*)([0-9]+)")
|
||||
(define (all-status-page user)
|
||||
(let* ([l (sort
|
||||
(map path->string
|
||||
(append (directory-list active-dir)
|
||||
(with-handlers ([exn:fail? (lambda (x) null)])
|
||||
(directory-list inactive-dir))))
|
||||
(lambda (a b)
|
||||
(let ([am (regexp-match re:base a)]
|
||||
[bm (regexp-match re:base b)])
|
||||
(if (and am bm
|
||||
(string=? (cadr am) (cadr bm)))
|
||||
(or (< (string->number (caddr am))
|
||||
(string->number (caddr bm)))
|
||||
(string<? a b))
|
||||
(string<? a b)))))]
|
||||
[next
|
||||
(define (cell . texts) `(td ([bgcolor "white"]) ,@texts))
|
||||
(define (rcell . texts) `(td ([bgcolor "white"] [align "right"]) ,@texts))
|
||||
(define (header . texts) `(td ([bgcolor "#f0f0f0"]) (big (strong ,@texts))))
|
||||
(define ((row k active?) dir)
|
||||
(let ([hi (assignment<->dir dir)])
|
||||
`(tr ([valign "top"])
|
||||
,(apply header hi
|
||||
(if active? `((br) (small (small "[active]"))) '()))
|
||||
,(apply cell (handin-link k user hi))
|
||||
,(rcell (handin-grade user hi))
|
||||
,(apply cell (solution-link k hi)))))
|
||||
(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"] [cellpadding "6"] [align "center"])
|
||||
(tr () ,@(map header '(nbsp "Files" "Grade" "Solution")))
|
||||
,@(map (lambda (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)))))]
|
||||
,@(append (map (row k #t) (get-conf 'active-dirs))
|
||||
(map (row k #f) (get-conf 'inactive-dirs)))))))]
|
||||
[tag (select-k next)])
|
||||
(download user tag)))
|
||||
|
||||
(define (download who tag)
|
||||
(define (check path elts)
|
||||
(define (check path elts allow-active?)
|
||||
(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)
|
||||
;; must be rooted in a submission directory (why build-path instead
|
||||
;; of using `path'? -- because path will have a trailing slash)
|
||||
(member (build-path base name)
|
||||
(get-conf (if allow-active? 'all-dirs 'inactive-dirs)))
|
||||
(and (cond [(eq? '* check) #t]
|
||||
[(regexp? check) (regexp-match check name)]
|
||||
[(regexp? check) (regexp-match? check name)]
|
||||
[(string? check)
|
||||
(or (equal? name check)
|
||||
(member check (regexp-split #rx" *[+] *" name)))]
|
||||
|
@ -194,16 +180,16 @@
|
|||
(with-handlers ([exn:fail? (lambda (exn)
|
||||
(make-page "Error" "Illegal file access"))])
|
||||
;; Make sure the user is allowed to read the requested file:
|
||||
(or (check file `(* ,who *))
|
||||
(check file `(* #rx"^solution"))
|
||||
(check file `(* #rx"^solution" *))
|
||||
(or (check file `(,who *) #t)
|
||||
(check file `(#rx"^solution") #f)
|
||||
(check file `(#rx"^solution" *) #f)
|
||||
(error "Boom!"))
|
||||
(log-line "Status file-get: ~s ~a" who file)
|
||||
;; Return the downloaded file
|
||||
(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)])
|
||||
[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"]
|
||||
|
|
Loading…
Reference in New Issue
Block a user