Index: openacs-4/contrib/packages/project-manager/tcl/task-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/contrib/packages/project-manager/tcl/Attic/task-procs.tcl,v diff -u -r1.4 -r1.4.2.1 --- openacs-4/contrib/packages/project-manager/tcl/task-procs.tcl 12 Mar 2004 13:44:43 -0000 1.4 +++ openacs-4/contrib/packages/project-manager/tcl/task-procs.tcl 20 May 2004 17:30:04 -0000 1.4.2.1 @@ -14,6 +14,135 @@ +ad_proc -public pm::task::dependency_options { + {-edit_p "f"} + -project_item_id + {-task_item_id ""} + {-dependency_task_ids ""} + {-number "0"} + {-current_number "0"} +} { + Returns a list of lists suitable for use in a select list for + ad_form. Contains a list of possible tasks that this task can + depend upon. + +

+ + There is one special case that we handle: if you are creating new + tasks (not editing), you can have them depend on each other. + So if you create two tasks at the same time, you may want task + 2 to depend on task 1. Instead of a task_item_id, we then + specify a value of this form: + +

+ numX +
+ + where X represents the number of the new task, ranging from 1 + to n. + +

+ + To be more efficient when creating multiple tasks at the same + time, we should cache the database calls. + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-05-13 + + @param edit_p Is this for a task being edited? Or a new task? + + @param project_item_id The project we're finding tasks from + + @param task_item_id The task ID. This is used because we do not + want a task to depend on itself, so it is excluded from the list. + + @param dependency_task_ids For edited tasks, the current task_ids + that it depends on. Used because sometimes it can be closed, and + it wouldn't otherwise appear on the list. This is a list. + + @param number When the list is returned, it includes entries for + number new tasks, in the numX format described in these docs. + + @param current_number The current number. Used for new tasks. It + prevents allowing dependencies on the task being created. + + @return + + @error +} { + + # get tasks this task can depend on + + if {[exists_and_not_null dependency_task_ids]} { + + set union_clause " + UNION + SELECT + r.item_id, + r.title as task_title + FROM + pm_tasks_revisionsx r, + cr_items i, + pm_tasks t + WHERE + r.parent_id = :project_item_id and + r.revision_id = i.live_revision and + i.item_id = t.task_id + and t.task_id in ([join $dependency_task_ids ","])" + } else { + set union_clause "" + } + + set keys [list] + + db_foreach get_dependency_tasks { } { + set options($task_title) $item_id + lappend keys $task_title + } + + set keys [lsort $keys] + + + set dependency_options_full "{\"--None--\" \"\"} " + + if {!$edit_p} { + # now set up dependency options + + for {set j 1} {$j <= $number} {incr j} { + if {![string equal $current_number $j]} { + append dependency_options_full "{\"New Task \#$j\" \"num$j\"} " + } + } + } + + # for editing tasks, we skip ourselves (because depending on + # ourselves just sometimes isn't an option) + + if {[string equal $edit_p t]} { + foreach key $keys { + + # make sure we're not dependent on ourselves + + if {![string equal $task_item_id $options($key)]} { + # check for case when there is a quote in the name of + # a task. We have to filter this out, or we get an error. + append dependency_options_full "{{$key} $options($key)} " + } + } + } else { + foreach key $keys { + + # check for case when there is a quote in the name of + # a task. We have to filter this out, or we get an error. + append dependency_options_full "{{$key} $options($key)} " + } + } + + + return $dependency_options_full +} + + ad_proc -public pm::task::dependency_delete_all { -task_item_id:required @@ -92,8 +221,17 @@ project.item_id = :project_item_id "] - set loop_limit [llength $project_tasks] + # we do not allow tasks to depend on items outside of their + # project. So if it's not in the list of tasks for that project, + # we reject it + if {[lsearch $project_tasks $parent_id] < 0} { + set loop_limit 0 + set valid_p FALSE + } else { + set loop_limit [llength $project_tasks] + } + if {$loop_limit > 0} { set dep_list [list] @@ -132,7 +270,7 @@ db_dml insert_dep "insert into pm_task_dependency (dependency_id, task_id, parent_task_id, dependency_type) values (:dependency_id, :task_item_id, :parent_id, 'finish_before_start')" } else { - ns_log Notice "Task $task_item_id was not added due to looping" + ns_log Notice "Task dependency for $task_item_id on $parent_id was not added due to looping or being outside of the current project" } } @@ -184,7 +322,13 @@ array set task_state [nsv_array get task_node_status] - set used $task_state($tNode) + # this should only happen if dependencies span projects + # they shouldn't, but I check anyway. + if {![info exists task_state($tNode)]} { + set used 0 + } else { + set used $task_state($tNode) + } if {[string equal $used 1]} { return FALSE @@ -225,9 +369,9 @@ @return task_item_id - @error + @error Returns -1 if there is no such task } { - set return_val [db_string get_item_id { }] + set return_val [db_string get_item_id { } -default "-1"] return $return_val } @@ -246,15 +390,37 @@ @return task_item_id - @error + @error If there is no such task item, then returns -1 } { - set return_val [db_string get_revision_id { }] + set return_val [db_string get_revision_id { } -default "-1"] return $return_val } +ad_proc -public pm::task::current_status { + -task_item_id:required +} { + Returns the current status value for open tasks +} { + set return_val [db_string get_current_status { }] + + return $return_val +} + + +ad_proc -public pm::task::open_p { + -task_item_id:required +} { + Returns 1 if the task is open, 0 otherwise +} { + set return_val [db_string open_p { }] + + return $return_val +} + + ad_proc -public pm::task::default_status_open {} { Returns the default status value for open tasks } { @@ -279,6 +445,7 @@ -project_item_id:required -title:required -description:required + {-mime_type "text/plain"} -end_date:required -percent_complete:required -estimated_hours_work:required @@ -303,6 +470,8 @@ @param description + @param mime_type + @param end_date @param percent_complete @@ -327,16 +496,26 @@ @error } { - if {$percent_complete >= 100} { - set status_id [pm::task::default_status_closed] + if {![exists_and_not_null status_id]} { + set status_id [pm::task::current_status \ + -task_item_id $task_item_id] } - if {![exists_and_not_null status_id]} { - set status_id [pm::task::default_status_open] + if {$estimated_hours_work_min > $estimated_hours_work_max} { + set temp $estimated_hours_work_max + set estimated_hours_work_max $estimated_hours_work_min + set estimated_hours_work_min $temp } set return_val [db_exec_plsql new_task_revision { *SQL }] + # if the we've done 100% of the work, then we close the task + if {$percent_complete >= 100} { + pm::task::close -task_item_id $task_item_id + } else { + pm::task::open -task_item_id $task_item_id + } + return $return_val } @@ -346,6 +525,7 @@ -project_id:required -title:required {-description ""} + {-mime_type "text/plain"} {-end_date ""} {-percent_complete "0"} {-estimated_hours_work "0"} @@ -361,13 +541,39 @@ set status_id [pm::task::default_status_open] } + if {$estimated_hours_work_min > $estimated_hours_work_max} { + set temp $estimated_hours_work_max + set estimated_hours_work_max $estimated_hours_work_min + set estimated_hours_work_min $temp + } + set return_val [db_exec_plsql new_task_item { *SQL }] return $return_val } +ad_proc -public pm::task::delete { + -task_item_id:required +} { + Deletes a given task + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-03-10 + + @param task_item_id + + @return 1, no matter once + + @error No error thrown if there is no such task. +} { + db_exec_plsql delete_task "select pm_task__delete_task_item(:task_item_id)" + + return 1 +} + + ad_proc -public pm::task::get_url { object_id } { @@ -496,3 +702,1527 @@ return $total_logged_hours } +<<<<<<< task-procs.tcl + + +ad_proc -public pm::task::link { + -task_item_id_1:required + -task_item_id_2:required +} { + Links two tasks together + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-03-10 + + @param task_item_id_1 + + @param task_item_id_2 + + @return + + @error +} { + + if {[string equal $task_item_id_1 $task_item_id_2]} { + # do nothing + ns_log Notice "Project-manager: Cannot link a task to itself!" + } elseif {$task_item_id_1 < $task_item_id_2} { + db_dml link_tasks " + INSERT INTO + pm_task_xref + (task_id_1, task_id_2) + VALUES + (:task_item_id_1, :task_item_id_2)" + } else { + db_dml link_tasks " + INSERT INTO + pm_task_xref + (task_id_1, task_id_2) + VALUES + (:task_item_id_2, :task_item_id_1)" + } + +} + + +ad_proc -public pm::task::assign_remove_everyone { + -task_item_id:required +} { + Removes all assignments for a task + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-09 + + @param task_item_id + + @return + + @error +} { + db_dml remove_assignment " + delete from pm_task_assignment where task_id = :task_item_id" +} + + +ad_proc -public pm::task::assign { + -task_item_id:required + -party_id:required + {-role_id ""} +} { + Assigns party_id to task_item_id + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-05 + + @param task_item_id + + @param party_id + + @param role_id the role under which the person is assigned + + @return + + @error +} { + if {![exists_and_not_null role_id]} { + set role_id [pm::role::default] + } + + db_dml add_assignment " + insert into pm_task_assignment + (task_id, + role_id, + party_id) + values + (:task_item_id, + :role_id, + :party_id) + " +} + + +ad_proc -public pm::task::open { + -task_item_id:required +} { + Opens a task, and sends notifications, unless it was already + open. If it was already open, does nothing. + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-22 + + @param task_item_id + + @return + + @error +} { + # find out what the status of the task was, and while we're at it, + # get other interesting information about the task, in case we + # want to close it. Then we can put this info in the email. + + db_1row get_status " + SELECT + t.status, + s.status_type, + s.description as status_description, + r.title as task_title, + r.estimated_hours_work, + r.estimated_hours_work_min, + r.estimated_hours_work_max, + to_char(r.earliest_start, 'YYYY-MM-DD HH24:MI:SS') as earliest_start_ansi, + to_char(r.earliest_finish, 'YYYY-MM-DD HH24:MI:SS') as earliest_finish_ansi, + to_char(r.latest_start, 'YYYY-MM-DD HH24:MI:SS') as latest_start_ansi, + to_char(r.latest_finish, 'YYYY-MM-DD HH24:MI:SS') as latest_finish_ansi, + r.description as task_description, + project_revision.title as project_name + FROM + pm_tasks t, + cr_items task_item, + pm_task_status s, + pm_tasks_revisionsx r, + cr_items project_item, + cr_revisions project_revision + WHERE + r.parent_id = project_item.item_id and + t.task_id = task_item.item_id and + task_item.live_revision = r.revision_id and + project_item.live_revision = project_revision.revision_id and + r.item_id = t.task_id and + t.status = s.status_id and + t.task_id = :task_item_id" + + if {[string equal $status_type "o"]} { + + # this is already open + return + + } + + # set the new status + + set status_code [pm::task::default_status_open] + + db_dml update_status " + UPDATE + pm_tasks + SET + status = :status_code + WHERE + task_id = :task_item_id" + + # send out an email notification + + set earliest_start [lc_time_fmt $earliest_start_ansi "%x"] + set earliest_finish [lc_time_fmt $earliest_finish_ansi "%x"] + set latest_start [lc_time_fmt $latest_start_ansi "%x"] + set latest_finish [lc_time_fmt $latest_finish_ansi "%x"] + + set assignees [db_list get_assignees " + select + email + FROM + pm_task_assignment a, + parties p + WHERE + task_id = :task_item_id and + a.party_id = p.party_id + "] + + if {[llength $assignees] > 0} { + + set to_address [join $assignees ", "] + + set user_id [ad_conn user_id] + + set from_address [db_string get_from_email "select email from parties where party_id = :user_id" -default "nobody@nowhere.com"] + + set task_url "[parameter::get_from_package_key -package_key acs-kernel -parameter SystemURL][ad_conn package_url]task-one?task_id=$task_item_id" + + set subject "Task reopened (was $status_description): $task_title" + + if {[parameter::get_from_package_key -package_key project-manager -parameter UseUncertainCompletionTimesP]} { + set estimated_work "\nHrs work (min): $estimated_hours_work_min\nHrs work (max): $estimated_hours_work_max" + } else { + set estimated_work "\nHrs work: $estimated_hours_work" + + } + + set notification_text "Task reopened, was $status_description\n\n" + + append notification_text " +------------- +Task ID: \#$task_item_id +Description: $task_title +Project: $project_name + +Link: $task_url + +--------------- +Estimated work: +---------------$estimated_work + +------ +Dates: +------ +Earliest start: $earliest_start +Earliest finish: $earliest_finish +Latest start: $latest_start +Latest finish $latest_finish + +----------- +Description +----------- +$task_description" + + + append notification_text "\n" + + acs_mail_lite::send \ + -to_addr $to_address \ + -from_addr $from_address \ + -subject $subject \ + -body $notification_text + } + + return +} + +ad_proc -public pm::task::close { + -task_item_id:required +} { + Closes a task, and sends notifications, unless it was already + closed. If it was already closed, does nothing. + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-22 + + @param task_item_id + + @return + + @error +} { + # find out what the status of the task was + + db_1row get_status " + SELECT + t.status, + s.status_type, + s.description as status_description, + r.title as task_title, + r.estimated_hours_work, + r.estimated_hours_work_min, + r.estimated_hours_work_max, + to_char(r.earliest_start, 'YYYY-MM-DD HH24:MI:SS') as earliest_start_ansi, + to_char(r.earliest_finish, 'YYYY-MM-DD HH24:MI:SS') as earliest_finish_ansi, + to_char(r.latest_start, 'YYYY-MM-DD HH24:MI:SS') as latest_start_ansi, + to_char(r.latest_finish, 'YYYY-MM-DD HH24:MI:SS') as latest_finish_ansi, + r.description as task_description, + project_revision.title as project_name + FROM + pm_tasks t, + cr_items task_item, + pm_task_status s, + pm_tasks_revisionsx r, + cr_items project_item, + cr_revisions project_revision + WHERE + r.parent_id = project_item.item_id and + t.task_id = task_item.item_id and + task_item.live_revision = r.revision_id and + project_item.live_revision = project_revision.revision_id and + r.item_id = t.task_id and + t.status = s.status_id and + t.task_id = :task_item_id" + + if {[string equal $status_type "c"]} { + + # this is already closed + return + + } + + # set the new status + + set status_code [pm::task::default_status_closed] + + db_dml update_status " + UPDATE + pm_tasks + SET + status = :status_code + WHERE + task_id = :task_item_id" + + # send out an email notification + + set earliest_start [lc_time_fmt $earliest_start_ansi "%x"] + set earliest_finish [lc_time_fmt $earliest_finish_ansi "%x"] + set latest_start [lc_time_fmt $latest_start_ansi "%x"] + set latest_finish [lc_time_fmt $latest_finish_ansi "%x"] + + + set assignees [db_list get_assignees " + select + email + FROM + pm_task_assignment a, + parties p + WHERE + task_id = :task_item_id and + a.party_id = p.party_id + "] + + if {[llength $assignees] > 0} { + + set to_address [join $assignees ", "] + + set user_id [ad_conn user_id] + + set from_address [db_string get_from_email "select email from parties where party_id = :user_id" -default "nobody@nowhere.com"] + + set last_time_stamp "" + set work_log "----------------------\nWork done on this task\n----------------------\n\n" + + db_foreach get_logged_time " + SELECT + to_char(le.time_stamp, 'fmDyfm fmMMfm-fmDDfm-YYYY') as time_stamp_pretty, + le.value, + le.description, + r.title as task_name, + submitter.first_names || ' ' || submitter.last_name as user_name + FROM + logger_entries le, + cr_items i, + cr_revisions r, + pm_task_logger_proj_map m, + logger_projects lp, + acs_objects ao, + acs_users_all submitter + WHERE + r.item_id = m.task_item_id and + i.live_revision = r.revision_id and + r.item_id = :task_item_id and + le.project_id = lp.project_id and + ao.object_id = le.entry_id and + le.entry_id = m.logger_entry and + ao.creation_user = submitter.user_id + ORDER BY + le.time_stamp desc" { + if {![string equal $time_stamp_pretty $last_time_stamp]} { + append work_log "* $time_stamp_pretty\n\n" + } + append work_log "[pm::util::string_truncate_and_pad -length 25 -string "$user_name:"] $description ($value hrs)\n" + + set last_time_stamp $time_stamp_pretty + } + + set task_url "[parameter::get_from_package_key -package_key acs-kernel -parameter SystemURL][ad_conn package_url]task-one?task_id=$task_item_id" + + set subject "Task closed (was $status_description) $task_title" + + if {[parameter::get_from_package_key -package_key project-manager -parameter UseUncertainCompletionTimesP]} { + set estimated_work "\nHrs work (min): $estimated_hours_work_min\nHrs work (max): $estimated_hours_work_max" + } else { + set estimated_work "\nHrs work: $estimated_hours_work" + + } + + set notification_text "Task closed, was $status_description\n\n" + + append notification_text " +------------- +Task ID: \#$task_item_id +Description: $task_title +Project: $project_name + +Link: $task_url +" + append notification_text "\n\n$work_log" + + append notification_text " +--------------- +Estimated work: +---------------$estimated_work + +------ +Dates: +------ +Earliest start: $earliest_start +Earliest finish: $earliest_finish +Latest start: $latest_start +Latest finish $latest_finish + +----------- +Description +----------- +$task_description" + + + acs_mail_lite::send \ + -to_addr $to_address \ + -from_addr $from_address \ + -subject $subject \ + -body $notification_text + } + + return +} + + + +ad_proc -public pm::task::email_status {} { + + set send_email_p [parameter::get_from_package_key -package_key "project-manager" -parameter SendDailyEmail -default "0"] + + if {[string equal $send_email_p "0"]} { + ns_log Notice "Parameter SendDailyEmail for project manager says skip email today" + return + } + + acs_mail_lite::send \ + -to_addr jader@bread.com \ + -from_addr jader@bread.com \ + -subject "Reminder: pm::task::email_status is starting..." \ + -body "It is starting" + + set parties [list] + + # what if the person assigned is no longer a part of the subsite? + # right now, we still email them. + + db_foreach get_all_open_tasks " + SELECT + ts.task_id, + ts.task_id as item_id, + ts.task_number, + t.task_revision_id, + t.title, + to_char(t.earliest_start,'J') as earliest_start_j, + to_char(current_timestamp,'J') as today_j, + to_char(t.latest_start,'J') as latest_start_j, + to_char(t.latest_start,'YYYY-MM-DD HH24:MI') as latest_start, + to_char(t.latest_finish,'YYYY-MM-DD HH24:MI') as latest_finish, + t.percent_complete, + t.estimated_hours_work, + t.estimated_hours_work_min, + t.estimated_hours_work_max, + case when t.actual_hours_worked is null then 0 + else t.actual_hours_worked end as actual_hours_worked, + to_char(t.earliest_start,'YYYY-MM-DD HH24:MI') as earliest_start, + to_char(t.earliest_finish,'YYYY-MM-DD HH24:MI') as earliest_finish, + to_char(t.latest_start,'YYYY-MM-DD HH24:MI') as latest_start, + to_char(t.latest_finish,'YYYY-MM-DD HH24:MI') as latest_finish, + p.first_names || ' ' || p.last_name as full_name, + p.party_id, + (select one_line from pm_roles r where ta.role_id = r.role_id) as role + FROM + pm_tasks ts, + pm_tasks_revisionsx t, + pm_task_assignment ta, + acs_users_all p, + cr_items i, + pm_task_status s + WHERE + ts.task_id = t.item_id and + i.item_id = t.item_id and + t.task_revision_id = i.live_revision and + ts.status = s.status_id and + s.status_type = 'o' and + t.item_id = ta.task_id and + ta.party_id = p.party_id + ORDER BY + t.latest_start asc" { + set earliest_start_pretty [lc_time_fmt $earliest_start "%x"] + set earliest_finish_pretty [lc_time_fmt $earliest_finish "%x"] + set latest_start_pretty [lc_time_fmt $latest_start "%x"] + set latest_finish_pretty [lc_time_fmt $latest_finish "%x"] + + if {[exists_and_not_null earliest_start_j]} { + set slack_time [pm::task::slack_time \ + -earliest_start_j $earliest_start_j \ + -today_j $today_j \ + -latest_start_j $latest_start_j] + + } + + if {[lsearch $parties $party_id] == -1} { + lappend parties $party_id + } + + lappend task_list($party_id) $task_id + set titles_arr($task_id) $title + set ls_arr($task_id) $latest_start_pretty + set lf_arr($task_id) $latest_finish_pretty + set slack_arr($task_id) $slack_time + + # how many tasks does this person have? + if {[info exists task_count($party_id)]} { + incr task_count($party_id) + } else { + set task_count($party_id) 1 + } + } + + # transitions are < this value + set OVERDUE_THRESHOLD 0 + set PRESSING_THRESHOLD 7 + set LONGTERM_THRESHOLD 90 + + set TASK_LENGTH 70 + set TASK_ID_LENGTH 9 + + foreach party $parties { + + set subject "Daily Task status report" + set address [db_string get_email "select email from parties where party_id = :party" -default "jade-errors@bread.com"] + + set overdue [list] + set pressing [list] + set longterm [list] + + foreach task $task_list($party) { + + if {$slack_arr($task) < $OVERDUE_THRESHOLD} { + set which_pile overdue + } elseif {$slack_arr($task) < $PRESSING_THRESHOLD} { + set which_pile pressing + } elseif {$slack_arr($task) < $PRESSING_THRESHOLD} { + set which_pile longterm + } else { + set which_pile "" + } + + if {![empty_string_p $which_pile]} { + + set trimmed_task [pm::util::string_truncate_and_pad -length $TASK_ID_LENGTH -string $task] + set trimmed_title [pm::util::string_truncate_and_pad -length $TASK_LENGTH -string $titles_arr($task)] + + lappend $which_pile "$trimmed_task $trimmed_title" + lappend $which_pile "[string repeat " " $TASK_ID_LENGTH] LS: $ls_arr($task) LF: $lf_arr($task) Slack: $slack_arr($task)" + lappend $which_pile "" + } + + } + + set description [list] + + set overdue_title "OVERDUE TASKS" + set overdue_title [pm::util::string_truncate_and_pad -length $TASK_LENGTH -string $overdue_title] + + set overdue_description "consult with people affected, and let them know deadlines are affected" + + set pressing_title "PRESSING TASKS" + set pressing_title [pm::util::string_truncate_and_pad -length $TASK_LENGTH -string $pressing_title] + + set pressing_description "you need to start working on these soon to avoid affecting deadlines" + + set longterm_title "LONG TERM TASKS" + set longterm_title [pm::util::string_truncate_and_pad -length $TASK_LENGTH -string $longterm_title] + set longterm_description "look over these to plan ahead" + + # okay, let's now set up the email body + + lappend description "This is a daily reminder of tasks that are assigned to you" + lappend description "You current have $task_count($party) tasks assigned to you" + + lappend description "" + lappend description "\# $overdue_title" + + set length [string length $overdue_description] + lappend description [string repeat "_" $length] + + lappend description $overdue_description + + lappend description "" + foreach overdue_item $overdue { + lappend description $overdue_item + } + + lappend description "" + lappend description "\# $pressing_title" + + set length [string length $pressing_description] + lappend description [string repeat "_" $length] + + lappend description $pressing_description + + + lappend description "" + foreach pressing_item $pressing { + lappend description $pressing_item + } + + lappend description "" + lappend description "\# $longterm_title" + + set length [string length $longterm_description] + lappend description [string repeat "_" $length] + + lappend description $longterm_description + + lappend description "" + foreach longterm_item $longterm { + lappend description $longterm_item + } + + acs_mail_lite::send \ + -to_addr $address \ + -from_addr $address \ + -subject $subject \ + -body [join $description "\n"] + + } + + # consider also sending out emails to people who have assigned + # tickets to nobody + +} + + + +ad_proc -private pm::task::email_status_init { +} { + Schedules the daily emailings + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-14 + + @return + + @error +} { + ns_log Notice "Scheduling daily email notifications for project manager to 5:00 am" + ad_schedule_proc -thread t -debug t -schedule_proc ns_schedule_daily "5 0" pm::task::email_status +} +======= + + +ad_proc -public pm::task::link { + -task_item_id_1:required + -task_item_id_2:required +} { + Links two tasks together + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-03-10 + + @param task_item_id_1 + + @param task_item_id_2 + + @return + + @error +} { + + if {[string equal $task_item_id_1 $task_item_id_2]} { + # do nothing + ns_log Notice "Project-manager: Cannot link a task to itself!" + } elseif {$task_item_id_1 < $task_item_id_2} { + db_dml link_tasks " + INSERT INTO + pm_task_xref + (task_id_1, task_id_2) + VALUES + (:task_item_id_1, :task_item_id_2)" + } else { + db_dml link_tasks " + INSERT INTO + pm_task_xref + (task_id_1, task_id_2) + VALUES + (:task_item_id_2, :task_item_id_1)" + } + +} + + +ad_proc -public pm::task::assign_remove_everyone { + -task_item_id:required +} { + Removes all assignments for a task + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-09 + + @param task_item_id + + @return + + @error +} { + db_dml remove_assignment " + delete from pm_task_assignment where task_id = :task_item_id" +} + + +ad_proc -public pm::task::assign { + -task_item_id:required + -party_id:required + {-role_id ""} +} { + Assigns party_id to task_item_id + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-05 + + @param task_item_id + + @param party_id + + @param role_id the role under which the person is assigned + + @return + + @error +} { + if {![exists_and_not_null role_id]} { + set role_id [pm::role::default] + } + + db_dml add_assignment " + insert into pm_task_assignment + (task_id, + role_id, + party_id) + values + (:task_item_id, + :role_id, + :party_id) + " +} + + +ad_proc -public pm::task::open { + -task_item_id:required +} { + Opens a task, and sends notifications, unless it was already + open. If it was already open, does nothing. + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-22 + + @param task_item_id + + @return + + @error +} { + # find out what the status of the task was, and while we're at it, + # get other interesting information about the task, in case we + # want to close it. Then we can put this info in the email. + + db_1row get_status " + SELECT + t.status, + s.status_type, + s.description as status_description, + r.title as task_title, + r.estimated_hours_work, + r.estimated_hours_work_min, + r.estimated_hours_work_max, + to_char(r.earliest_start, 'YYYY-MM-DD HH24:MI:SS') as earliest_start_ansi, + to_char(r.earliest_finish, 'YYYY-MM-DD HH24:MI:SS') as earliest_finish_ansi, + to_char(r.latest_start, 'YYYY-MM-DD HH24:MI:SS') as latest_start_ansi, + to_char(r.latest_finish, 'YYYY-MM-DD HH24:MI:SS') as latest_finish_ansi, + r.description as task_description, + project_revision.title as project_name + FROM + pm_tasks t, + cr_items task_item, + pm_task_status s, + pm_tasks_revisionsx r, + cr_items project_item, + cr_revisions project_revision + WHERE + r.parent_id = project_item.item_id and + t.task_id = task_item.item_id and + task_item.live_revision = r.revision_id and + project_item.live_revision = project_revision.revision_id and + r.item_id = t.task_id and + t.status = s.status_id and + t.task_id = :task_item_id" + + if {[string equal $status_type "o"]} { + + # this is already open + return + + } + + # set the new status + + set status_code [pm::task::default_status_open] + + db_dml update_status " + UPDATE + pm_tasks + SET + status = :status_code + WHERE + task_id = :task_item_id" + + # send out an email notification + + set earliest_start [lc_time_fmt $earliest_start_ansi "%x"] + set earliest_finish [lc_time_fmt $earliest_finish_ansi "%x"] + set latest_start [lc_time_fmt $latest_start_ansi "%x"] + set latest_finish [lc_time_fmt $latest_finish_ansi "%x"] + + set assignees [db_list get_assignees " + select + email + FROM + pm_task_assignment a, + parties p + WHERE + task_id = :task_item_id and + a.party_id = p.party_id + "] + + if {[llength $assignees] > 0} { + + set to_address [join $assignees ", "] + + set user_id [ad_conn user_id] + + set from_address [db_string get_from_email "select email from parties where party_id = :user_id" -default "nobody@nowhere.com"] + + set task_url "[parameter::get_from_package_key -package_key acs-kernel -parameter SystemURL][ad_conn package_url]task-one?task_id=$task_item_id" + + set subject "Task reopened (was $status_description): $task_title" + + if {[parameter::get_from_package_key -package_key project-manager -parameter UseUncertainCompletionTimesP]} { + set estimated_work "\nHrs work (min): $estimated_hours_work_min\nHrs work (max): $estimated_hours_work_max" + } else { + set estimated_work "\nHrs work: $estimated_hours_work" + + } + + set notification_text "Task reopened, was $status_description\n\n" + + append notification_text " +------------- +Task ID: \#$task_item_id +Description: $task_title +Project: $project_name + +Link: $task_url + +--------------- +Estimated work: +---------------$estimated_work + +------ +Dates: +------ +Earliest start: $earliest_start +Earliest finish: $earliest_finish +Latest start: $latest_start +Latest finish $latest_finish + +----------- +Description +----------- +$task_description" + + + append notification_text "\n" + + acs_mail_lite::send \ + -to_addr $to_address \ + -from_addr $from_address \ + -subject $subject \ + -body $notification_text + } + + return +} + +ad_proc -public pm::task::close { + -task_item_id:required +} { + Closes a task, and sends notifications, unless it was already + closed. If it was already closed, does nothing. + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-22 + + @param task_item_id + + @return + + @error +} { + # find out what the status of the task was + + db_1row get_status " + SELECT + t.status, + s.status_type, + s.description as status_description, + r.title as task_title, + r.estimated_hours_work, + r.estimated_hours_work_min, + r.estimated_hours_work_max, + to_char(r.earliest_start, 'YYYY-MM-DD HH24:MI:SS') as earliest_start_ansi, + to_char(r.earliest_finish, 'YYYY-MM-DD HH24:MI:SS') as earliest_finish_ansi, + to_char(r.latest_start, 'YYYY-MM-DD HH24:MI:SS') as latest_start_ansi, + to_char(r.latest_finish, 'YYYY-MM-DD HH24:MI:SS') as latest_finish_ansi, + r.description as task_description, + project_revision.title as project_name + FROM + pm_tasks t, + cr_items task_item, + pm_task_status s, + pm_tasks_revisionsx r, + cr_items project_item, + cr_revisions project_revision + WHERE + r.parent_id = project_item.item_id and + t.task_id = task_item.item_id and + task_item.live_revision = r.revision_id and + project_item.live_revision = project_revision.revision_id and + r.item_id = t.task_id and + t.status = s.status_id and + t.task_id = :task_item_id" + + if {[string equal $status_type "c"]} { + + # this is already closed + return + + } + + # set the new status + + set status_code [pm::task::default_status_closed] + + db_dml update_status " + UPDATE + pm_tasks + SET + status = :status_code + WHERE + task_id = :task_item_id" + + # send out an email notification + + set earliest_start [lc_time_fmt $earliest_start_ansi "%x"] + set earliest_finish [lc_time_fmt $earliest_finish_ansi "%x"] + set latest_start [lc_time_fmt $latest_start_ansi "%x"] + set latest_finish [lc_time_fmt $latest_finish_ansi "%x"] + + + set assignees [db_list get_assignees " + select + email + FROM + pm_task_assignment a, + parties p + WHERE + task_id = :task_item_id and + a.party_id = p.party_id + "] + + if {[llength $assignees] > 0} { + + set to_address [join $assignees ", "] + + set user_id [ad_conn user_id] + + set from_address [db_string get_from_email "select email from parties where party_id = :user_id" -default "nobody@nowhere.com"] + + set last_time_stamp "" + set work_log "----------------------\nWork done on this task\n----------------------\n\n" + + db_foreach get_logged_time " + SELECT + to_char(le.time_stamp, 'fmDyfm fmMMfm-fmDDfm-YYYY') as time_stamp_pretty, + le.value, + le.description, + r.title as task_name, + submitter.first_names || ' ' || submitter.last_name as user_name + FROM + logger_entries le, + cr_items i, + cr_revisions r, + pm_task_logger_proj_map m, + logger_projects lp, + acs_objects ao, + acs_users_all submitter + WHERE + r.item_id = m.task_item_id and + i.live_revision = r.revision_id and + r.item_id = :task_item_id and + le.project_id = lp.project_id and + ao.object_id = le.entry_id and + le.entry_id = m.logger_entry and + ao.creation_user = submitter.user_id + ORDER BY + le.time_stamp desc" { + if {![string equal $time_stamp_pretty $last_time_stamp]} { + append work_log "* $time_stamp_pretty\n\n" + } + append work_log "[pm::util::string_truncate_and_pad -length 25 -string "$user_name:"] $description ($value hrs)\n" + + set last_time_stamp $time_stamp_pretty + } + + set task_url "[parameter::get_from_package_key -package_key acs-kernel -parameter SystemURL][ad_conn package_url]task-one?task_id=$task_item_id" + + set subject "Task closed (was $status_description) $task_title" + + if {[parameter::get_from_package_key -package_key project-manager -parameter UseUncertainCompletionTimesP]} { + set estimated_work "\nHrs work (min): $estimated_hours_work_min\nHrs work (max): $estimated_hours_work_max" + } else { + set estimated_work "\nHrs work: $estimated_hours_work" + + } + + set notification_text "Task closed, was $status_description\n\n" + + append notification_text " +------------- +Task ID: \#$task_item_id +Description: $task_title +Project: $project_name + +Link: $task_url +" + append notification_text "\n\n$work_log" + + append notification_text " +--------------- +Estimated work: +---------------$estimated_work + +------ +Dates: +------ +Earliest start: $earliest_start +Earliest finish: $earliest_finish +Latest start: $latest_start +Latest finish $latest_finish + +----------- +Description +----------- +$task_description" + + + acs_mail_lite::send \ + -to_addr $to_address \ + -from_addr $from_address \ + -subject $subject \ + -body $notification_text + } + + return +} + + + +ad_proc -public pm::task::email_status {} { + + set send_email_p [parameter::get_from_package_key -package_key "project-manager" -parameter SendDailyEmail -default "0"] + + if {[string equal $send_email_p "0"]} { + ns_log Notice "Parameter SendDailyEmail for project manager says skip email today" + return + } + + # also don't send reminders on weekends. + + set today_j [db_string get_today "select to_char(current_timestamp,'J')"] + if {![pm::project::is_workday_p $today_j]} { + return + } + + set parties [list] + + # what if the person assigned is no longer a part of the subsite? + # right now, we still email them. + + db_foreach get_all_open_tasks " + SELECT + ts.task_id, + ts.task_id as item_id, + ts.task_number, + t.task_revision_id, + t.title, + to_char(t.earliest_start,'J') as earliest_start_j, + to_char(current_timestamp,'J') as today_j, + to_char(t.latest_start,'J') as latest_start_j, + to_char(t.latest_start,'YYYY-MM-DD HH24:MI') as latest_start, + to_char(t.latest_finish,'YYYY-MM-DD HH24:MI') as latest_finish, + t.percent_complete, + t.estimated_hours_work, + t.estimated_hours_work_min, + t.estimated_hours_work_max, + case when t.actual_hours_worked is null then 0 + else t.actual_hours_worked end as actual_hours_worked, + to_char(t.earliest_start,'YYYY-MM-DD HH24:MI') as earliest_start, + to_char(t.earliest_finish,'YYYY-MM-DD HH24:MI') as earliest_finish, + to_char(t.latest_start,'YYYY-MM-DD HH24:MI') as latest_start, + to_char(t.latest_finish,'YYYY-MM-DD HH24:MI') as latest_finish, + p.first_names || ' ' || p.last_name as full_name, + p.party_id, + (select one_line from pm_roles r where ta.role_id = r.role_id) as role + FROM + pm_tasks ts, + pm_tasks_revisionsx t, + pm_task_assignment ta, + acs_users_all p, + cr_items i, + pm_task_status s + WHERE + ts.task_id = t.item_id and + i.item_id = t.item_id and + t.task_revision_id = i.live_revision and + ts.status = s.status_id and + s.status_type = 'o' and + t.item_id = ta.task_id and + ta.party_id = p.party_id + ORDER BY + t.latest_start asc" { + set earliest_start_pretty [lc_time_fmt $earliest_start "%x"] + set earliest_finish_pretty [lc_time_fmt $earliest_finish "%x"] + set latest_start_pretty [lc_time_fmt $latest_start "%x"] + set latest_finish_pretty [lc_time_fmt $latest_finish "%x"] + + if {[exists_and_not_null earliest_start_j]} { + set slack_time [pm::task::slack_time \ + -earliest_start_j $earliest_start_j \ + -today_j $today_j \ + -latest_start_j $latest_start_j] + + } + + if {[lsearch $parties $party_id] == -1} { + lappend parties $party_id + } + + lappend task_list($party_id) $task_id + set titles_arr($task_id) $title + set ls_arr($task_id) $latest_start_pretty + set lf_arr($task_id) $latest_finish_pretty + set slack_arr($task_id) $slack_time + + # how many tasks does this person have? + if {[info exists task_count($party_id)]} { + incr task_count($party_id) + } else { + set task_count($party_id) 1 + } + } + + # transitions are < this value + set OVERDUE_THRESHOLD 0 + set PRESSING_THRESHOLD 7 + set LONGTERM_THRESHOLD 90 + + set TASK_LENGTH 70 + set TASK_ID_LENGTH 9 + + foreach party $parties { + + set subject "Daily Task status report" + set address [db_string get_email "select email from parties where party_id = :party" -default "jade-errors@bread.com"] + + set overdue [list] + set pressing [list] + set longterm [list] + + foreach task $task_list($party) { + + if {$slack_arr($task) < $OVERDUE_THRESHOLD} { + set which_pile overdue + } elseif {$slack_arr($task) < $PRESSING_THRESHOLD} { + set which_pile pressing + } elseif {$slack_arr($task) < $PRESSING_THRESHOLD} { + set which_pile longterm + } else { + set which_pile "" + } + + if {![empty_string_p $which_pile]} { + + set trimmed_task [pm::util::string_truncate_and_pad -length $TASK_ID_LENGTH -string $task] + set trimmed_title [pm::util::string_truncate_and_pad -length $TASK_LENGTH -string $titles_arr($task)] + + lappend $which_pile "$trimmed_task $trimmed_title" + lappend $which_pile "[string repeat " " $TASK_ID_LENGTH] LS: $ls_arr($task) LF: $lf_arr($task) Slack: $slack_arr($task)" + lappend $which_pile "" + } + + } + + set description [list] + + set overdue_title "OVERDUE TASKS" + set overdue_title [pm::util::string_truncate_and_pad -length $TASK_LENGTH -string $overdue_title] + + set overdue_description "consult with people affected, and let them know deadlines are affected" + + set pressing_title "PRESSING TASKS" + set pressing_title [pm::util::string_truncate_and_pad -length $TASK_LENGTH -string $pressing_title] + + set pressing_description "you need to start working on these soon to avoid affecting deadlines" + + set longterm_title "LONG TERM TASKS" + set longterm_title [pm::util::string_truncate_and_pad -length $TASK_LENGTH -string $longterm_title] + set longterm_description "look over these to plan ahead" + + # okay, let's now set up the email body + + lappend description "This is a daily reminder of tasks that are assigned to you" + lappend description "You current have $task_count($party) tasks assigned to you" + + lappend description "" + lappend description "\# $overdue_title" + + set length [string length $overdue_description] + lappend description [string repeat "_" $length] + + lappend description $overdue_description + + lappend description "" + foreach overdue_item $overdue { + lappend description $overdue_item + } + + lappend description "" + lappend description "\# $pressing_title" + + set length [string length $pressing_description] + lappend description [string repeat "_" $length] + + lappend description $pressing_description + + + lappend description "" + foreach pressing_item $pressing { + lappend description $pressing_item + } + + lappend description "" + lappend description "\# $longterm_title" + + set length [string length $longterm_description] + lappend description [string repeat "_" $length] + + lappend description $longterm_description + + lappend description "" + foreach longterm_item $longterm { + lappend description $longterm_item + } + + acs_mail_lite::send \ + -to_addr $address \ + -from_addr $address \ + -subject $subject \ + -body [join $description "\n"] + + } + + # consider also sending out emails to people who have assigned + # tickets to nobody + +} + + + +ad_proc -private pm::task::email_status_init { +} { + Schedules the daily emailings + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-04-14 + + @return + + @error +} { + ns_log Notice "Scheduling daily email notifications for project manager to 5:00 am" + ad_schedule_proc -thread t -debug t -schedule_proc ns_schedule_daily "5 0" pm::task::email_status +} + + + +ad_proc -public pm::task::email_alert { + -task_item_id:required + {-user_id ""} + {-assignee_id ""} + {-assignee_role_name ""} + {-edit_p "t"} + {-comment ""} + {-description ""} + {-old_description ""} + {-subject ""} + {-work ""} + {-work_min ""} + {-work_max ""} + {-project_name ""} + {-earliest_start ""} + {-earliest_finish ""} + {-latest_start ""} + {-latest_finish ""} + {-url ""} +} { + Sends out an email notification when changes have been made to a task + +

+ + If any of the following are missing, fills in from the database: + subject, work, work_min, work_max, project_name, earliest_start, + earliest_finish, latest_start, latest_finish + + @author Jade Rubick (jader@bread.com) + @creation-date 2004-05-03 + + @param task_item_id + + @param user_id The user making the change + + @param assignee_id The party_id of the user assigned to the task. + + @param assignee_role_name The role name for what the party is + assigned to do + + @param edit_p Is this an edited task, or a new one? t for edited, + f otherwise. + + @param description + + @param old_description + + @param subject The one line description of the task + + @param work Estimated hours work + + @param work_min Estimated minimimum hours work + + @param work_max Estimated maximum hours work + + @param project_name + + @param earliest_start + + @param earliest_finish + + @param latest_start + + @param latest_finish + + @param url Optionally, a URL that the user is directed to + + @return + + @error +} { + + set task_term [parameter::get -parameter "Taskname" -default "Task"] + set task_term_lower [parameter::get -parameter "taskname" -default "task"] + set use_uncertain_completion_times_p [parameter::get -parameter "UseUncertainCompletionTimesP" -default "0"] + + # from address + + if {![exists_and_not_null $user_id]} { + set user_id [ad_conn user_id] + } + + db_1row get_from_address_and_more { + SELECT + p.email as from_address, + p2.first_names || ' ' || p2.last_name as mod_username + FROM + parties p, + persons p2 + WHERE + p.party_id = :user_id and + p.party_id = p2.person_id + } + + # to address + + if {![exists_and_not_null assignee_id]} { + + # bug: we should get the list of assignees here. + ns_log Error "the proc pm::task::email_alert is not complete: assignee" + + } + + set to_address [db_string get_email "select email from parties where party_id = :assignee_id"] + + + # if they left out any of the task info, then we get it from the database + if { \ + ![exists_and_not_null subject] || \ + ![exists_and_not_null work] || \ + ![exists_and_not_null work_min] || \ + ![exists_and_not_null work_max] || \ + ![exists_and_not_null project_name] || \ + ![exists_and_not_null earliest_start] || \ + ![exists_and_not_null earliest_finish] || \ + ![exists_and_not_null latest_start] || \ + ![exists_and_not_null latest_finish] \ + } { + + db_1row get_task_info { + SELECT + t.title as subject, + to_char(t.earliest_start,'MM-DD-YYYY') as earliest_start, + to_char(t.earliest_finish,'MM-DD-YYYY') as earliest_finish, + to_char(t.latest_start,'MM-DD-YYYY') as latest_start, + to_char(t.latest_finish,'MM-DD-YYYY') as latest_finish, + t.estimated_hours_work as work, + t.estimated_hours_work_min as work_min, + t.estimated_hours_work_max as work_max, + t.percent_complete, + p.title as project_name + FROM + pm_tasks_revisionsx t, + cr_items i, + cr_items project, + pm_projectsx p + WHERE + t.item_id = :task_item_id and + t.revision_id = i.live_revision and + t.item_id = i.item_id and + t.parent_id = project.item_id and + project.item_id = p.item_id and + project.live_revision = p.revision_id + } + + } + + + if {[string equal $edit_p "t"]} { + set subject_out "Edited $task_term \#$task_item_id: $subject" + set intro_text "$mod_username edited this $task_term_lower" + } else { + set subject_out "New $task_term \#$task_item_id: $subject" + set intro_text "$mod_username assigned you to a new $task_term_lower" + } + + if {[empty_string_p $comment]} { + set comment_text "" + } else { + set comment_text "--------\nCOMMENT:\n--------\n$comment\n\n" + } + + if {[exists_and_not_null url]} { + set task_url $url + } else { + set task_url "unavailable" + } + + + + if {![string equal $description $old_description] && [string equal $edit_p t]} { + set description_out "$description \n\n-------\nOld description:\n-------\n\n[ad_html_to_text $old_description]" + + append intro_text "\nSee below to see the changes in the description" + + } else { + set description_out [ad_html_to_text $description] + } + + if {[string equal $use_uncertain_completion_times_p 1]} { + set estimated_work "\nHrs work (min): $work_min" + append estimated_work "\nHrs work (max): $work_max" + } else { + set estimated_work "\nHrs work: $work" + } + + set notification_text "$intro_text\n\n$comment_text-------------" + append notification_text "\nTask overview\n-------------" + append notification_text "\nTask ID: \#$task_item_id" + append notification_text "\nSubject: $subject" + append notification_text "\nProject: $project_name" + append notification_text "\nYour role: $assignee_role_name" + append notification_text "\nLink: $task_url" + append notification_text "\n\n\n\n---------------" + append notification_text "\nEstimated work:\n---------------$estimated_work" + append notification_text "\n\n------\nDates:" + append notification_text "\n------\nEarliest start: $earliest_start" + append notification_text "\nEarliest finish: $earliest_finish" + append notification_text "\nLatest start: $latest_start" + append notification_text "\nLatest finish *$latest_finish" + append notification_text "\n\n-----------\nDescription\n-----------" + append notification_text "\n$description_out" + + acs_mail_lite::send \ + -to_addr $to_address \ + -from_addr $from_address \ + -subject $subject_out \ + -body $notification_text + + +} + + +>>>>>>> 1.8