View · Search · Index
No registered users in community rubick
in last 10 minutes

Notes on ad_form

Introduction

OpenACS has a decent form manager called ad_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:
if { something or another } 
  set checked_p [list checked ""]
} else {
  set checked_p ""
}
Then, in your ad_form declaration:
 {
  use_dependency_p:text(checkbox) 
  {label "Use dependency"} 
  {options {{"" "t"}}} 
  {html $checked_p} 
}
However, the options section doesn't seem to work. If it is checked, the value will be "on"

Dynamically filling in select forms

In the ad_form declaration:
    {category_type:text(select)
        {label "Type"}
        {options {[db_list_of_lists get_category_types { }]} }
    }
In the .xql file:
  <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:

ad_page_contract {

...

} {

    {planned_end_date ""}
    ...
}
Then, in your ad_form declaration:
    {planned_end_date:date(date),optional 
        {label "Planned end date"}
        {help}
    }
In your .xql file, when inserting:
  <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>
In your .xql file, when selecting:
<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:

Element 'task_title' already exists in form 'add_edit'.
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:
for {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
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=52016 

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.day

After 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.

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} \
                 ] \
            ] 
}
Then, inside the .xql file:
  <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_p 

Setting 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, view 

Getting errors like no command [something]? (and how to get around this huge security problem)

See: http://openacs.org/forums/message-view?message_id=133637 

Using variables from ad_form in your pages

See how to include variables from ad_forms in your pages 

Getting the number of formerrors in the standard template

See getting the number of formerrors in the standard template 

Compound keys, or when one key doesn't fit

forum thread on compound keys 

Short 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