Search Box with Filter and Large Drop Down Menu

http://tympanus.net/codrops/2010/07/14/ui-elements-search-box/

http://www.web-delicious.com/jquery-plugins/

This entry moved to http://www.project-open.org/documentation/rfe_advanced_financial_project_planning

The package provides the following features:

  • Provide a single access point for all tasks assigned to an user
  • Gives users the possibility to create a sortable list of tasks
  • Tasks can added from a list of existing project tasks, forum tasks and workflow tasks
  • Let users add arbitrary tasks not managed with ]po[
  • Provide users with quick access to often used features such as time sheet tracking and relevant task data.
  • Speeds up adminstrative tasks and provides convenience to users working in multitasking environments, such as Help-Desk, Call-Center, Support, etc.

Principal architectural objectives:

  • Implementing a RESTful Web Service
  • Evaluate Open Source JS libraries for use in ]po[
  • Security aspects and error handling when using xmlhttp requests (XHR)

Protoytype

To-Do's

  • Fix Bug: time - logging
  • Show err mess when no tasks have been found for project
  • Allow to delete tasks from To-Do list by drag&drop them to wastebasket
  • Add WF tab (shows all tasks resulting from WF instances)
  • Add Forum tab (shows all FORUM tasks)

Developer Documentation

The package is available from the ]po[ CVS server ("experimental status"):

Package Name:
intranet-gtd-dashboard
CVSROOT:
":pserver:anonymous@cvs.openacs.org:/home/cvsroot"

It had been created in 2008/2009. The ]po[ RESTful interface was not yet available.

                Table "public.im_gtd_tasks"
       Column       |           Type           | Modifiers
--------------------+--------------------------+-----------
 gtd_task_id        | integer                  | not null
 ref_id             | integer                  | not null
 owner_id           | integer                  |
 created_date       | timestamp with time zone |
 description        | text                     |
 last_modified      | date                     |
 gtd_type_id        | integer                  | not null
 gtd_task_status_id | integer                  | not null
Indexes:
    "im_gtd_tasks_pkey" PRIMARY KEY, btree (gtd_task_id)
Foreign-key constraints:
    "im_gtd_task_type_fk" FOREIGN KEY (gtd_type_id) REFERENCES im_categories(category_id)
    "im_gtd_task_type_status_fk" FOREIGN KEY (gtd_task_status_id) REFERENCES im_categories(category_id)
    "im_gtd_tasks_owner_id_fkey" FOREIGN KEY (owner_id) REFERENCES users(user_id)

See also:

Control flow / Conditions

Check for existing variable

  • [exists_and_not_null project_id]
  • [info exists project_id]

Localization

Find message in DB:

select * from lang_messages where message like '%Type%'

OpenACS string localization:

[lang::message::lookup "" intranet-core.Execution_Project "Create an 'Execution Project'"]

OpenACS string localization (short version):

[_ intranet-core.Name]
#intranet-core.Filter_Projects#

Localize date to user-locale:

[lc_time_fmt '2009-12-15' %x]

Add message:

SELECT im_lang_add_message('en_US','intranet-audit','Earned_Value','Earned Value');

Localize numbers / beautifying them for output:

/intranet-invoices/www/view.tcl

set rounding_precision 2
set locale [lang::user::locale]
set amount_pretty [lc_numeric [im_numeric_add_trailing_zeros [expr $amount+0] $rounding_precision] "" $locale]
set val_hours_output [format "%0.2f" $val_hours]

Localize labels etc.

[lang::message::lookup "" intranet-core.Execution_Project "Create an Execution Project"]

Localize categories
Pls. see below

Categories

Create new Category

SELECT im_category_new ('71', 'Potential', 'Intranet Project Status');

Create new Category Hierarchy

SELECT im_category_hierarchy_new (58, 56);

Return id of categories with name CATEGORY_NAME

[im_sub_categories CATEGORY_NAME]

Returns name of category

[im_category_from_id CATEGORY_ID]

Transitive closure – Returns categories including sub-categories

im_sub_categories [ -include_disabled_p include_disabled_p ] category_list

Note: category_list is a list of category id’s (integer)

Category drop-down in ad_form – Default
Currently return an empty entry for ‘All’

    ...
    -form {
        {absence_type_id:text(im_category_tree),optional {label "[_ intranet-timesheet2.Absence_Type]"} {value $absence_type_id} {custom {category_type "Intranet Absence Type" translate_p 1} } }
     .. 
    }

Category drop-down in ad_form – Custom, using VIEW

 
    set absences_types [im_memoize_list select_absences_types "select absence_type_id, absence_type from im_absence_types order by lower(ABSENCE_TYPE)"]
    set absences_types [linsert $absences_types 0 "All"]
    set absences_types [linsert $absences_types 0 -1]
    set absence_type_list [list]
    foreach { value text } $absences_types {
        regsub -all " " $text "_" category_key
        set text [lang::message::lookup "" intranet-core.$category_key $text]
        lappend absence_type_list [list $text $value]
    }
    ...
    -form {
        {absence_type_id:text(select),optional {label "[_ intranet-timesheet2.Absence_Type]"} {options $absence_type_list }}     .. 
    }

Plain SQL

SELECT
     im_categories.category_id,
     im_categories.category AS absence_type
FROM
     im_categories
WHERE
     im_categories.category_type::text = 'Intranet Absence Type'::text;

Permissions for pages

Is User logged in?:

set current_user_id [ad_maybe_redirect_for_registration]

Using existing permission procedures:

im_timesheet_task_permissions $current_user_id $project_id view read write admin

sets variables $read $write $admin to true/false

Checking privilege:

if {![im_permission $current_user_id "view_projects_all"]} {
    ad_return_complaint 1 "[_ intranet-core.lt_You_dont_have_suffici_2]"
}

Search for privileges:

SELECT * FROM acs_privileges WHERE pretty_name LIKE '%assign%';

Permissions & Privileges

Create a new privilege:

select acs_privilege__create_privilege('add_tickets','Add Tickets','');
select acs_privilege__add_child('admin', 'add_tickets_for_customers');
select im_priv_create('add_tickets', 'P/O Admins');
select im_priv_create('add_tickets', 'Senior Managers');
select im_priv_create('add_tickets', 'Project Managers');
select im_priv_create('add_tickets', 'Employees');
select im_priv_create('add_tickets', 'Customers');

Check if user has privilege

[im_permission $user_id view_costs]

Remove priviliges

delete from acs_permissions where privilege = 'add_view_internal_rates';
delete from acs_privilege_hierarchy where child_privilege = 'add_view_internal_rates';
delete from acs_privileges where privilege = 'add_view_internal_rates';

Packages

Parameters

[parameter::get -package_id [apm_package_id_from_key intranet-mail-import] -parameter "ScanMails" -default 60]

Does PAckage exist

set im_survey_installed_p [llength [info proc [db_string get_view_id "select package_id from apm_packages where package_key = 'intranet-simple-survey'" -default 0]]]

OR

set survey_exists_p [llength [info commands im_package_survsimp_id]]

Dynfields

Create DynField

 
SELECT im_dynfield_attribute_new (
        'im_timesheet_task', 'effort_driven_type_id', 'Fixed Task Type', 'gantt_fixed_task_type', 'integer', 'f', 0, 'f', 'im_timesheet_tasks'
);

Set DynField permissions

PERFORM acs_permission__grant_permission(v_dynfield_id, (select group_id from groups where group_name=''Employees''), ''read'');
create or replace function inline_0 ()
returns integer as '
declare
        v_dynfield_id           integer;
begin

        SELECT im_dynfield_attribute_new (
                'im_timesheet_task', 'effort_driven_type_id', 'Fixed Task Type', 'gantt_fixed_task_type', 'integer', 'f', 0, 'f', 'im_timesheet_tasks'
        );

        PERFORM acs_permission__grant_permission(v_dynfield_id, (select group_id from groups where group_name=''Employees''), ''read'');
        PERFORM acs_permission__grant_permission(v_dynfield_id, (select group_id from groups where group_name=''Employees''), ''write'');

        return 0;

end;' language 'plpgsql';
select inline_0 ();
drop function inline_0 ();

Views

Create View

insert into im_views (view_id, view_name, visible_for, view_type_id)
values (80, 'office_list', 'view_offices', 1400);

Create View Columns

insert into im_view_columns (column_id, view_id, group_id, column_name, column_render_tcl,
extra_select, extra_where, sort_order, visible_for) values (2221,22,NULL,'Quote',
'$quote_date','','',18,'');

User Management

Is user member of a project?

[im_biz_object_member_p $user_id $project_id]

Is user employee?

[im_user_is_employee_p $user_id]

Is user employee?

[im_profile::member_p -profile_id [im_employee_group_id] -user_id $user_id]

Is user customer?

[im_profile::member_p -profile_id [im_customer_group_id] -user_id $user_id]

Is user freelancer?

[im_profile::member_p -profile_id [im_freelance_group_id] -user_id $user_id]

Is user customer?

[im_profile::member_p -profile_id [im_customer_group_id] -user_id $user_id]

Is user Project Manager?

[im_biz_object_admin_p $user_id $project_id]

Is user admin?

[im_is_user_site_wide_or_intranet_admin $user_id]

Current user

po34klaus=# select current_user;
 current_user
--------------
 po34klaus
(1 row)

Database

db_foreach

    set column_sql "
        select
                *
        from
                im_view_columns
        where
                view_id=:view_id
                and group_id is null
        order by
                sort_order
    "
    db_foreach column_list_sql $column_sql {
        if {"" == $visible_for || [eval $visible_for]} {
            lappend column_headers "$column_name"
            lappend column_vars "$column_render_tcl"
        } 
        ....
    }

db_1row
Use: Set variables for a query returning exactly one row.

Sample:

         db_1row sender_get_info_1 "
 
                select
                        pe.first_names as accounting_first_name,
                        pe.last_name as accounting_last_name
                from
                        persons
                where
                        person_id = $accounting_contact_id
 
                       "

db_0or1row
Like db_1row but returns an error if more than one row is returned.

db_string
Use: Getting a single string value from the db

Sample:

set view_id [db_string get_view_id "select view_id from im_views where view_name=:view_name" -default 0]

db_dml – performs a dml statement

db_dml delete "
                delete from im_object_freelance_skill_map
                where
                        object_id = :object_id
                        and skill_type_id = :skill_type_id
                        and skill_id in ([join $skill_id ","])
"

Create a sequence

create sequence im_inquiries_files_seq start 1;

Get id from sequence

set inquiry_files_id [db_string nextval "select nextval('im_inquiries_files_seq');"]

Check if table exists

<ul>
  <li>db_table_exists TABLE_NAME </li>
  <li>[im_table_exists im_freelance_rfqs]</li>
</ul>

DB Transactions

    db_transaction {
        db_dml test "nonsense"
    } on_error {
        ad_return_error "Error in blah/foo/bar" "The error was: $errmsg"
    }

String operations

String concatenation

Alternative to concat & append – in case var’s are ambigious:

set var_amount "amount.${user_id}_$project_type_id"
</p>
 
<strong>Check for empty string</strong>
<ul>
	<li>empty_string_p [VAR]</li>
</ul>

Sample:

if { ![empty_string_p $where_clause] } {
    set where_clause " and $where_clause"
}

Compare strings in tcl

if { -1 != [string compare $list_order_by "min(p.price)"] } {
                ad_return_complaint 1 "here"
 
                set sorted_table_rows [qsort $table_rows [lambda {s} { lindex $s 2 }]]
        } else {
 
                set sorted_table_rows $table_rows
        }

Formating for amounts, etc.

set amount_pretty [im_numeric_add_trailing_zeros [expr $amount+0] 2]

Error handling

Handling expr error

         if {"" != $visible_tcl} {
            set visible 0
            set errmsg ""
            if [catch {
                set visible [expr $visible_tcl]
            } errmsg] {
                ad_return_complaint 1 "$visible_tcl\n$errmsg"
            }
            if {!$visible} { continue }
        }

Handling db error

    if {[catch { db_dml target_languages " select * from im_projects" } errmsg ]} {
        append errors "..."
    }

Page contract

Optional parameters

ad_proc im_costs_base_component { user_id {company_id ""} {project_id ""} } {
    Returns a HTML table containing a list of costs for a particular
    company or project.
} {
...
    }

Passing URL parameters

  1. set primary_contact_link "<a href=primary-contact?[export_url_vars company_id limit_to_users_in_group_id]>Add primary contact</a>\n"

Forms

Arrays – Create form

  1. <input type='checkbox' name='payed_p.$cost_id'>

Arrays – Handle form

ad_proc im_costs_base_component {
   ...
} {
    payed_p:array,optional
}

Add hidden vars to form

  1. [export_form_vars company_id return_url]

Create list from form var array:

set payed_p_list [array names payed_p]
 
set hours_list [array names hours]
foreach hour_rec $hours_list {
    ns_log NOTICE "hour_rec $hour_rec /  value: $hours($hour_rec)"
}

Sanity checks

if {![im_column_exists im_hours internal_note]} {
    ad_return_complaint 1 "Internal error in intranet-timesheet2:<br>
        The field im_hours.internal_note is missing.<br>
        Please notify your system administrator to upgrade
        your system to the latest version.<br>
    "
    ad_script_abort
}

Date & Time operations

Moved to:
http://www.project-open.org/documentation/date_and_time_operations

Math operations


using round in sql

        /packages/intranet-reporting-finance/www/finance-expenses.tcl
        select
                ...
                round((c.paid_amount * im_exchange_rate(c.effective_date::date, c.currency, 'EUR')) :: numeric, 2) as paid_amount_converted,
        from ...

Menus


Add menu:

create or replace function inline_1 ()
returns integer as '
declare
        v_menu                  integer;
        v_parent_menu         integer;
        v_employees             integer;
begin
        select group_id into v_employees from groups where group_name = ''Employees'';
 
        select menu_id into v_parent_menu
        from im_menus where label = ''projects_admin'';
 
        v_menu := im_menu__new (
                null,                                   -- p_menu_id
                ''im_menu'',                            -- object_type
                now(),                                  -- creation_date
                null,                                   -- creation_user
                null,                                   -- creation_ip
                null,                                   -- context_id
                ''intranet-cust-kw'',   -- package_name
                ''project_closure_wf'', -- label
                ''Close Project'',      -- name
                ''/intranet-cust-kw/workflow-close-project?project_id=$project_id'',   -- url
                -10,                                    -- sort_order
                v_parent_menu,                          -- parent_menu_id
                null                                    -- p_visible_tcl
        );
 
        PERFORM acs_permission__grant_permission(v_menu, v_employees, ''read'');
        return 0;
end;' language 'plpgsql';
select inline_1 ();
drop function inline_1();

Get menu items for side menu:

[im_menu_ul_list -no_uls 1 "projects_admin" {}]

Setting Subnavbar:

# Setup the subnavbar
set bind_vars [ns_set create]
ns_set put $bind_vars project_id $project_id
set project_menu_id [db_string parent_menu "select menu_id from im_menus where label='project'" -default 0]
 
set sub_navbar [im_sub_navbar \
    -components \
    -base_url "/intranet/projects/view?project_id=$project_id" \
    $project_menu_id \
    $bind_vars "" "pagedesriptionbar" "project_timesheet_task"]

Setting menu label active:

set menu_label "reporting-finance-expenses"

Context Help

a) Add to [file].tcl

set show_context_help_p 1

b) Add to [file].adp

<property name="show_context_help_p">@show_context_help_p;noquote@</property>

AOL Server API


Avoid absolute file paths when using ‘ns_returnredirect’

ns_returnredirect "upload-files.tcl?inq_id=$security_token"
ns_returnfile 200 "application/zip" $filepath
ns_return 200 text/html 1
set result [ad_parse_template -params $params "/packages/intranet-core/www/related-objects-component"]
[ns_conn url] #returns for example: /intranet/projects
[im_url_with_query] # returns for example: /intranet/projects/view?project%5fid=34352

Portlet/Plugin

Create Plugin

SELECT  im_component_plugin__new (
        null,                           -- plugin_id
        'acs_object',                -- object_type
        now(),                        -- creation_date
        null,                           -- creation_user
        null,                           -- creation_ip
        null,                           -- context_id
        'Home Random Portrait', -- plugin_name
        'intranet-core',             -- package_name
        'right',                        -- location
        '/intranet/index',           -- page_url
        null,                           -- view_name
        5,                              -- sort_order
        'im_random_employee_component'  -- component_tcl
);

Set permissions for plugin

        select  plugin_id
        into    v_plugin_id
        from    im_component_plugins pl
        where   plugin_name = ''Requests for Quote'';
 
        PERFORM im_grant_permission(v_plugin_id, v_cust_id, ''read'');

Workflow

Actions

im_workflow__set_object_status_id
im_workflow__assign_to_owner

Templates / Control structures in ADP pages

VAR replacements

<%= [eval ad_context_bar $context_bar] %>

Control Structures & Loops

  <multiple name="user_stuff">
     <if @user_stuff.age@ le 21>
         <tr bgcolor="red">
     </if>
     <else>
         <tr bgcolor="green">
     </else>
         <td>@user_stuff.user_name@</td>
         <td>@user_stuff.age@</td>
         <td>@user_stuff.shoesize@</td>
     </tr>
  </multiple>

Callbacks

Define Callback:

Callbacks require a callback definition in *-init.tcl:
Example: intranet-core-init.tcl

ad_proc -public -callback im_dynfield_attribute_after_update {
    {-object_type:required}
    {-attribute_name:required}
} {
    Callback to be executed after an attribute has been changed
} -

Please note the hyphen at the end of the code!

Callback implementation:

Add the function to your [PACKAGE_NAME]-procs.tcl:

ad_proc -public -callback im_dynfield_attribute_after_update -impl intranet-core  {
    {-object_type:required}
    {-attribute_name:required}
} {
    << EXPLAIN >>
} {
    << FUNCTION BODY >>
}

Callback Call

Place a call to the callback anywhere in your code

callback im_dynfield_attribute_after_update -object_type "im_project" -attribute_name "christmas_card"

Include JS/CSS ( ]po[ V4.0 )

 
template::head::add_css -href "/intranet-sencha/css/ext-all.css" -media "screen" -order 1
template::head::add_javascript -src "/intranet-sencha/js/ext-all-debug-w-comments.js" -order 1

Parameters

UPDATE
    apm_parameter_values
SET
    attr_value=0
WHERE
    parameter_id in (
        select
            parameter_id
        from
            apm_parameters
        where
            parameter_name = 'SuppressHttpPort' and
            package_key = 'acs-tcl'
      )

Notifications:

/intranet-helpdesk/www/new.tcl

    notification::new \
        -type_id [notification::type::get_type_id -short_name ticket_notif] \
        -object_id $ticket_id \
        -response_id “” \
        -notif_subject $ticket_name \
        -notif_text $message

One thing we have learned today is that going to AJAX requires also a review of current HTML. Even though most pages show only warnings when HTML is validated, they are still not conform when using script libraries.

In our example we’d like extend the Home Page Project Component to introduce tabs. “open” projects and “potential” projects should be shown in the same component, accessible through tabs. In order to implement tabs we are using the YUI TabView Control.

Here’s the current code to create the main table that holds the table of projects (intranet-component-procs.tcl):


Implementation is done as follows:


This results in:

List of projects (erroneous)

As we see, Tab Two Content is visible, clicking on the tabs also results in undesired behaviour.

Firebugs shows us the following code:
Resulting code as listed in Firebug

FF renders based on the definition for each element an additional font tags which then breaks the JS code.

Nov 252008

Before a user can create or view a Financial Document, a permission check is performed. As a sample we will have a closer look what is checks are performed when a user wants to create a new quote.

Permission checks creation of a new quote: /intranet-invoices/new?cost_type_id=3702

1) Depending on the required permission type (read/write) a list of cost_type_ids is generated:

set create_cost_types [im_cost_type_write_permissions $user_id]

“im_cost_type_write_permissions” performs the following checks:

    set can_write [expr [im_permission $user_id add_costs] || [im_permission $user_id add_invoices]]
    if {!$can_write} { return [list] }

    set result [db_list writable_cost_centers "
        select distinct
                ct.cost_type_id
        from
                im_cost_centers cc,
                im_cost_types ct,
                acs_permissions p,
                party_approved_member_map m,
                acs_object_context_index c,
                acs_privilege_descendant_map h
        where
                cc.cost_center_id = c.object_id
                and ct.write_privilege = h.descendant
                and p.object_id = c.ancestor_id
                and m.member_id = :user_id
                and p.privilege = h.privilege
                and p.grantee_id = m.party_id
    "]
    return $result

Let’s have a closer look at this sql statement and the tables and views involved.

Table: im_cost_centers

… all Cost Centers …

im_cost_centers.png

View: im_cost_types

im_cost_types contains all privileges related to the “Financial Documents”

im_cost_types.png

Table: acs_permissions

Our well known acs_permissions, mapping object_id (1st column), grantee_id (2nd column) and privilege (3rd column).
More information here.

acs_permissions.png

Records in acs_permissions are added/removed setting values in /intranet/admin/profiles/
In case a new privilege is set, a record will be added to this table, if a privilege is removed, the respective record will be removed.

Table: party_approved_member_map

If user is a member of more than one profile, his accumulates the permissions of all profile he’s a member of. User/Profile relationship is managed in table “parties”.
Table “party_approved_member_map” maps a party to the fully expanded list of parties represented by that party including the party itself. So if a party is an individual this view will have exactly one mapping hat is from that party to itself. If a view is a group containing three individuals,this view will have four rows for that party, one for each member,and one from the party to itself. As the table name indicates, only approved members are considered. More information on Open ACS parties here

party_approved_memeber_map.png

Table: acs_privilege_descendant_map

.. pretty obvious too, manages the relationship between related parent/child privileges:

| oid | privilege | descendant |
acs_privilege_descendant_map.png

Table: acs_object_context_index

This table manages the inheritance of privileges. See OpenACS Permissions Tediously Explained – by Vadim Nasardinov for further information.

| object_id | ancestor_id | n_generations |

acs_object_context_index.png

Based from what we have learned about the tables involved we now investigate further the “where” clause:

...
        where
                cc.cost_center_id = c.object_id
                and ct.write_privilege = h.descendant
                and p.object_id = c.ancestor_id
                and m.member_id = :user_id
                and p.privilege = h.privilege
                and p.grantee_id = m.party_id

a) If a permission is granted for the parent “Cost Center”, it should also be granted for its childs:

im_cost_centers.cost_center_id = acs_object_context_index.object_id" 

b) The same is true for cost types, a privilege granted on a higher level should be inherited to its childs:

im_cost_types.write_privilege = acs_privilege_descendant_map.descendant

c) … and for parties (user):

acs_permissions.object_id = acs_object_context_index.ancestor_id

d) Get (approved ) parties related to user to look up:

party_approved_member_map.member_id = :user_id

e) Include all sub privileges:

acs_permissions.privilege = acs_privilege_descendant_map.privilege

f) Limit acs_permissions to approved members

acs_permissions.grantee_id = party_approved_member_map.party_id

You might want to take some time to digest that …

2) In a second step we check, if the Financial Document to be shown/created is member of this list:

    if {[lsearch -exact $create_cost_types $cost_type_id] == -1} {
        ad_return_complaint "Insufficient Privileges" "
  • You don't have sufficient privileges to create a [db_string t "select im_category_from_id(:cost_type_id)"]." return }