attribute-procs.tcl

Does not contain a contract.

Location:
/packages/acs-subsite/tcl/attribute-procs.tcl

Related Files

[ hide source ] | [ make this the default ]

File Contents

# /packages/mbryzek-subsite/tcl/attribute-procs.tcl

ad_library {

    Procs to help with attributes for object types

    @author mbryzek@arsdigita.com
    @creation-date Thu Dec  7 10:30:57 2000
    @cvs-id $Id: attribute-procs.tcl,v 1.20.2.10 2024/02/08 18:04:04 gustafn Exp $
}

ad_page_contract_filter attribute_dynamic_p { name value } {
    Checks whether the value (assumed to be an integer) is an
    attribute of a dynamic type.

    @author Michael Bryzek (mbryzek@arsdigita.com)
    @creation-date 12/30/2000

} {
    set dynamic_p [db_0or1row attribute_for_dynamic_object_p {
        select 1 from dual where exists
        (select 1 from acs_attributes a, acs_object_types t
         where t.dynamic_p = 't'
         and a.object_type = t.object_type
         and a.attribute_id = :value)
    }]
    if {!$dynamic_p} {
        ad_complain "Attribute does not belong to a dynamic object and cannot be modified"
    }
    return $dynamic_p
}


namespace eval attribute {

    d_proc -public exists_p {
        { -convert_p "t" }
        object_type
        orig_attribute
    } {
        Returns 1 if the object type already has an attribute of the given name.

        @author Michael Bryzek (mbryzek@arsdigita.com)
        @creation-date 12/2000

        @param convert_p If <code>t</code>, we convert the attribute using
        plsql_utility::generate_oracle_name

        @param orig_attribute The attribute in which we are
        interested. Note that if <code>convert_p</code> is set to
        <code>t</code>, we will internally look for the converted attribute
        name

        @return 1 if the object type already has an attribute of the
        specified name. 0 otherwise

    } {
        if { $convert_p == "t" } {
            set attribute [plsql_utility::generate_oracle_name $orig_attribute]
        } else {
            set attribute $orig_attribute
        }

        set attr_exists_p [db_string attr_exists_p {
           select 1 from acs_attributes a
           where (a.attribute_name = :attribute or a.column_name = :attribute)
           and a.object_type = :object_type
        } -default 0]

        if { $attr_exists_p || $convert_p == "f" } {
            return $attr_exists_p
        }
        return [exists_p -convert_p f $object_type $orig_attribute]
    }

    d_proc -public add {
        { -default "" }
        { -min_n_values "" }
        { -max_n_values "" }
        object_type
        datatype
        pretty_name
        pretty_plural
    } {
        wrapper for the <code>acs_attribute.create_attribute</code>
        call. Note that this procedure assumes type-specific storage.

        @author Michael Bryzek (mbryzek@arsdigita.com)
        @creation-date 12/2000

        @return The <code>attribute_id</code> of the newly created
        attribute

    } {

        set default_value $default

        # We always use type-specific storage. Grab the tablename from the
        # object_type
        if { ![db_0or1row select_table {
            select t.table_name
              from acs_object_types t
             where t.object_type = :object_type
        }] } {
            error "Specified object type \"$object_type\" does not exist"
        }

        # In OpenACS, where we care that SQL must be separate from code, we don't
        # use these annoying formatting procs on our SQL. We write out the queries in full. (ben)

        # Attribute name returned from this function will be oracle
        # friendly and is thus used as the column name
        set attribute_name [plsql_utility::generate_oracle_name $pretty_name]

        # set attr_list [list]
        # lappend attr_list [list "object_type" '$object_type']
        # lappend attr_list [list "attribute_name" '$attribute_name']
        # lappend attr_list [list "min_n_values" '$min_n_values']
        # lappend attr_list [list "max_n_values" '$max_n_values']
        # lappend attr_list [list "default_value" '$default']
        # lappend attr_list [list "datatype" '$datatype']
        # lappend attr_list [list "pretty_name" '$pretty_name']
        # lappend attr_list [list "pretty_plural" '$pretty_plural']

        # A note (by ben, OpenACS)
        # the queries are empty because they are pulled out later in db_exec_plsql

        set plsql [list]
        lappend plsql_drop [list db_exec_plsql "drop_attribute" "FOO"]
        lappend plsql [list db_exec_plsql "create_attribute" "FOO"]

        set sql_type [datatype_to_sql_type -default $default_value $table_name $attribute_name $datatype]

        lappend plsql_drop [list db_dml "drop_attr_column" "FOO"]
        lappend plsql [list db_dml "add_column" "FOO"]

        for { set i 0 } { $i < [llength $plsql] } { incr i } {
            set cmd [lindex $plsql $i]
            if { [catch $cmd err_msg] } {
                # Rollback what we've done so far. The loop conditionals are:
                #  start at the end of the plsql_drop list (Drop things in reverse order of creation)
                # execute drop statements until we reach position $i+1
                #  This position represents the operation on which we failed, and thus
                #  is not executed
                for { set inner [expr {[llength $plsql_drop] - 1}] } { $inner > $i + 1 } { incr inner -1 } {
                    set drop_cmd [lindex $plsql_drop $inner]
                    if { [catch $drop_cmd err_msg_2] } {
                        append err_msg "\nAdditional error while trying to roll back: $err_msg_2"
                        return -code error $err_msg
                    }
                }
                return -code error $err_msg
            }
        }

        return [db_string select_attribute_id {
            select a.attribute_id
              from acs_attributes a
             where a.object_type = :object_type
               and a.attribute_name = :attribute_name
        }]

    }


    d_proc -private datatype_to_sql_type {
        { -default "" }
        table
        column
        datatype
    } {
        Returns the appropriate sql type for a table definition
        based on the table, column, datatype, and default value. Note that for
        default values, this proc automatically generates appropriate
        constraint names as well.

        @author Michael Bryzek (mbryzek@arsdigita.com)
        @creation-date 12/2000

        @param default If specified, we add a default clause to the SQL statement

    } {
        set type ""
        set constraint ""

        switch -- $datatype {
            "string" { set type "varchar(1000)" }
            "boolean" { set type "char(1)"
                        set constraint "[plsql_utility::generate_constraint_name $table $column "ck"] check ($column in ('t','f'))" }
            "number" { set type "number" }
            "money" { set type "number (12,2)" }
            "date" { set type "date" }
            "text" { set type "varchar(4000)" }
            "integer" { set type "integer" }
            "enumeration" { set type "varchar(100)" }
            "keyword" { set type "varchar(1000)" }
            default {error "Unsupported datatype. Datatype $datatype is not implemented at this time"}
        }

        set sql "$type"

        if { $default ne "" } {
            # This is also pretty nasty - we have to make sure we
            # treat db literals appropriately - null is much different
            # than 'null' - mbryzek
            set vars [list null sysdate]
            if {[string tolower $default] ni $vars} {
                set default "'$default'"
            }
            append sql " default $default"
        }
        if { $constraint ne "" } {
            append sql " constraint $constraint"
        }
        return $sql
    }


    ad_proc -public delete { attribute_id } {
        Delete the specified attribute id and all its values. This is
        irreversible. Returns 1 if the attribute was actually deleted. 0
        otherwise.

        @author Michael Bryzek (mbryzek@arsdigita.com)
        @creation-date 12/2000

    } {

        # 1. Drop the attribute with its column
        # 2. Return

        if { ![db_0or1row select_attr_info {
            select a.object_type, a.attribute_name,
            case when a.storage = 'type_specific' then t.table_name else a.table_name end as table_name,
            coalesce(a.column_name, a.attribute_name) as column_name
            from acs_attributes a, acs_object_types t
            where a.attribute_id = :attribute_id
            and t.object_type = a.object_type
        }] } {
            # Attribute doesn't exist
            return 0
        }

        if { $table_name eq "" || $column_name eq "" } {
            # We have to have both a nonempty table name and column name
            error "We do not have enough information to automatically remove this attribute. Namely, we are missing either the table name or the column name"
        }

        set drop_table_column_p [expr {[db_column_exists $table_name $column_name] ? "t" : "f"}]

        db_exec_plsql drop_attribute {}

        return 1
    }

    ad_proc -public value_add {attribute_id enum_value sort_order} {
        adds the specified enumeration value to the attribute.

        @author Ben Adida (ben@openforce.net)
        @creation-date 08/2001

        @param attribute_id The attribute to which we are adding
        @param enum_value The value which we are adding to the enum
    } {
        # Just insert it if we can
        db_dml insert_enum_value {
            insert into acs_enum_values
            (attribute_id, sort_order, enum_value, pretty_name)
            select :attribute_id, :sort_order, :enum_value, :enum_value
            from dual
            where not exists (select 1
            from acs_enum_values v2
            where v2.pretty_name = :enum_value
            and v2.attribute_id = :attribute_id)
        }
    }

    ad_proc -public value_delete { attribute_id enum_value } {
        deletes the specified enumeration value from the attribute. The
        net effect is that this attribute will have one fewer options for
        acceptable values.

        @author Michael Bryzek (mbryzek@arsdigita.com)
        @creation-date 12/2000

        @param attribute_id The attribute from which we are deleting
        @param enum_value The value of we are deleting

    } {
        # This function should remove all occurrences of the
        # attribute, but we don't do that now.

        if { ![db_0or1row select_last_sort_order {
            select sort_order as old_sort_order
              from acs_enum_values
             where attribute_id = :attribute_id
               and enum_value = :enum_value
        }] } {
            # nothing to delete
            return
        }

        db_dml delete_enum_value {
            delete from acs_enum_values
            where attribute_id = :attribute_id
            and enum_value = :enum_value
        }
        if { [db_resultrows] > 0 } {
            # update the sort order
            db_dml update_sort_order {
                update acs_enum_values
                   set sort_order = sort_order - 1
                 where attribute_id = :attribute_id
                   and sort_order > :old_sort_order
            }
        }

        return

    }


    d_proc -public translate_datatype {
        datatype
    } {
        translates the datatype into one that can be
        validated. Default datatype is text (when no validator is found)

        @author Michael Bryzek (mbryzek@arsdigita.com)
        @creation-date 12/2000

    } {
        if { [datatype_validator_exists_p $datatype] } {
            return $datatype
        }
        switch -- $datatype {
            boolean { set datatype "text" }
            keyword { set datatype "text" }
            money { set datatype "integer" }
            number { set datatype "integer" }
            string { set datatype "text" }
        }
        if { [datatype_validator_exists_p $datatype] } {
            return $datatype
        }
        # No validator exists... return text as default
        return "text"
    }

    d_proc -public datatype_validator_exists_p {
        datatype
    } {

        Returns 1 if we have a validator for this datatype. 0
        otherwise. We currently do not support the "date" datatype and
        hardcoded support for enumeration. This
        is hardcoded in this procedure. Also, this procedure assumes that
        validators are procedures named
        <code>::template::data::validate::$datatype</code>

        @author Michael Bryzek (mbryzek@arsdigita.com)
        @creation-date 12/2000

    } {
        if {$datatype eq "date"} {
            return 0
        }
        if {$datatype eq "enumeration"} {
            return 1
        }
        if { [namespace which ::template::data::validate::$datatype] eq "" } {
            return 0
        }
        return 1
    }

    d_proc -public array_for_type {
        { -start_with "acs_object" }
        { -include_storage_types {type_specific} }
        array_name
        enum_array_name
        object_type
    } {

        Fills in 2 arrays used for displaying attributes

        @author Michael Bryzek (mbryzek@arsdigita.com)
        @creation-date 1/8/2001

        @param array_name The name of the array to hold the basic
        attribute information. The attributes defined are:
        <code>
          * array_name(pretty_name:$name) The pretty_name of the attribute
          * array_name(id:$name) The attribute_id of the attribute
          * array_name(datatype:$name) The datatype of the attribute
        </code>

        @param enum_array_name The name of the array to hold the pretty
        name of the values of an enumeration. This is only used when the
        datatype of the attribute_name is enumeration. This array is a
        mapping from "$attribute_name:enum_value" to value_pretty_name.

        @param object_type The object for which we are looking up
        attributes

        @return A list of all the names of attributes we looked up. This
        list can be used to iterated through the arrays:
        <pre>
            set attr_list [attribute::array_for_type attr_props enum_values "group"]
            foreach key $attr_list {
                set attribute_id $attr_props(id:$attribute_name)
                ...
            }
        </pre>

    } {
        upvar $array_name attr_props
        upvar $enum_array_name enum_values
        set attr_list [list]

        if {$include_storage_types ne ""} {
            set storage_clause "and a.storage in ([ns_dbquotelist $include_storage_types])"
        } else {
            set storage_clause ""
        }

        db_foreach select_attributes [subst -nocommands {
            with recursive object_type_hierarchy as (
                select object_type,
                       0 as type_level
                  from acs_object_types
                 where object_type = :start_with

                union all

                select t.object_type,
                       h.type_level + 1 as type_level
                  from acs_object_types t,
                       object_type_hierarchy h
                 where t.supertype = h.object_type
            )
            select coalesce(a.column_name, a.attribute_name) as name,
                   a.pretty_name,
                   a.attribute_id,
                   a.datatype,
                   v.enum_value,
                   v.pretty_name as value_pretty_name
            from acs_object_type_attributes a left outer join
                   acs_enum_values v using (attribute_id),
                   object_type_hierarchy t
             where a.object_type = :object_type
               and t.object_type = a.ancestor_type $storage_clause
            order by t.type_level, a.sort_order
        }] {
            # Enumeration values show up more than once...
            if {$name ni $attr_list} {
                lappend attr_list $name
                set attr_props(pretty_name:$name$pretty_name
                set attr_props(datatype:$name$datatype
                set attr_props(id:$name$attribute_id
            }
            if {$datatype eq "enumeration"} {
                set enum_values($name:$enum_value$value_pretty_name
            }
        }
        return $attr_list
    }

    d_proc -public multirow {
        { -start_with "acs_object" }
        { -include_storage_types {type_specific} }
        { -datasource_name "attributes" }
        { -object_type "" }
        { -return_url "" }
        object_id
    } {
        Sets up a multirow datasource containing the attribute values of object_id.
        We only support specific storage attributes.
        We include all attributes of the object's type, or any of its supertypes,
        up to $start_with.
    } {

        upvar $datasource_name attributes

        if {$object_type eq ""} {
            set object_type [db_string object_type_query {
                select object_type from acs_objects where object_id = :object_id
            }]
        }

        if {$return_url eq ""} {
            set return_url "[ad_conn url]?[ad_conn query]"
        }

        # Build up the list of attributes for the type specific lookup
        set attr_list [attribute::array_for_type \
            -start_with $start_with \
            -include_storage_types $include_storage_types \
            attr_props enum_values $object_type]

        # Build up a multirow datasource to present these attributes to the user
        template::multirow create $datasource_name pretty_name value export_vars

        set package_object_view [package_object_view \
            -start_with "acs_object" \
            $object_type]

        if { [array size attr_props] > 0 } {
            db_foreach attribute_select [subst -nocommands {
                select *
                from ($package_object_view) dummy
                where object_id = :object_id
            }] {
                foreach key $attr_list {
                    set col_value [set $key]
                    set attribute_id $attr_props(id:$key)
                    if { $attr_props(datatype:$key) eq "enumeration" && [info exists enum_values($key:$col_value)] } {
                        # Replace the value stored in the column with the
                        # pretty name for that attribute
                        set col_value $enum_values($key:$col_value)
                    }
                    template::multirow append $datasource_name $attr_props(pretty_name:$key$col_value "id_column=$object_id&[export_vars {attribute_id return_url}]"
                }
            }
        }
    }

    d_proc -public add_form_elements {
        { -form_id "" }
        { -start_with "acs_object" }
        { -object_type "acs_object" }
        { -variable_prefix "" }
    } {
        Adds form elements to the specified form_id.  Each form element
        corresponds to an attribute belonging to the given object_type.

        @param form_id ID of a form to add form elements to.
        @param start_with Object type to start with.  Defaults to acs_object.
        @param object_type Object type to extract attributes for.
        Defaults to acs_object.
        @param variable_prefix Variable prefix.
    } {

        if {$form_id eq ""} {
            error "attribute::add_form_elements - form_id not specified"
        }

        if {$object_type eq ""} {
            error "attribute::add_form_elements - object type not specified"
        }

        if {$variable_prefix ne ""} {
            append variable_prefix "."
        }

        # pull out all the attributes up the hierarchy from this object_type
        # to the $start_with object type
        set attr_list_of_lists [package_object_attribute_list -start_with $start_with $object_type]

        foreach row $attr_list_of_lists {
            lassign $row  attribute_id . attribute_name pretty_name datatype required_p default
            # Might translate the datatype into one for which we have a
            # validator (e.g. a string datatype would change into text).
            set datatype [translate_datatype $datatype]

            if {$datatype eq "enumeration"} {
            # For enumerations, we generate a select box of all the possible values
                set option_list [db_list_of_lists select_enum_values {
                    select enum.pretty_name, enum.enum_value
                    from acs_enum_values enum
                    where enum.attribute_id = :attribute_id
                    order by enum.sort_order
                }]
                if {$required_p == "f"} {
                # This is not a required option list... offer a default
                    lappend option_list [list " (no value) " ""]
                }
                template::element create $form_id "$variable_prefix$attribute_name" \
                    -datatype "text" [expr {$required_p eq "f" ? "-optional" : ""}] \
                    -widget select \
                    -options $option_list \
                    -label "$pretty_name" \
                    -value $default
            } else {
                template::element create $form_id "$variable_prefix$attribute_name" \
                    -datatype $datatype [expr {$required_p eq "f" ? "-optional" : ""}] \
                    -widget text \
                    -label $pretty_name \
                    -value $default
            }
        }
    }
}

# Local variables:
#    mode: tcl
#    tcl-indent-level: 4
#    indent-tabs-mode: nil
# End: