Index: openacs-4/packages/acs-tcl/tcl/form-processing-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/form-processing-procs.tcl,v diff -u -r1.6 -r1.7 --- openacs-4/packages/acs-tcl/tcl/form-processing-procs.tcl 31 May 2002 18:01:48 -0000 1.6 +++ openacs-4/packages/acs-tcl/tcl/form-processing-procs.tcl 5 Jul 2002 23:14:47 -0000 1.7 @@ -5,37 +5,295 @@ @author Don Baccus (dhogaza@pacifier.net) } -ad_proc -public ad_form_prototype { +ad_proc -public ad_form { args } { - I'll be adding more documentation as I get time (obviously) - I know error checking's incomplete, too ... + This procedure implements a high-level, declarative syntax for the generation and + handling of HTML forms. It includes special syntax for the handling of forms tied to + database entries, including the automatic generation and handling of primary keys generated + from sequences. You can declare code blocks to be executed when the form is submitted, new + data is to be added, or existing data modified. You can declare form validation blocks that + are similar in spirit to those found in ad_page_contract. - Three hidden values of interest are available to the caller of gp_form when processing - a submit: +
- 1. __new_p + We use the standard ATS form builder's form and element create procedures to generate forms, + and its state-tracking code to determine when to execute various code blocks. Because of + this, you can use any form builder datatype or widget with this procedure, and extending its + functionality is a simple matter of implementing new ones. +
+ + In general the full functionality of the form builder is exposed by ad_form, but with a + much more user-friendly and readable syntax and with state management handled automatically. + +
+ + Here's an example of a simple page implementing an add/edit form: + +
+ ++ + ad_page_contract { + + Simple add/edit form + + } { + my_table_key:optional + } + + ad_form -name form_name -form { + + my_table_key:key(my_table_sequence) + + {value:text(textarea) {{label "Enter text"} + {html {rows 4 cols 50}}} + } -select_query { + select value from my_table where my_table_key = :my_table_key + } -validate { + {value + {[string length $value] >= 3} + "\"value\" must be a string containing three or more characters" + } + } -new_data { + db_dml do_insert " + insert into my_table + (my_table_key, value) + values + (:key, :value)" + ad_returnredirect "somewhere" + return + } -edit_data { + db_dml do_update " + update my_table + set value = :value + where my_table_key = :key" + ad_returnredirect "somewhere" + return + } + + gp_return_template + +
+ + In this example, ad_form will first check to see if "my_table_key" was passed to the script. If + not, the database will be called to generate a new key value from "my_table_sequence" (the sequence + name defaults to acs_object_id_seq). If defined, the query defined by "-select_query" will be used + to fill the form elements with existing data (an error will be thrown if the query fails). + +
+ + The call to gp_return_template then renders the page - it is your responsibility to render the form + in your template by use of the ATS formtemplate tag. + +
+ + On submission, the validation block checks that the user has entered at least three characters into the + textarea (yes, this is a silly example). If the validation check fails the "value" element will be tagged + with the error message, which will be displayed in the form when it is rendered. + + If the validation check returns true, one of the new_data or edit_data code blocks will be executed depending + on whether or not "my_table_key" was defined during the initial request. "my_table_key" is passed as a hidden + form variable and is signed and verified, reducing the opportunity for key spoofing by malicious outsiders. + +
+ + This example includes dummy redirects to a script named "somewhere" to make clear the fact that after + executing the new_data or edit_data block ad_form returns to the caller. + +
+ + Here's a complete list of switches that are supported by ad_form: + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Declaring form elements
- 3. __refreshing_p + ad_form uses the form builder's form element create procedure to generate elements declared in the -form + block. ad_form does rudimentary error checking to make sure the data type and widget exist, and + that options are legal. - This should be set true by Javascript widgets which change a form element then - submit the form to refresh values. +
-} { + The -form block is a list of form elements, which themselves are lists consisting of one or two + elements. The first member of each element sublist declares the form element name, type, widget, whether or + not the element is a multiple element (multiselect, for instance), and optional conversion arguments. The second, + optional member consists of a list of form element parameters and values. All parameters accepted by the form + element create procedure are allowed. +
+ Some form builder datatypes build values that do not directly correspond to database types. When using + the form builder directly these are converted by calls to datatype::get_property and datatype::set_property. + When using ad_form, "to_html(property)", "to_sql(property)" and "from_sql(property)" declare the appropriate + properties to be retrieved or set before calling code blocks that require the converted values. The "to_sql" + operation is performed before any on_submit, new_data or edit_data block is executed. The "from_sql" operation + is performed after a select_query or select_query_name query is executed. No automatic conversion is performed + for edit_request blocks (which manually set form values). The "to_html" operation is performed before execution + of a confirm template. + +
+ + Currently only the date and currency datatypes require conversion these conversion operations. + +
+ + In the future the form builder will be enhanced so that ad_form can determine the proper conversion operation + automatically, freeing the programmer from the need to specify them. When this is implemented the current notation + will be retained for backwards compatibility. + +
+ + ad_form defines a "key" pseudotype. Only one element of type "key" is allowed per form, and it is assigned + the integer datatype. Only keys which are generated from a database sequence are managed automatically by + ad_form. If the sequence name is not specified, the sequence acs_object_id_seq is used to generate new keys. + + Examples: + +
+ ++ my_key:key ++ + Define the key "my_key", assigning new values by calling acs_object_id_seq.nextval + +
+
+ ++ my_key:key(some_sequence_name) ++ + Define the key "my_key", assigning new values by calling some_sequence_name.nextval + +
+
+ ++ {my_key:text(multiselect),multiple {{label "select some values"} + {options {first second third fourth fifth}} + {html {size 4}}} + ++ + Define a multiple select element with five choices, in a four-line select box. + +
+
+ ++ {hide_me:text(hidden) {{value 3}} ++ + Define the hidden form element "hide_me" with the value 3 + +
+
+ +} { + set level [template::adp_level] # Are we extending the form? @@ -53,9 +311,8 @@ return -code error "No arguments to ad_form" } - set valid_args { form method action html name select_query select_query_name new_data \ - edit_data validate on_submit confirm_template \ - new_request edit_request }; + set valid_args { form method action html name select_query select_query_name new_data on_refresh + edit_data validate on_submit confirm_template new_request edit_request }; ad_arg_parser $valid_args $args @@ -103,6 +360,12 @@ return -code error "No \"form\" block has been specified for form \"$form_name\"" } + # If we're not extending + if { !$extend_p } { + global gp_conn + incr gp_conn(form_count) + } + #################### # # Step 1: Parse the form specification @@ -123,47 +386,50 @@ set element_names [list] array set af_element_parameters [list] - foreach element $form { - set element_name_part [lindex $element 0] + if { [info exists form] } { + foreach element $form { + set element_name_part [lindex $element 0] - # This can easily be generalized if we add more embeddable form commands ... + # This can easily be generalized if we add more embeddable form commands ... - if { [string equal $element_name_part "-section"] } { - lappend af_element_names($form_name) "[list "-section" [uplevel [list subst [lindex $element 1]]]]" - } else { - if { ![regexp {^([^ \t:]+)(?::([a-zA-Z0-9_,(|)]*))?$} $element_name_part match element_name flags] } { - return -code error "Form element '$element_name_part' doesn't have the right format. It must be var\[:flag\[,flag ...\]\]" - } + if { [string equal $element_name_part "-section"] } { + lappend af_element_names($form_name) "[list "-section" [uplevel [list subst [lindex $element 1]]]]" + } else { + if { ![regexp {^([^ \t:]+)(?::([a-zA-Z0-9_,(|)]*))?$} $element_name_part match element_name flags] } { + return -code error "Form element '$element_name_part' doesn't have the right format. It must be var\[:flag\[,flag ...\]\]" + } - lappend af_element_names($form_name) $element_name - set af_extra_args($element_name) [lrange $element 1 end] - set pre_flag_list [split [string tolower $flags] ,] - set af_flag_list(${form_name}__$element_name) [list] + lappend af_element_names($form_name) $element_name + set af_extra_args($element_name) [lrange $element 1 end] + set pre_flag_list [split [string tolower $flags] ,] + set af_flag_list(${form_name}__$element_name) [list] - # find parameterized flags. We only allow one parameter. - foreach flag $pre_flag_list { - set af_element_parameters($element_name:$flag) [list] - set left_paren [string first "(" $flag] - if { $left_paren != -1 } { - if { ![string equal [string index $flag end] ")"] } { - return -code error "Missing or misplaced end parenthesis for flag '$flag' on argument '$element_name'" + # find parameterized flags. We only allow one parameter. + foreach flag $pre_flag_list { + set af_element_parameters($element_name:$flag) [list] + set left_paren [string first "(" $flag] + if { $left_paren != -1 } { + if { ![string equal [string index $flag end] ")"] } { + return -code error "Missing or misplaced end parenthesis for flag '$flag' on argument '$element_name'" + } + set flag_stem [string range $flag 0 [expr $left_paren - 1]] + lappend af_element_parameters($element_name:$flag_stem) [string range $flag [expr $left_paren + 1] [expr [string length $flag]-2]] + lappend af_flag_list(${form_name}__$element_name) $flag_stem + } else { + lappend af_flag_list(${form_name}__$element_name) $flag } - set flag_stem [string range $flag 0 [expr $left_paren - 1]] - lappend af_element_parameters($element_name:$flag_stem) [string range $flag [expr $left_paren + 1] [expr [string length $flag]-2]] - lappend af_flag_list(${form_name}__$element_name) $flag_stem - } else { - lappend af_flag_list(${form_name}__$element_name) $flag } } + lappend element_names [lindex $af_element_names($form_name) end] } - lappend element_names [lindex $af_element_names($form_name) end] } # Check the validation block for boneheaded errors if it exists. We explicitly allow a form element # to appear twice in the validation block so the caller can pair different error messages to different # checks. We implement this by building a global list of validation elements global af_validate_elements + set af_validate_elements($form_name) [list] if { [info exists validate] } { foreach validate_element $validate { @@ -203,13 +469,13 @@ # if a confirm template has been specified, it will be returned unless __confirmed_p is set # true. This is most easily done by including resources/forms/confirm-button in the confirm # template. - + template::element create $form_name __confirmed_p -datatype integer -widget hidden -value 0 - + # javascript widgets can change a form value and submit the result in order to allow the # generating script to fill in a value such as an image. The widget must set __refreshing_p # true. - + template::element create $form_name __refreshing_p -datatype integer -widget hidden -value 0 } @@ -367,7 +633,7 @@ set key_name $af_key_name($form_name) upvar #$level $key_name $key_name - upvar #$level __gp_form_values__ values + upvar #$level __ad_form_values__ values # Check to see if we're editing an existing database value if { [info exists $key_name] } { @@ -479,6 +745,12 @@ } } + if { [template::form is_submission $form_name] && + [uplevel #$level {set __refreshing_p}] && + [info exists on_refresh] } { + ad_page_contract_eval uplevel #$level $on_refresh + } + if { [template::form is_valid $form_name] && ![uplevel #$level {set __refreshing_p}] } { # Run confirm and preview templates before we do final processing of the form @@ -510,18 +782,14 @@ # 1. an on_submit block (useful for forms that don't touch the database or can share smart Tcl API # for both add and edit forms) - # 2. an new_data block (when form_name:add_p is true) - # 3. an edit_data block (when form_name:add_p is false) + # 2. an new_data block (when __new_p is true) + # 3. an edit_data block (when __new_p is false) # We don't need to interrogate the af_parts structure because we know we're in the last call to # to ad_form at this point and that this call contained the "action blocks". - if { [info exists on_submit] } { - ad_page_contract_eval uplevel #$level $on_submit - } - # Execute our to_sql filters, if any, before passing control to the caller's - # new_data or edit_data blocks + # on_submit, new_data or edit_data blocks foreach element_name $af_element_names($form_name) { if { [llength $element_name] == 1 } { @@ -534,6 +802,10 @@ } } + if { [info exists on_submit] } { + ad_page_contract_eval uplevel #$level $on_submit + } + upvar #$level __new_p __new_p if { [info exists new_data] && $__new_p } { @@ -554,13 +826,13 @@ value } { Set the value of a particular element in the current form being built by - gp_form. + ad_form. @param element The name of the element @parma value The value to set } { - upvar #[template::adp_level] __gp_form_values__ values + upvar #[template::adp_level] __ad_form_values__ values set values($element) $value }+ start_date:date,to_sql(sql_date),from_html(sql_date),optional ++ + Define the optional element "start_date" of type "date", get the sql_date property before executing + any new_date, edit_date or on_submit block, set the sql_date property after performing any + select_query. + +
+