10/22/2003 [Stefan Vogel] I just wanted to get familar with metakits. So I build up a very simply but still useful (at least for me) ''Webadministration-Interface'' for [MetaKit]. This webadministration '''Metakit@Web''' works with [Tclhttpd]. With ''Metakit@Web'' you can: * create metakit-database-files * create, modify, delete views * create, edit, delete rows * view binaries * test queries ... Installation is quite simple (as always with Tclhttpd): * Extract the following script or download it from http://www.vogel-nest.de/tcl * Copy the script "mkweb.tcl" to your tclhttpd-custom-directory (may be you have to patch your Tclhttpd so that custom-files are read: http://sourceforge.net/tracker/index.php?func=detail&aid=635083&group_id=12884&atid=112884 ) * Create a directory where you place your metakits (let's say: /metakits or c:/metakits) * Adapt the directory in mkweb.tcl (e.g. set the array aConfig(databaseDir) to /metakits or c:/metakits) * restart Tclhttpd * Go to http:///mkweb '''Attention''' This Webadmin-interface was developed only for me and my local installation of Tclhttpd. * Currently there is no security implemented! Everyone who can access your webserver can access your metakits!! * Only one user at a time should access Metakit@Web because the configuration-values are globally set (no sessions). Maybe someone wants to make this script session-aware? * Be sure that no spider follows the links on the page (the delete-operation is available as a link, so be careful). Next steps/Missing features: * subviews are not handled correctly * make it session-aware You've been warned. But now ... have fun: # ------------------------------------------------------------------------------ # Metakit@Web -- Webadministration-interface for Metakit with Tclhttpd # * Simply copy this file to /custom # * Adapt "databaseDir" # * Restart Tclhttpd and go to "http://your.domain/mkweb" # * Because of security-reasons you should be sure that nobody has # access to your Tclhttpd :-) # # 10/22/2003: Revision 0.2 by Stefan Vogel (stefan at vogel-nest dot de) # ------------------------------------------------------------------------------ package require Mk4tcl package require html # ------------------------------------------------------- # | db | main # | execute/listdb | execute/listmain (db/view) # | ------------------------ # | views | # | execute/listviews (db) | # |_______________________ # | config # | execute/listconfig # |_______________________ namespace eval metakit::web { variable aConfig array set aConfig { prefix mkweb databaseDir /metakits maxRows {50} maxChars {250} rowColsTexts {10 20} displayBinary 0 title "Metakit@Web" } ::html::init [list input.size 15] ::html::headTag [subst {link rel="stylesheet" href="/$aConfig(prefix)/mkweb.css"}] ::html::headTag [subst {script language="JavaScript" src="/$aConfig(prefix)/mkweb.js" type="text/javascript"> <h2>Frame Alert</h2> <p>This document is designed to be viewed using the frames feature. If you see this message, you are using a non-frame-capable web client.</p> }] 0] } proc metakit::web::execute/listdb {args} { array set _args $args variable aConfig set result "" set redirect "" if {![info exists _args(mode)]} { set _args(mode) select } switch -- [string tolower $_args(mode)] { new { set result [subst {[::html::h1 "Create metakit"]
[::html::textInputRow "Filename:" filename]
}] } create { if {![info exists _args(filename)] || $_args(filename) == ""} { error "Invalid filename in listdb - create" } OpenDb $_args(filename) CloseDb set redirect listdb } delete { if {![info exists _args(db)] || ![file exists [file join $aConfig(databaseDir) $_args(db)]]} { error "Invalid filename in listdb - delete" } file delete -force [file join $aConfig(databaseDir) $_args(db)] set redirect listdb } select - "" { append dbList [subst {FilenameSizeNew}] if {![file isdirectory $aConfig(databaseDir)]} { set result "

Error

Database-directory: '$aConfig(databaseDir)' does not exist. Please create it or modify mkweb.tcl metakit::web::execute::aConfig(databaseDir)." } else { foreach db [set lDb [glob -nocomplain -- $aConfig(databaseDir)/*]] { set _db [file tail $db] append dbList [subst {$_db[format "%.2f kB" [expr [file size $db]/1024.0]] Delete}] } set result [subst {[::html::h1 "Files"] $dbList
}] } } default { error "Unknown mode '$_args(mode)' for listdb" } } ReturnResult $result $redirect } proc metakit::web::execute/listviews {args} { variable aConfig array set _args $args if {![info exists _args(mode)]} { set _args(mode) select } if {[info exists _args(db)]} { set db $_args(db) } else { return [generateHtml ""] } set result "" set redirect "" # set result "$args
" OpenDb $db switch -- [string tolower $_args(mode)] { select { set viewList [subst {ViewColumnsDatatypeNew View}] foreach view [set lView [mk::file views db]] { append viewList [subst {$view Modify Delete}] foreach col [mk::view layout db.$view] { foreach {colName datatype} [split $col ":"] {break} append viewList [subst {$colName [expr {$datatype != "" ? $datatype : "S"}] }] } append viewList {} } mk::file close db append result [subst {[::html::h1 "Views of metakit: '$db'"] $viewList
}] } newview { append result [subst {
Viewname:

Structure:

Structure: e.g. id:I name:S street:S salary:F
:S string
:I integer
:L long
:F float
:D double
:B binary
}] } create { mk::view layout db.$_args(viewname) $_args(structure) mk::file commit db set redirect "listviews?db=$db" } delete { mk::view delete db.$_args(view) mk::file commit db set redirect "listviews?db=$db" } modifyview { append result [subst {
[::html::textInputRow "Structure:
" structure [mk::view layout db.$_args(view)]]
Modifying a structure may delete the whole content!
Structure: e.g. id:I name:S street:S salary:F
:S string
:I integer
:L long
:F float
:D double
:B binary
}] } modify { mk::view layout db.$_args(view) $_args(structure) mk::file commit db set redirect "listviews?db=$db" } default { error "No such mode '$_args(mode)' in listviews" } } CloseDb ReturnResult $result $redirect } # needs db, view-parameter proc metakit::web::execute/listmain {args} { variable aConfig array set _args $args if {[info exists _args(db)]} { OpenDb [set db $_args(db)] } else { return [generateHtml ""] } if {![info exists _args(mode)]} { set _args(mode) browse } if {![info exists _args(query)]} { set query "" } else { set query $_args(query) } set lStdLink [list db=$db view=$_args(view)] set result "" set redirect "" switch -- [set mode [string tolower $_args(mode)]] { query - browse { # the following parameters are used to browse through content # index - startindex # count - number of rows to show # sort - either sort or rsort # column - sort - column set view [mk::view open db.$_args(view)] foreach col [mk::view layout db.$_args(view)] { if {[llength [set _col [split $col :]]] > 1} { set aDatatype([lindex $_col 0]) [lindex $_col 1] } else { set aDatatype([lindex $_col 0]) S } } set lAddLink {} # attention, e.g for wikit-database we need to escape some html-chars set cmd "mk::select db.$_args(view)" if {[info exists _args(sort)]} { append cmd " -$_args(sort) $_args(column)" eval lappend lAddLink [list sort=$_args(sort) column=$_args(column)] } if {[info exists _args(first)]} { append cmd " -first $_args(first)" lappend lAddLink "first=$_args(first)" } else { set _args(first) 0 } set cursor "" if {[info exists aConfig(maxRows)] && $aConfig(maxRows) != ""} { append cmd " -count $aConfig(maxRows)" set cursor [AddCursor $_args(first) $aConfig(maxRows) [$view size] "listmain?[join $lStdLink &]&[join $lAddLink &]"] } set sContent [subst {un/ mark allindex }] foreach col [mk::view layout db.$_args(view)] { set queryLink "listmain?[join $lStdLink &]&[join $lAddLink &]&column=[lindex [split $col :] 0]&sort=" append sContent [subst {$col  }] } append sContent [subst {New row}] append cmd " $query" set i 0 foreach r [eval $cmd] { append sContent [subst {$r}] foreach {key value} [mk::get db.$_args(view)!$r] { append sContent "[prepareData $value $aDatatype($key) download.bin?db=$db&view=$_args(view)&index=$r&column=$key] " } append sContent [subst {Delete\n}] } foreach {key value} $args { lappend queryParam "$key=$value" } set result [subst {[::html::h1 "Content of metakit: '$db' view: '$_args(view)'"] Total view-size: [$view size] rows
Query-Command: $cmd
$cursor
$sContent

E.g. -glob name stefan*
prop valueNumeric or case-insensitive match
-min prop valueProperty must be greater or equal to value (case is ignored)
-max propvalueProperty must be less or equal to value (case is ignored)
-exact prop valueExact case-sensitive string match
-glob prop patternMatch "glob-style" expression wildcard
-globnc prop patternMatch "glob-style" expression, ignoring case
-regexp prop patternMatch specified regular expression
-keyword prop wordMatch word as free text or partial prefix
$cursor}] } newrow - edit { if {$mode == "newrow"} { set msg [subst {

New row in view '$_args(view)' in metakit '$db'

}] set index "" } else { # mode == edit set msg [subst {

Edit row $_args(index) in view '$_args(view)' in metakit '$db'

}] set index "" } set layout [mk::view layout db.$_args(view)] append result [subst { $msg}] foreach col $layout { if {[llength [set _col [split $col :]]] > 1} { set aDatatype([lindex $_col 0]) [lindex $_col 1] } else { set aDatatype([lindex $_col 0]) S } append result "" } append result \n$index if {$mode == "newrow"} { foreach col $layout { set col [lindex [split $col :] 0] append result [subst {}] } append result {
index$_args(index)
$col
[prepareInput $col $aDatatype($col)]
} } else { # mode == edit set lRow [mk::get db.$_args(view)!$_args(index)] foreach {col value} $lRow { append result [subst {[prepareInput $col $aDatatype($col) $value]}] } append result [subst {
}] } } {delete marked} - delete { # action from delete if {[info exists _args(index)]} { # delete a row (given by index from view) mk::row delete db.$_args(view)!$_args(index) } else { # marked multiple elements to delete (via checkboxes) set lResult {} foreach elem [array names _args delete_col_*] { set i [string range $elem 11 end] mk::row delete db.$_args(view)!$i } } set redirect "listmain?[join $lStdLink &]" } default { error "No such mode '$_args(mode)' in listmain" } } CloseDb ReturnResult $result $redirect } # issued from edit/safe or new row/create proc metakit::web::execute/saferow {args} { set result "" set redirect "" foreach {n v} [ncgi::nvlist] { set _args($n) [lindex $v 1] } OpenDb [set db $_args(db)] set lStdLink [list db=$db view=$_args(view)] switch -- [string tolower $_args(mode)] { create { # action from newrow foreach var [array names _args colvalue_*] { lappend lNewRow [string range $var 9 end] [set _args($var)] } append result "args: $args lNewRow: $lNewRow ncgi::value colvalue_data: [ncgi::value colvalue_data]" set vw [mk::view open db.$_args(view)] eval mk::row append db.$_args(view) $lNewRow set redirect "listmain?[join $lStdLink &]" } safe { # action from clicking "safe" in edit-form foreach var [array names _args colvalue_*] { set index [string range $var 9 end] append result "index
info exists _args(bool_colvalue_$index): [info exists _args(bool_colvalue_$index)]
" if {[info exists _args(bool_colvalue_$index)]} { append result "_args(bool_colvalue_$index): $_args(bool_colvalue_$index)
" } if {![info exists _args(bool_colvalue_$index)] || $_args(bool_colvalue_$index) != 1} { lappend lRow $index [set _args($var)] } } append result "lRow: $lRow" eval mk::set db.$_args(view)!$_args(index) $lRow set redirect "listmain?[join $lStdLink &]" } } CloseDb ReturnResult $result $redirect } proc metakit::web::execute/download.bin {args} { array set _args $args set ::metakit::web::execute/download.bin application/octet-stream OpenDb $_args(db) set result [mk::get db.$_args(view)!$_args(index) $_args(column)] CloseDb return $result } proc metakit::web::execute/listconfig {args} { variable aConfig array set _args $args set result "" set redirect "" if {[info exists _args(mode)] && $_args(mode) == "safe"} { # set retrieved values into config-array and go back foreach var {maxRows maxChars displayBinaries} { if {[info exists _args($var)]} { set aConfig($var) $_args($var) } else { unset -nocomplain aConfig($var) } } set aConfig(rowColsTexts) [list $_args(rowsText) $_args(colsText)] set redirect listconfig } else { # browse-mode set result [subst {
Configuration:
Rows/Cols for edit-textareas: / Display Binaries?
Display max. rows of view/characters of view-content: /   Help
}] } ReturnResult $result $redirect } ################################################################################ # Help proc metakit::web::execute/help {} { variable aConfig set result [subst {

Welcome to Metakit@Web

Just to get myself familar with metakit I tried to locate a good "viewer". But unfortunately all those metakit-viewers around either don't work or they don't do what I want.

That was the time I decided to write a little web-frontend (nowadays it's popular to have a web-frontend for almost anything, e.g. MySql). But because the web-frontend should be as easy as possible I chosed Tcl and the Tcl-Webserver (Tclhttpd) for this purpose.

And what should I say? Tclhttpd is the coolest webserver I've ever seen :-)

Features of Metakit@Web are:
  • easy installation in Tclhttpd (only one file)
  • easy editing, manipulating metakit-files (useful for rapid-prototyping).
Missing features are (resp. pay ATTENTION!!):
  • no authorization, everybody can modify or delete the metakit-files (this tool is only meant to be used by you on your local-machine)
  • no support for subviews
  • only useful for one user (no sessions)
  • Only Javascript-confirmation for deleting. Site should not be spidered, because if a spider follows a "delete"-link ... boom!!

Installation/Configuration

As you seem to be able to read this, you have successfully installed Metakit@Web.
Just to mention a few things:

You should create a directory where you place your metakit-files which should be editable from Metakit@Web. This directory has to be configured in the mkweb.tcl-file (Variable: aConfig(databaseDir) currently set to: $aConfig(databaseDir)).

Furthermore you can set the aConfig(prefix) (currently: $aConfig(prefix)). This variable sets the url-prefix under which you can access Metakit@Web, e.g. now it is:
http://$::env(HTTP_HOST)/$aConfig(prefix).

You can also configure some values via the configuration-frame below. Simply enter a different value and click "Go". Afterwards you should "reload" the pages in which you expect the changes.

Configuration-options (see below in configuration-frame) are:
Rows/Cols for edit-textareasThe size of the textarea when editing (:S - string)-values. If you have stored large strings it may make sense to enlarge the size.
Max. rows of viewHow many rows of a view should be displayed? If you set this to "" all rows will be shown (possibly only a good idea for small views).
Max. characters of view-contentWhen viewing the content of a view, all string-values will be truncated to the size given here. If you want to see the full length, set this value to "".

General remarks

The frames of Metakit@Web looks like this
DB-frame
This lists all the files from aConfig(databaseDir). Be careful that you are using metakits here. Deleting a database will delete the file physically!
Main-frame
Not "mainframe" :-) This frame displays the content of the views (and the help).
View-frame
If you select a file from the DB-frame, the views and the structure of the views will be shown here. You can modify the views.
configuration-frame
Common configuration-options to be changed globally.

In general the navigation (back, refresh, ..) is left to you and your browser, so you won't find any "back"-buttons in Metakit@Web.
Normally the names of database-files, view-names, row-indices link to the content of the item.
The last column of the table (database, views or rows) contains the "New ..."-link for each row an editing link.

Usually you go to the "File/database"-frame, select a file, go to the "View"-frame and select a view (the content is displayed in the main-area).

Thanks

To Jean-Claude Wippler for his cool Metakit and the whole Tcl-community for their never-ending patience and helpfulness comments. You can find this script on the Tcl'ers Wiki or go to my Homepage.

This script is free software.
 But don't blame me if something get's wrong.
 

Suggestions or bug-fixes are always welcome, simply drop me an email or edit the script on the Wiki.
Stefan Vogel -- stefan at vogel-nest dot de

}] return [generateHtml $result] } ################################################################################ # CSS and JS proc metakit::web::execute/mkweb.css {} { set ::metakit::web::execute/mkweb.css text/css set result {body { font-family:Arial,sans-serif; font-size:10pt; } h1 { font-size:14pt; color:#000080; } h2 { font-size:12pt; color:#000080; } td { font-family:Arial,sans-serif; font-size:10pt; vertical-align:top; } th { font-family:Arial,sans-serif; font-size:10pt; vertical-align:top; } .dml td,.cnt td { border:1px solid black; } .dml th { border:1px solid black; } .cnt th { border:1px solid black; background-color:#a4a4ff; color:#ffffff; } .scnd { background-color:#d9d9ff; } .button { background-color:#a4a4ff; color:#ffffff; border:1px solid black; font-weight:bold; } } return $result } proc metakit::web::execute/mkweb.js {} { set ::metakit::web::execute/mkweb.js application/x-javascript set result { function linkClick(msg) { return confirm(msg); } function setChecks(check) { var form = document.browse; for (var c = 0; c < form.elements.length; c++) if (form.elements[c].type == 'checkbox') form.elements[c].checked = check; } } return $result } ################################################################################# # Helperfunctions # TODO add different types proc metakit::web::prepareData {text datatype link} { variable aConfig set result $text # datatype: $datatype -- $text" if {[info exists aConfig(displayBinaries)] && $aConfig(displayBinaries) == 1} { # treat binaries as string set datatype S } switch -- [string tolower $datatype] { b { # only set to "--" if there is anything in this binary column if {[string length $text]} { set result [subst {not displayed
Download}] } } default { if {[info exists aConfig(maxChars)] && $aConfig(maxChars) != "" && [string length $text] > $aConfig(maxChars)} { set result [string range $text 0 $aConfig(maxChars)]... } set result [string map {< <} $result] } } # avoid html-confusion return $result } proc metakit::web::prepareInput {col datatype {value ""}} { variable aConfig if {[info exists aConfig(displayBinaries)] && $aConfig(displayBinaries) == 1} { # treat binaries as string set datatype S } switch -- [string tolower $datatype] { i - l - f - d { set result [::html::textInput colvalue_$col $value] } b { set result [subst {}] if {$value != ""} { append result [subst {
Leave value as is.

There are 3 alternatives here:

  • You want to change the binary value of this row: Upload a new file and "deselect" the checkbox.
  • Don't touch the binary value of this row: Select checkbox (default).
  • Delete the value of this row: "Deselect" the checkbox and don't upload a file.
The checkbox has priority, so if the checkbox is selected the binary value of this row won't be changed.

}] } } default { # S set result [::html::textarea colvalue_$col "rows=\"[lindex $aConfig(rowColsTexts) 0]\" cols\"[lindex $aConfig(rowColsTexts) 1]\"" $value] } } return $result } proc metakit::web::OpenDb {filename} { variable aConfig catch {mk::file close db} mk::file open db [file join $aConfig(databaseDir) $filename] } proc metakit::web::CloseDb {} { catch {mk::file close db} } proc metakit::web::ReturnResult {result {redirect ""}} { variable aConfig if {$redirect != ""} { Httpd_RedirectSelf /$aConfig(prefix)/$redirect $::env(sock) } else { return [generateHtml $result] } } proc metakit::web::AddCursor {pos count maxCount link} { set result "" for {set i -1} {[expr $i+$count+1]<$maxCount} {incr i $count} { set str "[expr $i+1] - [set ipc [expr $i + $count]]" if {$pos>=$ipc || $pos<$i} { set str [subst {$str}] } append result " $str" } return $result } proc metakit::web::generateHtml {body {withBodyTag 1}} { variable aConfig if {$withBodyTag} {set body $body} return [::html::head $aConfig(title)]$body\n } ---- ''Wow! -[jcw]'' [http://mini.net/pub/mkweb.png] ---- [rmax] - This looks very nice indeed! But I keep getting the following error whenever I invoke a link, that changes anything (creating/deleting databases, modifying views, adding rows, etc.): can't read "::env(sock)": no such variable while executing "Httpd_RedirectSelf /$aConfig(prefix)/$redirect $::env(sock)" (procedure "ReturnResult" line 4) invoked from within "ReturnResult $result $redirect" (procedure "metakit::web::execute/listdb" line 51) invoked from within "metakit::web::execute/listdb mode delete db test" invoked from within "catch $cmd result" invoked from within "DirectRespond $sock $code $result $type" (procedure "DirectDomain" line 29) invoked from within "DirectDomain metakit::web::execute sock8 /listdb" ("eval" body line 1) invoked from within "eval $Url(command,$prefix) [list $sock $suffix]"