Over the last days, the script has become more complex and unreadable but also more powerfull.... It is unlikely yet that someone ever will understand the code. But for now, it works! '''Here are three examples of what it's all about:''' Showing a table (note the tooltip): [http://www.8ung.at/matthias_hoffmann/prog/csvedit1.jpg] Editing a row of that table, optionally with column-dependent select-boxes to offer predefined values: [http://www.8ung.at/matthias_hoffmann/prog/csvedit2.jpg] Inserting a date via calendar-popup: [http://www.8ung.at/matthias_hoffmann/prog/csvedit3.jpg] With this [CGI] script your are able to edit predefined standard [csv] files over the web, using your web browser. It is not possible to add or delete columns (what means: the ''file structure'' is fix), but to add new rows, delete rows, edit cells and sort in ascending or descending order. As an addition, '''two cool javascripts''' are used to choose date from a popup calendar (see the noted web references for more details) and to implement ''tooltips''. To declare a field as a date field, simply add '''@date''' as the last word in it and the see what happens after klicking '''edit''' again... To generate a '''Link''' in a cell, use '''''LinkName url''''' '''@link'''-notation. To show it '''bold''', add '''@bold''' (other gimmicks not to mention...). To install the script and make it work: * Download the following script and copy it to some location below your '''cgi-bin''' * Optionally download the DatePopup-Javascript-Routines and install them under '''''httproot''''''''/jscalendar-1.0''' (or to another location, but then you must alter the source ;-) * (again optionally) download and copy the '''wz_tooltip.js''' to your webserver-root (or elsewhere, but beware) * create a standard [csv] file (that is, fields surrounded with Doublequotes and separated by commas), e.g. with [Excel] or - much better ;-) - with [OpenOffice] - somewhere below the /cgi-bin, e.g. /cgi-bin/data * point your browser to '''.../cgi-bin/csvedit.tcl?file=[[path/]]yourcsvfile''' (file relative to script path!) (optional args may follow, like '''sort'''...) * of course, your webserver must be able to serve [CGI] scripts coded in [tcl]... (what about [tclhttpd] ;-?) * optionally, you can create a '''''csvname''''''''.rc'''-file which holds special configuration options per csv-file. Such a file is shown near the bottom of this document The scripts create a ''file''.bak before each write access and stores ''deleted'' and ''overwritten'' records to a "transaction"-log named after the csv file. I implemented a simple '''todo-list''' with this script... (that's the original reason I started to wrote it). ---- #=============================================================================== # CSVEdit 0.4 (c) M.Hoffmann 2006 # 01.04.2006 # ToDo/Bugs: # - Bei Refresh: ACHTUNG DIE SEITE ENTHÄLT POSTDATEN ??? # - evtl. ToolTips nur dann und überall dort einblenden, wenn Text gekürzt wurde! # - CSS für EditElemente (Input, Button) werden vom IE ignoriert... # - evtl. Bubble-Help für Vollanzeige benutzen, oder Klick auf Details ganz # rechts zeigt VollText unten etc. # - Statt input type=text immer textarea verwenden; Anzeige wahlweise EXPANDen # = nowrap entfernen o.ä.; NOWRAP konfigurierbar machen + WIDTH # - Mehrere Sortierfelder (Datum, PRIO) - aber nicht per Interface (geht nicht # von Haus aus mit LSORT, daher komplex) # - Bei EDIT POSITIONIEREN; IST DAS MÖGLICH?! # geht nicht in CGI-URLs!? evtl. # über JS:focus (aber: welches Ziel?) # - NOSCRIPT-Auswertung! # - LfdNr als eigene Spalte mitführen/nur anzeigen? # - Wahlweise später: Insert_Before, _After, MoveUp/Dn etc. - ein ganzes Muster # an Befehlen ist in der rechten Spalte denkbar! (aber nur mit Sequenz-Vor- # haltung) # - Reset()-Button? # - MEHRERE Links mit @link handeln # - Zeilen mit @date-Einträgen in der Vergangenheit -> überfällig (markieren)? # - ALLE Farben konfigurierbar # - Auch deforder über .rc vorgebbar (geht wg. Parsingreihenfolge nicht...) # - Alternatives Editieren im 'FullScreen/Dialog'-Modus # - Diese Idee für einen generischen INIEditor nutzen! # ------------------------------------------------------------------------------ # - Löschen MUSS indirekt erfolgen, damit POS gesetzt ist! # - Wenn .log neu, auch Header schreiben. Dann ist sie auch normal einsehbar # (Link auf 'Done', dort dann evtl. 'UnDone'; oder unterscheiden zwischen # 'del' und 'done') #=============================================================================== package require ncgi; # tcllib package require html; # tcllib package require csv; # tcllib package require lock; # http://wiki.tcl.tk/15173 package require readprof; # http://wiki.tcl.tk/12647 #------------------------------------------------------------------------------- # Einträge für proc addToHead {} { puts { } # Pfad noch variabel gestalten, nur wenn existent, einbinden; bei noscript überspringen! # muss eigentlich nur im EDITMODE eingebunden werden!! Siehe http://dynarch.com/mishoo/ puts { } } # Baut Argumentliste für Links auf: link?var1=value1&var2=value2... proc makeLink {title args} { lappend args file $::file sort $::sort order $::order back $::back readonly $::readonly mtime $::mtime foreach {p v} $args {lappend ret $p=[ncgi::encode $v]} return "$title" } # dito, aber ohne # später dies beides Verschachteln, um doppelten Code zu sparen! proc makeLink1 args { lappend args file $::file sort $::sort order $::order back $::back readonly $::readonly mtime $::mtime foreach {p v} $args {lappend ret $p=[ncgi::encode $v]} return [ncgi::urlStub]?[join $ret &] } # Baut Argumentliste für Links auf: link?var1=value1&var2=value2..., aber OHNE ORDER UND SORT proc makeLink2 {title args} { lappend args file $::file back $::back readonly $::readonly mtime $::mtime foreach {p v} $args {lappend ret $p=[ncgi::encode $v]} return "$title" } # Setzt globale Variablen aus gleichnamigen CGI-Vars, falls fehlend mit Vorgaben # es fehlt: Range-Check! proc readVars args { foreach {varname default} $args { set temp [ncgi::value $varname] set ::$varname [expr {[string length $temp] ? $temp : $default}] } } # Hidden-Inputfelder erzeugen (muss $value ESCAPEd werden???) proc makeHidden args { foreach {varname value} $args { puts "" } } # (einige) HTML-Sonderzeichen umsetzen, siehe auch http://wiki.tcl.tk/13008 proc quote in { return [string map \ {ä ä Ä Ä ö ö Ö Ö ü ü Ü Ü ß ß} \ [html::quoteFormValue $in]] } # Special-quoting for tooltips (see http://www.walterzorn.de) proc quotett in { if {[lindex $in end] == "@bold"} { set bPos [string last " @bold" $in]; incr bPos -1 set in [string range $in 0 $bPos] } return [string map \ {ä ä Ä Ä ö ö Ö Ö ü ü Ü Ü ß ß \\ \\\\ ' \\' & "& amp;" < "& lt;" > "& gt;"} \ [html::quoteFormValue $in]] } # "überlange" Anzeigen b.a.W. abkürzen, später optional expandable! I.d.Z.: kann CSV \n speichern? proc shortenCell {ix cellIn} { if {$::debug == 1} { puts "shortenCell " } set mW $::opts(maxDisplayWidth_$ix) if {$::debug == 1} { puts "mw := $mW | actLen := [string length $cellIn]
" } if {[string length $cellIn] > $mW} { incr mW -4; # Achtung: bedingt mindestwert > 4! return "[string range $cellIn 0 $mW]..." } else { return $cellIn } } # ggF. Spezialcodes in Zellen umsetzen, 1. Versuch proc formatCell {ix cellIn} { if {[lindex $cellIn end] == {@link}} { # name ziel @link; später erweitern: mehrere Name-Link-Paare return "[quote [shortenCell $ix [lindex $cellIn 0]]]" } elseif {[lindex $cellIn end] == {@date}} { # ACHTUNG: dieser Code ist abgestimmt auf das 'ifFormat'!!! foreach {Y m d} [split [lindex $cellIn 0] .] {} set temp "$d.$m.$Y"; if {[string range $temp 0 1] == ".."} {return [lindex $cellIn 0]} else {return $temp} } elseif {[lindex $cellIn end] == "@bold"} { set bPos [string last " @bold" $cellIn]; incr bPos -1 return "[quote [shortenCell $ix [string range $cellIn 0 $bPos]]]" } else { return [quote [shortenCell $ix $cellIn]] } } # Javascriptcode für Datumsauswahlbutton erzeugen/stacken, siehe http://dynarch.com/mishoo/ proc pushJSDate id { # durch eigene onSelect-Funktion könnte Feld zeitgleich aktualisiert werden! append ::jsCode " \n" } # Farbe zeilenweise umschalten, Test proc toggleColor {} { global lineColor if {$lineColor == "#fffacd"} { set lineColor "white" } else { set lineColor "#fffacd" } return $lineColor } # Some important security feature: don't allow editing arbitrary files! # prevent from leaving the http-root! Paths are always relative to script's # [pwd], but can contain dir(s). Don't note error if spec is absolute. proc relFile file { if {[string range $file 1 1] == ":"} {set file [string replace $file 0 1]}; # MS-Win set file [string trimleft $file {./\\}]; # remove problematic chars return $file } # primitive AbortHandler (don't care about open html-tags...) proc abort {args} { puts "

\n \n \n
Fehler:
[join $args]
(Programm-Abbruch)\n

" exit 1; } # transactionLog (save overwritten or deleted records and opcode in csv-format) proc tranLog {file op hdr rec} { if {[catch { set f [open [file rootname $file].log a] if {[tell $f] == 0} { puts $f [csv::join [lappend hdr op Zeit]] } puts $f [csv::join [lappend rec $op [clock format [clock seconds] -format {%d.%m.%Y %H:%M:%S}]]] close $f } rc]} { puts "Fehler beim Schreiben des Transaktions-Logs: $rc" puts "Die Operation $op wird verworfen!" ncgi::setValue cmd ""; set ::cmd ""; # ausnahmsweise hier... ncgi::setValue pos ""; set ::pos ""; # GLOBALE Vars setzen! return 0 } else { return 1 } } #------------------------------------------------------------------------------- set lineColor "#fffacd" ncgi::reset ncgi::parse ncgi::header # pos zuvor 0 readVars file data/demo.dat sort -1 cmd "" pos "" order increasing back "" debug 0 readonly 0 mtime 0 set file [relFile $file] if {[string length $back] == 0} { set uri {}; catch {set uri $::env(REQUEST_URI)} set ref {}; catch {set ref $::env(HTTP_REFERER)} if {$uri != $ref} { set back $ref } } puts {CSVEdit 0.4} addToHead puts {} puts "Tabelle: $file " if {[string length $cmd]} {puts "Modus: $cmd"} puts {

} puts [makeLink Refresh] if {[string length $back]} {puts " Zurück"} puts {
} puts {

} set jsCode {} set hdr {} set rows {} if {[catch { set f [open $file r] gets $f hdr set hdr [csv::split $hdr] set cols [llength $hdr] while {[gets $f rec] > -1} { lappend rows [csv::split $rec] } close $f } rc]} then { abort $rc } # Profile-Handling: set prf [file rootname $file].rc set defopts {} for {set i 0} {$i < [llength $hdr]} {incr i} { lappend defopts default_$i {} lappend defopts select_$i {} lappend defopts maxDisplayWidth_$i 12; # mindestens erforderlich für Datum! } lappend defopts defsort $sort readonly $readonly array set opts [readprof::readprof1 $prf $defopts]; unset defopts if {$sort == -1} { set sort $opts(defsort) } set rows [lsort -dictionary -$order -index $sort $rows]; # vor Edit/Repl, damit 'pos' stimmt! for {set i 0} {$i < [llength $hdr]} {incr i} { if {$opts(default_$i) == "@date"} { set opts(default_$i) [clock format [clock seconds] -format {%Y.%m.%d @date}]; # Achtung: Format muss passen! } if {$opts(maxDisplayWidth_$i) < 10} { set opts(maxDisplayWidth_$i) 10 } } if {$debug == 1} { foreach x [array names opts] {puts "$x := $opts($x)
"}; # DEBUG <<<<<<<<<< } set saveFile 0 if {$readonly == 0} { if {($cmd == "save" || $cmd == "add" || $cmd == "del!" || $cmd == "edit" || $cmd == "reqdel") && $mtime != [file mtime $file]} { puts "Operation abgebrochen, Datei wurde zwischenzeitlich geändert!" puts "Bitte die Operation wiederholen!" ncgi::setValue cmd ""; set cmd "" ncgi::setValue pos ""; set pos "" } elseif {$cmd == "save"} { # wird gerufen über SUBMIT-Button nach Feld-Änderungen for {set i 0} {$i < $cols} {incr i} {lappend newRow [ncgi::value f$i]} if {[tranLog $file $cmd $hdr [lindex $rows $pos]]} { lset rows $pos $newRow set rows [lsort -dictionary -$order -index $sort $rows] set saveFile 1 } } elseif {$cmd == "add"} { for {set i 0} {$i < [llength $hdr]} {incr i} { lappend newRow [join $opts(default_$i)] } lappend rows $newRow set rows [lsort -dictionary -$order -index $sort $rows] set saveFile 1 } elseif {$cmd == "del!"} { if {[tranLog $file $cmd $hdr [lindex $rows $pos]]} { set rows [lreplace $rows $pos $pos] set saveFile 1 } } # Fehler hier noch auswerten! if {$saveFile} { ######## schlecht: CSV wird SORTIERT abgespeichert, ist das ein Problem? ## ggF. Satznummer mitführen, und immer vor Speichern nach Satz# sortieren if {[catch {lock::withLock { file copy -force $file $file.bak set f [open $file w] puts $f [csv::join $hdr] foreach row $rows { puts $f [csv::join [string map {\n \\n} $row]] } close $f set mtime [file mtime $file] } 5000 [file join $::env(temp) _gentable.lock]} rc]} { puts "Timeout beim Schreibzugriff: $rc" } ncgi::setValue cmd ""; # Kommando zurücksetzen!! nützt nichts für Browser URL, Back!!! set cmd "" ncgi::setValue pos ""; # Kommando zurücksetzen!! nützt nichts für Browser URL, Back!!! set pos "" # ncgi::redirect [makeLink1]; # URL-Zeile um Parameter bereinigen!!! Klappt nicht! } } set mtime [file mtime $file] puts "

} puts {} set i 0 foreach h $hdr { if {$i == $sort} {puts {" incr i } if {$readonly == 0} { puts {} } else { puts {} } puts {} set rowIx 0 set focus "" foreach row $rows { puts "" set cellIx 0 if {$cmd == "edit" && $pos == $rowIx && $readonly == 0} { foreach cell $row { puts " if {$focus == ""} { append ::jsCode " \n" set focus done } incr cellIx } puts "" } elseif {$cmd == "reqdel" && $pos == $rowIx && $readonly == 0} { foreach cell $row { if {![string length $cell]} { puts {} ; # dafür sorgen, daß der Rahmen bei leeren Felder erscheint } else { puts "" } incr cellIx } puts "" } else { foreach cell $row { if {![string length $cell]} { puts {} ; # dafür sorgen, daß der Rahmen bei leeren Felder erscheint } else { if {$cellIx == 0} { # leider gelingt hier aus unbekannten Gründen noch nicht die indirekte Einbindung über # eine Funktion! puts "" incr cellIx } if {$readonly == 0} { puts "" } } puts "" incr rowIx } makeHidden sort $sort file $file pos $pos order $order back $back readonly $readonly mtime $mtime puts {
}} else {puts {}} # IE kann die dicken Pfeile ⇓ und ⇑ nicht! puts "[makeLink2 &darr\; sort $i order increasing] $h [makeLink2 &uarr\; sort $i order decreasing]
" if {[lindex $cell end] == "@date"} { puts "" pushJSDate f$cellIx } if {[llength $opts(select_$cellIx)]} { puts "} } puts [makeLink cancel]  [formatCell $cellIx $cell][makeLink cancel]  " } else { puts {} } } puts "[formatCell $cellIx $cell][makeLink edit cmd edit pos $rowIx] [makeLink del cmd reqdel pos $rowIx]
} puts {
} puts "

Letzte Dateiänderung: [clock format $mtime -format {%d.%m.%Y %H:%M:%S}]," puts " [llength $rows] Zeile(n)" puts {


} puts [makeLink Refresh] if {[string length $back]} { puts " Zurück" } puts {
} if {[string length $back]} { } puts $jsCode puts { } puts {} exit 0 ---- '''Example profile (.rc) file''', belonging to the above [csv] file * The format of such a profile is explained here (I hope): http://wiki.tcl.tk/12647 * Parameters ending with '''_''''''''n''''' belong to the whole column '''''n''''' (leftmost column is '''0''') * the '''default_''''''''n''''' values control what appears in the fields of a newly added record; '''@date''' is replaced by current date (the format is fixed for now) * '''select_''''''''n''''' displays a select-Box with the given values when editing this field (Javascript required to put a list value in the edit field) * '''readonly 1''' prevents from altering (display only-mode) * '''maxDisplayWidth_''''''''n''''' cuts long texts afer '''''n''''' characters (sure, only on screen...) * the following profile controls what is displayed in the three pictures at the top of this page defsort 6 default_2 @date default_4 HMK default_5 3 default_6 offen @date default_8 neu select_1 FEHLER Klärung Betrieb NetInstall Kommunizieren Testen Projekt Doku Idee Entwicklung Kunde Orga Meeting Persönlich select_4 DAK HMK HEK select_5 1 2 3 select_8 neu inArbeit wartet ausgesetzt QS Abnahme AV-Transfer Produktion maxDisplayWidth_0 55 ---- '''Bug's:''' * The table does not print very well with [Mozilla], the reason is unknown; perhaps I forgot to generate some s... * M$-IE 6 does not (yet) respect certain [CSS] styles for buttons and entry fields (at least the 5 or so Windows systems I've tested on...) * Sometimes klicking the browser's ''refresh''-buttons leads to a warning regarding ''Post-data''.... There must be something with my chaotical parameter-handling (due to the mix and match of post and get-operations) ---- '''ToDo's and Ideas:''' * '''code consolidation''' (rewrite everything based on the given functionality) (puuuuhh.......!), e.g.: make use of [html]::-functions, comment everything, structure the code, removing duplicate code sequences etc. etc... * reduce excessive use of global variables, modularize the code (but keeping the ideas behind the chaos...) * Implement alternate input dialogs and tableformats (big input fields, wrapping etc.) * Show a tooltip for every field which is displayed truncated, not only for column 0 as implemented now ---- '''History:''' * 31.03.2006: Added code to handle simultaneous access from multiple users * 31.03.2006: Mapping '''\n''' to '''\\n''' before writing to avoid destroying the file structure * 31.03.2006: Added ''tooltips'' * 01.04.2006: Extended the logging to store records before beeing overwritten and to store timestamps and optypes in the log; the log gets a header and is itself a csv-file