1# stapi/display/display.tcl -- derived from diodisplay.tcl
2
3# Copyright 2006 Superconnect
4
5# diodisplay.tcl --
6
7# Copyright 2002-2004 The Apache Software Foundation
8
9# Licensed under the Apache License, Version 2.0 (the "License");
10# you may not use this file except in compliance with the License.
11# You may obtain a copy of the License at
12
13#	http://www.apache.org/licenses/LICENSE-2.0
14
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS,
17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20#
21# $Id$
22#
23
24package require Itcl
25package require form
26package require st_client
27package require stapi_extend
28
29#
30# Only load ::csv:: if it's actually wanted.
31#
32namespace eval ::stapi::display {
33	variable csv_loaded 0
34	proc load_csv {} {
35		variable csv_loaded
36		if $csv_loaded {
37			return
38		}
39		uplevel #0 package require csv
40	}
41}
42
43catch { ::itcl::delete class STDisplay }
44
45::itcl::class ::STDisplay {
46	constructor {args} {
47		eval configure $args
48		load_response
49
50		# allow 'ctable' instead of 'table' as a historical alias (interim)
51		if {![info exists table] && [info exists ctable]} {
52			set table $ctable
53			unset ctable
54		}
55
56		# If it's not already an extended table, treat it like a URI
57		if {[info exists table] && ![::stapi::extend::extended $table]} {
58			set uri $table
59			unset table
60		}
61
62		if {![info exists table]} {
63			if ![info exists uri] {
64				return -code error "No table or uri"
65			}
66
67			if ![info exists keyfields] {
68				if [info exists key] {
69					set keyfields [list $key]
70				}
71			}
72
73			if [info exists keyfields] {
74				set table [::stapi::connect $uri -keys $keyfields]
75			} else {
76				set table [::stapi::connect $uri]
77			}
78		}
79
80		if ![info exists keyfields] {
81			if [info exists key] {
82				set keyfields [list $key]
83			} else {
84				set mlist [$table methods]
85
86				if {[lsearch $mlist "key"] >= 0} {
87					set keyfields [list [$table key]]
88				} else {
89					set keyfields [$table keys]
90				}
91			}
92		}
93
94		if {![info exists key]} {
95			if {[llength $keyfields] == 1} {
96				set key [lindex $keyfields 0]
97			}
98		}
99
100		if {![info exists key]} {
101			set cause $table
102			if {[info exists uri]} { set cause $uri }
103			return -code error "No key or keyfields, and $cause doesn't know how to tell me"
104		}
105
106		if {[lempty $form]} {
107			set form [namespace which [::form #auto -defaults response]]
108		}
109
110		set document [env DOCUMENT_NAME]
111
112		if {[info exists response(num)] \
113				&& ![lempty $response(num)]} {
114			set pagesize $response(num)
115		}
116
117		read_css_file
118	}
119
120	destructor {
121		if {$cleanup} { do_cleanup }
122	}
123
124	method destroy {} {
125		::itcl::delete object $this
126	}
127
128	method debug {args} {
129		set show $debug
130
131		if {"[lindex $args 0]" == "-force"} {
132			set show 1
133			set args [lrange $args 1 end]
134		}
135		if {$show} {
136			eval ::stapi::debug $args
137		}
138	}
139
140	## Glue routines for the mismatch between DIO and remote ctables.
141	## The way DIO builds SQL that can be exposed outside DIO in assembling
142	## a request is used by DIODisplay. We have to make that more abstract
143
144	## New exposed configvars for STDisplay
145	public variable table
146	public variable ctable	;# Alias
147	public variable uri
148	public variable keyfields
149	public variable key
150	public variable debug 0
151
152	## Background configvars
153	private variable ct_selection
154
155	private variable case
156
157	#
158	# configvar - a convenient helper for creating methods that can
159	#  set and fetch one of the object's variables
160	#
161	method configvar {varName string {defval ""}} {
162		if {"$string" == "$defval"} { return [set $varName] }
163		configure -$varName $string
164	}
165
166	#
167	# is_function - return true if name is known to be a function
168	# such as Search List Add Edit Delete Details Main Save DoDelete Cancel
169	# etc.
170	#
171	method is_function {name} {
172		if {[lsearch $functions $name] >= 0} { return 1 }
173		if {[lsearch $allfunctions $name] >= 0} { return 1 }
174		return 0
175	}
176
177	#
178	# do_cleanup - clean up our field subobjects, DIO objects, forms, and the
179	# like.
180	#
181	method do_cleanup {} {
182		## Destroy all the fields created.
183		foreach field $allfields { catch { $field destroy } }
184
185		## Destroy the form object.
186		catch { $form destroy }
187	}
188
189	#
190	# handle_error - emit an error message
191	#
192	method handle_error {error args} {
193		puts "<B>An error has occurred processing your request</B>"
194		puts "<PRE>"
195		if {$debug > 1} {
196			puts ""
197			if [llength $args] {
198				puts [join $args "\n\n"]
199			} else {
200				puts "$::errorInfo"
201			}
202		}
203		puts "$error"
204		puts "</PRE>"
205	}
206
207	# escape string for display within HTML
208	protected method escape_cgi {str} {
209		if {[catch {escape_sgml_chars $str} result] == 0} {
210			# we were running under Apache Rivet and could use its existing command.
211			return $result
212		} else {
213			# This duplicates the Rivet escape_sgml_chars command:
214			return [string map { & &amp; < &lt; > &gt; " &quot; ' &#39; } $str]
215		}
216	}
217
218	# escape string for creation of a URL
219	protected method escape_url {str} {
220		if {[catch {escape_string $str} result] == 0} {
221			# we were running under Apache Rivet and could use its existing command.
222			return $result
223		} else {
224			# TODO: this is not very good; should also hex-encode many other things.
225			foreach \
226				src " &	       {\"}      <       > " \
227				dst { {\&amp;} {\&quot;} {\&lt;} {\&gt;} } {
228					regsub -all $src $str $dst str
229				}
230			return $str
231		}
232	}
233
234	# minimal escape string for protecting HTML only
235	# Avoids changing more than necessary to avoid stepping on legacy filters
236	protected method escape_html {str} {
237		return [string map { & &amp; < &lt; > &gt; } $str]
238	}
239
240	#
241	# read_css_file - parse and read in a CSS file so we can
242	#  recognize CSS info and emit it in appropriate places
243	#
244	method read_css_file {} {
245		if {"$css_file" != ""} {
246			if {![catch {open [virtual_filename $css_file]} fp]} {
247				set contents [read $fp]
248				close $fp
249			}
250		} else {
251			foreach file $css_files {
252				if {![catch {open [virtual_filename $file]} fp]} {
253					set css_file $file
254					set contents [read $fp]
255					close $fp
256				}
257			}
258		}
259		if ![info exists contents] {
260			return
261		}
262		if {[catch {array set tmpArray $contents}]} { return }
263		foreach class [array names tmpArray] {
264			set cssArray([string toupper $class]) $tmpArray($class)
265		}
266	}
267
268	#
269	# get_css_class - figure out which CSS class we want to use.
270	# If class exists, we use that.	 If not, we use default.
271	#
272	method get_css_class {tag default class} {
273
274		# if tag.class exists, use that
275		if {[info exists cssArray([string toupper $tag.$class])]} {
276			return $class
277		}
278
279		# if .class exists, use that
280		if {[info exists cssArray([string toupper .$class])]} {
281			return $class
282		}
283
284		# use the default
285		return $default
286	}
287
288	#
289	# parse_css_class - given a class and the name of an array, parse
290	# the named CSS class (read from the style sheet) and return it as
291	# key-value pairs in the named array.
292	#
293	method parse_css_class {class arrayName} {
294
295		# if we don't have an entry for the specified class, give up
296		if {![info exists cssArray($class)]} {
297			return
298		}
299
300		# split CSS entry on semicolons, for each one...
301		upvar 1 $arrayName array
302		foreach line [split $cssArray($class) \;] {
303
304			# trim leading and trailing spaces
305			set line [string trim $line]
306
307			# split the line on a colon into property and value
308			lassign [split $line :] property value
309
310			# map the property to space-trimmed lowercase and
311			# space-trim the value, then store in the passed array
312			set property [string trim [string tolower $property]]
313			set value [string trim $value]
314			set array($property) $value
315		}
316	}
317
318	#
319	# button_image_src - return the value of the image-src element in
320	# the specified class (from the CSS style sheet), or an empty
321	# string if there isn't one.
322	#
323	method button_image_src {class} {
324		set class [string toupper input.$class]
325		parse_css_class $class array
326		if {![info exists array(image-src)]} {
327			return
328		}
329		return $array(image-src)
330	}
331
332	# state - return a list of name-value pairs that represents the current
333	# state of the query, which can be used to properly populate links
334	# outside STDisplay.
335	method state {} {
336		set state {}
337		foreach var {mode query by how sort rev num page} {
338			if [info exists response($var)] {
339				lappend state $var $response($var)
340			}
341		}
342		return $state
343	}
344
345	# DName - convert a field name to a display name
346	protected method DName {fld} {
347		if {[info exists NameTextMap($fld)]} {
348			return $fld
349		}
350		if {[info exists FieldNameMap($fld)]} {
351			return [$FieldNameMap($fld) text]
352		}
353		if {"$fld" == "_key"} {
354			return "-key-"
355		}
356		return $fld
357	}
358
359	# FName - convert a display or column to a field name
360	protected method FName {fld {complain 1}} {
361		if {[info exists NameTextMap($fld)]} {
362			set fld $NameTextMap($fld)
363		}
364
365		if {[info exists FieldNameMap($fld)]} {
366			set fld $FieldNameMap($fld)
367		}
368
369		if {[lsearch $fields $fld] < 0} {
370			if {$complain} {
371				return -code error "No field name for $fld"
372			} else {
373				return ""
374			}
375		}
376		return $fld
377	}
378
379	# CName - convert a field or column to a canonical name
380	protected method CName {fld {complain 1}} {
381		if {[info exists NameTextMap($fld)]} {
382			return $NameTextMap($fld)
383		}
384
385		if {[info exists FieldNameMap($fld)]} {
386			return $fld
387		}
388
389		if {[lsearch $fields $fld] < 0} {
390			if {"$fld" == "-key-"} {
391				if {[llength $keyfields] == 1} {
392					return [lindex $keyfields 0]
393				} else {
394					return _key
395				}
396			}
397
398			if {$complain} {
399				return -code error "No field name for $fld"
400			}
401			return ""
402		}
403		return [$fld name]
404	}
405
406	method show {} {
407		if {[llength $fields] <= 0} {
408			foreach key $keyfields {
409				if {"$key" == "_key"} {
410					field $key -text "Key"
411				} else {
412					set text $key
413					regsub -all {_} $text { } text
414					field $key -text [string totitle $text]
415				}
416			}
417
418			foreach fld [$table fields] {
419				if {[lsearch $keyfields $fld] < 0} {
420					set text $fld
421					regsub -all {_} $text { } text
422					field $fld -text [string totitle $text]
423				}
424			}
425		}
426
427		if {[llength $fields] <= 0} {
428			return -code error "No fields defined for display."
429		}
430
431		# If readonly get rid of write functions, sanitize mode
432		if {$readonly} {
433			set skipfunctions $writefunctions
434			if [info exists response(mode)] {
435				if {[lsearch $writefunctions $response(mode)] >= 0} {
436					set response(mode) List
437				}
438			}
439		} else {
440			set skipfunctions {}
441		}
442
443		# If no details, get rid of Details
444		if {!$details} {
445			lappend skipfunctions Details
446		}
447
448		if {[llength $skipfunctions]} {
449			foreach list {functions rowfunctions} {
450				set new {}
451				foreach fun [set $list] {
452					if {[lsearch $skipfunctions $fun] < 0} {
453						lappend new $fun
454					}
455				}
456				set $list $new
457			}
458		}
459
460		# if there's a mode in the response array, use that, else leave mode
461		# as the default (List unless caller specified otherwise)
462		if {[info exists response(mode)]} {
463			set mode $response(mode)
464			if {[string match "*-*" $mode]} {
465				set mode List
466			} elseif {[string match {*[+ ]*} $mode]} {
467				set mode List
468				add_search_to_selection
469			}
470			set response(mode) $mode
471		}
472		puts "<!--Generated by $this show, mode=$mode-->"
473
474		# sanitize "by":
475		# If it's empty, remove it.
476		# If it's a label, change it to a field
477		if [info exists response(by)] {
478			if {"$response(by)" == ""} {
479				unset response(by)
480			} else {
481				set response(by) [DName $response(by)]
482			}
483		}
484
485		# if there was a request to generate a CSV file, generate it
486		if {[info exists response(ct_csv)]} {
487			gencsvfile $response(ct_csv)
488			if $csvredirect {
489				headers redirect $csvurl
490				destroy
491				return
492			}
493		}
494
495		# if there is a style sheet defined, emit HTML to reference it
496		if {![lempty $css_file]} {
497			puts "<LINK REL=\"stylesheet\" TYPE=\"text/css\" HREF=\"$css_file\">"
498		}
499
500		# put out the table header
501		puts {<TABLE WIDTH="100%" CLASS="DIO">}
502		puts {<TR CLASS="DIO">}
503		puts {<TD VALIGN="center" CLASS="DIO">}
504
505		# if mode isn't Main and persistentmain is set (the default),
506		# use Main
507		if {$mode != "Main" && $persistentmain} {
508			Main
509		}
510
511		if {![is_function $mode]} {
512			puts "<H2>Invalid function '$mode'</H2>"
513			puts "
514		<P>This may be due to an error in an external link
515		or in this web page. You may be able to use the back
516		button in your browser to return to the previous page
517		and try this query again. If you continue to get this
518		error, please contact the operator of this system.</P>"
519			puts "</TD>"
520			puts "</TR>"
521			puts "</TABLE>"
522			return
523		}
524
525		if {[catch [list $this $mode] error]} {
526			puts "</TD>"
527			puts "</TR>"
528			puts "</TABLE>"
529			if !$trap_errors {
530				if {$cleanup} { destroy }
531				error $error $::errorInfo
532			}
533			puts "<H2>Internal Error</H2>"
534			handle_error "$this $mode => $error"
535		}
536
537		puts "</TD>"
538		puts "</TR>"
539		puts "</TABLE>"
540
541		if {$cleanup} { destroy }
542	}
543
544	method showview {} {
545		puts {<TABLE CLASS="DIOView">}
546		set row 0
547		foreach field $fields {
548			$field showview [lindex {"" "Alt"} $row]
549			set row [expr {1 - $row}]
550		}
551		puts "</TABLE>"
552	}
553
554	protected method hide_hidden_vars {f} {
555		foreach var [array names hidden] {
556			$f hidden $var -value [escape_cgi $hidden($var)]
557		}
558	}
559
560	protected method hide_selection {f {op ""} {val ""}} {
561		if {"$op" == "+"} {
562			set all 1
563		} else {
564			set all 0
565		}
566		set selection [get_selection $all]
567
568		if {"$op" == "-"} {
569			set first [lsearch $selection $val]
570			if {$first >= 0} {
571				set selection [lreplace $selection $first $first]
572			}
573		}
574
575		$f hidden ct_sel -value [escape_cgi $selection]
576	}
577
578	protected method hide_cgi_vars {f args} {
579		# Special cases first
580		if [info exists response(mode)] {
581			set val $response(mode)
582
583			if [string match {*[+ -]*} $val] {
584				set val [lindex {List Search} [info exists response(query)]]
585			}
586			$f hidden mode -value [escape_cgi $val]
587		}
588
589		# Just copy the rest
590		foreach cgi_var {query by how sort rev num} {
591			if {[lsearch $args $cgi_var] < 0} {
592				if [info exists response($cgi_var)] {
593					$f hidden $cgi_var -value [escape_cgi $response($cgi_var)]
594				}
595			}
596		}
597	}
598
599	#
600	# showform - emit a form for inserting a new record
601	#
602	# response(by) will contain whatever was in the "where" field
603	# response(query) will contain whatever was in the "is" field
604	#
605	method showform {} {
606		get_field_values array
607
608		set save [button_image_src DIOFormSaveButton]
609		set cancel [button_image_src DIOFormCancelButton]
610
611		$form start -method post -name save_form
612		#$form hidden pizza -value pepperoni
613		hide_hidden_vars $form
614		hide_selection $form
615		$form hidden mode -value Save
616
617		if [info exists response(mode)] {
618			$form hidden DIODfromMode -value [escape_cgi $response(mode)]
619		}
620
621		$form hidden DIODkey -value [escape_cgi [makekey array]]
622		puts {<TABLE CLASS="DIOForm">}
623
624		# emit the fields for each field using the showform method
625		# of the field.	 if they've typed something into the
626		# search field and it matches one of the fields in the
627		# record (and it should), put that in as the default
628		foreach field $fields {
629			set name [$field name]
630
631			if [info exists alias($name)] {
632				continue
633			}
634
635			if {[info exists response(by)] && $response(by) == $name} {
636				if {![$field readonly] && $response(query) != ""} {
637					$field value $response(query)
638				}
639			}
640			$field showform
641		}
642		puts "</TABLE>"
643
644		puts {<TABLE CLASS="DIOFormSaveButton">}
645		puts {<TR CLASS="DIOFormSaveButton">}
646		puts {<TD CLASS="DIOFormSaveButton">}
647
648		if {![lempty $save]} {
649			$form image save -src $save -class DIOFormSaveButton
650		} else {
651			$form submit save.x -value "Save" -class DIOFormSaveButton
652		}
653		puts "</TD>"
654		puts {<TD CLASS="DIOFormSaveButton">}
655
656		if {![lempty $cancel]} {
657			$form image cancel -src $cancel -class DIOFormSaveButton
658		} else {
659			$form submit cancel.x -value "Cancel" -class DIOFormCancelButton
660		}
661		puts "</TD>"
662		puts "</TR>"
663		puts "</TABLE>"
664
665		$form end
666	}
667
668	method page_buttons {end {count 0}} {
669		if {$pagesize <= 0} { return }
670
671		if {![info exists response(page)]} { set response(page) 1 }
672
673		set pref DIO$end
674		if {!$count} {
675			set count [perform request]
676		}
677
678		set pages [expr {($count + $pagesize - 1) / $pagesize}]
679
680		if {$pages <= 1} {
681			return
682		}
683
684		set first [expr {$response(page) - 3}]
685		if {$first > $pages - 5} {
686			set first [expr {$pages - 5}]
687		}
688
689		if {$first > 1} {
690			lappend pagelist 1 1
691
692			if {$first > 10} {
693				lappend pagelist ".." 0
694				set mid [expr {$first / 2}]
695				if {$mid > 20 && $response(page) > $pages - 20} {
696					set quarter [expr {$mid / 2}]
697					lappend pagelist $quarter $quarter
698					lappend pagelist ".." 0
699				}
700
701				if {$first < $pages - 4} {
702					set first [expr {$response(page) - 1}]
703				}
704
705				lappend pagelist $mid $mid
706				if {$first - $mid > 10 && $response(page) > $pages - 20} {
707					lappend pagelist ".." 0
708					set quarter [expr {( $first + $mid ) / 2}]
709					lappend pagelist $quarter $quarter
710				}
711			}
712
713			if {$first > 3} {
714				lappend pagelist ".." 0
715			} elseif {$first > 2} {
716				lappend pagelist 2 2
717			}
718		} else {
719			set first 1
720		}
721
722		set last [expr {$response(page) + 3}]
723		if {$last < $pages - 10 && $last > 3} {
724			set last [expr {$response(page) + 1}]
725		}
726
727		if {$last < 5} {
728			set last 5
729		}
730
731		if {$last > $pages} {
732			set last $pages
733		}
734
735		for {set i $first} {$i <= $last} {incr i} {
736			lappend pagelist $i $i
737		}
738
739		if {$last < $pages} {
740			if {$last < $pages - 2} {
741				lappend pagelist ".." 0
742			} elseif {$last < $pages - 1} {
743				incr last
744				lappend pagelist $last $last
745			}
746
747			if {$last < $pages - 10} {
748				set mid [expr {( $pages + $last ) / 2}]
749				if {$last < $mid - 10 && $response(page) < 20} {
750					set quarter [expr {( $mid + $last ) / 2}]
751					lappend pagelist $quarter $quarter
752					lappend pagelist ".." 0
753				}
754
755				lappend pagelist $mid $mid
756				lappend pagelist ".." 0
757				if {$mid < $pages - 20 && $response(page) < 20} {
758					set quarter [expr {( $mid + $pages ) / 2}]
759					lappend pagelist $quarter $quarter
760					lappend pagelist ".." 0
761				}
762			}
763			lappend pagelist $pages $pages
764		}
765
766		foreach {n p} $pagelist {
767			if {$p == 0 || $p == $response(page)} {
768				lappend navbar $n
769			} else {
770				set html "<A HREF=\""
771				set list {}
772
773				foreach var {mode query by how sort rev num} {
774					if {[info exists response($var)]} {
775						lappend list $var $response($var)
776					}
777				}
778
779				lappend list page $p
780				append html [document $list]
781				append html "\">$n</A>"
782				lappend navbar $html
783			}
784		}
785
786		if {"$end" == "Bottom"} {
787			puts "<BR/>"
788		}
789
790		set class [get_css_class TABLE DIONavButtons ${pref}NavButtons]
791		puts "<TABLE WIDTH=\"100%\" CLASS=\"$class\">"
792		puts "<TR CLASS=\"$class\">"
793		puts "<TD CLASS=\"$class\">"
794		puts "<FONT SIZE=-1>"
795
796		if {"$end" == "Top"} {
797			puts "$count records; page:"
798		} else {
799			puts "Go to page"
800		}
801
802		foreach link $navbar {
803			puts "$link&nbsp;"
804		}
805
806		puts "</FONT>"
807		puts "</TD>"
808
809		if {"$end" == "Top" && $pages>10} {
810			set f [::form #auto]
811			$f start -method get
812			hide_hidden_vars $f
813			hide_selection $f
814			hide_cgi_vars $f
815			puts "<TD ALIGN=RIGHT>"
816			puts "<FONT SIZE=-1>"
817			puts "Jump to"
818			$f text page -size 4 -value $response(page)
819			$f submit submit -value "Go"
820			puts "</FONT>"
821			puts "</TD>"
822			$f end
823			$f destroy
824		}
825		puts "</TR>"
826		puts "</TABLE>"
827		if {"$end" == "Top"} {
828			puts "<BR/>"
829		}
830	}
831
832
833	method rowheader {{total 0}} {
834		set fieldList $fields
835		if {![lempty $rowfields]} {
836			set fieldList $rowfields
837		}
838
839		set rowcount 0
840
841		puts "<P>"
842
843		if {$topnav} {
844			page_buttons Top $total
845		}
846
847		puts {<TABLE BORDER WIDTH="100%" CLASS="DIORowHeader">}
848		puts {<TR CLASS="DIORowHeader">}
849		set W [expr {100 / [llength $fieldList]}]
850
851		foreach field $fieldList {
852			set name [$field name]
853			set text [$field text]
854
855			regsub -all $labelsplit $text "<BR>" text
856			set col_title ""
857			set col_title_text $text
858
859			if [info exists hovertext($name)] {
860				set col_title " title=\"$hovertext($name)\""
861				set col_title_text "<span$col_title>$text</span>"
862			}
863
864			if {![sortable $name]} {
865				set html $col_title_text
866			} else {
867				set html ""
868				set list {}
869
870				foreach var {mode query by how num} {
871					if {[info exists response($var)]} {
872						lappend list $var $response($var)
873						set sep "&"
874					}
875				}
876
877				lappend list sort $name
878				set a_attr ""
879
880				if {[info exists response(sort)] && "$response(sort)" == "$name"} {
881					set rev 1
882					if {[info exists response(rev)]} {
883						set rev [expr {1 - $response(rev)}]
884					}
885
886					lappend list rev $rev
887					append html "$col_title_text&nbsp;"
888
889					set desc $rev
890					if [info exists order($name)] {
891						switch -glob -- [string tolower $order($name)] {
892							desc* {
893								set desc [expr {1 - $desc}]
894							}
895						}
896					}
897
898					set text [lindex $arrows $desc]
899					set a_attr { class="DIOArrow"}
900				}
901				append html "<A HREF=\""
902				append html [document $list]
903				append html "\"$a_attr$col_title>$text</A>"
904			}
905			set class [get_css_class TH DIORowHeader DIORowHeader-$name]
906			puts "<TH CLASS=\"$class\" WIDTH=\"$W%\">$html</TH>"
907		}
908
909		if {![lempty $rowfunctions] && "$rowfunctions" != "-"} {
910			puts {<TH CLASS="DIORowHeaderFunctions" WIDTH="0%">&nbsp;</TH>}
911		}
912		puts "</TR>"
913	}
914
915	private method altrow {} {
916		incr rowcount
917		if !$alternaterows { return "" }
918		if {$rowcount % 2} { return "" }
919		return Alt
920	}
921
922	method showrow {arrayName} {
923		upvar 1 $arrayName a
924
925		set alt [altrow]
926
927		set fieldList $fields
928		if {![lempty $rowfields]} {
929			set fieldList $rowfields
930		}
931
932		puts "<TR CLASS=\"DIORowField$alt\">"
933		foreach field $fieldList {
934			set name [$field name]
935			set column $name
936
937			if [info exists alias($name)] {
938				set column $alias($name)
939			}
940
941			set class [get_css_class TD DIORowField$alt DIORowField$alt-$name]
942
943			set text [column_value $name a]
944
945			if ![string length $text] {
946				set text "&nbsp;"
947			}
948
949			set attr NOWRAP
950			if [info exists attributes($name)] {
951				append attr " $attributes($name) "
952				if [regsub -nocase { +wrap +} " $attr " { } attr] {
953					set attr $attributes($name)
954				}
955				set attr [string trim $attr]
956			}
957			puts "<TD CLASS=\"$class\" $attr>$text</TD>"
958		}
959
960		if {![lempty $rowfunctions] && "$rowfunctions" != "-"} {
961			set f [::form #auto]
962			$f start -method get
963			puts "<TD NOWRAP CLASS=\"DIORowFunctions$alt\">"
964			hide_hidden_vars $f
965			hide_selection $f
966			$f hidden query -value [escape_cgi [makekey a]]
967			$f hidden by -value [escape_cgi $key]
968
969			if {[llength $rowfunctions] > 2} {
970				$f select mode -values $rowfunctions -class DIORowFunctionSelect$alt
971				$f submit submit -value "Go" -class DIORowFunctionButton$alt
972			} else {
973				foreach func $rowfunctions {
974					$f submit mode -value $func -class DIORowFunctionButton$alt
975				}
976			}
977			puts "</TD>"
978			$f end
979			$f destroy
980		}
981
982		puts "</TR>"
983	}
984
985	method rowfooter {{total 0}} {
986		if [array exists lastrow] {
987			set rowclass "DIORowField[altrow]"
988
989			set fieldList $fields
990			if {![lempty $rowfields]} { set fieldList $rowfields }
991
992			set skip 0
993			set row {}
994
995			foreach field $fieldList {
996				set name [$field name]
997				if [info exists lastrow($name)] {
998					if {$skip > 0} {
999						lappend row "<TD CLASS=\"$rowclass\" span=\"$skip\">&nbsp;</TD>"
1000					}
1001					set skip 0
1002					lappend row "<TD CLASS=\"$rowclass\">$lastrow($name)</TD>"
1003				} else {
1004					incr skip
1005				}
1006			}
1007
1008			if [llength $row] {
1009				if {![lempty $rowfunctions] && "$rowfunctions" != "-"} {
1010					incr skip
1011				}
1012
1013				if {$skip > 0} {
1014					lappend row "<TD CLASS=\"$rowclass\" span=\"$skip\">&nbsp;</TD>"
1015				}
1016
1017				puts "<TR CLASS=\"rowclass\">"
1018				puts [join $row " "]
1019				puts "</TR>"
1020			}
1021		}
1022		puts "</TABLE>"
1023
1024		if {$bottomnav} {
1025			page_buttons Bottom $total
1026		}
1027	}
1028
1029	## Check field's "sortability"
1030	protected method sortable {name} {
1031		## If allowsort is false, nothing is sortable.
1032		if !$allowsort {
1033			return 0
1034		}
1035
1036		## If there's a list of sortfields, it's only sortable if it's in that
1037		if {![lempty $sortfields]} {
1038			if {[lsearch $sortfields $name] < 0} {
1039				return 0
1040			}
1041		}
1042
1043		## Otherwise if it's searchable, it's sortable
1044		return [searchable $name]
1045	}
1046
1047	## Check field's "searchability"
1048	protected method searchable {name} {
1049		## If it's marked as searchable
1050		if {[lsearch $searchfields $name] < 0} {
1051			return 1
1052		}
1053
1054		## If it's filtered and the filter isn't reversible one way or another
1055		if {
1056			[info exists filters($name)] &&
1057			![info exists unfilters($name)] &&
1058			![string match "*_ok" $filters($name)]
1059		} {
1060			return 0
1061		}
1062
1063		## If it's an alias field
1064		if [info exists alias($name)] {
1065			return 0
1066		}
1067
1068		# Otherwise it's searchable
1069		return 1
1070	}
1071
1072	## Define a new function.
1073	method function {name} {
1074		lappend allfunctions $name
1075	}
1076
1077	## Define a field in the object.
1078	method field {name args} {
1079		import_keyvalue_pairs data $args
1080
1081		set class STDisplayField
1082		if {[info exists data(type)]} {
1083			if {![lempty [::itcl::find classes *STDisplayField_$data(type)]]} {
1084				set class STDisplayField_$data(type)
1085			}
1086		}
1087
1088		set field [
1089				   eval [
1090						 list $class #auto -name $name -display $this -form $form
1091						] $args
1092				  ]
1093		lappend fields $field
1094		lappend allfields $field
1095		lappend allnames $name
1096
1097		set FieldNameMap($name) $field
1098		set NameTextMap([$field text]) $name
1099	}
1100
1101	private method make_limit_selector {values _selector {_array ""}} {
1102		if ![info exists limit] { return 0 }
1103
1104		upvar 1 $_selector selector
1105		if {"$_array" != ""} {
1106			upvar 1 $_array array
1107		}
1108
1109		foreach val $values name $keyfields {
1110			lappend selector [list = $name $val]
1111		}
1112
1113		foreach {k v} $limit {
1114			regsub {^-} $k "" k
1115			lappend selector [list = $k $v]
1116			set array($k) $v
1117		}
1118
1119		return 1
1120	}
1121
1122	# Simplify a "compare" operation in a search to make it compatible with
1123	# standard ctables
1124	method simplify_compare {_compare} {
1125		upvar 1 $_compare compare
1126
1127		set new_compare {}
1128		set changed 0
1129		foreach list $compare {
1130			set op [lindex $list 0]
1131
1132			if {"$op" == "<>"} {
1133				set list [concat {!=} [lrange $list 1 end]]
1134				set changed 1
1135			} elseif {[regexp {^(-?)(.)match} $op _ not ch]} {
1136				set op [lindex {match notmatch} [string length $not]]
1137				unset -nocomplain fn
1138				switch -exact -- [string tolower $ch] {
1139					u { append op _case; set fn toupper }
1140					l { append op _case; set fn tolower }
1141					x { append op _case }
1142				}
1143
1144				set pat [lindex $list 2]
1145				if [info exists fn] {
1146					set pat [string $fn $pat]
1147				}
1148
1149				set list [concat $op [lindex $list 1] [list $pat]]
1150				set changed 1
1151			}
1152			lappend new_compare $list
1153		}
1154
1155		if {$changed} {
1156			set compare $new_compare
1157		}
1158	}
1159
1160	# Perform an extended "search" request bundled in an array
1161	method perform {_request args} {
1162		upvar 1 $_request request
1163		array set search [array get request]
1164		array set search $args
1165		uplevel 1 [list $table search] [array get search]
1166	}
1167
1168	method fetch {keyVal arrayName} {
1169		upvar 1 $arrayName array
1170		if [make_limit_selector $keyVal selector] {
1171			set result [$table search -compare $selector -array_with_nulls array]
1172		} else {
1173			set list [$table array_get_with_nulls $keyVal]
1174			set result [llength $list]
1175			if {$result} {
1176				array set array $list
1177			}
1178		}
1179		return $result
1180	}
1181
1182	# SHorthand to make a key from table
1183	method makekey {arrayName} {
1184		upvar 1 $arrayName array
1185
1186		set list {}
1187		foreach kf $keyfields {
1188			if [info exists array($kf)] {
1189				lappend list $array($kf)
1190			}
1191		}
1192
1193		if {[llength $list] == 0} {
1194			if [info exists array(_key)] {
1195				return $array(_key)
1196			} else {
1197				return -code error "No key in array"
1198			}
1199		}
1200
1201		if {[llength $list] == 1} {
1202			return [lindex $list 0]
1203		}
1204
1205		return $list
1206	}
1207
1208	# SHorthand to store table
1209	method store {arrayName} {
1210		upvar 1 $arrayName array
1211		if [make_limit_selector {} selector array] {
1212			if ![$table search -compare $selector -key _] {
1213				return 0
1214			}
1215		}
1216		return [$table store [array get array]]
1217	}
1218
1219	method delete {keyVal} {
1220		if [make_limit_selector $keyVal selector] {
1221			if ![$table search -compare $selector -key keyVal] {
1222				return 0
1223			}
1224		}
1225		return [$table delete $keyVal]
1226	}
1227
1228	method pretty_fields {list} {
1229		set labels {}
1230		foreach field $list {
1231			lappend labels [$field text]
1232		}
1233		return $labels
1234	}
1235
1236	method set_field_values {arrayName} {
1237		upvar 1 $arrayName array
1238
1239		# for all the elements in the specified array, try to invoke
1240		# the field for that name, invoking the method "value" to
1241		# set the value to the specified value
1242		foreach name [array names array] {
1243			if [info exists FieldNameMap($name)] {
1244				$FieldNameMap($name) configure -value $array($name)
1245			}
1246		}
1247	}
1248
1249	method get_field_values {arrayName} {
1250		upvar 1 $arrayName array
1251
1252		foreach field $allfields {
1253			set v [$field value]
1254			set n [$field name]
1255			if {"$v" == "" && [info exists blankval($n)]} {
1256				if {"$blankval($n)" != "$v"} continue
1257			}
1258			set array($n) $v
1259		}
1260	}
1261
1262	method make_request {_request} {
1263		upvar 1 $_request request
1264		unset -nocomplain request
1265		array unset request
1266	}
1267
1268	method set_limit {_request {selector {}}} {
1269		upvar 1 $_request request
1270
1271		if [info exists request(-compare)] {
1272			set request(-compare) [concat $request(-compare) $selector]
1273		} else {
1274			set request(-compare) $selector
1275		}
1276
1277		make_limit_selector {} request(-compare)
1278		if [llength $request(-compare)] {
1279			return 1
1280		}
1281
1282		unset request(-compare)
1283		return 0
1284	}
1285
1286	method set_order {_request} {
1287		upvar 1 $_request request
1288
1289		if {"[set sort [request_to_sort]]" != ""} {
1290			set request(-sort) $sort
1291		}
1292	}
1293
1294	method set_page {_request} {
1295		upvar 1 $_request request
1296
1297		set recno [get_offset]
1298		if {$recno > 0} {
1299			set request(-offset) $recno
1300		}
1301
1302		if {$pagesize > 0} {
1303			set request(-limit) $pagesize
1304		}
1305	}
1306
1307	method gencsvfile {selector} {
1308		if {"$csvfile" == ""} {
1309			return
1310		}
1311
1312		::stapi::display::load_csv
1313
1314		make_request request
1315		set_limit request
1316		set_order request
1317
1318		if [catch {set fp [open $csvfile w]} err] {
1319			$r destroy
1320			return
1321		}
1322
1323		set columns {}
1324
1325		foreach field $fields {
1326			set name [$field name]
1327			# Don't put alias fields in unless there's a csv filter for them
1328			if [info exists alias($name)] {
1329				if ![info exists csvfilters($name)] {
1330					continue
1331				}
1332			}
1333			lappend columns $name
1334			set label [$field text]
1335			regsub -all { *<[^>]*> *} $label " " label
1336			lappend textlist $label
1337		}
1338
1339		if [info exists textlist] {
1340			puts $fp [::csv::join $textlist]
1341		}
1342
1343		perform request -array_with_nulls a -key k -code {
1344
1345			# If there's no fields defined, then use the columns we got from
1346			# the query and put their names out as the first line
1347
1348			if {![llength $columns]} {
1349				set columns [array names a]
1350				puts $fp [::csv::join $columns]
1351			}
1352			set list {}
1353			foreach name $columns {
1354				lappend list [column_value $name a csv]
1355			}
1356			puts $fp [::csv::join $list]
1357		}
1358
1359		close $fp
1360
1361		$r destroy
1362	}
1363
1364	method showcsvform {query} {
1365		$form start -method get
1366		puts "<TR CLASS='DIOForm'><TD CLASS='DIOForm' VALIGN='MIDDLE' WIDTH='100%'>"
1367		# save hidden vars
1368		hide_hidden_vars $form
1369
1370		# save form vars so state isn't lost
1371		foreach {n v} [state] {
1372			$form hidden $n -value [escape_cgi $v]
1373		}
1374
1375		# save search
1376		hide_selection $form
1377		# save query for generation
1378		$form hidden ct_csv -value [escape_cgi $query]
1379
1380		if $csvredirect {
1381			set csvlabel "Download CSV file"
1382		} else {
1383			set csvlabel "Generate CSV file"
1384		}
1385
1386		$form submit submit -value $csvlabel \
1387			-class DIOMainSubmitButton
1388
1389		if [file exists $csvfile] {
1390			if ![catch {file stat $csvfile st}] {
1391				if $csvredirect {
1392					puts "Previous:&nbsp;"
1393				}
1394				set filename $csvfile
1395				regsub {^.*/} $filename "" filename
1396				puts "<A HREF=\"$csvurl\">$filename</A>:"
1397				puts "$st(size) bytes,"
1398				puts [clock format $st(mtime) -format "%d-%b-%Y %H:%M:%S"]
1399			}
1400		}
1401
1402		puts "</TD></TR>"
1403		$form end
1404	}
1405
1406	method DisplayRequest {selector} {
1407		make_request request
1408		set partial [set_limit request $selector]
1409
1410		if {!$partial} {
1411			if {$rows} {
1412				set total $rows
1413			} else {
1414				set total [$table count]
1415			}
1416		} else {
1417			set total [perform request]
1418		}
1419
1420		if {$total <= [get_offset]} {
1421			puts "Could not find any matching records."
1422			return
1423		}
1424
1425		rowheader $total
1426
1427		set_order request
1428		set_page request
1429		perform request -array_with_nulls a -code { showrow a }
1430
1431		rowfooter $total
1432
1433		if {"$csvfile" != "" && "$csvurl" != ""} {
1434			showcsvform $query
1435		}
1436	}
1437
1438	method Main {} {
1439		puts "<TABLE BORDER='0' WIDTH='100%' CLASS='DIOForm'>"
1440
1441		display_selection {"&nbsp;"} {}
1442
1443		set skipfunctions {}
1444		if {[lsearch $functions Search] >= 0} {
1445			foreach f "Edit Delete" {
1446				if {[lsearch $functions $f] >= 0
1447					&& [lsearch $rowfunctions $f] >= 0} {
1448					lappend skipfunctions $f
1449				}
1450			}
1451		}
1452
1453		puts "<TR CLASS='DIOForm'>"
1454		puts "<TD CLASS='DIOForm' ALIGN='LEFT' VALIGN='MIDDLE' WIDTH='1%' NOWRAP>"
1455
1456		set selfunctions {}
1457		foreach f $functions {
1458			if {"$f" != "List"} {
1459				if {[lsearch $skipfunctions $f] < 0} {
1460					lappend selfunctions $f
1461				}
1462			} else {
1463				set listform [::form #auto]
1464				puts "<DIV STYLE='display:none'>"
1465				$listform start -method get
1466				puts "</DIV>"
1467				hide_hidden_vars $listform
1468				# hide_selection $listform
1469				$listform hidden mode -value "List"
1470				$listform hidden query -value ""
1471				$listform submit submit -value "Show All" \
1472					-class DIORowFunctionButton
1473				puts "<DIV STYLE='display:none'>"
1474				$listform end
1475				$listform destroy
1476				puts "</DIV>"
1477			}
1478		}
1479		puts "</TD>"
1480		puts "<TD CLASS='DIOForm' ALIGN='LEFT' VALIGN='MIDDLE' WIDTH='1%' NOWRAP>"
1481
1482		puts "<DIV STYLE='display:none'>"
1483		$form start -method get
1484		puts "</DIV>"
1485		puts "&nbsp;"
1486
1487		hide_hidden_vars $form
1488		hide_selection $form
1489
1490		if {[llength $selfunctions] > 2} {
1491			$form select mode -values $selfunctions -class DIOMainFunctionsSelect
1492			puts "where"
1493		} else {
1494			puts "Select:"
1495		}
1496
1497		set fieldList $fields
1498		if {![lempty $searchfields]} { set fieldList $searchfields }
1499
1500		set first "-column-"
1501		if [info exists response(by)] {
1502			set first $response(by)
1503			if {"$first" == "_key"} {
1504				set first "-key-"
1505			}
1506		}
1507
1508		set labels [list $first]
1509		foreach field $fieldList {
1510			if ![searchable [$field name]] { continue }
1511			set label [$field text]
1512			if {"$label" != "$first"} {
1513				lappend labels $label
1514			}
1515		}
1516
1517		$form select by -values $labels -class DIOMainSearchBy
1518
1519		puts "</TD>"
1520		puts "<TD CLASS='DIOForm' ALIGN='LEFT' VALIGN='MIDDLE' WIDTH='1%' NOWRAP>"
1521		if [string match {*[Ss]earch*} $selfunctions] {
1522			$form select how -values {"=" "<" "<=" ">" ">=" "<>"}
1523		} else {
1524			puts "is"
1525		}
1526
1527		puts "</TD>"
1528		puts "<TD CLASS='DIOForm' ALIGN='LEFT' VALIGN='MIDDLE' WIDTH='1%' NOWRAP>"
1529		if [info exists response(query)] {
1530			$form text query -value [escape_cgi $response(query)] -class DIOMainQuery
1531		} else {
1532			$form text query -value "" -class DIOMainQuery
1533		}
1534
1535		puts "</TD>"
1536		puts "<TD CLASS='DIOForm' ALIGN='LEFT' VALIGN='MIDDLE' WIDTH='100%' NOWRAP>"
1537
1538		if [string match {*[sS]earch*} $selfunctions] {
1539			display_add_button $form
1540		}
1541
1542		if {[llength $selfunctions] > 2} {
1543			$form submit submit -value "GO" -class DIOMainSubmitButton
1544		} else {
1545			foreach f $selfunctions {
1546				$form submit mode -value $f -class DIOMainSubmitButton
1547			}
1548		}
1549
1550		if {![lempty $numresults]} {
1551			puts "</TD></TR>"
1552			puts "<TR CLASS='DIOForm'><TD CLASS='DIOForm'>Results per page: "
1553			$form select num -values $numresults -class DIOMainNumResults
1554		}
1555
1556		puts "</TD></TR>"
1557
1558		puts "<DIV STYLE='display:none'>"
1559		$form end
1560		puts "</DIV>"
1561		puts "</TABLE>"
1562	}
1563
1564	protected method parse_order {name list {reverse 0}} {
1565		set descending 0
1566		foreach word $list {
1567			if [info exists nextvar] {
1568				set $nextvar $word
1569				unset nextvar
1570				continue
1571			}
1572
1573			switch -glob -- $word {
1574				asc* { set descending 0 }
1575				desc* { set descending 1 }
1576				null* { set nextvar null }
1577			}
1578		}
1579
1580		# ctables doesn't handle this yet
1581		#	if [info exists null] {
1582		#		set field "COALESCE($field,$null)"
1583		#	}
1584
1585		if {$reverse} {
1586			set descending [expr {1 - $descending}]
1587		}
1588		if $descending {
1589			set name -$name
1590		}
1591
1592		return $name
1593	}
1594
1595	method request_to_sort {} {
1596		if {[info exists response(sort)] && ![lempty $response(sort)]} {
1597			set name [CName $response(sort) 0]
1598			if {"$name" == ""} {
1599				unset response(sort)
1600			} else {
1601				set rev 0
1602				if {[info exists response(rev)] && $response(rev)} {
1603					set rev 1
1604				}
1605
1606				set ord ascending
1607				if [info exists order($name)] {
1608					set ord $order($name)
1609				}
1610
1611				return [list [parse_order $name $ord $rev]]
1612			}
1613		}
1614
1615		if {"$defaultsortfield" != ""} {
1616			if [regexp {^-(.*)} $defaultsortfield _ name] {
1617				set ord descending
1618			} else {
1619				set ord ascending
1620				set name $defaultsortfield
1621			}
1622			return [list [parse_order [CName $name] $ord]]
1623		}
1624
1625		return {}
1626	}
1627
1628	method get_offset {} {
1629		if {$pagesize <= 0} { return 0 }
1630		if {![info exists response(page)]} { return 0 }
1631		return [expr {($response(page) - 1) * $pagesize}]
1632	}
1633
1634	protected method display_selection {precells postcells} {
1635		set selection [get_selection 0]
1636		if {![llength $selection]} {
1637			return
1638		}
1639		set span [expr {4 + [llength $precells] + [llength $postcells]}]
1640		puts "<TR><TD CLASS='DIOFormHeader' COLSPAN='$span'>"
1641		puts {<font color="#444444"><b>Current filters:</b></font>}
1642		puts {</TD></TR>}
1643		foreach search $selection {
1644			foreach {how col what} $search { break }
1645			puts {<TR CLASS="DIOSelect">}
1646			set f [::form #auto]
1647			puts {<DIV STYLE="display:none">}
1648			$f start -method get
1649			puts "</DIV>"
1650			hide_hidden_vars $f
1651			hide_cgi_vars $f mode
1652			hide_selection $f - $search
1653			if {[string match "*-*match*" $how]} {
1654				set how "is not like"
1655			} elseif {[string match "*match*" $how]} {
1656				set how "is like"
1657			}
1658
1659			foreach cell $precells {
1660				puts "<TD CLASS='DIOSelect' WIDTH='1%'>$cell</TD>"
1661			}
1662			foreach \
1663				cell [list [DName $col] $how $what] \
1664				align {right middle left} \
1665				{
1666					puts "<TD CLASS='DIOSelect' ALIGN='$align' WIDTH='1%'>[escape_cgi $cell]</TD>"
1667				}
1668			puts {<TD CLASS="DIOSelect" WIDTH="100%" ALIGN="LEFT">}
1669			$f submit mode -value "-" -class DIOForm
1670			puts "</TD>"
1671			foreach cell $postcells {
1672				puts "<TD CLASS='DIOSelect'>$cell</TD>"
1673			}
1674			puts "<DIV STYLE='display:none'>"
1675			$f end
1676			$f destroy
1677			puts "</DIV>"
1678			puts "</TR>"
1679		}
1680		puts "<TR><TD CLASS='DIOFormHeader' COLSPAN='$span'>"
1681		puts ""
1682		puts "</TD></TR>"
1683	}
1684
1685	protected method display_add_button {f} {
1686		$f submit mode -value "+" -class DIOForm
1687	}
1688
1689	protected method add_search_to_selection {} {
1690		set search_list [get_selection 1]
1691		array unset ct_selection
1692	}
1693
1694	protected method get_selection {searching} {
1695		if [info exists ct_selection($searching)] {
1696			return $ct_selection($searching)
1697		}
1698
1699		if ![info exists search_list] {
1700			if [info exists response(ct_sel)] {
1701				set search_list $response(ct_sel)
1702				if {[llength $search_list] == 3
1703					&& [llength [lindex $search_list 0]] == 1} {
1704					set search_list [list $search_list]
1705				}
1706			} else {
1707				set search_list {}
1708			}
1709		}
1710		set ct_selection(0) $search_list
1711		if !$searching {
1712			return $search_list
1713		}
1714		set new_list $search_list
1715
1716		if [info exists response(by)] {
1717			set name [CName $response(by)]
1718
1719			set what $response(query)
1720
1721			set how "="
1722			if {[info exists response(how)] && [string length $response(how)]} {
1723				set how $response(how)
1724			}
1725
1726			if {[string match {*[*?]*} $what]} {
1727				if {"$how" == "="} {
1728					set how "match"
1729				} elseif {"$how" == "<>"} {
1730					set how "notmatch"
1731				}
1732			}
1733			if {[string match "*like*" $how] || [string match "*match*" $how]} {
1734				switch -glob -- $how {
1735					*not* { set how "notmatch" }
1736					-*	  { set how "match-" }
1737					default { set how "match" }
1738				}
1739				if {[info exists case($name)]} {
1740					switch -glob -- [string tolower $case(name)] {
1741						u* {
1742							set what [string toupper $what]
1743							append how "_case"
1744						}
1745						l* {
1746							set what [string tolower $what]
1747							append how "_case"
1748						}
1749						x* {
1750							append how "_case"
1751						}
1752					}
1753				}
1754			}
1755
1756			if {"$how" == "<>"} {
1757				set how "!="
1758			}
1759
1760			set search [list $how $name $what]
1761			if {[lsearch $new_list $search] < 0} {
1762				lappend new_list [list $how $name $what]
1763			}
1764		}
1765		set ct_selection(1) $new_list
1766		return $new_list
1767	}
1768
1769	method Search {} {
1770		display_request_with_selection [get_selection 1]
1771	}
1772
1773	method List {} {
1774		display_request_with_selection [get_selection 0]
1775	}
1776
1777	protected method display_request_with_selection {selection} {
1778		set request {}
1779		foreach target $selection {
1780			foreach {how column what} $target { break }
1781
1782			if {[info exists unfilters($column)] && "$unfilters($column)" != "-"} {
1783				set what [$unfilters($column) $what]
1784			}
1785
1786			lappend request [list $how $column $what]
1787		}
1788		DisplayRequest $request
1789	}
1790
1791	method Add {} {
1792		showform
1793	}
1794
1795	method Edit {} {
1796		if {![fetch $response(query) array]} {
1797			puts "That record does not exist in the database."
1798			return
1799		}
1800
1801		set_field_values array
1802
1803		showform
1804	}
1805
1806	##
1807	## When we save, we want to set all the fields' values and then get
1808	## them into a new array.  We do this because we want to clean any
1809	## unwanted variables out of the array but also because some fields
1810	## have special handling for their values, and we want to make sure
1811	## we get the right value.
1812	##
1813	method Save {} {
1814		if {[info exists response(cancel.x)]} {
1815			Cancel
1816			return
1817		}
1818
1819		## We need to see if the key exists.  If they are adding a new
1820		## entry, we just want to see if the key exists.  If they are
1821		## editing an entry, we need to see if they changed the keyfield
1822		## while editing.  If they didn't change the keyfield, there's no
1823		## reason to check it.
1824		set adding [expr {$response(DIODfromMode) == "Add"}]
1825		if {$adding} {
1826			set keyVal [makekey response]
1827			set list [$table array_get_with_nulls $keyVal]
1828			if {[llength $list]} {
1829				array set a $list
1830			}
1831		} else {
1832			set keyVal $response(DIODkey)
1833			set newkey [makekey response]
1834
1835			## If we have a new key, and the newkey doesn't exist in the
1836			## database, we are moving this record to a new key, so we
1837			## need to delete the old key.
1838			if {$keyVal != $newkey} {
1839				if {![fetch $newkey a]} {
1840					delete $keyVal
1841				}
1842			}
1843		}
1844
1845		if {[array exists a]} {
1846			puts "That record ($keyVal) already exists in the database."
1847			return
1848		}
1849
1850		set_field_values response
1851		get_field_values storeArray
1852
1853		# Don't try and write readonly values.
1854		foreach name [array names storeArray] {
1855			if [[FName $name] readonly] {
1856				unset storeArray($name)
1857			}
1858		}
1859
1860		# Because an empty string is not EXACTLY a null value and not always
1861		# a legal value, if the array element is empty and we're adding a
1862		# new row or there is no legal null value for the type
1863		# remove it from the array -- PdS Jul 2006
1864		foreach {n v} [array get storeArray] {
1865			if {"$v" == ""} {
1866				if $adding {
1867					unset storeArray($n)
1868				} elseif {![info exists FieldNameMap($n)]} {
1869					unset storeArray($n)
1870				} elseif {![$FieldNameMap($n) null_ok]} {
1871					unset storeArray($n)
1872				}
1873			}
1874		}
1875
1876		store storeArray
1877		headers redirect [document]
1878	}
1879
1880	# return a URL containing all of the current state
1881	protected method document {{extra {}}} {
1882		set url $document
1883		set ch "?"
1884		foreach {n v} $extra {
1885			append url $ch $n = [escape_url $v]
1886			set ch "&"
1887		}
1888		foreach {n v} [array get hidden] {
1889			append url $ch $n = [escape_url $v]
1890			set ch "&"
1891		}
1892		set selection [get_selection 0]
1893		if [llength $selection] {
1894			append url $ch ct_sel = [escape_url $selection]
1895			set ch "&"
1896		}
1897		return $url
1898	}
1899
1900	method Delete {} {
1901		if {![fetch $response(query) array]} {
1902			puts "That record does not exist in the database."
1903			return
1904		}
1905
1906		if {!$confirmdelete} {
1907			DoDelete
1908			return
1909		}
1910
1911		puts "<CENTER>"
1912		puts {<TABLE CLASS="DIODeleteConfirm">}
1913		puts "<TR CLASS='DIODeleteConfirm'>"
1914		puts {<TD COLSPAN=2 CLASS="DIODeleteConfirm">}
1915		puts "Are you sure you want to delete this record from the database?"
1916		puts "</TD>"
1917		puts "</TR>"
1918		puts "<TR CLASS='DIODeleteConfirmYesButton'>"
1919		puts {<TD ALIGN="center" CLASS="DIODeleteConfirmYesButton">}
1920		set f [::form #auto]
1921		$f start -method post
1922		hide_hidden_vars $f
1923		hide_selection $f
1924		$f hidden mode -value DoDelete
1925		$f hidden query -value [escape_cgi $response(query)]
1926		$f submit submit -value Yes -class DIODeleteConfirmYesButton
1927		$f end
1928		$f destroy
1929		puts "</TD>"
1930		puts {<TD ALIGN="center" CLASS="DIODeleteConfirmNoButton">}
1931		set f [::form #auto]
1932		$f start -method post
1933		hide_hidden_vars $f
1934		hide_selection $f
1935		$f submit submit -value No -class "DIODeleteConfirmNoButton"
1936		$f end
1937		$f destroy
1938		puts "</TD>"
1939		puts "</TR>"
1940		puts "</TABLE>"
1941		puts "</CENTER>"
1942	}
1943
1944	method DoDelete {} {
1945		if [catch {delete $response(query)} err] {
1946			error "delete $response(query) => $err" $::errorInfo
1947		}
1948
1949		headers redirect [document]
1950	}
1951
1952	method Details {} {
1953		if {![fetch $response(query) array]} {
1954			puts "That record does not exist in the database."
1955			return
1956		}
1957
1958		set_field_values array
1959
1960		showview
1961	}
1962
1963	method Cancel {} {
1964		headers redirect [document]
1965	}
1966
1967	###
1968	## Define variable functions for each variable.
1969	###
1970
1971	private method names2fields {nameList} {
1972		set fieldList {}
1973		foreach name $nameList {
1974			lappend fieldList [FName $name]
1975		}
1976		return $fieldList
1977	}
1978
1979	protected method fields2names {fieldList} {
1980		set nameList {}
1981		foreach field $fieldList {
1982			lappend nameList [$field name]
1983		}
1984		return $nameList
1985	}
1986
1987	method fields {{list ""}} {
1988		if {[lempty $list]} { return [fields2names $fields] }
1989		set fields [names2fields $list]
1990	}
1991
1992	method searchfields {{list ""}} {
1993		if {[lempty $list]} { return [fields2names $searchfields] }
1994		set searchfields [names2fields $list]
1995	}
1996
1997	method rowfields {{list ""}} {
1998		if {[lempty $list]} { return [fields2names $rowfields] }
1999		set rowfields [names2fields $list]
2000	}
2001
2002	method lastrow {name {value ""}} {
2003		if [string length $value] {
2004			set lastrow($name) $value
2005		} elseif {[info exists lastrow($name)]} {
2006			set value $lastrow($name)
2007		}
2008		return $value
2009	}
2010
2011	method alias {name {value ""}} {
2012		if [string length $value] {
2013			set alias($name) $value
2014		} elseif {[info exists alias($name)]} {
2015			set value $alias($name)
2016		} else {
2017			set value $name
2018		}
2019		return $value
2020	}
2021
2022	protected method column_value {name _row {type ""}} {
2023		upvar 1 $_row row
2024
2025		set val ""
2026
2027		set column $name
2028		if [info exists alias($name)] {
2029			set column $alias($name)
2030		}
2031
2032		if [info exists row($column)] {
2033			set val [apply_filter $name [escape_html $row($column)] row $type]
2034		}
2035
2036		return $val
2037	}
2038
2039	method apply_filter {name val {_row ""} {which ""}} {
2040		if [info exists ${which}filters($name)] {
2041			set cmd [list [set ${which}filters($name)] $val]
2042
2043			if {"$_row" != "" && [info exists ${which}filtercols($name)]} {
2044				upvar 1 $_row row
2045				foreach n [set ${which}filtercols($name)] {
2046					if [info exists row($n)] {
2047						lappend cmd $row($n)
2048					}
2049				}
2050			}
2051
2052			set val [eval $cmd]
2053		}
2054		return $val
2055	}
2056
2057	method filter {name {value ""} args} {
2058		if [string length $value] {
2059			set f [uplevel 1 [list namespace which $value]]
2060			if {"$f" == ""} {
2061				return -code error "Unknown filter $value"
2062			}
2063			set value $f
2064			set filters($name) $f
2065			if [llength $args] {
2066				set filtercols($name) $args
2067			}
2068		} elseif {[info exists filters($name)]} {
2069			set value $filters($name)
2070		}
2071		return $value
2072	}
2073
2074	method smartfilter {args} {
2075		uplevel 1 [concat $this filter $args]
2076	}
2077
2078	method csvfilter {name {value ""} args} {
2079		if [string length $value] {
2080			set f [uplevel 1 [list namespace which $value]]
2081			if {"$f" == ""} {
2082				return -code error "Unknown filter $value"
2083			}
2084			set value $f
2085			set csvfilters($name) $f
2086			if [llength $args] {
2087				set csvfiltercols($name) $args
2088			}
2089		} elseif {[info exists csvfilters($name)]} {
2090			set value $csvfilters($name)
2091		}
2092		return $value
2093	}
2094
2095	method order {name {value ""}} {
2096		if [string length $value] {
2097			set order($name) $value
2098		} elseif {[info exists order($name)]} {
2099			set value $order($name)
2100		}
2101		return $value
2102	}
2103
2104	method hovertext {name {value ""}} {
2105		if [string length $value] {
2106			set hovertext($name) $value
2107		} elseif {[info exists hovertext($name)]} {
2108			set value $hovertext($name)
2109		}
2110		return $value
2111	}
2112
2113	method blankval {name {value ""}} {
2114		if [string length $value] {
2115			set blankval($name) $value
2116		} elseif {[info exists blankval($name)]} {
2117			set value $blankval($name)
2118		}
2119		return $value
2120	}
2121
2122	method limit {args} {
2123		if [string length $args] {
2124			set limit $args
2125		} elseif {[info exists limit]} {
2126			set args $limit
2127		}
2128		return $args
2129	}
2130
2131	method case {name {value ""}} {
2132		if [string length $value] {
2133			set case($name) $value
2134		} else {
2135			if [info exists case($name)] {
2136				set value $case($name)
2137			}
2138		}
2139		return $value
2140	}
2141
2142	method unfilter {name {value ""}} {
2143		if [string length $value] {
2144			if {"$value" != "-"} {
2145				set f [uplevel 1 [list namespace which $value]]
2146				if {"$f" == ""} {
2147					return -code error "Unknown filter $value"
2148				}
2149				set value $f
2150			}
2151			set unfilters($name) $value
2152		} elseif {[info exists unfilters($name)]} {
2153			set value $unfilters($name)
2154		}
2155		return $value
2156	}
2157
2158	method attributes {name {value ""}} {
2159		if [string length $value] {
2160			set attributes($name) $value
2161		} elseif {[info exists attributes($name)]} {
2162			set value $attributes($name)
2163		}
2164		return $value
2165	}
2166
2167	method hidden {name {value ""}} {
2168		if [string length $value] {
2169			set hidden($name) $value
2170		} elseif {[info exists hidden($name)]} {
2171			set value $hidden($name)
2172		}
2173		return $value
2174	}
2175
2176	method details {{string ""}} { configvar details $string }
2177	method readonly {{string ""}} { configvar readonly $string }
2178	method mode {{string ""}} { configvar mode $string }
2179	method csvfile {{string ""}} { configvar csvfile $string }
2180
2181	method title {{string ""}} { configvar title $string }
2182	method functions {{string "--"}} { configvar functions $string "--" }
2183	method pagesize {{string ""}} { configvar pagesize $string }
2184	method form {{string ""}} { configvar form $string }
2185	method cleanup {{string ""}} { configvar cleanup $string }
2186	method confirmdelete {{string ""}} { configvar confirmdelete $string }
2187
2188	method css {{string ""}} { configvar css $string }
2189	method persistentmain {{string ""}} { configvar persistentmain $string }
2190	method alternaterows {{string ""}} { configvar alternaterows $string }
2191	method allowsort {{string ""}} { configvar allowsort $string }
2192	method sortfields {{string ""}} { configvar sortfields $string }
2193	method topnav {{string "--"}} { configvar topnav $string "--" }
2194	method bottomnav {{string "--"}} { configvar bottomnav $string "--" }
2195	method numresults {{string ""}} { configvar numresults $string }
2196	method defaultsortfield {{string ""}} { configvar defaultsortfield $string }
2197	method labelsplit {{string ""}} { configvar labelsplit $string }
2198
2199	method rowfunctions {{string "--"}} { configvar rowfunctions $string "--" }
2200	method arrows {{string ""}} { configvar arrows $string }
2201
2202	method rows {{string 0}} { configvar rows $string 0 }
2203
2204	## OPTIONS ##
2205
2206	public variable rows	 0
2207	public variable title	 ""
2208	public variable fields	 ""
2209	public variable searchfields ""
2210	public variable functions	 "Search List Add Edit Delete Details"
2211	public variable pagesize	 25
2212	public variable form	 ""
2213	public variable cleanup	 1
2214	public variable confirmdelete 1
2215	public variable mode	List
2216	public variable trap_errors	0
2217
2218	public variable css_file	""	{
2219		if {![lempty $css_file]} {
2220			catch {unset cssArray}
2221			read_css_file
2222		}
2223	}
2224
2225	public variable css_files		{"display.css" "diodisplay.css"} {
2226		if {![lempty $css_files]} {
2227			catch {unset cssArray}
2228			read_css_file
2229		}
2230	}
2231
2232	public variable persistentmain	1
2233	public variable alternaterows	1
2234	public variable allowsort		1
2235	public variable sortfields		""
2236	public variable topnav		1
2237	public variable bottomnav		1
2238	public variable numresults		""
2239	public variable defaultsortfield	""
2240	public variable labelsplit		"\n"
2241
2242	protected variable rowfields	 ""
2243	public variable rowfunctions "Details Edit Delete"
2244
2245	public variable details 1
2246	public variable readonly 0
2247
2248	public variable response
2249	public variable cssArray
2250	public variable document	 ""
2251	protected variable allfields	{}
2252	protected variable allnames		{}
2253	protected variable NameTextMap
2254	protected variable FieldNameMap
2255	protected variable writefunctions { Add Edit Delete Save DoDelete }
2256	public variable allfunctions {
2257		Search
2258		List
2259		Add
2260		Edit
2261		Delete
2262		Details
2263		Main
2264		Save
2265		DoDelete
2266		Cancel
2267	}
2268
2269	# -csv, -csvfile, -csvurl
2270	# If -csvfile is provided and is in the same directory, gen -csvurl
2271	public variable csv		0 {
2272		if {$csv && "$csvfile" == ""} {
2273			set csvfile "download.csv"
2274			set csvurl "download.csv"
2275		}
2276	}
2277	public variable csvfile	"" {
2278		set csv 1
2279		if {"$csvurl" == ""} {
2280			if ![regexp {^[.]*/} $csvfile] {
2281				set csvurl $csvfile
2282			}
2283		}
2284	}
2285	public variable csvurl ""
2286	public variable csvredirect	0
2287
2288	public variable arrows {"&darr;" "&uarr;"}
2289
2290	private variable blankval
2291	private variable rowcount
2292	private variable filters
2293	private variable alias
2294	private variable lastrow
2295	private variable filtercols
2296	private variable hovertext
2297	private variable csvfilters
2298	private variable csvfiltercols
2299	private variable order
2300	private variable unfilters
2301	private variable attributes
2302	private variable hidden
2303	private variable limit
2304	private variable search_list
2305
2306} ; ## ::itcl::class STDisplay
2307
2308catch { ::itcl::delete class ::STDisplayField }
2309
2310#
2311# STDisplayField object -- defined for each field we're displaying
2312#
2313::itcl::class ::STDisplayField {
2314
2315	constructor {args} {
2316		## We want to simulate Itcl's configure command, but we want to
2317		## check for arguments that are not variables of our object.  If
2318		## they're not, we save them as arguments to the form when this
2319		## field is displayed.
2320		import_keyvalue_pairs data $args
2321		foreach var [array names data] {
2322			if {![info exists $var]} {
2323				lappend formargs -$var $data($var)
2324			} else {
2325				set $var $data($var)
2326			}
2327		}
2328
2329		# if text (field description) isn't set, prettify the actual
2330		# field name and use that
2331		if {[lempty $text]} { set text [pretty [split $name _]] }
2332	}
2333
2334	destructor {
2335
2336	}
2337
2338	method destroy {} {
2339		::itcl::delete object $this
2340	}
2341
2342	#
2343	# get_css_class - ask the parent DIODIsplay object to look up
2344	# a CSS class entry
2345	#
2346	method get_css_class {tag default class} {
2347		return [$display get_css_class $tag $default $class]
2348	}
2349
2350	#
2351	# get_css_tag -- set tag to select or textarea if type is select
2352	# or textarea, else to input
2353	#
2354	method get_css_tag {} {
2355		switch -- $type {
2356			"select" {
2357				set tag select
2358			}
2359			"textarea" {
2360				set tag textarea
2361			}
2362			default {
2363				set tag input
2364			}
2365		}
2366	}
2367
2368	#
2369	# pretty -- prettify a list of words by uppercasing the first letter
2370	#  of each word
2371	#
2372	method pretty {string} {
2373		set words ""
2374		foreach w $string {
2375			lappend words \
2376				[string toupper [string index $w 0]][string range $w 1 end]
2377		}
2378		return [join $words " "]
2379	}
2380
2381	#
2382	# configvar - a convenient helper for creating methods that can
2383	#  set and fetch one of the object's variables
2384	#
2385	method configvar {varName string {defval ""}} {
2386		if {"$string" == "$defval"} { return [set $varName] }
2387		configure -$varName $string
2388	}
2389
2390	#
2391	# showview - emit a table row of either DIOViewRow, DIOViewRowAlt,
2392	# DIOViewRow-fieldname (this object's field name), or
2393	# DIOViewRowAlt-fieldname, a table data field of either
2394	# DIOViewHeader or DIOViewHeader-fieldname, and then a
2395	# value of class DIOViewField or DIOViewField-fieldname
2396	#
2397	method showview {{alt ""}} {
2398		set class [get_css_class TR DIOViewRow$alt DIOViewViewRow$alt-$name]
2399		puts "<TR CLASS=\"$class\">"
2400
2401		set class [get_css_class TD DIOViewHeader DIOViewHeader-$name]
2402		puts "<TD CLASS=\"$class\">$text:</TD>"
2403
2404		set class [get_css_class TD DIOViewField DIOViewField-$name]
2405		puts "<TD CLASS=\"$class\">$value</TD>"
2406
2407		puts "</TR>"
2408	}
2409
2410	#
2411	# showform -- like showview, creates a table row and table data, but
2412	# if readonly isn't set, emits a form field corresponding to the type
2413	# of this field
2414	#
2415	method showform {} {
2416		set class [get_css_class TD DIOFormHeader DIOFormHeader-$name]
2417
2418		puts "<TR CLASS=\"$class\">"
2419		puts "<TD CLASS=\"$class\">$text:</TD>"
2420
2421		set class [get_css_class TD DIOFormField DIOFormField-$name]
2422		puts "<TD CLASS=\"$class\">"
2423		if {$readonly} {
2424			puts "$value"
2425		} else {
2426			set tag [get_css_tag]
2427			set class [get_css_class $tag DIOFormField DIOFormField-$name]
2428
2429			set text $value
2430			regsub -all "\"" $text {\&quot;} text
2431			if {$type == "select"} {
2432				$form select $name -values $values -class $class -value $text
2433			} else {
2434				eval $form $type $name -value [list $text] $formargs -class $class
2435			}
2436		}
2437		puts "</TD>"
2438		puts "</TR>"
2439	}
2440
2441	method null_ok {} {
2442		return [expr {"$type" == "text"}]
2443	}
2444
2445	# methods that give us method-based access to get and set the
2446	# object's variables...
2447	method display	{{string ""}} { configvar display $string }
2448	method form	 {{string ""}} { configvar form $string }
2449	method formargs	 {{string ""}} { configvar formargs $string }
2450	method name	 {{string ""}} { configvar name $string }
2451	method text	 {{string ""}} { configvar text $string }
2452	method type	 {{string ""}} { configvar type $string }
2453	method value {{string ""}} { configvar value $string }
2454	method readonly {{string ""}} { configvar readonly $string }
2455
2456	public variable display		""
2457	public variable form		""
2458	public variable formargs		""
2459
2460	# values - for fields of type "select" only, the values that go in
2461	# the popdown (or whatever) selector
2462	public variable values				""
2463
2464	# name - the field name
2465	public variable name		""
2466
2467	# text - the description text for the field. if not specified,
2468	#  it's constructed from a prettified version of the field name
2469	public variable text		""
2470
2471	# value - the default value of the field
2472	public variable value		""
2473
2474	# type - the data type of the field
2475	public variable type		"text"
2476
2477	# readonly - if 1, we don't allow the value to be changed
2478	public variable readonly		0
2479
2480} ; ## ::itcl::class STDisplayField
2481
2482catch { ::itcl::delete class ::STDisplayField_boolean }
2483
2484#
2485# STDisplayField_boolen -- superclass of STDisplayField that overrides
2486# a few methods to specially handle booleans
2487#
2488::itcl::class ::STDisplayField_boolean {
2489	inherit ::STDisplayField
2490
2491	constructor {args} {eval configure $args} {
2492		eval configure $args
2493	}
2494
2495	method add_true_value {string} {
2496		lappend trueValues $string
2497	}
2498
2499	#
2500	# showform -- emit a form field for a boolean
2501	#
2502	method showform {} {
2503		set class [get_css_class TD DIOFormHeader DIOFormHeader-$name]
2504		puts "<TR CLASS=\"$class\">"
2505		puts "<TD CLASS=\"$class\">$text:</TD>"
2506
2507		set class [get_css_class TD DIOFormField DIOFormField-$name]
2508		puts "<TD CLASS=\"$class\">"
2509		if {$readonly} {
2510			if {[boolean_value]} {
2511				puts $true
2512			} else {
2513				puts $false
2514			}
2515		} else {
2516			if {[boolean_value]} {
2517				$form default_value $name $true
2518			} else {
2519				$form default_value $name $false
2520			}
2521			eval $form radiobuttons $name \
2522				-values [list "$true $false"] $formargs
2523		}
2524		puts "</TD>"
2525		puts "</TR>"
2526	}
2527
2528	#
2529	# showview -- emit a view for a boolean
2530	#
2531	method showview {{alt ""}} {
2532		set class [get_css_class TR DIOViewRow$alt DIOViewRow$alt-$name]
2533		puts "<TR CLASS=\"$class\">"
2534
2535		set class [get_css_class TD DIOViewHeader DIOViewHeader-$name]
2536		puts "<TD CLASS=\"$class\">$text:</TD>"
2537
2538		set class [get_css_class TD DIOViewField DIOViewField-$name]
2539		puts "<TD CLASS=\"$class\">"
2540		if {[boolean_value]} {
2541			puts $true
2542		} else {
2543			puts $false
2544		}
2545		puts "</TD>"
2546
2547		puts "</TR>"
2548	}
2549
2550	#
2551	# boolean_value -- return 1 if value is found in the values list, else 0
2552	#
2553	method boolean_value {} {
2554		set val [string tolower $value]
2555		if {[lsearch -exact $values $val] >= 0} { return 1 }
2556		return 0
2557	}
2558
2559	method value {{string ""}} { configvar value $string }
2560
2561	public variable true	"Yes"
2562	public variable false	"No"
2563	public variable values	"1 y yes t true on"
2564
2565	public variable value "" {
2566		if {[boolean_value]} {
2567			set value $true
2568		} else {
2569			set value $false
2570		}
2571	}
2572
2573	method null_ok {} {
2574		return 0
2575	}
2576
2577} ; ## ::itcl::class ::STDisplayField_boolean
2578
2579package provide st_display 1.13.12
2580
2581