PluginProbe
UpStream: a Project Management Plugin for WordPress / 1.39.3
UpStream: a Project Management Plugin for WordPress v1.39.3
trunk 1.39.0 1.39.1 1.39.2 1.39.3 2.0.7 2.1.0
upstream / includes / class-up-project.php

class-up-project.php in UpStream: a Project Management Plugin for WordPress 1.39.3, at includes/class-up-project.php

734 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Exit if accessed directly
4 if ( ! defined('ABSPATH')) {
5 exit;
6 }
7
8 //This include shouldn't be necessary however the wp_check_post_lock call fails
9 //in the frontend edit module which is odd
10 @include_once ABSPATH . '/wp-admin/includes/post.php';
11
12
13 /**
14 * UpStream_Project Class
15 *
16 * @since 1.0.0
17 */
18 class UpStream_Project
19 {
20
21 /**
22 * The project ID
23 *
24 * @since 1.0.0
25 */
26 public $ID = 0;
27
28 /**
29 * Meta key prefix
30 *
31 * @since 1.0.0
32 */
33 public $meta_prefix = '_upstream_project_';
34
35 /**
36 * Project Meta keys
37 *
38 * @since 1.0.0
39 */
40 public $meta = [
41 'milestones',
42 'tasks',
43 'bugs',
44 'status',
45 'owner',
46 'client',
47 'client_users',
48 'start',
49 'end',
50 'files',
51 'progress',
52 'members',
53 'comments',
54 'activity',
55 ];
56
57 /**
58 * Declare the default properties in WP_Post as we can't extend it
59 * Anything we've declared above has been removed.
60 */
61 public $post_author = 0;
62 public $post_date = '0000-00-00 00:00:00';
63 public $post_date_gmt = '0000-00-00 00:00:00';
64 public $post_content = '';
65 public $post_title = '';
66 public $post_excerpt = '';
67 public $post_status = 'publish';
68 public $comment_status = 'open';
69 public $ping_status = 'open';
70 public $post_password = '';
71 public $post_name = '';
72 public $to_ping = '';
73 public $pinged = '';
74 public $post_modified = '0000-00-00 00:00:00';
75 public $post_modified_gmt = '0000-00-00 00:00:00';
76 public $post_content_filtered = '';
77 public $post_parent = 0;
78 public $guid = '';
79 public $menu_order = 0;
80 public $post_mime_type = '';
81 public $comment_count = 0;
82 public $filter;
83
84 /**
85 * Get things going
86 *
87 * @since 1.0.0
88 */
89 public function __construct($_id = false, $_args = [])
90 {
91
92 // if no id is sent, then go through the varous ways of getting the id
93 // may need to check the order more closely to ensure we get it right
94 if ( ! $_id) {
95 $_id = get_the_ID();
96 }
97 if ( ! $_id) {
98 $_id = isset($_GET['post']) ? (int)$_GET['post'] : false;
99 }
100 if ( ! $_id) {
101 $_id = isset($_POST['post']) ? (int)$_POST['post'] : false;
102 }
103 if ( ! $_id) {
104 $_id = isset($_POST['post_ID']) ? (int)$_POST['post_ID'] : false;
105 }
106 if ( ! $_id) {
107 $_id = isset($_POST['post_id']) ? (int)$_POST['post_id'] : false;
108 }
109
110 // validity will be checked in setup_project
111 $project = WP_Post::get_instance($_id);
112
113 return $this->setup_project($project);
114 }
115
116 /**
117 * Given the project data, let's set the variables
118 *
119 * @param object $project The Project Object
120 *
121 * @return bool If the setup was successful or not
122 * @since 1.0.0
123 *
124 */
125 public function setup_project($project)
126 {
127 if ( ! is_object($project)) {
128 return false;
129 }
130
131 if ( ! is_a($project, 'WP_Post')) {
132 return false;
133 }
134
135 if ('project' !== $project->post_type) {
136 return false;
137 }
138
139 // sets the value of each key
140 foreach ($project as $key => $value) {
141 switch ($key) {
142 default:
143 $this->$key = $value;
144 break;
145 }
146 }
147
148 $this->init();
149
150 return true;
151 }
152
153 public function init()
154 {
155
156 // RSD: commented b/c this doesn't ever get called because init was already executed
157 // add_action('init', [$this, 'hooks']);
158
159 $this->hooks();
160 }
161
162 public function hooks()
163 {
164 add_action('wp_insert_post', [$this, 'update_project_meta_admin'], 1, 3);
165 }
166
167 /**
168 * Get the clients name
169 *
170 * @return string|null
171 * @since 1.0.0
172 */
173 public function get_client_name()
174 {
175 if ( ! $this->get_meta('client')) {
176 return;
177 }
178 $client = get_post((int)$this->get_meta('client'));
179 if ($client->ID === $this->ID) {
180 return;
181 }
182
183 return $client->post_title;
184 }
185
186 /**
187 * Get a meta value
188 *
189 * @param string $meta the meta field (without prefix)
190 *
191 * @return mixed
192 * @since 1.0.0
193 *
194 */
195 public function get_meta($meta)
196 {
197 $result = get_post_meta($this->ID, $this->meta_prefix . $meta, true);
198 if ( ! $result) {
199 $result = null;
200 }
201
202 return $result;
203 }
204
205 /**
206 * Get an item (milestone, task or bug) by it's id
207 *
208 * @param $item_id
209 * @param $type
210 *
211 * @return array|null
212 * @throws Exception
213 * @since 1.0.0
214 *
215 */
216 public function get_item_by_id($item_id, $type)
217 {
218 if ( ! $item_id) {
219 return;
220 }
221
222 if (is_array($type)) {
223 $type = reset($type);
224 }
225
226 if ($type === 'milestones') {
227 try {
228 $milestone = \UpStream\Factory::getMilestone($item_id)->convertToLegacyRowset();
229 $milestone['type'] = $type;
230 } catch (\UpStream\Exception $e) {
231 $milestone = null;
232 }
233
234 return $milestone;
235 }
236
237 $data = $this->get_meta($type);
238 if ( ! $data) {
239 return;
240 }
241
242 foreach ($data as $key => $item) {
243 if ($item_id == $item['id']) {
244 $item['type'] = $type;
245
246 return $item;
247 }
248 }
249 }
250
251 public function get_item_colors($item_id, $type, $field)
252 {
253 if ( ! $item_id || ! $type || ! $field) {
254 return;
255 }
256
257 $data = $this->get_meta($type);
258 if ( ! $data) {
259 return;
260 }
261
262 $option_name = $field == 'status' ? $field . 'es' : $field;
263 $option = get_option("upstream_{$type}");
264 $colors = wp_list_pluck($option[$option_name], 'color', 'name');
265
266 foreach ($data as $key => $item) {
267 if ($item_id == $item['id']) {
268 if (isset($item[$field])) {
269 $field_name = $item[$field];
270 if (isset($field_name) && ! empty($field_name)) {
271 return $colors[$field_name];
272 }
273 }
274 }
275 }
276
277 return;
278 }
279
280 /**
281 * Get the current count of statuses for a particular item type
282 *
283 * @param string $type the type of item (milestone, task or bug)
284 *
285 * @return array|null
286 * @since 1.0.0
287 *
288 */
289 public function get_statuses_counts($type)
290 {
291 if ( ! $this->get_statuses($type)) {
292 return;
293 }
294 $counts = array_filter($this->get_statuses($type)); // remove entries with blank statuses
295 $counts = array_count_values($counts);
296
297 return $counts;
298 }
299
300 /**
301 * Get the current statuses used in the project for a particular item type
302 *
303 * @param string $type the type of item (milestone, task or bug)
304 *
305 * @return array|null
306 * @since 1.0.0
307 *
308 */
309 public function get_statuses($type)
310 {
311 $found = false;
312
313 $meta = $this->get_meta($type);
314
315 if ( ! $meta) {
316 return;
317 }
318
319 $statuses = [];
320 foreach ($meta as $key => $value) {
321 if (array_key_exists('status', $value)) {
322 $statuses[] = $value['status'];
323 }
324 }
325
326 return $statuses;
327 }
328
329 public function get_project_status_type()
330 {
331 if ( ! $this->get_meta('status')) {
332 return;
333 }
334 $result = null;
335 $option = get_option('upstream_projects');
336 $statuses = isset($option['statuses']) ? $option['statuses'] : '';
337
338 if ( ! $statuses) {
339 return null;
340 }
341
342
343 $types = [];
344 foreach ($statuses as $s) {
345 if (isset($s['name']) && isset($s['type'])) {
346 $types[$s['name']] = $s['type'];
347 }
348 }
349
350
351 foreach ($types as $key => $value) {
352 if ($key == $this->get_meta('status')) {
353 $result = $value;
354 }
355 }
356
357 return $result;
358 }
359
360 /**
361 * Update a project with various missing meta values (this runs from admin only via wp_insert_post action)
362 *
363 * @return null
364 * @since 1.0.0
365 */
366 public function update_project_meta_admin($post_id, $post, $update)
367 {
368 // RSD: performance enhancement test
369 static $has_run_for_post = [];
370 if (in_array($post_id, $has_run_for_post)) {
371 return;
372 }
373 $has_run_for_post[] = $post_id;
374
375 // If this is an auto draft
376 if ($post->post_status == 'auto-draft') {
377 return;
378 }
379
380 // If this is a revision
381 if (wp_is_post_revision($post_id)) {
382 return;
383 }
384
385 $this->update_project_meta();
386 }
387
388
389 /**
390 * Loop through the meta keys and update with missing meta values
391 * This runs from admin and is also called directly if updating via frontend
392 *
393 * @param $posted_data array the posted data from the front end
394 *
395 * @return null
396 * @since 1.0.0
397 *
398 */
399 public function update_project_meta($frontend = null)
400 {
401 $meta_keys = [
402 'milestones',
403 'tasks',
404 'bugs',
405 'files',
406 'discussion',
407 ];
408
409 foreach ($meta_keys as $meta_key) {
410 $this->update_missing_meta($meta_key, $frontend);
411 }
412
413 $this->update_tasks_milestones();
414
415 $this->update_project_members();
416
417 do_action('upstream:project.updateProjectMeta', $this->ID, $frontend);
418 }
419
420 /**
421 * Update our missing meta data
422 * Ran on every project update & when items are added/edited
423 *
424 * @param string|array $data either a meta_key or an array of POSTed data (from frontend)
425 *
426 * @return array|null
427 * @since 1.0.0
428 *
429 */
430 public function update_missing_meta($meta_key, $frontend = null)
431 {
432
433 // ignore quick edit
434 if (isset($_POST['action']) && sanitize_text_field($_POST['action']) == 'inline-save') {
435 return;
436 }
437
438 // if( [_wp_http_referer] => /upstreamplugin/wp-admin/post-new.php?post_type=project)
439 //[original_post_status] => auto-draft
440 // if no posted_data from frontend, set it as $_POST
441 if ( ! $frontend) {
442 //$data = isset( $_POST ) && ! empty( $_POST ) ? $_POST[$this->meta_prefix . $meta_key] : $this->get_meta( $meta_key );
443 $data = $this->get_meta($meta_key);
444 } else {
445 $data = $this->get_meta($meta_key);
446 }
447
448 $meta_key = $this->meta_prefix . $meta_key;
449
450 // if we have data
451 if ($data) {
452 foreach ($data as $i => $value) {
453 // add unique id
454 if ( ! isset($data[$i]['id']) || empty($data[$i]['id'])) {
455 $data[$i]['id'] = upstream_admin_set_unique_id();
456 }
457
458 // add the user id who created this
459 if ( ! isset($data[$i]['created_by']) || empty($data[$i]['created_by'])) {
460 $data[$i]['created_by'] = upstream_current_user_id();
461 }
462
463 // add the created date
464 if ( ! isset($data[$i]['created_time'])
465 || empty($data[$i]['created_time'])
466 ) {
467 // Prior to v1.15.1, 'created_time' was stored as a non-gmt timestamp,
468 // which doesn't make sense since local time might change.
469
470 // Stores 'created_time' as a UTC/GMT value.
471 $data[$i]['created_time'] = (int)current_time('timestamp', true);
472 // Flag indicating that 'created_time' is in UTC.
473 // Useful to convert old 'created_time' data into UTC/GMT in the future.
474 $data[$i]['created_time__in_utc'] = '1';
475 }
476 }
477 }
478
479 $data = apply_filters('upstream:project.onBeforeUpdateMissingMeta', $data, $this->ID, $meta_key);
480
481
482 $updated = update_post_meta($this->ID, $meta_key, $data);
483 }
484
485 /*
486 *
487 */
488 public function update_tasks_milestones()
489 {
490 $tasks = $this->get_meta('tasks');
491 $milestones = \UpStream\Milestones::getInstance()->getMilestonesFromProject_NoPerms($this->ID);
492
493
494 $wp_lock_check = false;
495 if (function_exists('wp_check_post_lock')) {
496 $wp_lock_check = wp_check_post_lock($this->ID);
497 }
498
499
500 $options = get_option('upstream_general');
501 $allow_override = isset($options['override_locking']) ? (bool)$options['override_locking'] : false;
502
503 if ($wp_lock_check && !$allow_override) {
504 $user_info = get_userdata($wp_lock_check);
505 throw new \Exception(__("This project is being edited by " . $user_info->user_login . ". The other user must save their work.", 'upstream'));
506 } else {
507 delete_post_meta($this->ID , '_edit_lock');
508 }
509
510 $i = 0;
511 $totals = [];
512
513 $counted_tids = [];
514
515 $percentage_project = 0;
516 $sum_project = 0;
517 $count_project = 0;
518
519 if (!empty($milestones)) {
520
521 // loop through each milestone
522 foreach ($milestones as $milestone) {
523 // ^ add reference to make changes
524 $milestone = \UpStream\Factory::getMilestone($milestone);
525
526 $sum = 0;
527 $count = 0;
528 $open = 0;
529
530 if ($tasks) {
531 // loop through each task
532 foreach ($tasks as $task) {
533 // if a milestone has a task assigned to it
534 if (isset($task['milestone']) && (int)$task['milestone'] === $milestone->getId()) { // if it matches
535
536 $counted_tids[] = $task['id'];
537
538 $sum += isset($task['progress']) ? (int)$task['progress'] : 0; // add task progress to get the sum progress of all tasks
539 $count++; // count
540
541 // add open tasks count to the milestone
542 if ((!isset($task['status']) || empty($task['status'])) || (isset($task['status']) && $this->is_open_tasks($task['status']))) {
543 $open++;
544 }
545 $sum_project += isset($task['progress']) ? (int)$task['progress'] : 0;
546 $count_project++;
547 }
548 }
549 }
550
551 // maths to work out total percentage of this milestone
552 $percentage = $count > 0 ? $sum / ($count * 100) * 100 : 0;
553 $percentage_project = $count_project > 0 ? $sum_project / ($count_project * 100) * 100 : 0;
554
555 $milestone->setProgress(round($percentage, 1)); // add the percentage into our new progress key
556 $milestone->setTaskCount($count); // add the number of tasks in this milestone
557
558 if (isset($open)) {
559 $milestone->setTaskOpen($open++);
560 } // add the number of open tasks in this milestone
561
562 // make sure the milestone has at lea st 1 task assigned otherwise it doesn't count
563 if ($count > 0) {
564 $totals[$milestone->getId()]['count'] = $count;
565 $totals[$milestone->getId()]['progress'] = $percentage;
566 }
567
568 $i++;
569 }
570
571 }
572
573 if (!empty($tasks)) {
574
575 foreach ($tasks as $task) {
576
577 if (!in_array($task['id'], $counted_tids)) {
578
579 $sum_project += isset($task['progress']) ? (int)$task['progress'] : 0;
580 $count_project++;
581 $percentage_project = $count_project > 0 ? $sum_project / ($count_project * 100) * 100 : 0;
582 }
583
584 }
585
586 }
587
588 update_post_meta($this->ID, '_upstream_project_tasks', $tasks);
589
590 // maths for the total project progress
591 // do it down here out of the way
592 $project_progress = $percentage_project;
593 /* if ($totals) {
594 $totalsCount = count((array)$totals);
595 foreach ($totals as $milestone) {
596 $project_progress += $milestone['progress'] / ($totalsCount * 100) * 100;
597 }
598 }*/
599 update_post_meta($this->ID, '_upstream_project_progress', round($project_progress, 1));
600 }
601
602
603 /*
604 * Create/update list of registered project users for easy retrieval later and easy permission checking
605 * Includes WP and client users
606 */
607
608 /**
609 * Returns the count of open tasks
610 *
611 * @return null|int
612 */
613 public function is_open_tasks($task_status)
614 {
615 if ( ! $task_status) {
616 return;
617 }
618
619
620 $option = get_option('upstream_tasks');
621 $statuses = isset($option['statuses']) ? $option['statuses'] : '';
622
623 if ( ! $statuses) {
624 return;
625 }
626
627 $types = wp_list_pluck($statuses, 'type', 'id');
628
629 foreach ($types as $name => $type) {
630 if ($type == 'open' && $task_status == $name) {
631 return true;
632 }
633 }
634
635 return false;
636 }
637
638 public function update_project_members()
639 {
640 $owner = $this->get_meta('owner');
641 $tasks = $this->get_meta('tasks');
642 $bugs = $this->get_meta('bugs');
643 $files = $this->get_meta('files');
644
645 $milestones = \UpStream\Milestones::getInstance()->getMilestonesFromProject_NoPerms($this->ID);
646
647 $users = []; // start with fresh array
648
649 if ($owner) {
650 $users[] = $owner;
651 }
652
653 $current_user = get_current_user_id();
654
655 if ($this->post_author == $current_user) {
656 $users[] = $current_user;
657 }
658
659
660 if ($tasks) :
661 foreach ($tasks as $task) {
662 if (isset($task['created_by'])) {
663 $users[] = $task['created_by'];
664 }
665 if (isset($task['assigned_to'])) {
666 if (is_array($task['assigned_to'])) {
667 $users = array_merge($users, $task['assigned_to']);
668 } else {
669 $users[] = $task['assigned_to'];
670 }
671 }
672 }
673 endif;
674
675 if ($milestones) :
676 foreach ($milestones as $milestone) {
677
678 $milestone = \UpStream\Factory::getMilestone($milestone);
679
680 $c = $milestone->getCreatedBy();
681 if (isset($c)) {
682 $users[] = $c;
683 }
684
685 $c = $milestone->getAssignedTo();
686 if (isset($c)) {
687 if (is_array($c)) {
688 $users = array_merge($users, $c);
689 } else {
690 $users[] = $c;
691 }
692 }
693 }
694 endif;
695
696 if ($bugs) :
697 foreach ($bugs as $bug) {
698 if (isset($bug['created_by'])) {
699 $users[] = $bug['created_by'];
700 }
701 if (isset($bug['assigned_to'])) {
702 if (is_array($bug['assigned_to'])) {
703 $users = array_merge($users, $bug['assigned_to']);
704 } else {
705 $users[] = $bug['assigned_to'];
706 }
707 }
708 }
709 endif;
710
711 if ($files) :
712 foreach ($files as $file) {
713 if (isset($file['created_by'])) {
714 $users[] = $file['created_by'];
715 }
716 if (isset($file['assigned_to'])) {
717 if (is_array($file['assigned_to'])) {
718 $users = array_merge($users, $file['assigned_to']);
719 } else {
720 $users[] = $file['assigned_to'];
721 }
722 }
723 }
724 endif;
725
726 // some tidying up
727 $users = array_unique($users);
728 $users = array_values(array_filter($users));
729
730 // do the updating
731 update_post_meta($this->ID, '_upstream_project_members', $users);
732 }
733 }
734