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

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

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