in last 10 minutes
Notes on ad_form
Introduction
OpenACS has a decent form manager calledad_form
. Ad_form has nice error handing, and a very slick UI, including inline error reporting. However, it is very opaque sometimes, and after using some other form APIs on other platforms, it is confusing by comparison (writing a new form API for OpenACS would be a great contribution I think). My documentation here mostly deals with advanced issues with ad_form, or with common errors. Other documentation deals more with the basics.
Other references
How to debug your ad_forms
Jerry Asher came up with this very useful way of debugging ad_forms. Put this at the top of your ad_form page:ns_log notice it's my page! set mypage [ns_getform] if {$mypage eq ""} { ns_log notice "no form was submitted on my page" } else { ns_log notice "the following form was submitted on my page" ns_set print $mypage }
Different behavior for add and edit pages
No problem! Try using [ad_form_new_p -key mykeyvarname] I haven't, but it might work :-)Getting the values into your form while editing
From Alex Vorobiev:- put {var ""} into ad_page_contract - put {var {value $var}} into ad_form element
Checkboxes
There is a bug in the current version of ad_form which doesn't allow you to have a checkbox filled in as checked. There is a patch to fix it, but until then, here is a workaround, suggested by the ever-helpful daveb:Then, in your ad_form declaration:if { something or another } set checked_p [list checked ""] } else { set checked_p "" }
However, the options section doesn't seem to work. If it is checked, the value will be "on"{ use_dependency_p:text(checkbox) {label "Use dependency"} {options {{"" "t"}}} {html $checked_p} }
Dynamically filling in select forms
In the ad_form declaration:In the .xql file:{category_type:text(select) {label "Type"} {options {[db_list_of_lists get_category_types { }]} } }
<fullquery name="get_category_types"> <querytext> select short_name, category_id as my_cat_id from rl_resource_category_type </querytext> </fullquery>
Using dates with ad_form
The template builder, upon which ad_form is built, has poor support for dates. As a result, when Don built ad_form as a wrapper on top of the template builder, problems carried over. If you're a great programmer, you could go into the underlying core, add in better support for dates, and make a lot of programmers happy. Until then, you must suffer along with the rest of us. However, I'll try to make it a little easier for you.First of all, at the top of your .tcl page:
Then, in your ad_form declaration:ad_page_contract { ... } { {planned_end_date ""} ... }
In your .xql file, when inserting:{planned_end_date:date(date),optional {label "Planned end date"} {help} }
In your .xql file, when selecting:<fullquery name="new_project_item"> <querytext> select pm_project__new_project_item ( :project_id, ... to_timestamp(:planned_end_date,'YYYY MM DD HH24 MI SS'), ... ); </querytext> </fullquery>
<fullquery name="project_query"> <querytext> select item_id, ... to_char(planned_end_date,'YYYY MM DD') as planned_end_date, ... FROM pm_projectsx where project_id = :project_id </querytext> </fullquery> </queryset>
Substituting dynamic values into an ad_form definition
Let's say you want to make the label for an ad_form item be determined at run-time. I did this for project manager, because I let the terminology be determined in a parameter set by the user. You can do this by:{goal:text(textarea) {label "[set project_term] goal"} {optional} {value $goal} {html { rows 5 cols 40 wrap soft}}}
Creating dynamic forms in a for loop
I was faced with a situation in which I wanted to create anumber
of Tasks. The previous page had a form which passed in the number
of Tasks to create, but I wanted to have the ad_form let me create several tasks (number
) on the same page.
Initially, I tried to just use ad_form -extend inside a for loop. However, you get an error like this:
Roberto Mello suggested having the names of each element be different, and building a string to feed to ad_form. This is the route I went. I did something like this after my initial declaration of an ad_form:Element 'task_title' already exists in form 'add_edit'.
An even better solution, proposed by DaveB: http://www.openacs.org/irc/log/2003-08-01#T18-23-45 or http://openacs.org/forums/message-view?message_id=52016for {set i 0} {$i <= $number} {incr i} { append add_edit_definition " {task_title_$i:text {label \"Subject #$i\"} {html {size 59}} }" } ad_form -extend -name add_edit -form $add_edit_definition
Before I did this, I would look at Jeff's posting, which describes using lists instead of the string I built above.
Note that some of these methods may be incompatible with dates.
Using multiple hidden values
There is no facility for having multiple hidden values with the same name. There are workarounds linked in from this bug report.Using multiple dates with ad_form
Things really get sticky when you try to use multiple dates with ad_form. It's a mess, really. The problem is that dates are stored using arrays already. If you have a date in your form, try looking at the HTML generated. You'll see that it will say something like deadline.format deadline.year deadline.month deadline.dayAfter these are passed in, the ad_page_contract looks at them, and if the variable is specified as a date, then it handles it intelligently.
Unfortunately, this conflicts with the standard way of having multiple items with the same name in ad_form. So we have to resort to massive hacking, of the worst sort.
Then, inside the .xql file:ad_page_contract { ... end_date:array,optional ... } ... # The evilest hack of all time. # ------------------------------------------------------- # This is a workaround the fact that using multiple dates # with ad_form is extremely difficult. Dates are formatted # like arrays, with values like end_date.day, end_date.month, # end_date.year, and end_date.format . The problem is we want # to have multiple end_dates. Using the multiple method, we # then get entries like this: end_date.1.year, end_date.2.year, # end_date.2.day, etc.. # # What this loop does is go through the array, and rename the # values into other variables. We then feed these variables into # the SQL function that creates the new tasks. This works. I'm # sure there must be a better way to do it, but my posting on # the forums didn't result in any other suggestions. if {[info exists end_date]} { set searchToken [array startsearch end_date] while {[array anymore end_date $searchToken]} { set keyname [array nextelement end_date $searchToken] set keyvalu $end_date($keyname) # element_num is 1...n, element_type is year, format, day, month regexp {(.*)\.(.*)} $keyname match element_num element_type set end_date_[set element_type]($element_num) $keyvalu } for {set i 1} {$i <= $number} {incr i} { # set up date variable names set end_date_$i [list $end_date_year($i) $end_date_month($i) $end_date_day($i) {} {} {}] } } ... ad_form -name add_edit -form { task_id:key ... } -new_data { set task_revisions [list] # number is the number of dates or items for {set i 1} {$i <= $number} {incr i} { lappend task_revisions [db_exec_plsql new_task_item { *SQL* }] } } for {set i 0} {$i <= $number} {incr i} { # reading this code, you may wonder why we put the .$i at the end. # DaveB showed me this trick. It lets you make a multiple out of # the items by stuffing them in an array. Long live DaveB. ad_form -extend -name add_edit -form \ [list \ [list \ end_date.$i:date,to_sql(linear_date) \ {label "Deadline"} \ {format "MONTH DD YYYY"} \ {value {[util::date::today]}} \ {help} \ ] \ ] }
<fullquery name="new_task_item"> <querytext> select pm_task__new_task_item ( ... to_timestamp('[set end_date_$i]','YYYY MM DD HH24 MI SS'), ... now(), :user_id, :peeraddr, :package_id ); </querytext> </fullquery>
Using a validation block
This is from Alex Vorobiev:To take advantage of the ad_form built-in validation, remove validation rules from ad_page_contract, such as "integer,notnull". ad_page_contract validation returns a brief ugly error that instructs user to use the Back button and fix input. instead use the ad_form -validate block for complex validation, or nothing - this because ad_form by default makes all fields required (unless specified as optional). if a validation rule is not satisfied, ad_form will display an inline error message.
One can have multiple validation rules for the same input field within the ad_form -validate block.
one can write validation rules that check input against the database, such when it is necessary to make sure that a duplicate value is not being inserted into a column with a "unique" constraint.
See also: how to use validation blocks.
Adding sections
You can add a headline bar above an element like this:{myelement:text(text) {section "A section header"}}and thus group the form elements into different sections.
Customizing the layout
You can customize the layout of your forms. See this posting to see how. Or look at the code for project-manager.Setting __refreshing_p
I don't completely understand why you'd want to do this, but if you do: Setting __refreshing_pSetting the minute intervals of ad_form date widget
See http://openacs.org/forums/message-view?message_id=121082 and http://openacs.org/forums/message-view?message_id=125030.Add, edit, view page
forum thread on add, edit, viewGetting errors like no command [something]? (and how to get around this huge security problem)
See: http://openacs.org/forums/message-view?message_id=133637Using variables from ad_form in your pages
See how to include variables from ad_forms in your pagesGetting the number of formerrors in the standard template
See getting the number of formerrors in the standard templateCompound keys, or when one key doesn't fit
forum thread on compound keysShort tutorial on ad_form
This is a simple summary of things I learned while working with ad_form for the first time. Although many of the things I learned should probably have been intuitive they were not for me so I decided to write a simple tutorial in case others have similar problems. Before you go though these examples it would be a good idea to pick up Roberto's ad_form quick reference sheet at:
http://www.brasileiro.net/writings/openacs/ad-form-quick-ref.pdf
Example 1: Simple Multiple select list
see /doc/form-builder.html from your openACS instance.
Example 2: Setting variables in the on_request section. This is really straight forward you just need to
set foo value
where foo is a widget defined in the -form section and value is the value that you would like to set it to. Note That you will not be able to retrieve other values that are set in this section. If you will need the value in another location it would be better to set it above the ad_form section
Example 3: Using -extend
This is really pretty well documented but just make sure that you define your form elements before you start the submit or other sections. This really becomes useful when you need to leave out part of the form or dynamically build the form.
#this is where we are placing the header information
ad_form -name accomplish -form {
{period_id:text(hidden) }
{work_id:text(hidden) }
{period_title:text(inform) {label "Progress report for"} }
{employee:text(inform) {label "Employee"} }
}
...some extra tcl code here
#This is the logic that will omit past data if it is not present
if {$prev_info_p == 1} {
ad_form -extend -name accomplish -form {
{emp_eval:text(inform) {section "Information from last year" } \
{value $prev_info(emp_eval)} \
{label "Action Plans/Training and Career Development Goals"}}
{sup_eval:text(inform) {value $prev_info(sup_eval)} \
{label "Supervisor Summary Comments"} }
{emp_response:text(inform) {value $prev_info(emp_response)} \
{label "Employee Comments/Responses (Optional)" } }
}
}
This will make is if there is no previous information for this employee this section of the form is omitted.
Example 4: Validating the data -validate
In this case the ad_form documentation does a great job in explaining this feature. For the lazy here is the code from the api-doc.
-validate { {value {[string length $value] >= 3} "\"value\" must be a string containing three or more characters" }
These statements can be as complex as you would like them to be. Here is a snippet from a form that has two submit buttons. If you save your work no password is required but to sign your work you need to include a password. This is the code that does that
{passwd
{ ( [string length $passwd] == 0 && $submit == "Save Work" ) \
|| [ad_check_password $user_id $passwd] }
"You Have entered an incorrect password" }
Speaking of two submit buttons on a form to get the second one I had to use the after_html segment as part of one of the widget's. The after_html and before_html are easy to use and do just what you think that they do.
I am sure there is a lot I have missed here but this should put you will on your way to using ad_form.
-Trenton Cameron