/[drupal]/contributions/modules/project_forecast/project_forecast.module
ViewVC logotype

Contents of /contributions/modules/project_forecast/project_forecast.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.5 - (show annotations) (download) (as text)
Thu Oct 30 14:57:01 2008 UTC (12 months, 3 weeks ago) by jpetso
Branch: MAIN
CVS Tags: DRUPAL-6--1-0-RC3, DRUPAL-6--1-0, HEAD
Changes since 1.4: +6 -21 lines
File MIME type: text/x-php
Yay for usability: Eliminate the "Task filters" page and instead
put the filters next to the view settings on the central admin page.
Less error prone, better overview, outright damn cool.

With this in place, I feel confident to release an 1.0.
(But first, sevi's improved Task Status add-on shall still be merged.)
1 <?php
2 // $Id: project_forecast.module,v 1.4 2008/10/01 15:20:36 jpetso Exp $
3 /**
4 * @file
5 * Project Forecast - Estimate completion dates for your project's tasks.
6 *
7 * Copyright 2007, 2008 by Jakob Petsovits ("jpetso", http://drupal.org/user/56020)
8 */
9
10 define('PROJECT_FORECAST_FAR_TARGET_DATE', 0);
11
12 include_once(drupal_get_path('module', 'project_forecast') . '/project_forecast.providers.inc');
13
14
15 /**
16 * Implementation of hook_menu().
17 */
18 function project_forecast_menu() {
19 $items['admin/project/forecast'] = array(
20 'title' => 'Project forecast',
21 'description' => 'Configure settings for tasks completion date forecasts.',
22 'file' => 'project_forecast.admin.inc',
23 'page callback' => 'drupal_get_form',
24 'page arguments' => array('project_forecast_admin'),
25 'access arguments' => array('administer project forecast'),
26 );
27 $items['admin/project/forecast/task-filters-open/add'] = array(
28 'title' => 'Add filter for open tasks',
29 'description' => 'Add a filter for open tasks to the tasks view.',
30 'file' => 'project_forecast.admin.filters.inc',
31 'page callback' => 'drupal_get_form',
32 'page arguments' => array('project_forecast_admin_task_filters_open_add'),
33 'access arguments' => array('administer project forecast'),
34 'type' => MENU_CALLBACK,
35 );
36 $items['admin/project/forecast/task-filters-open/edit/%'] = array(
37 'title' => 'Edit filter',
38 'file' => 'project_forecast.admin.filters.inc',
39 'page callback' => 'drupal_get_form',
40 'page arguments' => array('project_forecast_admin_task_filters_open_edit', 5),
41 'access arguments' => array('administer project forecast'),
42 );
43 $items['admin/project/forecast/task-filters-open/delete/%'] = array(
44 'file' => 'project_forecast.admin.filters.inc',
45 'page callback' => 'project_forecast_admin_task_filters_open_delete',
46 'page arguments' => array(5),
47 'access arguments' => array('administer project forecast'),
48 );
49 return $items;
50 }
51
52 /**
53 * Implementation of hook_perm().
54 */
55 function project_forecast_perm() {
56 return array('administer project forecast');
57 }
58
59 /**
60 * Implementation of hook_cron():
61 * Update the forecast periodically, because there is no reliable trigger
62 * (apart from updates of the time budget or the admin settings).
63 */
64 function project_forecast_cron() {
65 if (variable_get('project_forecast_update_on_cron', 1)) {
66 project_forecast_recalculate_all();
67 }
68 }
69
70 /**
71 * Implementation of hook_value_provider_modified():
72 * Update the forecast if something happens to a node with time need data.
73 */
74 function project_forecast_value_provider_modified($node, $op, $modified_providers_by_type) {
75 $timeneed_provider_name = variable_get('project_forecast_timeneed_provider', '');
76 $modified = FALSE;
77
78 foreach ($modified_providers_by_type as $value_type => $modified_providers) {
79 if ($value_type == 'hours' && isset($modified_providers[$timeneed_provider_name])) {
80 $modified = TRUE;
81 break;
82 }
83 }
84 if ($modified) {
85 project_forecast_recalculate_all();
86 }
87 }
88
89 /**
90 * Implementation of hook_timebudget_updated().
91 */
92 function project_forecast_timebudget_updated($uid) {
93 project_forecast_recalculate_user($uid);
94 }
95
96 /**
97 * Calculate the estimated time when the given user will probably complete
98 * the open tasks, and update the database with this data.
99 */
100 function project_forecast_recalculate_user($uid) {
101 $user_targets = _project_forecast_tasks(array('assigned_uid' => $uid));
102 $user_task_targets = _project_forecast_user_task_targets($uid, $user_targets);
103 _project_forecast_db_update_user_task_targets($uid, $user_task_targets);
104 module_invoke_all('project_forecast_updated', $uid);
105 }
106
107 /**
108 * Calculate the estimated time when all users will probably complete
109 * their open tasks, and update the database with this data.
110 */
111 function project_forecast_recalculate_all() {
112 db_query("DELETE FROM {project_forecast_tasks}", $uid); // just to make sure
113
114 $tasks = _project_forecast_tasks(array('open_tasks_only' => TRUE));
115 $tasks_by_user = _project_forecast_tasks_by_user($tasks);
116
117 foreach ($tasks_by_user as $uid => $user_tasks) {
118 $user_task_targets = _project_forecast_user_task_targets($uid, $user_tasks);
119 _project_forecast_db_update_user_task_targets($uid, $user_task_targets);
120 }
121 module_invoke_all('project_forecast_updated');
122 }
123
124 function _project_forecast_db_update_user_task_targets($uid, $user_task_targets) {
125 db_query("DELETE FROM {project_forecast_tasks} WHERE uid = %d", $uid);
126
127 foreach ($user_task_targets as $project_nid => $user_project_task_targets) {
128 foreach ($user_project_task_targets['completed_tasks'] as $unit_timestamp => $task_target) {
129 foreach ($task_target['tasks'] as $task) {
130 db_query("INSERT INTO {project_forecast_tasks}
131 (nid, uid, unit, completed) VALUES (%d, %d, %d, %d)",
132 $task['nid'], $uid, $unit_timestamp, TRUE);
133 }
134 }
135 foreach ($user_project_task_targets['missing_tasks'] as $nid => $task) {
136 db_query("INSERT INTO {project_forecast_tasks}
137 (nid, uid, unit, completed) VALUES (%d, %d, %d, %d)",
138 $task['nid'], $uid, PROJECT_FORECAST_FAR_TARGET_DATE, FALSE);
139 }
140 }
141 }
142
143 /**
144 * The heart of the evaluation logic: Determine which tasks will probably
145 * be completed in which calendar week.
146 *
147 * @param $uid
148 * The user for whom the completion target dates are being calculated.
149 * @param $open_tasks
150 * The list of tasks that the above user needs to complete.
151 *
152 * @return
153 * An array with target weeks that looks like
154 * array(
155 * $project_nid =>
156 * 'completed_tasks' => array(
157 * $unit_begin_timestamp => array(
158 * 'unit' => $unit_begin_date,
159 * 'tasks' => array(
160 * $completed_task_1_nid => $completed_task_1,
161 * $completed_task_2_nid => $completed_task_2,
162 * ...
163 * ),
164 * ),
165 * ...
166 * ),
167 * 'missing_tasks' => array(
168 * $missing_task_1_nid => $missing_task_1,
169 * $missing_task_2_nid => $missing_task_2,
170 * ...
171 * ),
172 * ),
173 * ...
174 * ).
175 * A task array has the same format as one of the array elements
176 * returned by _project_forecast_tasks().
177 */
178 function _project_forecast_user_task_targets($uid, $open_tasks) {
179 $open_tasks = _project_forecast_tasks_by_project($open_tasks);
180 $budget = timebudget_by_unit($uid);
181 $targets_by_project = array();
182
183 foreach (array_keys($open_tasks) as $pnid) {
184 $targets_by_project[$pnid] = _project_forecast_user_project_task_targets(
185 $uid, $pnid, $budget, $open_tasks[$pnid]
186 );
187 }
188 return $targets_by_project;
189 }
190
191 function _project_forecast_user_project_task_targets($uid, $pnid, $budget, $open_tasks) {
192 $current_unit = timebudget_current_unit();
193 $current_unit_timestamp = (int) date_format($current_unit, 'U');
194
195 $current_unit_available = 0;
196
197 $targets = array('completed_tasks' => array(), 'missing_tasks' => array());
198
199 $initial = TRUE;
200 $mark_as_missing = FALSE;
201 $any_tasks_completed = FALSE;
202
203 if (!$uid) {
204 // Tasks without assigned user are doomed not to complete.
205 $mark_as_missing = TRUE;
206 }
207
208 foreach ($open_tasks as $task) {
209 if (!$mark_as_missing) {
210 // Create a new unit entry in $targets for the first unit or if there's
211 // no time for the remaining tasks in the current unit anymore.
212 while ($initial || $current_unit_available < $task['timeneed']) {
213
214 if ($initial) {
215 $initial = FALSE;
216 }
217 else {
218 timebudget_increment_unit($current_unit);
219 $current_unit_timestamp = (int) date_format($current_unit, 'U');
220 }
221 if (!isset($budget[$current_unit_timestamp])) {
222 break; // no time given anymore, makes no sense to evaluate stuff
223 }
224 // Ok, we still got a budget left, let's initialize the next unit.
225 // That's mainly determining how much time can be spent then.
226
227 // Add the time in the next unit to the one that was left over
228 // from the current one.
229 $current_unit_available = $current_unit_available +
230 $budget[$current_unit_timestamp][$pnid];
231
232 // Let extension modules alter the available time for this unit.
233 foreach (module_implements('project_forecast_available_time_alter') as $module) {
234 $function = $module . '_project_forecast_available_time_alter';
235 $function($hours, $uid, $project_nid, $project_tasks, $time_unit);
236 }
237
238 // The minimum available time is 0, don't go below that.
239 $current_unit_available = max(0, $current_unit_available);
240
241 $targets['completed_tasks'][$current_unit_timestamp] = array(
242 'unit' => $current_unit,
243 'tasks' => array(),
244 );
245 }
246 }
247 if (!isset($budget[$current_unit_timestamp])) {
248 $mark_as_missing = TRUE;
249 }
250
251 if ($mark_as_missing) {
252 $targets['missing_tasks'][$task['nid']] = $task;
253 }
254 else {
255 $any_tasks_completed = TRUE;
256 $targets['completed_tasks'][$current_unit_timestamp]['tasks'][$task['nid']] = $task;
257 $current_unit_available -= $task['timeneed'];
258 }
259 }
260 if (!$any_tasks_completed) {
261 $targets['completed_tasks'] = array();
262 }
263 return $targets;
264 }
265
266 /**
267 * Retrieves all tasks for the given user, sorted by project.
268 *
269 * @return
270 * An array in the following format:
271 * array(
272 * $project_nid => array(
273 * $task_nid => $task,
274 * ...
275 * ),
276 * ...
277 * ).
278 *
279 * @param $uid
280 * The user id of the user whose tasks should be retrieved.
281 */
282 function _project_forecast_tasks_by_project($tasks) {
283 $tasks_by_project = array();
284
285 foreach ($tasks as $nid => $task) {
286 // Resort tasks by project.
287 if (!isset($tasks_by_project[$task['project_nid']])) {
288 $tasks_by_project[$task['project_nid']] = array();
289 }
290 $tasks_by_project[$task['project_nid']][$nid] = $task;
291 }
292 return $tasks_by_project;
293 }
294
295 function _project_forecast_tasks_by_user($tasks) {
296 $tasks_by_user = array();
297
298 foreach ($tasks as $nid => $task) {
299 // Resort tasks by user.
300 if (!isset($tasks_by_user[$task['assigned_uid']])) {
301 $tasks_by_user[$task['assigned_uid']] = array();
302 }
303 $tasks_by_user[$task['assigned_uid']][$nid] = $task;
304 }
305 return $tasks_by_user;
306 }
307
308 /**
309 * Retrieves all tasks for the given user, considering the given constraints.
310 *
311 * @param $constraints
312 * An array of constraints that are applied as filters to the task list,
313 * with the following possible array keys:
314 *
315 * - 'assigned_uid': The user id of the user whose tasks should be retrieved.
316 * - 'assigned_users_only': Only tasks with assigned users should be retrieved.
317 */
318 function _project_forecast_tasks($constraints = array()) {
319 $view_settings = _project_forecast_tasks_view();
320 if (!$view_settings) {
321 return array();
322 }
323 $view_args = $view_settings['view_args'];
324 $view = $view_settings['view'];
325 $display_id = $view_settings['display_id'];
326
327 $value_providers = array();
328
329 foreach (module_implements('project_forecast_tasks_view_alter') as $module) {
330 $function = $module . '_project_forecast_tasks_view_alter';
331 $module_value_providers = $function($view, $display_id, $constraints);
332
333 if (is_array($module_value_providers)) {
334 $value_providers = $value_providers + $module_value_providers;
335 }
336 else if ($module_value_providers === FALSE) { // required settings are not set
337 return array();
338 }
339 }
340
341 // Make sure the query is not cached
342 $view->is_cacheable = FALSE;
343 $view->execute_display(NULL, $view_args);
344
345 // Parse the results into a readily usable result array.
346 $tasks = value_provider_results($view, $value_providers, 'nid', 'array');
347
348 // Let modules alter our tasks, probably by modifying the time need of some.
349 foreach (module_implements('project_forecast_tasks_alter') as $module) {
350 $function = $module . '_project_forecast_tasks_alter';
351 $function($tasks);
352 }
353 return $tasks;
354 }
355
356 /**
357 * Implementation of hook_project_forecast_tasks_view_alter():
358 * Add our own required fields to the tasks view -
359 * assigned user, time need, and project.
360 */
361 function project_forecast_project_forecast_tasks_view_alter(&$view, $display_id, $constraints) {
362 $timeneed_provider_name = variable_get('project_forecast_timeneed_provider', '');
363 $assigned_uid_provider_name = variable_get('project_forecast_assigned_uid_provider', '');
364 $project_nid_provider_name = variable_get('project_forecast_project_nid_provider', '');
365
366 $timeneed_provider = value_provider($timeneed_provider_name, 'hours');
367 $assigned_uid_provider = value_provider($assigned_uid_provider_name, 'uid');
368 $project_nid_provider = value_provider($project_nid_provider_name, 'nid');
369
370 if (!$timeneed_provider || !$assigned_uid_provider || !$project_nid_provider) {
371 return FALSE;
372 }
373
374 value_provider_add_fields($view, $display_id, $timeneed_provider);
375 value_provider_add_fields($view, $display_id, $assigned_uid_provider);
376 value_provider_add_fields($view, $display_id, $project_nid_provider);
377
378 // If any assigned user constraint is set, let's only get tasks for those users.
379 if (isset($constraints['assigned_uid'])) {
380 value_provider_add_filter($view, $display_id, $assigned_uid_provider,
381 'uid', $constraints['assigned_uid']);
382 }
383 if (isset($constraints['assigned_users_only'])) {
384 value_provider_add_filter($view, $display_id, $assigned_uid_provider,
385 'has_uid', TRUE);
386 }
387 if (!empty($constraints['open_tasks_only'])) {
388 $filters = variable_get('project_forecast_task_filters_open', array());
389 foreach ($filters as $filter) {
390 $handler = views_get_handler($filter['table'], $filter['field'], 'filter');
391 if (empty($handler)) {
392 continue;
393 }
394 $id = $view->add_item($display_id, 'filter', $filter['table'], $filter['field']);
395 $view->set_item($display_id, 'filter', $id, $filter['item']);
396 }
397 }
398
399 // We want the result to be inserted into the task array; return the property
400 // names and provider arrays so that project forecast does this for us.
401 return array(
402 'timeneed' => $timeneed_provider,
403 'assigned_uid' => $assigned_uid_provider,
404 'project_nid' => $project_nid_provider,
405 );
406 }
407
408 /**
409 * Return an array of view properties for a user's task list view,
410 * with the 'view' element holding the (already initialized) view object,
411 * 'view_args' holding the array of view arguments and 'timeneed_field'
412 * containing the CCK field that specifies to the time need for this task.
413 * NULL is returned if no view or field has been selected by the user.
414 */
415 function _project_forecast_tasks_view() {
416 $view_name = variable_get('project_forecast_tasks_view', '');
417
418 if (empty($view_name)) {
419 return NULL;
420 }
421 $view = views_get_view($view_name);
422
423 if (!$view) {
424 return NULL;
425 }
426 $view_args = variable_get('project_forecast_tasks_view_args', '');
427 $view_args = empty($view_args)
428 ? array()
429 : array_map('trim', explode(',', $view_args));
430
431 $display_id = 'default';
432
433 $view->init();
434 $view->set_display($display_id);
435
436 $view->display[$display_id]->handler->set_option('style_plugin', 'default');
437 $plugin = views_get_plugin('style', 'default');
438 $plugin->init($view, $view->display[$display_id]);
439
440 return array(
441 'view' => $view,
442 'view_args' => $view_args,
443 'display_id' => $display_id,
444 );
445 }
446
447 /**
448 * Implementation of hook_views_api().
449 */
450 function project_forecast_views_api() {
451 return array(
452 'api' => 2.0,
453 'path' => drupal_get_path('module', 'project_forecast') . '/views',
454 );
455 }

  ViewVC Help
Powered by ViewVC 1.1.2