Gauche:CGI:スケジュール予定表
- Gauche:CGI:スケジュール予定表:Shiro版
- Gauche:CGI:スケジュール予定表:Shiro版:ver1
- Gauche:CGI:スケジュール予定表:Shiro版:ver2
- Gauche:CGI:スケジュール予定表:Shiro版:ver3
- Gauche:CGI:スケジュール予定表:Shiro版:ver4
スケジュール予定表CGI
LL Weekend 2004最後のセッション「その場でどう書く」のお題になった スケジュール予定表CGIスクリプト。
- カレンダー表示
- 前後の月に移動
- 各日付にメモ入力
という機能+時間があればおまけをつけろというやつ。
で、とりあえず作ってみました。
- Shiro(2004/08/28 20:21:17 PDT): このお題、cgiスクリプトのチュートリアルに 丁度いいかも。ということでShiroバージョンも作ってみます。 Gauche:CGI:スケジュール予定表:Shiro版
ストレージには gdbm を使ってて最初の方に *mydbfile* にパス指定してるので
適宜書き換えてみてください。
本当はこういうのって誰でも確実に使える fsdbm をデフォルトにしておくべきかな。
あと、忙しくアクセスするとDBへのアクセスが一時的に使えなくて Internal Error
が発生するので、 dbm-open 〜 dbm-close 間をもっと局所化するべきかもしれない。
あるいは、 DB ファイルを月毎あるいは日毎に分ける方がいいのかな?
- というわけでDBを月毎にわける方向にしてみた。 かなりちゃかちゃかツツキまくったけどエラー発生しなくなります。 この辺(DB関係)プロのノウハウとかコメントあれば是非。
- Shiro: temporary unavailableなので、そのエラーだけ検出して
少し待ってリトライかけるようにするって手もあります。WiLiKiのコードで
やってます。もっとシビアな環境では、リクエストの統計情報から
平均トランザクション時間はこれ以下じゃなくちゃ、ってのを出して
チューンしてゆくことになります。
*** ERROR: couldn't open gdbm file "/home/cut-sea/data/sched.db": Resource temporarily unavailable Stack Trace: _______________________________________ 0 (gdbm-open path (slot-ref self 'bsize) rwopt (slot-ref self 'file- ... At line 87 of "/usr/local/share/gauche/0.8.1/lib/dbm/gdbm.scm" 1 (dbm-open <gdbm> :path *mydbfile* :rw-mode :write) At line 78 of "/usr/pkg/libexec/cgi-bin/sched.scm" [Sun Aug 29 10:27:54 2004] [error] [client 127.0.0.1] Premature end of script headers: /usr/pkg/libexec/cgi-bin/sched.scm- WiLiKi-0.5/src/wiliki/db.scm
;; private procedures (define (db-try-open dbpath dbtype rwmode) ;; Try to open the database. If it receives EAVAIL error, wait for ;; one second and try again, up to *retry-limit* times. (define (try retry mode) (with-error-handler (lambda (e) (cond ((>= retry *retry-limit*) (raise e)) ((string-contains-ci (ref e 'message) *EAVAIL-message*) (sys-sleep 1) (try (+ retry 1) mode)) (else ;; we don't want to show the path of db to unknown ;; visitors (raise (make <error> :message #`"Couldn't open database file to ,|rwmode|."))))) (lambda () (dbm-open dbtype :path dbpath :rw-mode mode)))) ;; If db file does not exist, we open it with :write mode, ;; regardless of rwmode arg, so that the empty DB is created. ;; Note that race condition will not happen here. If there's no ;; DB and two process simultaneously came to this code, only ;; one can grab the write access of DB, and another will ;; be kept waiting until the initial content is committed. (try 0 (if (dbm-db-exists? dbtype dbpath) rwmode :write)) )
- WiLiKi-0.5/src/wiliki/db.scm
gauche-devel-jp にてShiroさんが書かれていたencodeのポイント
uriを含むHTML文書を作成する場合の順序は、
(1) uriを構成する各要素 (パス名のコンポーネントや、
queryの属性名、属性値等) に対してuri-encodeをかける。
(2) uriの要素をくっつける (ここで、query-stringのセパレータである'?'や'&'、
fragmentのセパレータである'#'が挿入される。これらのセパレータは
uri-encodeしてはいけない)
(3) html-encodeする
HTMLを受け取り、uriを解釈する側の順序はこの逆で、
(1') html-decodeする
(2') uriを要素毎に切り離す
(3') uriの各要素についてuri-decodeする
となります。但し、cgiの場合は受け取るのがhtmlではないので、
上記(2'), (3')の手順だけです。(1')はクライアント側で行われます。
(注) html-encode は html-escape に相当する処理です
バグ
- 特にメモ入力文字列にスペースいれたら+になる。これはクライアントがencodeしたやつをdecodeできてないからだと思うが、どの部分で直せば良いのかがまだ分からん。
- メモ入力文字列に HTML タグとかいれると NG です。
- 上のは uri-decode-string にオプション :cgi-decode #t をつけることで回避。
- ようやくテキストエリアにHTMLを入力しても大丈夫になった。
ある程度出来てしまうと emacs 上で単純に評価しても、DB のアクセス権限で 蹴られたりして、うまく実行出来なかったりするので telnet localhost 80 から GET リクエスト出したり、 ページソースを見ることで html-escape や uri-encode の処理を確認したりした。 結構デバッグがめんどくさい。 あとは http のエラーログかな。 今気付いたけどアクセスログの方には GET/POST や フォームデータなんかも uri-encode されたのが見えるので、 この辺も今度からデバッグに有用そうな気がする。
#! /usr/local/bin/gosh
(use srfi-1)
(use srfi-13)
(use srfi-19)
(use util.list)
(use text.tree)
(use text.html-lite)
(use rfc.uri)
(use www.cgi)
(use dbm)
(use dbm.gdbm)
;;=================
;; Configure
;;=================
(define *dbclass* <gdbm>)
(define *mydbpath* "/home/cut-sea/data/schedule/") ;; close path name with "/"
;;=================
;; Trivial's
;;=================
(define (dec n) (- n 1))
(define (inc n) (+ n 1))
(define (divisible? n d) (= (modulo n d) 0))
(define (indivisible? n d) (not (= (modulo n d) 0)))
(define (split lst n off sp)
(cond ((null? lst) '())
((> n (length lst)) (list (append lst
(make-list (- n (length lst)) sp))))
(else (cons (append (make-list off sp)
(take* lst (- n off)))
(split (drop* lst (- n off)) n 0 sp)))))
(define (get-ymd ymd)
(lambda (params)
(if (null? params)
#f
(if (equal? ymd (caar params))
(cadar params)
((get-ymd ymd) (cdr params))))))
(define get-year (get-ymd "year"))
(define get-month (get-ymd "month"))
(define get-day (get-ymd "day"))
(define (query->alist query val->val)
(map (lambda (l)
(cons (uri-decode-string (car l) :cgi-decode #t)
(val->val (uri-decode-string (cadr l) :cgi-decode #t))))
(map (lambda (v)
(string-split v #\=))
(string-split query #\&))))
(define (call-with-db-file file proc)
(let ((db (dbm-open *dbclass* :path file :rw-mode :write)))
(let ((ret (proc db)))
(dbm-close db)
ret)))
(define (get-db-contents file)
(call-with-db-file
file (lambda (db)
(dbm-map db (lambda (key val) (cons key val))))))
(define (put-to-db file key val)
(call-with-db-file
file (lambda (db)
(dbm-put! db key val))))
;;=================
;; literal
;;=================
(define *self* (sys-getenv "SCRIPT_NAME"))
(define *query* (sys-getenv "QUERY_STRING"))
(define *query-alist* (if (equal? *query* "")
#f
(query->alist *query* x->number)))
(define *content* (if (sys-getenv "CONTENT_LENGTH")
(let ((len (x->number (sys-getenv "CONTENT_LENGTH"))))
(let loop ((count 1)
(ret '()))
(if (> count len)
(list->string (reverse ret))
(loop (+ count 1) (cons (read-char) ret)))))
#f))
(define *content-alist* (if *content*
(query->alist *content* identity)
#f))
(define *month* '("January" "February" "March" "April" "May" "June"
"July" "August" "September" "October" "November" "December"))
(define *weekday* '("Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat"))
;;=================
;; Calendar
;;=================
(define (leap? year)
(if (indivisible? year 4) #f
(if (indivisible? year 100) #t
(if (indivisible? year 400) #f #t))))
(define (days-of year . month)
(let* ((feb (if (leap? year) 29 28))
(days `(31 ,feb 31 30 31 30 31 31 30 31 30 31)))
(if (null? month)
days
(list-ref days (- (car month) 1)))))
;; Zeller's formula. ref.) UNIX USER 2003-07
;;
(define (weekday-of year month day)
(when (<= month 2)
(dec! year)
(inc! month 12))
(modulo (+ year
(quotient year 4)
(- (quotient year 100))
(quotient year 400)
(quotient (+ (* 13 month) 8) 5)
day)
7))
(define (calendar year month)
(letrec ((month-name (lambda (m) (list-ref *month* (dec m)))))
(lambda (msg)
(cond ((eq? msg 'year) year)
((eq? msg 'month) month)
((eq? msg 'caps) (string-append (month-name month) " " (x->string year)))
((eq? msg 'cal) (let ((days (days-of year month))
(wday (weekday-of year month 1)))
(split (iota days 1) 7 wday "")))
((eq? msg 'next) (if (= month 12)
(calendar (inc year) 1)
(calendar year (inc month))))
((eq? msg 'prev) (if (= month 1)
(calendar (dec year) 12)
(calendar year (dec month))))
((eq? msg 'query) (string-append *self*
"?year=" (x->string year)
"&month=" (x->string month)))
((eq? msg 'ymd) (lambda (day)
(string-append (x->string year) "-"
(x->string month) "-"
(x->string day))))
((eq? msg 'dbname) (lambda (path)
(string-append path (x->string year) "."
(x->string month))))
(else (error "ERROR: No such message found. -- CALENDAR --" msg))))))
(define (get-year calendar) (calendar 'year))
(define (get-month calendar) (calendar 'month))
(define (caption calendar) (calendar 'caps))
(define (get-calendar calendar) (calendar 'cal))
(define (next-month calendar) (calendar 'next))
(define (prev-month calendar) (calendar 'prev))
(define (get-query calendar) (calendar 'query))
(define (make-ymd-form calendar day) ((calendar 'ymd) day))
(define (dbname path calendar) ((calendar 'dbname) path))
(define (current-calendar)
(let ((now (current-date)))
(calendar (ref now 'year) (ref now 'month))))
(define (make-table calendar)
(let* ((caps (caption calendar))
(year (x->string (get-year calendar)))
(month (x->string (get-month calendar)))
(link (string-append (get-query calendar) "&day="))
(cal (get-calendar calendar)))
(html:table :border 1
(html:caption caps)
(html:tr (map (lambda (s)
(html:th s))
*weekday*))
(map (lambda (w)
(html:tr
(map (lambda (d)
(html:th
(html:a :href (string-append link (x->string d)) d)))
w)))
cal))))
;;=================
;; main logic
;;=================
(write-tree
(cgi-header))
(write-tree
(html-doctype))
(let* ((now-cal (if *query-alist*
(calendar (cdr (assoc "year" *query-alist*))
(cdr (assoc "month" *query-alist*)))
(current-calendar)))
(prev-cal (prev-month now-cal))
(next-cal (next-month now-cal)))
(write-tree
(html:html
(html:head (html:meta :http-equiv "Content-type" :content "text/html" :charset "euc-jp")
(html:title "スケジュール予定表"))
(html:body
(html:a :href (get-query prev-cal) "<<=")
" スケジュール予定表 "
(html:a :href (get-query next-cal) "=>>")
(make-table now-cal)
(if *query-alist*
(if (assoc "day" *query-alist*)
(let ((day (cdr (assoc "day" *query-alist*))))
(html:form :action (get-query now-cal) :method "POST"
(let ((ymd (make-ymd-form now-cal day)))
(html:textarea :rows 10 :cols 40
:name ymd :wrap "virtual"
(let ((dbfile (dbname *mydbpath* now-cal)))
(if (and (file-exists? dbfile)
(assoc ymd (get-db-contents dbfile)))
(html-escape-string (cdr (assoc ymd (get-db-contents dbfile))))
"")))) (html:br)
(html:input :type "submit" :name "Send" :value "Commit")))
"")
"")
(if *content-alist*
(let* ((date-content (car *content-alist*))
(date (car date-content))
(content (cdr date-content))
(dbfile (dbname *mydbpath* now-cal)))
(put-to-db dbfile date content)
(html:pre (html-escape-string (string-append date " : " content))))
"")
))))
;; EOF