Index: openacs-4/packages/bug-tracker/tcl/bug-tracker-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/bug-tracker/tcl/bug-tracker-procs.tcl,v diff -u -r1.17 -r1.18 --- openacs-4/packages/bug-tracker/tcl/bug-tracker-procs.tcl 13 Jan 2003 15:26:06 -0000 1.17 +++ openacs-4/packages/bug-tracker/tcl/bug-tracker-procs.tcl 5 Mar 2003 17:40:50 -0000 1.18 @@ -8,10 +8,14 @@ } -namespace eval ::bug_tracker {} +namespace eval bug_tracker {} -ad_proc ::bug_tracker::conn { args } { +ad_proc bug_tracker::package_key {} { + return "bug-tracker" +} +ad_proc bug_tracker::conn { args } { + global bt_conn set flag [lindex $args 0] @@ -32,7 +36,14 @@ return $bt_conn($var) } else { switch -- $var { - project_name - project_description - current_version_id - current_version_name { + bug - bugs - Bug - Bugs - + component - components - Component - Components { + get_pretty_names -array bt_conn + return $bt_conn($var) + } + project_name - project_description - + project_root_keyword_id - project_folder_id - + current_version_id - current_version_name { array set info [get_project_info] foreach name [array names info] { set bt_conn($name) $info($name) @@ -50,7 +61,10 @@ return $bt_conn($var) } } - component_id - filter - filter_human_readable - filter_where_clauses - filter_order_by_clause { + component_id - + filter - filter_human_readable - + filter_where_clauses - + filter_order_by_clause - filter_from_bug_clause { return {} } default { @@ -66,11 +80,27 @@ } } -ad_proc ::bug_tracker::get_bug_id { +ad_proc bug_tracker::get_pretty_names { + -array:required +} { + upvar $array row + + set row(bug) [parameter::get -parameter "TicketPrettyName" -default "bug"] + set row(bugs) [parameter::get -parameter "TicketPrettyPlural" -default "bugs"] + set row(Bug) [string totitle $row(bug)] + set row(Bugs) [string totitle $row(bugs)] + + set row(component) [parameter::get -parameter "ComponentPrettyName" -default "component"] + set row(components) [parameter::get -parameter "ComponentPrettyPlural" -default "components"] + set row(Component) [string totitle $row(component)] + set row(Components) [string totitle $row(components)] +} + +ad_proc bug_tracker::get_bug_id { {-bug_number:required} {-project_id:required} } { - return [db_string bug_id { select bug_id from bt_bugs where bug_number = :bug_number and project_id = :project_id }] + return [db_string bug_id {}] } ##### @@ -79,103 +109,107 @@ # ##### -ad_proc ::bug_tracker::get_project_info_internal { +ad_proc bug_tracker::get_project_info_internal { package_id } { - set found_p [db_0or1row project_info { - select pck.instance_name as project_name, - prj.description as project_description, - ver.version_id as current_version_id, - coalesce(ver.version_name, 'None') as current_version_name - from apm_packages pck, - bt_projects prj - left outer join bt_versions ver - on (ver.project_id = prj.project_id and active_version_p = 't') - where pck.package_id = :package_id - and prj.project_id = pck.package_id - } -column_array result] + db_1row project_info {} -column_array result - if { !$found_p } { - set count [db_string count_project { select count(*) from bt_projects where project_id = :package_id }] - if { $count == 0 } { - db_exec_plsql create_project { - select bt_project__new(:package_id) - } - # we call ourselves again, so we'll get the info this time - return [get_project_info_internal $package_id] - } else { - error "Couldn't find project in database" - } - } else { - return [array get result] - } + return [array get result] } -ad_proc ::bug_tracker::get_project_info { +ad_proc bug_tracker::get_project_info { -package_id } { if { ![info exists package_id] } { set package_id [ad_conn package_id] } - # temp hack: don't cache - return [get_project_info_internal $package_id] + return [util_memoize [list bug_tracker::get_project_info_internal $package_id]] +} - # return [util_memoize "bt_get_project_info_internal $package_id"] +ad_proc bug_tracker::get_project_info_flush { + -package_id +} { + if { ![info exists package_id] } { + set package_id [ad_conn package_id] + } + + util_memoize_flush [list bug_tracker::get_project_info_internal $package_id] } -ad_proc ::bug_tracker::set_project_name { +ad_proc bug_tracker::set_project_name { -package_id project_name } { if { ![info exists package_id] } { set package_id [ad_conn package_id] } - db_dml project_name_update { - update apm_packages - set instance_name = :project_name - where package_id = :package_id - } + db_dml project_name_update {} # Flush cache - util_memoize_flush "bt_get_project_info_internal $package_id" + util_memoize_flush [list bug_tracker::get_project_info_internal $package_id]] } + + + +##### +# +# Stats procs (cache eventually) +# +##### + + +ad_proc -public bug_tracker::bugs_exist_p { + {-package_id {}} +} { + Returns whether any bugs exist in a project +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + + return [util_memoize [list bug_tracker::bugs_exist_p_not_cached -package_id $package_id]] +} +ad_proc -public bug_tracker::bugs_exist_p_set_true { + {-package_id {}} +} { + Sets bug_exists_p true. Useful for when you add a new bug, so you know that a bug will exist. +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + + return [util_memoize_seed [list bug_tracker::bugs_exist_p_not_cached -package_id $package_id] 1] +} +ad_proc -public bug_tracker::bugs_exist_p_not_cached { + -package_id:required +} { + Returns whether any bugs exist in a project. Not cached. +} { + return [db_string select_bugs_exist_p {} -default 0] +} + ##### # # Cached user prefs procs # ##### -ad_proc ::bug_tracker::get_user_prefs_internal { +ad_proc bug_tracker::get_user_prefs_internal { package_id user_id } { - set found_p [db_0or1row user_info { - select u.first_names as user_first_names, - u.last_name as user_last_name, - u.email as user_email, - ver.version_id as user_version_id, - coalesce(ver.version_name, 'None') as user_version_name - from cc_users u, - bt_user_prefs up - left outer join bt_versions ver - on (ver.version_id = up.user_version) - where u.user_id = :user_id - and up.user_id = u.user_id - and up.project_id = :package_id - } -column_array result] + set found_p [db_0or1row user_info { } -column_array result] if { !$found_p } { - set count [db_string count_user_prefs { select count(*) from bt_user_prefs where project_id = :package_id and user_id = :user_id }] + set count [db_string count_user_prefs {}] if { $count == 0 } { - db_dml create_user_prefs { - insert into bt_user_prefs (user_id, project_id) values (:user_id, :package_id) - } + db_dml create_user_prefs {} # we call ourselves again, so we'll get the info this time return [get_user_prefs_internal $package_id $user_id] } else { @@ -186,7 +220,7 @@ } } -ad_proc ::bug_tracker::get_user_prefs { +ad_proc bug_tracker::get_user_prefs { -package_id -user_id } { @@ -198,10 +232,10 @@ set user_id [ad_conn user_id] } - return [util_memoize "bug_tracker::get_user_prefs_internal $package_id $user_id"] + return [util_memoize [list bug_tracker::get_user_prefs_internal $package_id $user_id]] } -ad_proc ::bug_tracker::get_user_prefs_flush { +ad_proc bug_tracker::get_user_prefs_flush { -package_id -user_id } { @@ -213,7 +247,7 @@ set user_id [ad_conn user_id] } - util_memoize_flush "bug_tracker::get_user_prefs_internal $package_id $user_id" + util_memoize_flush [list bug_tracker::get_user_prefs_internal $package_id $user_id] } @@ -223,11 +257,11 @@ # ##### -ad_proc ::bug_tracker::bug_type_get_options {} { +ad_proc bug_tracker::bug_type_get_options {} { return { { "Bug" bug } { "Suggestion" suggestion } { "Todo" todo } } } -ad_proc ::bug_tracker::bug_type_pretty { +ad_proc bug_tracker::bug_type_pretty { bug_type } { array set bug_types { @@ -249,30 +283,43 @@ # ##### -ad_proc ::bug_tracker::status_get_options {} { - return { { "Open" open } { "Resolved" resolved } { "Closed" closed } } +ad_proc bug_tracker::status_get_options { + {-package_id ""} +} { + if { [empty_string_p $package_id] } { + set package_id [ad_conn package_id] + } + + set workflow_id [bug_tracker::bug::get_instance_workflow_id -package_id $package_id] + set state_ids [workflow::fsm::get_states -workflow_id $workflow_id] + + set option_list [list] + foreach state_id $state_ids { + workflow::state::fsm::get -state_id $state_id -array state + lappend option_list [list "$state(pretty_name)" $state(short_name)] + } + + return $option_list } -ad_proc ::bug_tracker::status_pretty { +ad_proc bug_tracker::status_pretty { status } { - array set status_codes { - open "Open" - resolved "Resolved" - closed "Closed" - } - if { [info exists status_codes($status)] } { - return $status_codes($status) - } else { + set workflow_id [bug_tracker::bug::get_instance_workflow_id] + if { [catch {set state_id [workflow::state::fsm::get_id -workflow_id $workflow_id -short_name $status]} error] } { return "" } + + workflow::state::fsm::get -state_id $state_id -array state + + return $state(pretty_name) } -ad_proc ::bug_tracker::patch_status_get_options {} { +ad_proc bug_tracker::patch_status_get_options {} { return { { "Open" open } { "Accepted" accepted } { "Refused" refused } { "Deleted" deleted }} } -ad_proc ::bug_tracker::patch_status_pretty { +ad_proc bug_tracker::patch_status_pretty { status } { array set status_codes { @@ -294,14 +341,14 @@ # ##### -ad_proc ::bug_tracker::resolution_get_options {} { +ad_proc bug_tracker::resolution_get_options {} { return { { "Fixed" fixed } { "By Design" bydesign } { "Won't Fix" wontfix } { "Postponed" postponed } { "Duplicate" duplicate } { "Not Reproducable" norepro } { "Need Info" needinfo } } } -ad_proc ::bug_tracker::resolution_pretty { +ad_proc bug_tracker::resolution_pretty { resolution } { array set resolution_codes { @@ -328,10 +375,17 @@ # ##### -ad_proc ::bug_tracker::severity_codes_get_options { +ad_proc bug_tracker::severity_codes_get_options { } { +# XXX FIXME obsolete set package_id [ad_conn package_id] - + return [util_memoize [list bug_tracker::severity_codes_get_options_not_cached $package_id]] +} + +ad_proc bug_tracker::severity_codes_get_options_not_cached { + package_id +} { +# XXX FIXME obsolete set severity_list [db_list_of_lists severities { select sort_order || ' - ' || severity_name, severity_id from bt_severity_codes @@ -342,10 +396,17 @@ return $severity_list } -ad_proc ::bug_tracker::severity_get_default { +ad_proc bug_tracker::severity_get_default { } { +# XXX FIXME obsolete set package_id [ad_conn package_id] - + return [util_memoize [list bug_tracker::severity_get_default_not_cached $package_id]] +} + +ad_proc bug_tracker::severity_get_default_not_cached { + package_id +} { +# XXX FIXME obsolete set default_severity_id [db_string default_severity { select severity_id from bt_severity_codes @@ -358,10 +419,17 @@ return $default_severity_id } -ad_proc ::bug_tracker::priority_codes_get_options { +ad_proc bug_tracker::priority_codes_get_options { } { +# XXX FIXME obsolete set package_id [ad_conn package_id] - + return [util_memoize [list bug_tracker::priority_codes_get_options_not_cached $package_id]] +} + +ad_proc bug_tracker::priority_codes_get_options_not_cached { + package_id +} { +# XXX FIXME obsolete set priority_list [db_list_of_lists priorities { select sort_order || ' - ' || priority_name, priority_id from bt_priority_codes @@ -372,10 +440,17 @@ return $priority_list } -ad_proc ::bug_tracker::priority_get_default { +ad_proc bug_tracker::priority_get_default { } { +# XXX FIXME obsolete set package_id [ad_conn package_id] - + return [util_memoize [list bug_tracker::priority_get_default_not_cached $package_id]] +} + +ad_proc bug_tracker::priority_get_default_not_cached { + package_id +} { +# XXX FIXME obsolete set default_priority_id [db_string default_priority { select priority_id from bt_priority_codes @@ -389,21 +464,347 @@ } +##### +# +# Categories/Keywords +# +##### +ad_proc bug_tracker::category_parent_heading { + {-package_id ""} + -keyword_id:required +} { + foreach elm [get_keywords -package_id $package_id] { + set child_id [lindex $elm 0] + set child_heading [lindex $elm 1] + set parent_id [lindex $elm 2] + set parent_heading [lindex $elm 3] + + if { $child_id == $keyword_id } { + return $parent_heading + } + } +} + +ad_proc bug_tracker::category_heading { + {-package_id ""} + -keyword_id:required +} { + foreach elm [get_keywords -package_id $package_id] { + set child_id [lindex $elm 0] + set child_heading [lindex $elm 1] + set parent_id [lindex $elm 2] + set parent_heading [lindex $elm 3] + + if { $child_id == $keyword_id } { + return $child_heading + } + } +} + +ad_proc bug_tracker::category_types { + {-package_id ""} +} { + @return Returns the category types for this instance as an + array-list of { parent_id1 heading1 parent_id2 heading2 ... } +} { + array set heading [list] + set parent_ids [list] + + set last_parent_id {} + foreach elm [get_keywords -package_id $package_id] { + set child_id [lindex $elm 0] + set child_heading [lindex $elm 1] + set parent_id [lindex $elm 2] + set parent_heading [lindex $elm 3] + + if { $parent_id != $last_parent_id } { + set heading($parent_id) $parent_heading + lappend parent_ids $parent_id + set last_parent_id $parent_id + } + } + + set result [list] + foreach parent_id $parent_ids { + lappend result $parent_id $heading($parent_id) + } + return $result +} + +ad_proc bug_tracker::category_get_options { + {-package_id ""} + {-parent_id:required} +} { + @param parent_id The category type's keyword_id + @return options-list for a select widget for the given category type +} { + set options [list] + foreach elm [get_keywords -package_id $package_id] { + set elm_child_id [lindex $elm 0] + set elm_child_heading [lindex $elm 1] + set elm_parent_id [lindex $elm 2] + + if { $elm_parent_id == $parent_id } { + lappend options [list $elm_child_heading $elm_child_id] + } + } + return $options +} + + +## Cache maintenance + +ad_proc -private bug_tracker::get_keywords { + {-package_id ""} +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + return [util_memoize [list bug_tracker::get_keywords_not_cached -package_id $package_id]] +} + +ad_proc -private bug_tracker::get_keywords_flush { + {-package_id ""} +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + util_memoize_flush [list bug_tracker::get_keywords_not_cached -package_id $package_id] +} + +ad_proc -private bug_tracker::get_keywords_not_cached { + -package_id:required +} { + return [db_list_of_lists select_package_keywords {}] +} + + + + + +ad_proc -public bug_tracker::set_default_keyword { + {-package_id ""} + {-parent_id:required} + {-keyword_id:required} +} { + Set the default keyword for a given type (parent) +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + + # LARS NEW QUERIES + + db_dml delete_existing { + delete + from bt_default_keywords + where project_id = :package_id + and parent_id = :parent_id + } + + db_dml insert_new { + insert into bt_default_keywords (project_id, parent_id, keyword_id) + values (:package_id, :parent_id, :keyword_id) + } + get_default_keyword_flush -package_id $package_id -parent_id $parent_id +} + +ad_proc -public bug_tracker::get_default_keyword { + {-package_id ""} + {-parent_id:required} +} { + Get the default keyword for a given type (parent) +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + + return [util_memoize [list bug_tracker::get_default_keyword_not_cached -package_id $package_id -parent_id $parent_id]] +} + +ad_proc -public bug_tracker::get_default_keyword_flush { + {-package_id ""} + {-parent_id:required} +} { + Flush the cache for +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + + util_memoize_flush [list bug_tracker::get_default_keyword_not_cached -package_id $package_id -parent_id $parent_id] +} + + +ad_proc -private bug_tracker::get_default_keyword_not_cached { + {-package_id:required} + {-parent_id:required} +} { + Get the default keyword for a given type (parent), not cached. +} { + # LARS NEW QUERIES + + return [db_string default { + select keyword_id + from bt_default_keywords + where project_id = :package_id + and parent_id = :parent_id + } -default {}] +} + + + + + +ad_proc -public bug_tracker::get_default_configurations {} { + Get the package's default configurations for categories and parameters. +} { + return { + "Bug-Tracker" { + categories { + "Bug Type" { + "*Bug" + "Suggestion" + "Todo" + } + "Priority" { + "1 - High" + "*2 - Normal" + "3 - Low" + } + "Severity" { + "1 - Critical" + "2 - Major" + "*3 - Normal" + "4 - Minor" + "5 - Trivial" + "6 - Enhancement" + } + } + parameters { + TicketPrettyName "bug" + TicketPrettyPlural "bugs" + ComponentPrettyName "component" + ComponentPrettyPlural "components" + PatchesP "1" + VersionsP "1" + } + } + "Ticket-Tracker" { + categories { + "Ticket Type" { + "*Todo" + "Suggestion" + } + "Priority" { + "1 - High" + "*2 - Normal" + "3 - Low" + } + } + parameters { + TicketPrettyName "ticket" + TicketPrettyPlural "tickets" + ComponentPrettyName "area" + ComponentPrettyPlural "areas" + PatchesP "0" + VersionsP "0" + } + } + } +} + +ad_proc -public bug_tracker::delete_all_project_keywords { + {-package_id ""} +} { + Deletes all the keywords in a project +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + db_exec_plsql keywords_delete {} + bug_tracker::get_keywords_flush -package_id $package_id +} + +ad_proc -public bug_tracker::install_keywords_setup { + {-package_id ""} + -spec:required +} { + @param spec is an array-list of { Type1 { cat1 cat2 cat3 } Type2 { cat1 cat2 cat3 } } + Default category within type is denoted by letting the name start with a *, + which is removed before creating the keyword. +} { + set root_keyword_id [bug_tracker::conn project_root_keyword_id -package_id $package_id] + + foreach { category_type categories } $spec { + set category_type_id [cr::keyword::get_keyword_id \ + -parent_id $root_keyword_id \ + -heading $category_type] + + if { [empty_string_p $category_type_id] } { + set category_type_id [cr::keyword::new \ + -parent_id $root_keyword_id \ + -heading $category_type] + } + + foreach category $categories { + if { [string equal [string index $category 0] "*"] } { + set default_p 1 + set category [string range $category 1 end] + } else { + set default_p 0 + } + + set category_id [cr::keyword::get_keyword_id \ + -parent_id $category_type_id \ + -heading $category] + + if { [empty_string_p $category_id] } { + set category_id [cr::keyword::new \ + -parent_id $category_type_id \ + -heading $category] + } + + if { $default_p } { + bug_tracker::set_default_keyword \ + -parent_id $category_type_id \ + -keyword_id $category_id + } + } + } + bug_tracker::get_keywords_flush -package_id $package_id +} + +ad_proc -public bug_tracker::install_parameters_setup { + {-package_id ""} + -spec:required +} { + @param parameters as an array-list of { name value name value ... } +} { + foreach { name value } $spec { + parameter::set_value -package_id $package_id -parameter $name -value $value + } +} + + + ##### # # Versions # ##### -ad_proc ::bug_tracker::version_get_options { +ad_proc bug_tracker::version_get_options { + -package_id -include_unknown:boolean -include_undecided:boolean } { - set package_id [ad_conn package_id] + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } - set versions_list [db_list_of_lists versions \ - { select version_name, version_id from bt_versions where project_id = :package_id order by version_name }] + set versions_list [util_memoize [list bug_tracker::version_get_options_not_cached $package_id]] if { $include_unknown_p } { set versions_list [concat { { "Unknown" "" } } $versions_list] @@ -417,19 +818,65 @@ } +ad_proc bug_tracker::versions_p { + {-package_id ""} +} { + Is the versions feature turned on? +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + + return [parameter::get -package_id [ad_conn package_id] -parameter "VersionsP" -default 1] +} + + +ad_proc bug_tracker::versions_flush {} { + set package_id [ad_conn package_id] + util_memoize_flush [list bug_tracker::version_get_options_not_cached $package_id] +} + +ad_proc bug_tracker::version_get_options_not_cached { + package_id +} { + set versions_list [db_list_of_lists versions {}] + + return $versions_list +} + +ad_proc bug_tracker::version_get_name { + {-package_id ""} + {-version_id:required} +} { + if { [empty_string_p $version_id] } { + return {} + } + foreach elm [version_get_options -package_id $package_id] { + set name [lindex $elm 0] + set id [lindex $elm 1] + if { [string equal $id $version_id] } { + return $name + } + } + error "Version_id $version_id not found" +} + + ##### # # Components # ##### -ad_proc ::bug_tracker::components_get_options { +ad_proc bug_tracker::components_get_options { + {-package_id ""} -include_unknown:boolean } { - set package_id [ad_conn package_id] + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } - set components_list [db_list_of_lists components \ - { select component_name, component_id from bt_components where project_id = :package_id order by component_name }] + set components_list [util_memoize [list bug_tracker::components_get_options_not_cached $package_id]] if { $include_unknown_p } { set components_list [concat { { "Unknown" "" } } $components_list] @@ -438,42 +885,89 @@ return $components_list } +ad_proc bug_tracker::components_flush {} { + set package_id [ad_conn package_id] + util_memoize_flush [list bug_tracker::components_get_options_not_cached $package_id] + util_memoize_flush [list bug_tracker::components_get_url_names_not_cached -package_id $package_id] +} +ad_proc bug_tracker::components_get_options_not_cached { + package_id +} { + set components_list [db_list_of_lists components {}] + + return $components_list +} + +ad_proc bug_tracker::component_get_name { + {-package_id ""} + {-component_id:required} +} { + if { [empty_string_p $component_id] } { + return {} + } + foreach elm [components_get_options -package_id $package_id] { + set id [lindex $elm 1] + if { [string equal $id $component_id] } { + return [lindex $elm 0] + } + } + error "Component_id $component_id not found" +} + +ad_proc bug_tracker::component_get_url_name { + {-package_id ""} + {-component_id:required} +} { + if { [empty_string_p $component_id] } { + return {} + } + foreach { id url_name } [components_get_url_names -package_id $package_id] { + if { [string equal $id $component_id] } { + return $url_name + } + } + return {} +} + +ad_proc bug_tracker::components_get_url_names { + {-package_id ""} +} { + if { ![exists_and_not_null package_id] } { + set package_id [ad_conn package_id] + } + return [util_memoize [list bug_tracker::components_get_url_names_not_cached -package_id $package_id]] +} + +ad_proc bug_tracker::components_get_url_names_not_cached { + {-package_id:required} +} { + db_foreach select_component_url_names {} { + lappend result $component_id $url_name + } + return $result +} + + ##### # # Description # ##### -ad_proc ::bug_tracker::bug_convert_comment_to_html { - -comment - -format +ad_proc bug_tracker::bug_convert_comment_to_html { + {-comment:required} + {-format:required} } { - switch $format { - html { - return [ad_html_text_convert -from html -to html -- $comment] - } - pre { - return "
[ad_html_text_convert -from html -to html -- $comment]
" - } - default { - return [ad_html_text_convert -from text -to html -- $comment] - } - } + ns_log Notice "LARS: Format=$format, comment=$comment, formatted = [ad_html_text_convert -from $format -to text/html -- $comment]" + return [ad_html_text_convert -from $format -to text/html -- $comment] } -ad_proc ::bug_tracker::bug_convert_comment_to_text { - -comment - -format +ad_proc bug_tracker::bug_convert_comment_to_text { + {-comment:required} + {-format:required} } { - switch $format { - html { - return [ad_html_text_convert -from html -to text -- $comment] - } - default { - return [ad_html_text_convert -from text -to text -- $comment] - } - } + return [ad_html_text_convert -from $format -to text/plain -- $comment] } ##### @@ -482,7 +976,7 @@ # ##### -ad_proc ::bug_tracker::bug_action_pretty { +ad_proc bug_tracker::bug_action_pretty { action {resolution ""} } { @@ -513,7 +1007,7 @@ } } -ad_proc ::bug_tracker::patch_action_pretty { +ad_proc bug_tracker::patch_action_pretty { action } { @@ -536,7 +1030,7 @@ ##### # -# Users (assignee) +# Maintainers # ##### @@ -546,238 +1040,93 @@ if { ![info exists package_id] } { set package_id [ad_conn package_id] } - + set user_id [ad_conn user_id] - + # This picks out users who are already assigned to some bug in this set sql { - select distinct q.* - from ( - select u.first_names || ' ' || u.last_name || ' (' || u.email || ')' as name, u.user_id - from bt_bugs b, cc_users u - where b.project_id = :package_id - and u.user_id = b.assignee - union - select u.first_names || ' ' || u.last_name || ' (' || u.email || ')' as name, u.user_id - from cc_users u - where u.user_id = :user_id - ) q + select first_names || ' ' || last_name || ' (' || email || ')' as name, + user_id + from cc_users + where user_id in ( + select maintainer + from bt_projects + where project_id = :package_id + + union + + select maintainer + from bt_versions + where project_id = :package_id + + union + + select maintainer + from bt_components + where project_id = :package_id + ) + or user_id = :user_id order by name } - + set users_list [db_list_of_lists users $sql] - + set users_list [concat { { "Unassigned" "" } } $users_list] lappend users_list { "Search..." ":search:"} - + return $users_list } - +ad_proc ::bug_tracker::users_get_searchquery { + -package_id +} { +} + +ad_proc -private bug_tracker::get_maintainer_role_id { + -package_id +} { + if { ![info exists package_id] } { + set package_id [ad_conn package_id] + } + # We're using the assignee widget for a certain role to assign the version maintainer + set workflow_id [bug_tracker::bug::get_instance_workflow_id -package_id [ad_conn package_id]] + set role_ids [workflow::get_roles -workflow_id $workflow_id] + # LARS HACK: + # We'll use the last role in sort order + return [lindex $role_ids end] +} + + ##### # -# Notification +# Patches # ##### -ad_proc ::bug_tracker::bug_notify { - {-bug_id:required} - {-action ""} - {-comment ""} - {-comment_format ""} - {-resolution ""} - {-patch_summary ""} +ad_proc bug_tracker::patches_p {} { + Is the patch submission feature turned on? } { - set package_id [ad_conn package_id] - - db_1row bug { - select b.bug_id, - b.bug_number, - b.summary, - b.project_id, - o.creation_user as submitter_user_id, - submitter.first_names as submitter_first_names, - submitter.last_name as submitter_last_name, - submitter.email as submitter_email, - b.component_id, - c.component_name, - o.creation_date, - to_char(o.creation_date, 'fmMM/DDfm/YYYY') as creation_date_pretty, - b.severity, - sc.sort_order || ' - ' || sc.severity_name as severity_pretty, - b.priority, - pc.sort_order || ' - ' || pc.priority_name as priority_pretty, - b.status, - b.resolution, - b.bug_type, - b.user_agent, - b.original_estimate_minutes, - b.latest_estimate_minutes, - b.elapsed_time_minutes, - b.found_in_version, - coalesce((select version_name - from bt_versions found_in_v - where found_in_v.version_id = b.found_in_version), 'Unknown') as found_in_version_name, - b.fix_for_version, - coalesce((select version_name - from bt_versions fix_for_v - where fix_for_v.version_id = b.fix_for_version), 'Undecided') as fix_for_version_name, - b.fixed_in_version, - coalesce((select version_name - from bt_versions fixed_in_v - where fixed_in_v.version_id = b.fixed_in_version), 'Unknown') as fixed_in_version_name, - b.assignee as assignee_user_id, - assignee.first_names as assignee_first_names, - assignee.last_name as assignee_last_name, - assignee.email as assignee_email, - to_char(now(), 'fmMon/DDfm/YYYY') as now_pretty - from bt_bugs b left outer join - cc_users assignee on (assignee.user_id = b.assignee), - acs_objects o, - bt_components c, - bt_priority_codes pc, - bt_severity_codes sc, - cc_users submitter - where b.bug_id = :bug_id - and b.project_id = :package_id - and o.object_id = b.bug_id - and c.component_id = b.component_id - and pc.priority_id = b.priority - and sc.severity_id = b.severity - and submitter.user_id = o.creation_user - } -column_array bug - - set subject "Bug #$bug(bug_number). [ad_html_to_text -- [string_truncate -len 30 $bug(summary)]]: [bug_action_pretty $action $resolution] by [conn user_first_names] [conn user_last_name]" - - set body "Bug no: #$bug(bug_number) -Summary: $bug(summary) - -Component: $bug(component_name) -Status: [status_pretty $bug(status)] -Severity: $bug(severity_pretty) -Priority: $bug(priority_pretty) -Found in version: $bug(found_in_version_name) - -Action: [bug_action_pretty $action $resolution] -By user: [conn user_first_names] [conn user_last_name] <[conn user_email]> - -" - - if { ![string equal $action "patched"] } { - if { ![empty_string_p $comment] } { - append body "Comment:\n\n[bug_convert_comment_to_text -comment $comment -format $comment_format]\n\n" - } - - } else { - append body "\n\nSummary: $patch_summary\n\n" - } - - - append body "--\nTo comment on, edit, resolve, close, or reopen this bug, go to:\n[ad_url][ad_conn package_url]bug?[export_vars -url { { bug_number $bug(bug_number) } }]\n" - - # Use the Notification service to alert (could be immediately, or daily, or weekly) - # people who have signed up for notification on this bug - notification::new \ - -type_id [notification::type::get_type_id -short_name bug_tracker_bug_notif] \ - -object_id $bug(bug_id) \ - -response_id $bug(bug_id) \ - -notif_subject $subject \ - -notif_text $body - - # Use the Notification service to alert people who have signed up for notification - # in this bug tracker package instance - notification::new \ - -type_id [notification::type::get_type_id -short_name bug_tracker_project_notif] \ - -object_id $bug(project_id) \ - -response_id $bug(bug_id) \ - -notif_subject $subject \ - -notif_text $body -} - -ad_proc ::bug_tracker::add_instant_alert { - {-bug_id:required} - {-user_id:required} -} { - notification::request::new \ - -type_id [notification::type::get_type_id -short_name bug_tracker_bug_notif] \ - -user_id $user_id \ - -object_id $bug_id \ - -interval_id [notification::get_interval_id -name "instant"] \ - -delivery_method_id [notification::get_delivery_method_id -name "email"] -} - -ad_proc ::bug_tracker::get_notification_link { - {-type:required} - {-object_id:required} - {-pretty_name:required} - {-url:required} -} { - Returns a list with the url, label, and title for a notifications link (subscribe or unsubscribe). -} { - - set user_id [ad_conn user_id] - - set notification_link [list] - # Only present the link to logged in users. - if { $user_id != 0 } { - set type_id [notification::type::get_type_id -short_name $type] - if { [empty_string_p $type_id] } { - ns_log Error "Can't find notification of type '$type'" - return "" - } - - set request_id [notification::request::get_request_id -type_id $type_id -object_id $object_id -user_id $user_id] - - if { ![empty_string_p $request_id] } { - # The user is already subscribed - lappend notification_link [notification::display::unsubscribe_url -request_id $request_id -url $url] - lappend notification_link "Unsubscribe" - lappend notification_link "Unsubscribe from notifications for this $pretty_name" - } else { - # The user is not subscribed - lappend notification_link [notification::display::subscribe_url \ - -type bug_tracker_project_notif \ - -object_id $object_id \ - -url $url \ - -user_id $user_id \ - -pretty_name "a $pretty_name" - ] - lappend notification_link "Subscribe" - lappend notification_link "Subscribe to notifications for this $pretty_name" - } - } - - return $notification_link + return [parameter::get -package_id [ad_conn package_id] -parameter "PatchesP" -default 1] } -ad_proc ::bug_tracker::map_patch_to_bug { +ad_proc bug_tracker::map_patch_to_bug { {-patch_id:required} {-bug_id:required} } { - db_dml map_patch_to_bug { - insert into bt_patch_bug_map (patch_id, bug_id) values (:patch_id, :bug_id) - } + db_dml map_patch_to_bug {} } -ad_proc ::bug_tracker::unmap_patch_from_bug { +ad_proc bug_tracker::unmap_patch_from_bug { {-patch_number:required} {-bug_number:required} } { set package_id [ad_conn package_id] - db_dml unmap_patch_from_bug { - delete from bt_patch_bug_map - where bug_id = (select bug_id from bt_bugs - where bug_number = :bug_number - and project_id = :package_id) - and patch_id = (select patch_id from bt_patches - where patch_number = :patch_number - and project_id = :package_id) - } + db_dml unmap_patch_from_bug {} } -ad_proc ::bug_tracker::get_mapped_bugs { +ad_proc bug_tracker::get_mapped_bugs { {-patch_number:required} {-only_open_p "0"} } { @@ -787,26 +1136,28 @@ set bug_list [list] set package_id [ad_conn package_id] - set open_clause [ad_decode $only_open_p "1" "\n and bt_bugs.status = 'open'" ""] + if { $only_open_p } { + set workflow_id [bug_tracker::bug::get_instance_workflow_id] + set initial_state [workflow::fsm::get_initial_state -workflow_id $workflow_id] - db_foreach get_bugs_for_patch "select bt_bugs.bug_number, - bt_bugs.summary - from bt_bugs, bt_patch_bug_map - where bt_bugs.bug_id = bt_patch_bug_map.bug_id - and bt_patch_bug_map.patch_id = (select patch_id - from bt_patches - where patch_number = :patch_number - and project_id = :package_id - ) - $open_clause" { + set open_clause "\n and exists (select 1 + from workflow_cases cas, + workflow_case_fsm cfsm + where cas.case_id = cfsm.case_id + and cas.object_id = b.bug_id + and cfsm.current_state = :initial_state)" + } else { + set open_clause "" + } - lappend bug_list [list "$summary" "$bug_number"] + db_foreach get_bugs_for_patch {} { + lappend bug_list [list "[bug_tracker::conn Bug] #$bug_number: $summary" "$bug_number"] } return $bug_list } -ad_proc ::bug_tracker::get_bug_links { +ad_proc bug_tracker::get_bug_links { {-patch_id:required} {-patch_number:required} {-write_or_submitter_p:required} @@ -833,7 +1184,7 @@ } if { [llength $bug_link_list] != 0 } { - set bugs_string [join $bug_link_list ", "] + set bugs_string [join $bug_link_list "
"] } else { set bugs_string "No bugs." } @@ -842,7 +1193,7 @@ } } -ad_proc ::bug_tracker::get_patch_links { +ad_proc bug_tracker::get_patch_links { {-bug_id:required} {-show_patch_status "open"} } { @@ -857,15 +1208,7 @@ } } - db_foreach get_patches_for_bug \ - "select bt_patches.patch_number, - bt_patches.summary, - bt_patches.status - from bt_patch_bug_map, bt_patches - where bt_patch_bug_map.bug_id = :bug_id - and bt_patch_bug_map.patch_id = bt_patches.patch_id - $status_where_clause - " { + db_foreach get_patches_for_bug "" { set status_indicator [ad_decode $show_patch_status "all" "($status)" ""] lappend patch_list "$summary $status_indicator" @@ -880,29 +1223,22 @@ return $patches_string } -ad_proc ::bug_tracker::get_patch_submitter { +ad_proc bug_tracker::get_patch_submitter { {-patch_number:required} } { set package_id [ad_conn package_id] - return [db_string patch_submitter_id "select acs_objects.creation_user - from bt_patches, acs_objects - where bt_patches.patch_number = :patch_number - and bt_patches.project_id = :package_id - and bt_patches.patch_id = acs_objects.object_id"] + return [db_string patch_submitter_id {}] } -ad_proc ::bug_tracker::update_patch_status { +ad_proc bug_tracker::update_patch_status { {-patch_number:required} {-new_status:required} } { set package_id [ad_conn package_id] - db_dml update_patch_status "update bt_patches - set status = :new_status - where bt_patches.project_id = :package_id - and bt_patches.patch_number = :patch_number" + db_dml update_patch_status "" } -ad_proc ::bug_tracker::get_uploaded_patch_file_content { +ad_proc bug_tracker::get_uploaded_patch_file_content { } { set patch_file [ns_queryget patch_file] @@ -919,33 +1255,37 @@ return $content } -ad_proc ::bug_tracker::parse_filters { filter_array_name } { +ad_proc bug_tracker::parse_filters { filter_array_name } { Parses the array named in 'filter_array_name', setting local variables for the filter parameters, and constructing a chunk that can be used in a query, plus a human readable string. Sets the result in bug_tracker::conn as - 'filter_human_readable', 'filter_where_clauses', and + 'filter_human_readable', 'filter_where_clauses', 'filter_from_bug_clause', 'filter_order_by_clause'. } { upvar $filter_array_name filter - set where_clauses [list] + set where_clauses [list] + set from_bug_clause "bt_bugs b" + set workflow_id [bug_tracker::bug::get_instance_workflow_id] + set initial_state_id [workflow::fsm::get_initial_state -workflow_id $workflow_id] + set valid_filters { - status - bug_type + {status $initial_state_id} + {action_id} fix_for_version:integer - severity:integer - priority:integer assignee:integer + action_id:integer component_id:integer - actionby:integer + keyword:integer,multiple + {n_days 7} {orderby ""} } - + foreach name $valid_filters { if { [llength $name] > 1 } { - set default [lindex $name 1] + set default [subst [lindex $name 1]] set name [lindex $name 0] } else { if { [info exists default] } { @@ -958,124 +1298,163 @@ } else { set filters [list] } + + # special case for annoying tcl'ism, whereby if you say + # lappend foo(bar) {}, your foo(bar) entry will be equal to {{}}, + # which we run into, because the page defines filters as + # :array,multiple + if { [info exists filter($name)] && [string equal $filter($name) {{}}] } { + if { [lsearch -exact $filters "multiple"] != -1 } { + unset filter($name) + } else { + set filter($name) {} + } + } + if { [info exists filter($name)] } { - upvar __filter_$name var + upvar filter_$name var set var $filter($name) if { [lsearch -exact $filters "integer"] != -1 && ![empty_string_p $var]} { - validate_integer $name $var + if { [lsearch -exact $filters "multiple"] != -1 } { + foreach elm $var { + validate_integer $name $elm + } + } else { + validate_integer $name $var + } } + } elseif { [info exists default] } { - upvar __filter_$name var + upvar filter_$name var set var $default } # also upvar it under its real name - upvar __filter_$name __filter_$name + upvar filter_$name filter_$name } - - - if { ![info exists __filter_status] } { - if { [info exists __filter_actionby] } { - set __filter_status "" - } else { - set __filter_status "open" - } - } - - if { ![empty_string_p $__filter_status] } { - lappend where_clauses "b.status = :__filter_status" - set human_readable_filter "All $__filter_status bugs" + + if { [info exists filter_status] && ![string equal $filter_status "any"] } { + lappend where_clauses "cfsm.current_state = :filter_status" + + set status_pretty [workflow::state::fsm::get_element \ + -state_id $filter_status \ + -element pretty_name] + + set human_readable_filter "All $status_pretty [bug_tracker::conn bugs]" } else { - lappend where_clauses "b.status != 'closed'" - set human_readable_filter "All open and resolved bugs" + set human_readable_filter "[bug_tracker::conn Bugs] of any status" } - if { [info exists __filter_bug_type] } { - lappend where_clauses "b.bug_type = :__filter_bug_type" - append human_readable_filter " of type [bug_tracker::bug_type_pretty $__filter_bug_type]" + if { [info exists filter_bug_type] } { + lappend where_clauses "b.bug_type = :filter_bug_type" + append human_readable_filter " of type [bug_tracker::bug_type_pretty $filter_bug_type]" } - if { [info exists __filter_assignee] } { - if { [empty_string_p $__filter_assignee] } { - lappend where_clauses "b.assignee is null" + if { [info exists filter_assignee] } { + if { [empty_string_p $filter_assignee] } { + lappend where_clauses "assignee.party_id is null" + append human_readable_filter " that are unassigned" - } else { - lappend where_clauses "b.assignee = :__filter_assignee" - if { $__filter_assignee == [ad_conn user_id] } { + } else { + + lappend where_clauses "assignee.party_id = :filter_assignee" + + if { $filter_assignee == [ad_conn user_id] } { append human_readable_filter " assigned to me" } else { - append human_readable_filter " assigned to [db_string assignee_name { select first_names || ' ' || last_name from cc_users where user_id = :__filter_assignee }]" + append human_readable_filter " assigned to [db_string assignee_name {}]" } } - } - - if { [info exists __filter_actionby] } { - lappend where_clauses "((b.status = 'open' and b.assignee = :__filter_actionby) or (b.status = 'resolved' and o.creation_user = :__filter_actionby))" - if { $__filter_actionby == [ad_conn user_id] } { - append human_readable_filter " awaiting action by me" - } else { - append human_readable_filter " awaiting action by [db_string actionby_name { select first_names || ' ' || last_name from cc_users where user_id = :__filter_actionby }]" + } + + if { [info exists filter_keyword] } { + set keyword_human [list] + foreach keyword_id $filter_keyword { + lappend where_clauses [db_map keyword_filter] + set category_name [category_heading -keyword_id $keyword_id] + + # LARS: + # This is a hack to be smart about stripping out the "1 - " or "A - " part + # if people use that naming style + regsub {^[a-zA-Z0-9]\s[-*]*\s} $category_name {} category_name + + lappend keyword_human "[category_parent_heading -keyword_id $keyword_id] is $category_name" } + append human_readable_filter " where [join $keyword_human " and "]" } - if { [info exists __filter_severity] } { - lappend where_clauses "b.severity = :__filter_severity" - append human_readable_filter " where severity is [db_string severity_name { select severity_name from bt_severity_codes where severity_id = :__filter_severity }]" - } - - if { [info exists __filter_priority] } { - lappend where_clauses "b.priority = :__filter_priority" - append human_readable_filter " with a priority of [db_string priority_name { select priority_name from bt_priority_codes where priority_id = :__filter_priority }]" - } - if { ![empty_string_p [conn component_id]] } { - set __filter_component_id [conn component_id] + set filter_component_id [conn component_id] } - if { [info exists __filter_component_id] } { - lappend where_clauses "b.component_id = :__filter_component_id" - append human_readable_filter " in [db_string component_name { select component_name from bt_components where component_id = :__filter_component_id }]" - conn -set component_id $__filter_component_id + if { [info exists filter_component_id] } { + lappend where_clauses "b.component_id = :filter_component_id" + append human_readable_filter " in [component_get_name -component_id $filter_component_id]" + conn -set component_id $filter_component_id } - if { [info exists __filter_fix_for_version] } { - if { [empty_string_p $__filter_fix_for_version] } { + if { [info exists filter_fix_for_version] } { + if { [empty_string_p $filter_fix_for_version] } { lappend where_clauses "b.fix_for_version is null" append human_readable_filter " where fix for version is undecided" } else { - lappend where_clauses "b.fix_for_version = :__filter_fix_for_version" - append human_readable_filter " to be fixed in version [db_string version_name { select version_name from bt_versions where version_id = :__filter_fix_for_version }]" + lappend where_clauses "b.fix_for_version = :filter_fix_for_version" + append human_readable_filter " to be fixed in version [db_string version_name {}]" } } - switch -exact -- $__filter_orderby { - severity { - set order_by_clause "sc.sort_order, b.bug_number desc" - append human_readable_filter ", most severe bugs first" + if { [empty_string_p $filter_orderby] } { + set order_by_clause "b.bug_number desc" + } else { + append from_bug_clause [db_map orderby_filter_from_bug] + lappend where_clauses [db_map orderby_filter_where] + set order_by_clause "kw_order.heading, bug_number desc " + } + + if { ![empty_string_p $filter_n_days] } { + if { ![string equal $filter_n_days "all"] } { + lappend where_clauses [db_map n_days_filter] + append human_readable_filter " opened in the last $filter_n_days days" } - priority { - set order_by_clause "pc.sort_order, b.bug_number desc" - append human_readable_filter ", highest priority bugs first" - } - default { - set order_by_clause "b.bug_number desc" - } } conn -set filter [array get filter] conn -set filter_human_readable $human_readable_filter conn -set filter_where_clauses $where_clauses conn -set filter_order_by_clause $order_by_clause + conn -set filter_from_bug_clause $from_bug_clause } -ad_proc ::bug_tracker::context_bar { args } { +ad_proc bug_tracker::filter_url_vars { + {-array:required} + {-override:required} +} { + Returns query args for the URL string, overriding the existing filters with the new one given by name and value. + Existing orderby and n_days filters are kept, however, unless that's the one you're replacing + @param array the name of the array in the caller's scope holding the current filter values + @param override an array list of new values to set instead +} { + upvar $array cur_filters + + array set filter [list] + + foreach keeper { orderby n_days } { + if { [info exists cur_filters($keeper)] } { + set filter($keeper) $cur_filters($keeper) + } + } + + array set filter $override + return [export_vars { filter:array }] +} + +ad_proc bug_tracker::context_bar { args } { Context bar that takes the component information into account } { set component_id [conn component_id] if { ![empty_string_p $component_id] } { - db_1row component_name { - select component_name, url_name from bt_components where component_id = :component_id - } + set component_name [bug_tracker::component_get_name -component_id $component_id] + set url_name [bug_tracker::component_get_url_name -component_id $component_id] if { [llength $args] == 0 } { return [eval ad_context_bar [list $component_name]] } else { @@ -1086,7 +1465,7 @@ } } -ad_proc ::bug_tracker::security_violation { +ad_proc bug_tracker::security_violation { -user_id:required -bug_id:required -action:required @@ -1101,3 +1480,108 @@ " ad_script_abort } +ad_proc bug_tracker::bug_delete { bug_id } { + Delete a Bug Tracker bug. + This should only ever be run when un-instantiating a project! + + @author Mark Aufflick +} { + set case_id [db_string get_case_id {}] + db_exec_plsql delete_bug_case {} + set notifications [db_list get_notifications {}] + foreach notification_id $notifications { + db_exec_plsql delete_notification {} + } + db_dml unset_revisions {} + db_exec_plsql delete_cr_item {} +} + +ad_proc bug_tracker::project_delete { project_id } { + Delete a Bug Tracker project and all its data. + + @author Peter Marklund +} { + #manually delete all bugs to avoid wierd integrity constraints + while { [set bug_id [db_string min_bug_id {}]] > 0 } { + bug_delete $bug_id + } + db_exec_plsql delete_project {} +} + +ad_proc bug_tracker::project_new { project_id } { + Create a new Bug Tracker project for a package instance. + + @author Peter Marklund +} { + db_exec_plsql create_project {} +} + +ad_proc bug_tracker::bug_notify { + {-bug_id:required} + {-action ""} + {-comment ""} + {-comment_format ""} + {-resolution ""} + {-patch_summary ""} +} { + set package_id [ad_conn package_id] + + db_1row bug {} -column_array bug + set bug(found_in_version_name) [version_get_name -version_id $bug(found_in_version)] + set bug(fix_for_version_name) [version_get_name -version_id $bug(fix_for_version)] + set bug(fixed_in_version_name) [version_get_name -version_id $bug(fixed_in_version)] + + get_pretty_names -array pretty_names + + set subject "$pretty_names(Bug) #$bug(bug_number). [ad_html_to_text -- [string_truncate -len 30 $bug(summary)]]: [bug_action_pretty $action $resolution] by [conn user_first_names] [conn user_last_name]" + + set body "$pretty_names(Bug) no: #$bug(bug_number) +Summary: $bug(summary) + +$pretty_names(Component): $bug(component_name) +Status: $bug(status) +" + +foreach {category_id category_name} [bug_tracker::category_types] { + append body "$category_name: [cr::keyword::item_get_assigned -item_id $bug(bug_id) -parent_id $category_id] +" +} + +append body "Found in version: $bug(found_in_version_name) + +Action: [bug_action_pretty $action $resolution] +By user: [conn user_first_names] [conn user_last_name] <[conn user_email]> + +" + + if { ![string equal $action "patched"] } { + if { ![empty_string_p $comment] } { + append body "Comment:\n\n[bug_convert_comment_to_text -comment $comment -format $comment_format]\n\n" + } + + } else { + append body "\n\nSummary: $patch_summary\n\n" + } + + + append body "--\nTo comment on, edit, resolve, close, or reopen this bug, go to:\n[ad_url][ad_conn package_url]bug?[export_vars -url { { bug_number $bug(bug_number) } }]\n" + + # Use the Notification service to alert (could be immediately, or daily, or weekly) + # people who have signed up for notification on this bug + notification::new \ + -type_id [notification::type::get_type_id -short_name bug_tracker_bug_notif] \ + -object_id $bug(bug_id) \ + -response_id $bug(bug_id) \ + -notif_subject $subject \ + -notif_text $body + + # Use the Notification service to alert people who have signed up for notification + # in this bug tracker package instance + notification::new \ + -type_id [notification::type::get_type_id -short_name bug_tracker_project_notif] \ + -object_id $bug(project_id) \ + -response_id $bug(bug_id) \ + -notif_subject $subject \ + -notif_text $body +} +