mirror of
https://git.savannah.gnu.org/git/guile.git
synced 2025-05-11 00:00:49 +02:00
463 lines
17 KiB
Scheme
463 lines
17 KiB
Scheme
;"db2html.scm" Convert relational database to hyperlinked pages.
|
|
; Copyright 1997, 1998, 2000, 2001 Aubrey Jaffer
|
|
;
|
|
;Permission to copy this software, to redistribute it, and to use it
|
|
;for any purpose is granted, subject to the following restrictions and
|
|
;understandings.
|
|
;
|
|
;1. Any copy made of this software must include this copyright notice
|
|
;in full.
|
|
;
|
|
;2. I have made no warrantee or representation that the operation of
|
|
;this software will be error-free, and I am under no obligation to
|
|
;provide any services, by way of maintenance, update, or otherwise.
|
|
;
|
|
;3. In conjunction with products arising from the use of this
|
|
;material, there shall be no use of my name in any advertising,
|
|
;promotional, or sales literature without prior written consent in
|
|
;each case.
|
|
|
|
(require 'uri)
|
|
(require 'html-form)
|
|
(require 'net-clients)
|
|
(require 'string-search)
|
|
|
|
;;@code{(require 'db->html)}
|
|
|
|
;;@body
|
|
(define (html:table options . rows)
|
|
(apply string-append
|
|
(sprintf #f "<TABLE %s>\\n" (or options ""))
|
|
(append rows (list (sprintf #f "</TABLE>\\n")))))
|
|
|
|
;;@args caption align
|
|
;;@args caption
|
|
;;@2 can be @samp{top} or @samp{bottom}.
|
|
(define (html:caption caption . align)
|
|
(if (null? align)
|
|
(sprintf #f " <CAPTION>%s</CAPTION>\\n"
|
|
(html:plain caption))
|
|
(sprintf #f " <CAPTION ALIGN=%s>%s</CAPTION>\\n"
|
|
(car align)
|
|
(html:plain caption))))
|
|
|
|
;;@body Outputs a heading row for the currently-started table.
|
|
(define (html:heading columns)
|
|
(sprintf #f " <TR VALIGN=\"TOP\">\\n%s </TR>\\n"
|
|
(apply string-append
|
|
(map (lambda (datum)
|
|
(sprintf #f " <TH>%s</TH>\\n" (or datum "")))
|
|
columns))))
|
|
|
|
;;@body Outputs a heading row with column-names @1 linked to URIs @2.
|
|
(define (html:href-heading columns uris)
|
|
(html:heading
|
|
(map (lambda (column uri)
|
|
(if uri
|
|
(html:link uri column)
|
|
column))
|
|
columns uris)))
|
|
|
|
(define (row->anchor pkl row)
|
|
(sprintf #f "<A NAME=\"%s\"></A>" (uri:make-path (butnthcdr pkl row))))
|
|
|
|
;;@args k foreigns
|
|
;;
|
|
;;The positive integer @1 is the primary-key-limit (number of
|
|
;;primary-keys) of the table. @2 is a list of the filenames of
|
|
;;foreign-key field pages and #f for non foreign-key fields.
|
|
;;
|
|
;;@0 returns a procedure taking a row for its single argument. This
|
|
;;returned procedure returns the html string for that table row.
|
|
(define (html:linked-row-converter pkl foreigns)
|
|
(define idxs (do ((idx (length foreigns) (+ -1 idx))
|
|
(nats '() (cons idx nats)))
|
|
((not (positive? idx)) nats)))
|
|
(require 'pretty-print)
|
|
(lambda (row)
|
|
(define (present datum)
|
|
(if (or (string? datum) (symbol? datum))
|
|
(html:plain datum)
|
|
(let* ((str (pretty-print->string datum))
|
|
(len (+ -1 (string-length str))))
|
|
(cond ((eqv? (string-index str #\newline) len)
|
|
(string-append "<TT>" (substring str 0 len) "</TT>"))
|
|
(else (html:pre str))))))
|
|
(sprintf #f " <TR VALIGN=TOP>\\n%s </TR>\\n"
|
|
(apply string-append
|
|
(map (lambda (idx datum foreign)
|
|
(sprintf
|
|
#f " <TD>%s%s</TD>\\n"
|
|
(if (eqv? 1 idx) (row->anchor pkl row) "")
|
|
(cond ((or (not datum) (null? datum)) "")
|
|
((not foreign) (present datum))
|
|
((equal? "catalog-data.html" foreign)
|
|
(html:link (make-uri
|
|
(table-name->filename datum)
|
|
#f #f)
|
|
(present datum)))
|
|
(else (html:link (make-uri foreign #f datum)
|
|
(present datum))))))
|
|
idxs row foreigns)))))
|
|
|
|
;;@body
|
|
;;Returns the symbol @1 converted to a filename.
|
|
(define (table-name->filename table-name)
|
|
(and table-name (string-append
|
|
(string-subst (symbol->string table-name) "*" "" ":" "_")
|
|
".html")))
|
|
|
|
(define (table-name->column-table-name db table-name)
|
|
((((db 'open-table) '*catalog-data* #f) 'get 'coltab-name)
|
|
table-name))
|
|
|
|
;;@args caption db table-name match-key1 @dots{}
|
|
;;Returns HTML string for @2 table @3. Every foreign-key value is
|
|
;;linked to the page (of the table) defining that key.
|
|
;;
|
|
;;The optional @4 @dots{} arguments restrict actions to a subset of
|
|
;;the table. @xref{Table Operations, match-key}.
|
|
(define (table->linked-html caption db table-name . args)
|
|
(let* ((table ((db 'open-table) table-name #f))
|
|
(foreigns (table 'column-foreigns))
|
|
(tags (map table-name->filename foreigns))
|
|
(names (table 'column-names))
|
|
(primlim (table 'primary-limit)))
|
|
(apply html:table "CELLSPACING=0 BORDER=1"
|
|
(html:caption caption 'BOTTOM)
|
|
(html:href-heading
|
|
names
|
|
(append (make-list primlim
|
|
(table-name->filename
|
|
(table-name->column-table-name db table-name)))
|
|
(make-list (- (length names) primlim) #f)))
|
|
(html:heading (table 'column-domains))
|
|
(html:href-heading foreigns tags)
|
|
(html:heading (table 'column-types))
|
|
(map (html:linked-row-converter primlim tags)
|
|
(apply (table 'row:retrieve*) args)))))
|
|
|
|
;;@body
|
|
;;Returns a complete HTML page. The string @3 names the page which
|
|
;;refers to this one.
|
|
;;
|
|
;;The optional @4 @dots{} arguments restrict actions to a subset of
|
|
;;the table. @xref{Table Operations, match-key}.
|
|
(define (table->linked-page db table-name index-filename . args)
|
|
(string-append
|
|
(if index-filename
|
|
(html:head table-name
|
|
(html:link (make-uri index-filename #f table-name)
|
|
(html:plain table-name)))
|
|
(html:head table-name))
|
|
(html:body (apply table->linked-html table-name db table-name args))))
|
|
|
|
(define (html:catalog-row-converter row foreigns)
|
|
(sprintf #f " <TR VALIGN=TOP>\\n%s </TR>\\n"
|
|
(apply string-append
|
|
(map (lambda (datum foreign)
|
|
(sprintf #f " <TD>%s%s</TD>\\n"
|
|
(html:anchor (sprintf #f "%s" datum))
|
|
(html:link (make-uri foreign #f #f) datum)))
|
|
row foreigns))))
|
|
|
|
;;@body
|
|
;;Returns HTML string for the catalog table of @1.
|
|
(define (catalog->html db caption . args)
|
|
(apply html:table "BORDER=1"
|
|
(html:caption caption 'BOTTOM)
|
|
(html:heading '(table columns))
|
|
(map (lambda (row)
|
|
(cond ((and (eq? '*columns* (caddr row))
|
|
(not (eq? '*columns* (car row))))
|
|
"")
|
|
(else (html:catalog-row-converter
|
|
(list (car row) (caddr row))
|
|
(list (table-name->filename (car row))
|
|
(table-name->filename (caddr row)))))))
|
|
(apply (((db 'open-table) '*catalog-data* #f) 'row:retrieve*)
|
|
args))))
|
|
|
|
;;Returns complete HTML page (string) for the catalog table of @1.
|
|
(define (catalog->page db caption . args)
|
|
(string-append (html:head caption)
|
|
(html:body (apply catalog->html db caption args))))
|
|
|
|
;;@subsection HTML editing tables
|
|
|
|
;;@noindent A client can modify one row of an editable table at a time.
|
|
;;For any change submitted, these routines check if that row has been
|
|
;;modified during the time the user has been editing the form. If so,
|
|
;;an error page results.
|
|
;;
|
|
;;@noindent The behavior of edited rows is:
|
|
;;
|
|
;;@itemize @bullet
|
|
;;@item
|
|
;;If no fields are changed, then no change is made to the table.
|
|
;;@item
|
|
;;If the primary keys equal null-keys (parameter defaults), and no other
|
|
;;user has modified that row, then that row is deleted.
|
|
;;@item
|
|
;;If only primary keys are changed, there are non-key fields, and no
|
|
;;row with the new keys is in the table, then the old row is
|
|
;;deleted and one with the new keys is inserted.
|
|
;;@item
|
|
;;If only non-key fields are changed, and that row has not been
|
|
;;modified by another user, then the row is changed to reflect the
|
|
;;fields.
|
|
;;@item
|
|
;;If both keys and non-key fields are changed, and no row with the
|
|
;;new keys is in the table, then a row is created with the new
|
|
;;keys and fields.
|
|
;;@item
|
|
;;If fields are changed, all fields are primary keys, and no row with
|
|
;;the new keys is in the table, then a row is created with the new
|
|
;;keys.
|
|
;;@end itemize
|
|
;;
|
|
;;@noindent After any change to the table, a @code{sync-database} of the
|
|
;;database is performed.
|
|
|
|
;;@args table-name null-keys update delete retrieve
|
|
;;@args table-name null-keys update delete
|
|
;;@args table-name null-keys update
|
|
;;@args table-name null-keys
|
|
;;
|
|
;;Returns procedure (of @var{db}) which returns procedure to modify row
|
|
;;of @1. @2 is the list of @dfn{null} keys which indicate that the row
|
|
;;is to be deleted. Optional arguments @3, @4, and @5 default to the
|
|
;;@code{row:update}, @code{row:delete}, and @code{row:retrieve} of @1 in
|
|
;;@var{db}.
|
|
(define (command:modify-table table-name null-keys . args)
|
|
(define argc (length args))
|
|
(lambda (rdb)
|
|
(define table ((rdb 'open-table) table-name #t))
|
|
(let ((table:update (or (and (> argc 0) (car args)) (table 'row:update)))
|
|
(table:delete (or (and (> argc 1) (cadr args)) (table 'row:delete)))
|
|
(table:retrieve (or (and (> argc 2) (caddr args)) (table 'row:retrieve)))
|
|
(pkl (length null-keys)))
|
|
(define ptypes (butnthcdr pkl (table 'column-types)))
|
|
(if (> argc 4) (slib:error 'command:modify-table 'too-many-args
|
|
table-name null-keys args))
|
|
(lambda (*keys* *row-hash* . new-row)
|
|
(let* ((new-pkeys (butnthcdr pkl new-row))
|
|
(pkeys (uri:path->keys (uri:split-fields *keys* #\/) ptypes))
|
|
(row (apply table:retrieve pkeys))
|
|
(same-nonkeys? (equal? (nthcdr pkl new-row) (nthcdr pkl row))))
|
|
(cond ((equal? pkeys new-pkeys) ;did not change keys
|
|
(cond ((not row) '("Row deleted by other user"))
|
|
((equal? (crc:hash-obj row) *row-hash*)
|
|
(table:update new-row)
|
|
((rdb 'sync-database)) #t)
|
|
(else '("Row changed by other user"))))
|
|
((equal? null-keys new-pkeys) ;blanked keys
|
|
(cond ((not row) #t)
|
|
((equal? (crc:hash-obj row) *row-hash*)
|
|
;;(slib:warn (sprintf #f "Removing key: %#a => %#a" new-pkeys ))
|
|
(apply table:delete pkeys)
|
|
((rdb 'sync-database)) #t)
|
|
(else '("Row changed by other user"))))
|
|
(else ;changed keys
|
|
(set! row (apply table:retrieve new-pkeys))
|
|
(cond (row (list "Row already exists"
|
|
(sprintf #f "%#a" row)))
|
|
(else (table:update new-row)
|
|
(if (and same-nonkeys?
|
|
(not (null? (nthcdr pkl new-row))))
|
|
(apply table:delete pkeys))
|
|
((rdb 'sync-database)) #t)))))))))
|
|
|
|
;;@body Given @2 in @1, creates parameter and @code{*command*} tables
|
|
;;for editing one row of @2 at a time. @0 returns a procedure taking a
|
|
;;row argument which returns the HTML string for editing that row.
|
|
;;
|
|
;;Optional @3 are expressions (lists) added to the call to
|
|
;;@code{command:modify-table}.
|
|
;;
|
|
;;The domain name of a column determines the expected arity of the data
|
|
;;stored in that column. Domain names ending in:
|
|
;;
|
|
;;@table @samp
|
|
;;@item *
|
|
;;have arity @samp{nary};
|
|
;;@item +
|
|
;;have arity @samp{nary1}.
|
|
;;@end table
|
|
(define (command:make-editable-table rdb table-name . args)
|
|
(define table ((rdb 'open-table) table-name #t))
|
|
(let ((pkl (table 'primary-limit))
|
|
(columns (table 'column-names))
|
|
(domains (table 'column-domains))
|
|
(types (table 'column-types))
|
|
(idxs (do ((idx (length (table 'column-names)) (+ -1 idx))
|
|
(nats '() (cons (+ 2 idx) nats)))
|
|
((not (positive? idx)) nats)))
|
|
(ftn (((rdb 'open-table) '*domains-data* #f) 'get 'foreign-table)))
|
|
(define field-specs
|
|
(map (lambda (idx column domain type)
|
|
(let* ((dstr (symbol->string domain))
|
|
(len (+ -1 (string-length dstr))))
|
|
(define arity
|
|
(case (string-ref dstr len)
|
|
((#\*) 'nary)
|
|
((#\+) 'nary1)
|
|
(else 'single)))
|
|
(case (string-ref dstr len)
|
|
((#\* #\+)
|
|
(set! type (string->symbol (substring dstr 0 len)))
|
|
(set! domain type)))
|
|
`(,idx ,column ,arity ,domain
|
|
,(make-defaulter arity type) #f "")))
|
|
idxs columns domains types))
|
|
(define foreign-choice-lists
|
|
(map (lambda (domain-name)
|
|
(define tab-name (ftn domain-name))
|
|
(if tab-name (get-foreign-choices (rdb-open tab-name #f)) '()))
|
|
domains))
|
|
(define-tables rdb
|
|
`(,(symbol-append table-name '- 'params)
|
|
*parameter-columns* *parameter-columns*
|
|
((1 *keys* single string #f #f "")
|
|
(2 *row-hash* single string #f #f "")
|
|
,@field-specs))
|
|
`(,(symbol-append table-name '- 'pname)
|
|
((name string))
|
|
((parameter-index uint)) ;should be address-params
|
|
(("*keys*" 1)
|
|
("*row-hash*" 2)
|
|
,@(map (lambda (idx column) (list (symbol->string column) idx))
|
|
idxs columns)))
|
|
`(*commands*
|
|
desc:*commands* desc:*commands*
|
|
((,(symbol-append 'edit '- table-name)
|
|
,(symbol-append table-name '- 'params)
|
|
,(symbol-append table-name '- 'pname)
|
|
(command:modify-table ',table-name
|
|
',(map (lambda (fs)
|
|
(caadr (caddar (cddddr fs))))
|
|
(butnthcdr pkl field-specs))
|
|
,@args)
|
|
,(string-append "Modify " (symbol->string table-name))))))
|
|
(let ((arities (map caddr field-specs)))
|
|
(lambda (row)
|
|
(define elements
|
|
(map form:element
|
|
columns
|
|
arities
|
|
(map (lambda (fld arity) (case arity
|
|
((nary nary1) fld)
|
|
(else (list fld))))
|
|
row arities)
|
|
foreign-choice-lists))
|
|
(sprintf #f " <TR>\\n <TD>%s</TD>%s\\n </TR>\\n"
|
|
(string-append
|
|
(html:hidden '*row-hash* (crc:hash-obj row))
|
|
(html:hidden '*keys* (uri:make-path (butnthcdr pkl row)))
|
|
;; (html:hidden '*suggest* '<>)
|
|
(car elements)
|
|
(form:submit '<> (symbol-append 'edit '- table-name))
|
|
;; (form:image "Modify Row" "/icons/bang.png")
|
|
)
|
|
(apply string-append
|
|
(map (lambda (elt) (sprintf #f " <TD>%s</TD>\\n" elt))
|
|
(cdr elements))))))))
|
|
|
|
;;@args k names edit-point edit-converter
|
|
;;
|
|
;;The positive integer @1 is the primary-key-limit (number of
|
|
;;primary-keys) of the table. @2 is a list of the field-names. @3 is
|
|
;;the list of primary-keys denoting the row to edit (or #f). @4 is the
|
|
;;procedure called with @1, @2, and the row to edit.
|
|
;;
|
|
;;@0 returns a procedure taking a row for its single argument. This
|
|
;;returned procedure returns the html string for that table row.
|
|
;;
|
|
;;Each HTML table constructed using @0 has first @1 fields (typically
|
|
;;the primary key fields) of each row linked to a text encoding of these
|
|
;;fields (the result of calling @code{row->anchor}). The page so
|
|
;;referenced typically allows the user to edit fields of that row.
|
|
(define (html:editable-row-converter pkl names edit-point edit-converter)
|
|
(require 'pretty-print)
|
|
(let ((idxs (do ((idx (length names) (+ -1 idx))
|
|
(nats '() (cons idx nats)))
|
|
((not (positive? idx)) nats)))
|
|
(datum->html
|
|
(lambda (datum)
|
|
(if (or (string? datum) (symbol? datum))
|
|
(html:plain datum)
|
|
(let* ((str (pretty-print->string datum))
|
|
(len (+ -1 (string-length str))))
|
|
(cond ((eqv? (string-index str #\newline) len)
|
|
(string-append "<B>" (substring str 0 len) "</B>"))
|
|
(else (html:pre str))))))))
|
|
(lambda (row)
|
|
(string-append
|
|
(sprintf #f " <TR VALIGN=TOP>\\n%s </TR>\\n"
|
|
(apply string-append
|
|
(map (lambda (idx datum foreign)
|
|
(sprintf
|
|
#f " <TD>%s%s</TD>\\n"
|
|
(if (eqv? 1 idx) (row->anchor pkl row) "")
|
|
(cond ((or (not datum) (null? datum)) "")
|
|
((<= idx pkl)
|
|
(let ((keystr (uri:make-path
|
|
(butnthcdr pkl row))))
|
|
(sprintf #f "<A HREF=\"%s#%s\">%s</A>"
|
|
keystr keystr
|
|
(datum->html datum))))
|
|
(else (datum->html datum)))))
|
|
idxs row names)))
|
|
(if (and edit-point edit-converter
|
|
(equal? (butnthcdr pkl edit-point) (butnthcdr pkl row)))
|
|
(edit-converter row)
|
|
"")))))
|
|
|
|
;;@subsection HTML databases
|
|
|
|
;;@body @1 must be a relational database. @2 must be #f or a
|
|
;;non-empty string naming an existing sub-directory of the current
|
|
;;directory.
|
|
;;
|
|
;;@0 creates an html page for each table in the database @1 in the
|
|
;;sub-directory named @2, or the current directory if @2 is #f. The
|
|
;;top level page with the catalog of tables (captioned @4) is written
|
|
;;to a file named @3.
|
|
(define (db->html-files db dir index-filename caption)
|
|
(call-with-output-file (in-vicinity (if dir (sub-vicinity "" dir) "")
|
|
index-filename)
|
|
(lambda (port)
|
|
(display (catalog->page db caption) port)))
|
|
((((db 'open-table) '*catalog-data* #f) 'for-each-row)
|
|
(lambda (row)
|
|
(call-with-output-file
|
|
(in-vicinity (sub-vicinity "" dir) (table-name->filename (car row)))
|
|
(lambda (port)
|
|
(display (table->linked-page db (car row) index-filename) port))))))
|
|
|
|
;;@args db dir index-filename
|
|
;;@args db dir
|
|
;;@1 must be a relational database. @2 must be a non-empty
|
|
;;string naming an existing sub-directory of the current directory or
|
|
;;one to be created. The optional string @3 names the filename of the
|
|
;;top page, which defaults to @file{index.html}.
|
|
;;
|
|
;;@0 creates sub-directory @2 if neccessary, and calls
|
|
;;@code{(db->html-files @1 @2 @3 @2)}. The @samp{file:} URI of @3 is
|
|
;;returned.
|
|
(define (db->html-directory db dir . index-filename)
|
|
(set! index-filename (if (null? index-filename)
|
|
"index.html"
|
|
(car index-filename)))
|
|
(if (symbol? dir) (set! dir (symbol->string dir)))
|
|
(if (not (file-exists? dir)) (make-directory dir))
|
|
(db->html-files db dir index-filename dir)
|
|
(path->uri (in-vicinity (sub-vicinity "" dir) index-filename)))
|
|
|
|
;;@args db dir index-filename
|
|
;;@args db dir
|
|
;;@0 is just like @code{db->html-directory}, but calls
|
|
;;@code{browse-url-netscape} with the uri for the top page after the
|
|
;;pages are created.
|
|
(define (db->netscape . args)
|
|
(browse-url-netscape (apply db->html-directory args)))
|