Posts 2 Posts - Version 0.4

Version Description

  • introduced 'connected_from', 'connected_to', 'connected' vars to WP_Query
  • replaced $reciprocal with $data as the third argument
  • p2p_register_connection_type() accepts an associative array as arguments
  • removed p2p_list_connected()
  • added p2p_delete_connection()
  • more info
Download this release

Release Info

Developer scribu
Plugin Icon wp plugin Posts 2 Posts
Version 0.4
Comparing to
See all releases

Code changes from version 0.3 to 0.4

admin/admin.php DELETED
@@ -1,198 +0,0 @@
1
- <?php
2
-
3
- class P2P_Admin {
4
-
5
- private static $connections;
6
-
7
- function init( $file ) {
8
- add_action( 'admin_print_styles-post.php', array( __CLASS__, 'scripts' ) );
9
- add_action( 'admin_print_styles-post-new.php', array( __CLASS__, 'scripts' ) );
10
-
11
- add_action( 'add_meta_boxes', array( __CLASS__, 'register' ) );
12
-
13
- add_action( 'save_post', array( __CLASS__, 'save' ), 10 );
14
- add_action( 'wp_ajax_p2p_search', array( __CLASS__, 'ajax_search' ) );
15
-
16
- add_action( 'admin_notices', array( __CLASS__, 'migrate' ) );
17
- }
18
-
19
- function migrate() {
20
- if ( !isset( $_GET['migrate_p2p'] ) || !current_user_can( 'administrator' ) )
21
- return;
22
-
23
- global $wpdb;
24
-
25
- $rows = $wpdb->get_results( "
26
- SELECT post_id as post_a, meta_value as post_b
27
- FROM $wpdb->postmeta
28
- WHERE meta_key = '_p2p'
29
- " );
30
-
31
- $grouped = array();
32
- foreach ( $rows as $row )
33
- $grouped[ $row->post_a ][] = $row->post_b;
34
-
35
- foreach ( $grouped as $post_a => $post_b )
36
- p2p_connect( $post_a, $post_b );
37
-
38
- $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key = '_p2p'" );
39
-
40
- printf( "<div class='updated'><p>Migrated %s connections.</p></div>", count( $rows ) );
41
- }
42
-
43
- function scripts() {
44
- wp_enqueue_script( 'p2p-admin-js', plugins_url( 'admin.js', __FILE__ ), array( 'jquery' ), '0.2', true );
45
-
46
- ?>
47
- <style type="text/css">
48
- .p2p_connected {margin: 10px 4px}
49
- .p2p_results {margin: -5px 6px 10px}
50
- .p2p_metabox .waiting {vertical-align: -.4em}
51
- </style>
52
- <?php
53
- }
54
-
55
- function save( $post_a ) {
56
- $current_ptype = get_post_type( $post_a );
57
- if ( defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) || empty( $_POST ) || 'revision' == $current_ptype )
58
- return;
59
-
60
- self::cache_connections( $post_a );
61
-
62
- foreach ( p2p_get_connection_types( $current_ptype ) as $post_type ) {
63
- if ( !isset( $_POST['p2p_connected_ids_' . $post_type] ) )
64
- continue;
65
-
66
- $reciprocal = p2p_connection_type_is_reciprocal( $current_ptype, $post_type );
67
-
68
- $old_connections = (array) @self::$connections[ $post_type ];
69
- $new_connections = explode( ',', $_POST[ 'p2p_connected_ids_' . $post_type ] );
70
-
71
- p2p_disconnect( $post_a, array_diff( $old_connections, $new_connections ), $reciprocal );
72
- p2p_connect( $post_a, array_diff( $new_connections, $old_connections ), $reciprocal );
73
- }
74
- }
75
-
76
- private function cache_connections( $post_id ) {
77
- $posts = p2p_get_connected( $post_id, 'from', 'any', 'objects' );
78
-
79
- $connections = array();
80
- foreach ( $posts as $post )
81
- $connections[ $post->post_type ][] = $post->ID;
82
-
83
- self::$connections = $connections;
84
- }
85
-
86
- function register( $post_type ) {
87
- global $post;
88
-
89
- self::cache_connections( $post->ID );
90
-
91
- foreach ( p2p_get_connection_types( $post_type ) as $type ) {
92
- add_meta_box(
93
- 'p2p-connections-' . $type,
94
- __( 'Connected', 'posts-to-posts' ) . ' ' . get_post_type_object( $type )->labels->name,
95
- array( __CLASS__, 'box' ),
96
- $post_type,
97
- 'side',
98
- 'default',
99
- $type
100
- );
101
- }
102
- }
103
-
104
- function box( $post, $args ) {
105
- $post_type = $args['args'];
106
-
107
- $connected_ids = @self::$connections[ $post_type ];
108
-
109
- ?>
110
-
111
- <div class="p2p_metabox">
112
- <div class="hide-if-no-js checkboxes">
113
- <ul class="p2p_connected">
114
- <?php if ( empty( $connected_ids ) ) { ?>
115
- <li class="howto"><?php _e( 'No connections.', 'posts-to-posts' ); ?></li>
116
- <?php } else { ?>
117
- <?php foreach ( $connected_ids as $id ) {
118
- echo html( 'li', scbForms::input( array(
119
- 'type' => 'checkbox',
120
- 'name' => "p2p_checkbox_$id",
121
- 'value' => $id,
122
- 'checked' => true,
123
- 'desc' => get_the_title( $id ),
124
- 'extra' => array( 'autocomplete' => 'off' ),
125
- ) ) );
126
- } ?>
127
- <?php } ?>
128
- </ul>
129
-
130
- <?php echo html( 'p class="p2p_search"',
131
- scbForms::input( array(
132
- 'type' => 'text',
133
- 'name' => 'p2p_search_' . $post_type,
134
- 'desc' => __( 'Search', 'posts-to-posts' ) . ':',
135
- 'desc_pos' => 'before',
136
- 'extra' => array( 'autocomplete' => 'off' ),
137
- ) )
138
- . '<img alt="" src="' . admin_url( 'images/wpspin_light.gif' ) . '" class="waiting" style="display: none;">'
139
- ); ?>
140
-
141
- <ul class="p2p_results"></ul>
142
- <p class="howto"><?php _e( 'Start typing name of connected post type and click on it if you want to connect it.', 'posts-to-posts' ); ?></p>
143
- </div>
144
-
145
- <div class="hide-if-js">
146
- <?php echo scbForms::input( array(
147
- 'type' => 'text',
148
- 'name' => 'p2p_connected_ids_' . $post_type,
149
- 'value' => implode( ',', $connected_ids ),
150
- 'extra' => array( 'class' => 'p2p_connected_ids' ),
151
- ) ); ?>
152
- <p class="howto"><?php _e( 'Enter IDs of connected post types separated by commas, or turn on JavaScript!', 'posts-to-posts' ); ?></p>
153
- </div>
154
- </div>
155
- <?php
156
- }
157
-
158
- function ajax_search() {
159
- $post_type_name = $_GET['post_type'];
160
-
161
- if ( !post_type_exists( $post_type_name ) )
162
- die;
163
-
164
- $args = array(
165
- 's' => $_GET['q'],
166
- 'post_type' => $post_type_name,
167
- 'post_status' => 'any',
168
- 'posts_per_page' => 5,
169
- 'order' => 'ASC',
170
- 'orderby' => 'title',
171
- 'suppress_filters' => true,
172
- 'update_post_term_cache' => false,
173
- 'update_post_meta_cache' => false
174
- );
175
-
176
- $posts = new WP_Query( $args );
177
-
178
- $results = array();
179
- while ( $posts->have_posts() ) {
180
- $posts->the_post();
181
- $results[ get_the_ID() ] = get_the_title();
182
- }
183
-
184
- die( json_encode( $results ) );
185
- }
186
-
187
- private function get_post_list( $post_type ) {
188
- $args = array(
189
- 'post_type' => $post_type,
190
- 'post_status' => 'any',
191
- 'nopaging' => true,
192
- 'cache_results' => false,
193
- );
194
-
195
- return scbUtil::objects_to_assoc( get_posts( $args ), 'ID', 'post_title' );
196
- }
197
- }
198
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api.php CHANGED
@@ -4,153 +4,130 @@
4
  * Register a connection between two post types.
5
  * This creates the appropriate meta box in the admin edit screen
6
  *
7
- * @param string $post_type_a The first end of the connection
8
- * @param string|array $post_type_b The second end of the connection
9
- * @param bool $reciprocal Wether the connection should be reciprocal
 
 
 
10
  */
11
- function p2p_register_connection_type( $post_type_a, $post_type_b, $reciprocal = false ) {
12
- if ( !$ptype = get_post_type_object( $post_type_a ) )
13
- return;
14
-
15
- if ( empty( $post_type_b ) )
16
- return;
17
-
18
- if ( empty( $ptype->can_connect_to ) )
19
- $ptype->can_connect_to = array();
20
-
21
- $post_type_b = (array) $post_type_b;
22
-
23
- $ptype->can_connect_to = array_merge( $ptype->can_connect_to, $post_type_b );
24
-
25
- if ( $reciprocal )
26
- foreach ( $post_type_b as $ptype_b )
27
- p2p_register_connection_type( $ptype_b, $post_type_a, false );
28
  }
29
 
30
  /**
31
- * Get the registered connection types for a certain post type
32
- *
33
- * @param string $post_type_a The first end of the connection
34
  *
35
- * @return array[string] A list of post types
 
 
36
  */
37
- function p2p_get_connection_types( $post_type_a ) {
38
- return (array) @get_post_type_object( $post_type_a )->can_connect_to;
 
 
 
 
39
  }
40
 
41
  /**
42
- * Check wether a connection type is reciprocal
43
- *
44
- * @param string $post_type_a The first end of the connection
45
- * @param string $post_type_b The second end of the connection
46
  *
47
- * @return bool
 
 
48
  */
49
- function p2p_connection_type_is_reciprocal( $post_type_a, $post_type_b ) {
50
- return
51
- in_array( $post_type_b, p2p_get_connection_types( $post_type_a ) ) &&
52
- in_array( $post_type_a, p2p_get_connection_types( $post_type_b ) );
 
 
53
  }
54
 
55
  /**
56
- * Connect a post to another one
57
  *
58
- * @param int $post_a The first end of the connection
59
- * @param int|array $post_b The second end of the connection
60
- * @param bool $reciprocal Wether the connection is reciprocal or not
61
- */
62
- function p2p_connect( $post_a, $post_b, $reciprocal = false ) {
63
- Posts2Posts::connect( $post_a, $post_b );
64
-
65
- if ( $reciprocal )
66
- foreach ( $post_b as $single )
67
- Posts2Posts::connect( $single, $post_a );
68
- }
69
-
70
- /**
71
- * Disconnect a post from another one
72
  *
73
- * @param int $post_a The first end of the connection
74
- * @param int|array $post_b The second end of the connection
75
- * @param bool $reciprocal Wether the connection is reciprocal or not
76
  */
77
- function p2p_disconnect( $post_a, $post_b, $reciprocal = false ) {
78
- Posts2Posts::disconnect( $post_a, $post_b );
79
-
80
- if ( $reciprocal )
81
- foreach ( $post_b as $single )
82
- Posts2Posts::disconnect( $single, $post_a );
 
 
 
 
83
  }
84
 
85
  /**
86
  * See if a certain post is connected to another one
87
  *
88
- * @param int $post_a The first end of the connection
89
- * @param int $post_b The second end of the connection
 
90
  *
91
  * @return bool True if the connection exists, false otherwise
92
  */
93
- function p2p_is_connected( $post_a, $post_b, $reciprocal = false ) {
94
- $r = Posts2Posts::is_connected( $post_a, $post_b );
95
-
96
- if ( $reciprocal )
97
- $r = $r && Posts2Posts::is_connected( $post_b, $post_a );
98
 
99
- return $r;
100
  }
101
 
102
  /**
103
- * Get the list of connected posts
104
  *
105
- * @param int $post_id One end of the connection
106
- * @param string $direction The direction of the connection. Can be 'to' or 'from'
107
- * @param string|array $post_type The post type of the connected posts.
108
- * @param string $output Can be 'ids' or 'objects'
109
  *
110
- * @return array A list of post_ids if $output = 'ids'
111
- * @return object A WP_Query instance otherwise
112
  */
113
- function p2p_get_connected( $post_id, $direction = 'to', $post_type = 'any', $output = 'ids' ) {
114
- $ids = Posts2Posts::get_connected( $post_id, $direction );
115
-
116
- if ( empty( $ids ) )
117
- return array();
118
-
119
- if ( 'any' == $post_type && 'ids' == $output )
120
- return $ids;
121
-
122
- $args = array(
123
- 'post__in' => $ids,
124
- 'post_type'=> $post_type,
125
- 'post_status' => 'any',
126
- 'nopaging' => true,
127
- );
128
-
129
- $posts = get_posts( $args );
130
 
131
- if ( 'objects' == $output )
132
- return $posts;
133
 
134
- foreach ( $posts as &$post )
135
- $post = $post->ID;
 
136
 
137
- return $posts;
138
- }
139
 
140
- /**
141
- * Display the list of connected posts as an unordered list
142
- *
143
- * @param array $args See p2p_get_connected()
144
- */
145
- function p2p_list_connected( $post_id, $direction = 'to', $post_type = 'any' ) {
146
- $posts = p2p_get_connected( $post_id, $direction, $post_type, 'objects' );
147
 
148
- if ( empty( $posts ) )
149
- return;
 
 
 
150
 
151
- echo '<ul>';
152
- foreach ( $posts as $post )
153
- echo html( 'li', html_link( get_permalink( $post->ID ), get_the_title( $post->ID ) ) );
154
- echo '</ul>';
155
  }
156
 
4
  * Register a connection between two post types.
5
  * This creates the appropriate meta box in the admin edit screen
6
  *
7
+ * @param array $args Can be:
8
+ * - 'from' string|array The first end of the connection
9
+ * - 'to' string|array The second end of the connection
10
+ * - 'title' string The box's title
11
+ * - 'reciprocal' bool wether to show the box on both sides of the connection
12
+ * - 'box' string A class that handles displaying and saving connections. Default: P2P_Box_Multiple
13
  */
14
+ function p2p_register_connection_type( $args ) {
15
+ $argv = func_get_args();
16
+
17
+ if ( count( $argv ) > 1 ) {
18
+ $args = array();
19
+ list( $args['from'], $args['to'], $args['reciprocal'] ) = $argv;
20
+ }
21
+
22
+ foreach ( (array) $args['from'] as $from ) {
23
+ foreach ( (array) $args['to'] as $to ) {
24
+ $args['from'] = $from;
25
+ $args['to'] = $to;
26
+ P2P_Connection_Types::register( $args );
27
+ }
28
+ }
 
 
29
  }
30
 
31
  /**
32
+ * Connect a post to one or more other posts
 
 
33
  *
34
+ * @param int|array $from The first end of the connection
35
+ * @param int|array $to The second end of the connection
36
+ * @param array $data additional data about the connection
37
  */
38
+ function p2p_connect( $from, $to, $data = array() ) {
39
+ foreach ( (array) $from as $from ) {
40
+ foreach ( (array) $to as $to ) {
41
+ P2P_Connections::connect( $from, $to, $data );
42
+ }
43
+ }
44
  }
45
 
46
  /**
47
+ * Disconnect a post from or more other posts
 
 
 
48
  *
49
+ * @param int|array $from The first end of the connection
50
+ * @param int|array|string $to The second end of the connection
51
+ * @param array $data additional data about the connection to filter against
52
  */
53
+ function p2p_disconnect( $from, $to, $data = array() ) {
54
+ foreach ( (array) $from as $from ) {
55
+ foreach ( (array) $to as $to ) {
56
+ P2P_Connections::disconnect( $from, $to, $data );
57
+ }
58
+ }
59
  }
60
 
61
  /**
62
+ * Get a list of connected posts
63
  *
64
+ * @param int $post_id One end of the connection
65
+ * @param string $direction The direction of the connection. Can be 'to', 'from' or 'both'
66
+ * @param array $data additional data about the connection to filter against
 
 
 
 
 
 
 
 
 
 
 
67
  *
68
+ * @return array( p2p_id => post_id )
 
 
69
  */
70
+ function p2p_get_connected( $post_id, $direction = 'to', $data = array() ) {
71
+ if ( 'both' == $direction ) {
72
+ $to = P2P_Connections::get( $post_id, 'to', $data );
73
+ $from = P2P_Connections::get( $post_id, 'from', $data );
74
+ $ids = array_merge( $to, array_diff( $from, $to ) );
75
+ } else {
76
+ $ids = P2P_Connections::get( $post_id, $direction, $data );
77
+ }
78
+
79
+ return $ids;
80
  }
81
 
82
  /**
83
  * See if a certain post is connected to another one
84
  *
85
+ * @param int $from The first end of the connection
86
+ * @param int $to The second end of the connection
87
+ * @param array $data additional data about the connection to filter against
88
  *
89
  * @return bool True if the connection exists, false otherwise
90
  */
91
+ function p2p_is_connected( $from, $to, $data = array() ) {
92
+ $ids = p2p_get_connected( $from, $to, $data );
 
 
 
93
 
94
+ return !empty( $ids );
95
  }
96
 
97
  /**
98
+ * Delete one or more connections
99
  *
100
+ * @param int|array $p2p_id Connection ids
 
 
 
101
  *
102
+ * @return int Number of connections deleted
 
103
  */
104
+ function p2p_delete_connection( $p2p_id ) {
105
+ return P2P_Connections::delete( $p2p_id );
106
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ // Allows you to write query_posts( array( 'connected' => 123 ) );
109
+ class P2P_Query {
110
 
111
+ function init() {
112
+ add_filter( 'posts_where', array( __CLASS__, 'posts_where' ), 10, 2 );
113
+ }
114
 
115
+ function posts_where( $where, $wp_query ) {
116
+ global $wpdb;
117
 
118
+ $map = array(
119
+ 'connected' => 'both',
120
+ 'connected_to' => 'to',
121
+ 'connected_from' => 'from',
122
+ );
 
 
123
 
124
+ foreach ( $map as $qv => $direction ) {
125
+ if ( $id = $wp_query->get( $qv ) ) {
126
+ $where .= " AND $wpdb->posts.ID IN ( " . implode( ',', p2p_get_connected( $id, $direction ) ) . " )";
127
+ }
128
+ }
129
 
130
+ return $where;
131
+ }
 
 
132
  }
133
 
core.php DELETED
@@ -1,77 +0,0 @@
1
- <?php
2
-
3
- // Abstraction layer for connection storage
4
-
5
- class Posts2Posts {
6
- const TAX = 'p2p';
7
-
8
- function init() {
9
- add_action( 'init', array( __CLASS__, 'setup' ) );
10
- add_action( 'delete_post', array( __CLASS__, 'delete_post' ) );
11
- }
12
-
13
- function setup() {
14
- register_taxonomy( self::TAX, 'post', array( 'public' => false ) );
15
- }
16
-
17
- function delete_post( $post_id ) {
18
- wp_delete_term( self::convert( 'term', $post_id ), self::TAX );
19
- }
20
-
21
- function connect( $post_a, $post_b ) {
22
- if ( empty( $post_a ) )
23
- return;
24
-
25
- $terms = self::convert( 'term', $post_b );
26
-
27
- if ( empty( $terms ) )
28
- return;
29
-
30
- wp_set_object_terms( $post_a, $terms, self::TAX, true );
31
- }
32
-
33
- function disconnect( $post_a, $post_b ) {
34
- if ( empty( $post_a ) )
35
- return;
36
-
37
- $terms = self::convert( 'term', $post_b );
38
-
39
- if ( empty( $terms ) )
40
- return;
41
-
42
- $list = wp_get_object_terms( $post_a, self::TAX, 'fields=names' );
43
-
44
- wp_set_object_terms( $post_a, array_diff( $list, $terms ), self::TAX );
45
- }
46
-
47
- function is_connected( $post_a, $post_b ) {
48
- $terms = self::convert( 'term', $post_b );
49
-
50
- return is_object_in_term( $post_a, $terms, self::TAX );
51
- }
52
-
53
- function get_connected( $post_id, $direction ) {
54
- if ( 'from' == $direction ) {
55
- $terms = wp_get_object_terms( $post_id, self::TAX, array( 'fields' => 'names' ) );
56
- return self::convert( 'post', $terms );
57
- } else {
58
- $term = get_term_by( 'slug', reset( self::convert( 'term', $post_id ) ), self::TAX )->term_id;
59
- return get_objects_in_term( $term, self::TAX );
60
- }
61
- }
62
-
63
- // Add a 'p' to avoid confusion with term ids
64
- private function convert( $to, $ids ) {
65
- $ids = array_filter( (array) $ids );
66
-
67
- if ( 'term' == $to )
68
- foreach ( $ids as &$id )
69
- $id = 'p' . $id;
70
- else
71
- foreach ( $ids as &$id )
72
- $id = substr( $id, 1 );
73
-
74
- return $ids;
75
- }
76
- }
77
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lang/posts-to-posts.pot CHANGED
@@ -1,4 +1,4 @@
1
- # Translation of the WordPress plugin Posts 2 Posts 0.3-alpha2 by scribu.
2
  # Copyright (C) 2010 scribu
3
  # This file is distributed under the same license as the Posts 2 Posts package.
4
  # FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
@@ -6,9 +6,9 @@
6
  #, fuzzy
7
  msgid ""
8
  msgstr ""
9
- "Project-Id-Version: Posts 2 Posts 0.3-alpha2\n"
10
  "Report-Msgid-Bugs-To: http://wordpress.org/tag/posts-to-posts\n"
11
- "POT-Creation-Date: 2010-08-04 20:37+0300\n"
12
  "PO-Revision-Date: 2010-MO-DA HO:MI+ZONE\n"
13
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
  "Language-Team: LANGUAGE <LL@li.org>\n"
1
+ # Translation of the WordPress plugin Posts 2 Posts 0.3 by scribu.
2
  # Copyright (C) 2010 scribu
3
  # This file is distributed under the same license as the Posts 2 Posts package.
4
  # FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
6
  #, fuzzy
7
  msgid ""
8
  msgstr ""
9
+ "Project-Id-Version: Posts 2 Posts 0.3\n"
10
  "Report-Msgid-Bugs-To: http://wordpress.org/tag/posts-to-posts\n"
11
+ "POT-Creation-Date: 2010-08-04 20:39+0300\n"
12
  "PO-Revision-Date: 2010-MO-DA HO:MI+ZONE\n"
13
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
  "Language-Team: LANGUAGE <LL@li.org>\n"
posts-to-posts.php CHANGED
@@ -1,7 +1,7 @@
1
  <?php
2
  /*
3
  Plugin Name: Posts 2 Posts
4
- Version: 0.3
5
  Plugin Author: scribu
6
  Description: Create connections between posts of different types
7
  Author URI: http://scribu.net/
@@ -10,12 +10,12 @@ Text Domain: posts-to-posts
10
  Domain Path: /lang
11
 
12
 
13
- Copyright ( C ) 2010 scribu.net ( scribu AT gmail DOT com )
14
 
15
  This program is free software; you can redistribute it and/or modify
16
  it under the terms of the GNU General Public License as published by
17
  the Free Software Foundation; either version 3 of the License, or
18
- ( at your option ) any later version.
19
 
20
  This program is distributed in the hope that it will be useful,
21
  but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -29,15 +29,48 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
29
  require dirname( __FILE__ ) . '/scb/load.php';
30
 
31
  function _p2p_init() {
32
- require dirname( __FILE__ ) . '/core.php';
33
  require dirname( __FILE__ ) . '/api.php';
 
 
34
 
35
- Posts2Posts::init();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- if ( is_admin() ) {
38
- require dirname( __FILE__ ) . '/admin/admin.php';
39
- P2P_Admin::init( __FILE__ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
  }
42
- scb_init( '_p2p_init' );
43
 
1
  <?php
2
  /*
3
  Plugin Name: Posts 2 Posts
4
+ Version: 0.4
5
  Plugin Author: scribu
6
  Description: Create connections between posts of different types
7
  Author URI: http://scribu.net/
10
  Domain Path: /lang
11
 
12
 
13
+ Copyright (C) 2010 Cristi Burcă (scribu@gmail.com)
14
 
15
  This program is free software; you can redistribute it and/or modify
16
  it under the terms of the GNU General Public License as published by
17
  the Free Software Foundation; either version 3 of the License, or
18
+ (at your option) any later version.
19
 
20
  This program is distributed in the hope that it will be useful,
21
  but WITHOUT ANY WARRANTY; without even the implied warranty of
29
  require dirname( __FILE__ ) . '/scb/load.php';
30
 
31
  function _p2p_init() {
32
+ require dirname( __FILE__ ) . '/storage.php';
33
  require dirname( __FILE__ ) . '/api.php';
34
+ require dirname( __FILE__ ) . '/ui/ui.php';
35
+ require dirname( __FILE__ ) . '/ui/boxes.php';
36
 
37
+ P2P_Connections::init( __FILE__ );
38
+ P2P_Query::init();
39
+ P2P_Connection_Types::init();
40
+ P2P_Box_Multiple::init();
41
+
42
+ P2P_Migrate::init();
43
+ }
44
+ scb_init( '_p2p_init' );
45
+
46
+
47
+ class P2P_Migrate {
48
+
49
+ function init() {
50
+ add_action( 'admin_notices', array( __CLASS__, 'migrate' ) );
51
+ }
52
+
53
+ function migrate() {
54
+ if ( !isset( $_GET['migrate_p2p'] ) || !current_user_can( 'administrator' ) )
55
+ return;
56
 
57
+ $tax = 'p2p';
58
+
59
+ register_taxonomy( $tax, 'post', array( 'public' => false ) );
60
+
61
+ $count = 0;
62
+ foreach ( get_terms( $tax ) as $term ) {
63
+ $post_b = (int) substr( $term->slug, 1 );
64
+ $post_a = get_objects_in_term( $term->term_id, $tax );
65
+
66
+ p2p_connect( $post_a, $post_b );
67
+
68
+ wp_delete_term( $term->term_id, $tax );
69
+
70
+ $count += count( $post_a );
71
+ }
72
+
73
+ printf( "<div class='updated'><p>Migrated %d connections.</p></div>", $count );
74
  }
75
  }
 
76
 
readme.txt CHANGED
@@ -4,13 +4,13 @@ Donate link: http://scribu.net/paypal
4
  Tags: cms, custom post types, relationships, many-to-many
5
  Requires at least: 3.0
6
  Tested up to: 3.0
7
- Stable tag: 0.3
8
 
9
  Create connections between posts
10
 
11
  == Description ==
12
 
13
- This plugin allows you to create relationships between posts of different types. The relationships are stored in a hidden taxonomy.
14
 
15
  To register a connection type, add this code in your theme's functions.php file:
16
 
@@ -19,14 +19,13 @@ function my_connection_types() {
19
  if ( !function_exists('p2p_register_connection_type') )
20
  return;
21
 
22
- p2p_register_connection_type('book', 'author');
23
- p2p_register_connection_type('library', 'book');
24
  }
25
  add_action('init', 'my_connection_types', 100);
26
  `
27
  <br>
28
 
29
- Links: [API](http://plugins.trac.wordpress.org/browser/posts-to-posts/tags/0.3/api.php) | [Plugin News](http://scribu.net/wordpress/posts-to-posts) | [Author's Site](http://scribu.net)
30
 
31
  == Installation ==
32
 
@@ -50,6 +49,14 @@ Make sure your host is running PHP 5. The only foolproof way to do this is to ad
50
 
51
  == Changelog ==
52
 
 
 
 
 
 
 
 
 
53
  = 0.3 =
54
  * store connections using a taxonomy instead of postmeta
55
  * [more info](http://scribu.net/wordpress/posts-to-posts/p2p-0-3.html)
4
  Tags: cms, custom post types, relationships, many-to-many
5
  Requires at least: 3.0
6
  Tested up to: 3.0
7
+ Stable tag: 0.4
8
 
9
  Create connections between posts
10
 
11
  == Description ==
12
 
13
+ This plugin allows you to create many-to-many relationships between posts of all types.
14
 
15
  To register a connection type, add this code in your theme's functions.php file:
16
 
19
  if ( !function_exists('p2p_register_connection_type') )
20
  return;
21
 
22
+ p2p_register_connection_type( 'post', 'page' );
 
23
  }
24
  add_action('init', 'my_connection_types', 100);
25
  `
26
  <br>
27
 
28
+ Links: [API](http://plugins.trac.wordpress.org/browser/posts-to-posts/trunk/api.php) | [Plugin News](http://scribu.net/wordpress/posts-to-posts) | [Author's Site](http://scribu.net)
29
 
30
  == Installation ==
31
 
49
 
50
  == Changelog ==
51
 
52
+ = 0.4 =
53
+ * introduced 'connected_from', 'connected_to', 'connected' vars to WP_Query
54
+ * replaced $reciprocal with $data as the third argument
55
+ * p2p_register_connection_type() accepts an associative array as arguments
56
+ * removed p2p_list_connected()
57
+ * added p2p_delete_connection()
58
+ * [more info](http://scribu.net/wordpress/posts-to-posts/p2p-0-4.html)
59
+
60
  = 0.3 =
61
  * store connections using a taxonomy instead of postmeta
62
  * [more info](http://scribu.net/wordpress/posts-to-posts/p2p-0-3.html)
scb/AdminPage.php CHANGED
@@ -35,9 +35,6 @@ abstract class scbAdminPage {
35
  // l10n
36
  protected $textdomain;
37
 
38
- // Formdata used for filling the form elements
39
- protected $formdata = array();
40
-
41
 
42
  // ____________REGISTRATION COMPONENT____________
43
 
@@ -85,10 +82,8 @@ abstract class scbAdminPage {
85
 
86
  // Constructor
87
  function __construct( $file, $options = NULL ) {
88
- if ( NULL !== $options ) {
89
  $this->options = $options;
90
- $this->formdata = $this->options->get();
91
- }
92
 
93
  $this->file = $file;
94
  $this->plugin_url = plugin_dir_url( $file );
@@ -147,16 +142,18 @@ abstract class scbAdminPage {
147
 
148
  check_admin_referer( $this->nonce );
149
 
150
- $new_data = array();
151
- foreach ( array_keys( $this->formdata ) as $key )
152
- $new_data[$key] = @$_POST[$key];
 
 
 
153
 
154
  $new_data = stripslashes_deep( $new_data );
155
 
156
- $this->formdata = $this->validate( $new_data, $this->formdata );
157
 
158
- if ( isset( $this->options ) )
159
- $this->options->set( $this->formdata );
160
 
161
  $this->admin_msg();
162
  }
@@ -286,8 +283,8 @@ abstract class scbAdminPage {
286
  }
287
 
288
  function input( $args, $formdata = array() ) {
289
- if ( empty( $formdata ) )
290
- $formdata = $this->formdata;
291
 
292
  if ( isset( $args['name_tree'] ) ) {
293
  $tree = ( array ) $args['name_tree'];
@@ -383,7 +380,9 @@ abstract class scbAdminPage {
383
  if ( is_object( $screen ) )
384
  $screen = $screen->id;
385
 
386
- if ( $screen == $this->pagehook && $actual_help = $this->page_help() )
 
 
387
  return $actual_help;
388
 
389
  return $help;
35
  // l10n
36
  protected $textdomain;
37
 
 
 
 
38
 
39
  // ____________REGISTRATION COMPONENT____________
40
 
82
 
83
  // Constructor
84
  function __construct( $file, $options = NULL ) {
85
+ if ( is_a( $options, 'scbOptions' ) )
86
  $this->options = $options;
 
 
87
 
88
  $this->file = $file;
89
  $this->plugin_url = plugin_dir_url( $file );
142
 
143
  check_admin_referer( $this->nonce );
144
 
145
+ if ( !isset($this->options) ) {
146
+ trigger_error('options handler not set', E_USER_WARNING);
147
+ return false;
148
+ }
149
+
150
+ $new_data = scbUtil::array_extract( $_POST, array_keys( $this->options->get_defaults() ) );
151
 
152
  $new_data = stripslashes_deep( $new_data );
153
 
154
+ $new_data = $this->validate( $new_data, $this->options->get() );
155
 
156
+ $this->options->set( $new_data );
 
157
 
158
  $this->admin_msg();
159
  }
283
  }
284
 
285
  function input( $args, $formdata = array() ) {
286
+ if ( empty( $formdata ) && isset( $this->options ) )
287
+ $formdata = $this->options->get();
288
 
289
  if ( isset( $args['name_tree'] ) ) {
290
  $tree = ( array ) $args['name_tree'];
380
  if ( is_object( $screen ) )
381
  $screen = $screen->id;
382
 
383
+ $actual_help = $this->page_help();
384
+
385
+ if ( $screen == $this->pagehook && $actual_help )
386
  return $actual_help;
387
 
388
  return $help;
scb/BoxesPage.php CHANGED
@@ -43,17 +43,43 @@ abstract class scbBoxesPage extends scbAdminPage {
43
  function default_css() {
44
  ?>
45
  <style type="text/css">
46
- .postbox-container + .postbox-container {margin-left: 18px}
47
- .postbox-container {padding-right: 0}
48
-
49
- .inside {clear: both; overflow: hidden; padding: 10px 10px 0 10px !important}
50
- .inside table {margin: 0 !important; padding: 0 !important}
51
- .inside table td {vertical-align: middle !important}
52
- .inside table .regular-text {width: 100% !important}
53
- .inside .form-table th {width: 30%; max-width: 200px; padding: 10px 0 !important}
54
- .inside .widefat .check-column {padding-bottom: 7px !important}
55
- .inside p, .inside table {margin: 0 0 10px 0 !important}
56
- .inside p.submit {float:left !important; padding: 0 !important}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  </style>
58
  <?php
59
  }
@@ -121,9 +147,6 @@ abstract class scbBoxesPage extends scbAdminPage {
121
  if ( method_exists( $this, $handler ) )
122
  call_user_func_array( array( $this, $handler ), $args );
123
  }
124
-
125
- if ( $this->options )
126
- $this->formdata = $this->options->get();
127
  }
128
 
129
  function columns( $columns ) {
43
  function default_css() {
44
  ?>
45
  <style type="text/css">
46
+ .postbox-container + .postbox-container {
47
+ margin-left: 18px;
48
+ }
49
+ .postbox-container {
50
+ padding-right: 0;
51
+ }
52
+ .inside {
53
+ clear: both;
54
+ overflow: hidden;
55
+ padding: 10px 10px 0 !important;
56
+ }
57
+ .inside table {
58
+ margin: 0 !important;
59
+ padding: 0 !important;
60
+ }
61
+ .inside table td {
62
+ vertical-align: middle !important;
63
+ }
64
+ .inside table .regular-text {
65
+ width: 100% !important;
66
+ }
67
+ .inside .form-table th {
68
+ width: 30%;
69
+ max-width: 200px;
70
+ padding: 10px 0 !important;
71
+ }
72
+ .inside .widefat .check-column {
73
+ padding-bottom: 7px !important;
74
+ }
75
+ .inside p,
76
+ .inside table {
77
+ margin: 0 0 10px !important;
78
+ }
79
+ .inside p.submit {
80
+ float: left !important;
81
+ padding: 0 !important;
82
+ }
83
  </style>
84
  <?php
85
  }
147
  if ( method_exists( $this, $handler ) )
148
  call_user_func_array( array( $this, $handler ), $args );
149
  }
 
 
 
150
  }
151
 
152
  function columns( $columns ) {
scb/QueryManipulation.php CHANGED
@@ -19,7 +19,7 @@ class scbQueryManipulation {
19
  $this->callback = $callback;
20
 
21
  $this->enable();
22
-
23
  if ( !$once )
24
  return;
25
 
19
  $this->callback = $callback;
20
 
21
  $this->enable();
22
+
23
  if ( !$once )
24
  return;
25
 
scb/Rewrite.php DELETED
@@ -1,24 +0,0 @@
1
- <?php
2
-
3
- // Helper class for modifying the rewrite rules
4
- abstract class scbRewrite {
5
-
6
- public function __construct( $plugin_file = '' ) {
7
-
8
- add_action( 'init', array( $this, 'generate' ) );
9
- add_action( 'generate_rewrite_rules', array( $this, 'generate' ) );
10
-
11
- if ( $plugin_file )
12
- scbUtil::add_activation_hook( $plugin_file, array( __CLASS__, 'flush' ) );
13
- }
14
-
15
- // This is where the actual code goes
16
- abstract public function generate();
17
-
18
- static public function flush() {
19
- global $wp_rewrite;
20
-
21
- $wp_rewrite->flush_rules();
22
- }
23
- }
24
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scb/Util.php CHANGED
@@ -117,7 +117,7 @@ class scbUtil {
117
 
118
 
119
  if ( ! function_exists( 'html' ) ):
120
- function html($tag, $attributes = array(), $content = '') {
121
  if ( is_array( $attributes ) ) {
122
  $closing = $tag;
123
  foreach ( $attributes as $key => $value ) {
@@ -125,7 +125,7 @@ function html($tag, $attributes = array(), $content = '') {
125
  }
126
  } else {
127
  $content = $attributes;
128
- list($closing) = explode(' ', $tag, 2);
129
  }
130
 
131
  return "<{$tag}>{$content}</{$closing}>";
@@ -145,21 +145,6 @@ endif;
145
 
146
  //_____Compatibility layer_____
147
 
148
-
149
- // WP < 3.0
150
- if ( ! function_exists( '__return_false' ) ) :
151
- function __return_false() {
152
- return false;
153
- }
154
- endif;
155
-
156
- // WP < ?
157
- if ( ! function_exists( '__return_true' ) ) :
158
- function __return_true() {
159
- return true;
160
- }
161
- endif;
162
-
163
  // WP < ?
164
  if ( ! function_exists( 'set_post_field' ) ) :
165
  function set_post_field( $field, $value, $post_id ) {
117
 
118
 
119
  if ( ! function_exists( 'html' ) ):
120
+ function html( $tag, $attributes = array(), $content = '' ) {
121
  if ( is_array( $attributes ) ) {
122
  $closing = $tag;
123
  foreach ( $attributes as $key => $value ) {
125
  }
126
  } else {
127
  $content = $attributes;
128
+ list( $closing ) = explode(' ', $tag, 2);
129
  }
130
 
131
  return "<{$tag}>{$content}</{$closing}>";
145
 
146
  //_____Compatibility layer_____
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  // WP < ?
149
  if ( ! function_exists( 'set_post_field' ) ) :
150
  function set_post_field( $field, $value, $post_id ) {
scb/load.php CHANGED
@@ -1,9 +1,9 @@
1
  <?php
2
 
3
- $GLOBALS['_scb_data'] = array( 23, __FILE__, array(
4
  'scbUtil', 'scbOptions', 'scbForms', 'scbTable',
5
  'scbWidget', 'scbAdminPage', 'scbBoxesPage',
6
- 'scbQueryManipulation', 'scbRewrite', 'scbCron',
7
  ) );
8
 
9
  if ( !class_exists( 'scbLoad4' ) ) :
1
  <?php
2
 
3
+ $GLOBALS['_scb_data'] = array( 25, __FILE__, array(
4
  'scbUtil', 'scbOptions', 'scbForms', 'scbTable',
5
  'scbWidget', 'scbAdminPage', 'scbBoxesPage',
6
+ 'scbQueryManipulation', 'scbCron',
7
  ) );
8
 
9
  if ( !class_exists( 'scbLoad4' ) ) :
storage.php ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Connections {
4
+
5
+ function init( $file ) {
6
+ $table = new scbTable( 'p2p', $file, "
7
+ p2p_id bigint(20) unsigned NOT NULL auto_increment,
8
+ p2p_from bigint(20) unsigned NOT NULL,
9
+ p2p_to bigint(20) unsigned NOT NULL,
10
+ PRIMARY KEY (p2p_id),
11
+ KEY p2p_from (p2p_from),
12
+ KEY p2p_to (p2p_to)
13
+ " );
14
+
15
+ $table2 = new scbTable( 'p2pmeta', $file, "
16
+ meta_id bigint(20) unsigned NOT NULL auto_increment,
17
+ p2p_id bigint(20) unsigned NOT NULL default '0',
18
+ meta_key varchar(255) default NULL,
19
+ meta_value longtext,
20
+ PRIMARY KEY (meta_id),
21
+ KEY p2p_id (p2p_id),
22
+ KEY meta_key (meta_key)
23
+ " );
24
+
25
+ // FORCE UPDATE
26
+ #add_action('init', array($table, 'install'));
27
+ #add_action('init', array($table2, 'install'));
28
+
29
+ add_action( 'delete_post', array( __CLASS__, 'delete_post' ) );
30
+ }
31
+
32
+ function delete_post( $post_id ) {
33
+ self::disconnect( $post_id, 'from' );
34
+ self::disconnect( $post_id, 'to' );
35
+ }
36
+
37
+ /**
38
+ * Get a list of connections, given a certain post id
39
+ *
40
+ * @param int $from post id
41
+ * @param int|string $to post id or direction: 'from' or 'to'
42
+ * @param array $data additional data about the connection to filter against
43
+ *
44
+ * @return array( p2p_id => post_id ) if $to is string
45
+ * @return array( p2p_id ) if $to is int
46
+ */
47
+ function get( $from, $to, $data = array(), $_return_p2p_ids = false ) {
48
+ global $wpdb;
49
+
50
+ $select = "p2p_id";
51
+ $where = "";
52
+
53
+ switch ( $to ) {
54
+ case 'from':
55
+ $select .= $_return_p2p_ids ? '' : ', p2p_to AS post_id';
56
+ $where .= $wpdb->prepare( "p2p_from = %d", $from );
57
+ break;
58
+ case 'to':
59
+ $select .= $_return_p2p_ids ? '' : ', p2p_from AS post_id';
60
+ $where .= $wpdb->prepare( "p2p_to = %d", $from );
61
+ break;
62
+ default:
63
+ $where .= $wpdb->prepare( "p2p_from = %d AND p2p_to = %d", $from, $to );
64
+ $_return_p2p_ids = true;
65
+ }
66
+
67
+ if ( !empty( $data ) ) {
68
+ $clauses = array();
69
+ foreach ( $data as $key => $value ) {
70
+ $clauses[] = $wpdb->prepare( "WHEN %s THEN meta_value = %s ", $key, $value );
71
+ }
72
+
73
+ $where .= " AND p2p_id IN (
74
+ SELECT p2p_id
75
+ FROM $wpdb->p2pmeta
76
+ WHERE CASE meta_key
77
+ " . implode( "\n", $clauses ) . "
78
+ END
79
+ GROUP BY p2p_id HAVING COUNT(p2p_id) = " . count($data) . "
80
+ )";
81
+ }
82
+
83
+ $query = "SELECT $select FROM $wpdb->p2p WHERE $where";
84
+
85
+ if ( $_return_p2p_ids )
86
+ return $wpdb->get_col( $query );
87
+
88
+ $results = $wpdb->get_results( $query );
89
+
90
+ $r = array();
91
+ foreach ( $results as $row )
92
+ $r[ $row->p2p_id ] = $row->post_id;
93
+
94
+ return $r;
95
+ }
96
+
97
+ /**
98
+ * Connect two posts
99
+ *
100
+ * @param int $from post id
101
+ * @param int $to post id
102
+ * @param array $data additional data about the connection
103
+ *
104
+ * @return int|bool connection id or False on failure
105
+ */
106
+ function connect( $from, $to, $data = array() ) {
107
+ global $wpdb;
108
+
109
+ $from = absint( $from );
110
+ $to = absint( $to );
111
+
112
+ if ( !$from || !$to )
113
+ return false;
114
+
115
+ $p2p_ids = self::get( $from, $to, $data, true );
116
+
117
+ if ( !empty( $p2p_ids ) )
118
+ return $p2p_ids[0];
119
+
120
+ $wpdb->insert( $wpdb->p2p, array( 'p2p_from' => $from, 'p2p_to' => $to ), '%d' );
121
+
122
+ $p2p_id = $wpdb->insert_id;
123
+
124
+ foreach ( $data as $key => $value )
125
+ p2p_add_meta( $p2p_id, $key, $value );
126
+
127
+ return $p2p_id;
128
+ }
129
+
130
+ /**
131
+ * Disconnect two posts
132
+ *
133
+ * @param int $from post id
134
+ * @param int|string $to post id or direction: 'from' or 'to'
135
+ * @param array $data additional data about the connection to filter against
136
+ *
137
+ * @return int Number of connections deleted
138
+ */
139
+ function disconnect( $from, $to, $data = array() ) {
140
+ return self::delete( self::get( $from, $to, $data, true ) );
141
+ }
142
+
143
+ /**
144
+ * Delete one or more connections
145
+ *
146
+ * @param int|array $p2p_id Connection ids
147
+ *
148
+ * @return int Number of connections deleted
149
+ */
150
+ function delete( $p2p_id ) {
151
+ global $wpdb;
152
+
153
+ if ( empty( $p2p_id ) )
154
+ return 0;
155
+
156
+ $p2p_ids = array_map( 'absint', (array) $p2p_id );
157
+
158
+ $where = "WHERE p2p_id IN (" . implode( ',', $p2p_ids ) . ")";
159
+
160
+ $wpdb->query( "DELETE FROM $wpdb->p2p $where" );
161
+ $wpdb->query( "DELETE FROM $wpdb->p2pmeta $where" );
162
+
163
+ return count( $p2p_ids );
164
+ }
165
+ }
166
+
167
+
168
+ function p2p_get_meta($p2p_id, $key, $single = false) {
169
+ return get_metadata('p2p', $p2p_id, $key, $single);
170
+ }
171
+
172
+ function p2p_update_meta($p2p_id, $meta_key, $meta_value, $prev_value = '') {
173
+ return update_metadata('p2p', $p2p_id, $meta_key, $meta_value, $prev_value);
174
+ }
175
+
176
+ function p2p_add_meta($p2p_id, $meta_key, $meta_value, $unique = false) {
177
+ return add_metadata('p2p', $p2p_id, $meta_key, $meta_value, $unique);
178
+ }
179
+
180
+ function p2p_delete_meta($p2p_id, $meta_key, $meta_value = '') {
181
+ return delete_metadata('p2p', $p2p_id, $meta_key, $meta_value);
182
+ }
183
+
{admin → ui}/admin.js RENAMED
@@ -49,7 +49,7 @@ jQuery(document).ready(function($) {
49
  var $self = $(this);
50
  $metabox = $self.parents('.p2p_metabox'),
51
  $results = $metabox.find('.p2p_results'),
52
- post_type = $self.attr('name').split('_')[2],
53
  old_value = '',
54
  $spinner = $metabox.find('.waiting');
55
 
49
  var $self = $(this);
50
  $metabox = $self.parents('.p2p_metabox'),
51
  $results = $metabox.find('.p2p_results'),
52
+ post_type = $self.attr('name').replace('p2p_search_', ''),
53
  old_value = '',
54
  $spinner = $metabox.find('.waiting');
55
 
ui/boxes.php ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Box_Multiple extends P2P_Box {
4
+
5
+ protected $meta_keys = array();
6
+
7
+ function init() {
8
+ add_action( 'admin_print_styles-post.php', array( __CLASS__, 'scripts' ) );
9
+ add_action( 'admin_print_styles-post-new.php', array( __CLASS__, 'scripts' ) );
10
+
11
+ add_action( 'wp_ajax_p2p_search', array( __CLASS__, 'ajax_search' ) );
12
+ }
13
+
14
+ function scripts() {
15
+ wp_enqueue_script( 'p2p-admin-js', plugins_url( 'ui.js', __FILE__ ), array( 'jquery' ), '0.4-alpha6', true );
16
+
17
+ ?>
18
+ <style type="text/css">
19
+ .p2p_connected {margin: 10px 4px}
20
+ .p2p_results {margin: -5px 6px 10px}
21
+ .p2p_metabox .waiting {vertical-align: -.4em}
22
+ </style>
23
+ <?php
24
+ }
25
+
26
+ function save( $post_a, $data ) {
27
+ if ( !empty( $data['all'] ) )
28
+ p2p_delete_connection( array_diff( $data['all'], (array) @$data['enabled'] ) );
29
+
30
+ foreach ( explode( ',', $data[ 'ids' ] ) as $i => $post_b ) {
31
+ $meta = array();
32
+ foreach ( $this->meta_keys as $meta_key ) {
33
+ $meta_value = $data[ $meta_key ][ $i ];
34
+
35
+ if ( empty( $meta_value ) )
36
+ continue;
37
+
38
+ $meta[ $meta_key ] = $meta_value;
39
+ }
40
+
41
+ if ( $this->reversed )
42
+ p2p_connect( $post_b, $post_a, $meta );
43
+ else
44
+ p2p_connect( $post_a, $post_b, $meta );
45
+ }
46
+ }
47
+
48
+ function box( $post_id ) {
49
+ $connected_ids = $this->get_connected_ids( $post_id );
50
+
51
+ foreach ( array_keys( $connected_ids ) as $p2p_id ) { ?>
52
+ <input type="hidden" name="<?php echo $this->input_name( array( 'all', '' ) ); ?>" value="<?php echo $p2p_id; ?>">
53
+ <?php } ?>
54
+
55
+ <div class="p2p_metabox">
56
+ <div class="hide-if-no-js checkboxes">
57
+ <ul class="p2p_connected">
58
+ <?php if ( empty( $connected_ids ) ) { ?>
59
+ <li class="howto"><?php _e( 'No connections.', 'posts-to-posts' ); ?></li>
60
+ <?php } else {
61
+ foreach ( $connected_ids as $p2p_id => $post_b ) {
62
+ $this->connection_template( $post_b, $p2p_id );
63
+ }
64
+ } ?>
65
+ </ul>
66
+
67
+ <?php echo html( 'p class="p2p_search"',
68
+ scbForms::input( array(
69
+ 'type' => 'text',
70
+ 'name' => 'p2p_search_' . $this->to,
71
+ 'desc' => __( 'Search', 'posts-to-posts' ) . ':',
72
+ 'desc_pos' => 'before',
73
+ 'extra' => array( 'autocomplete' => 'off' ),
74
+ ) )
75
+ . '<img alt="" src="' . admin_url( 'images/wpspin_light.gif' ) . '" class="waiting" style="display: none;">'
76
+ ); ?>
77
+
78
+ <ul class="p2p_results"></ul>
79
+ <p class="howto"><?php _e( 'Start typing name of connected post type and click on it if you want to connect it.', 'posts-to-posts' ); ?></p>
80
+ </div>
81
+
82
+ <div class="hide-if-js">
83
+ <?php echo scbForms::input( array(
84
+ 'type' => 'text',
85
+ 'name' => $this->input_name( 'ids' ),
86
+ 'value' => '',
87
+ 'extra' => array( 'class' => 'p2p_to_connect' ),
88
+ ) ); ?>
89
+ <p class="howto"><?php _e( 'Enter IDs of posts to connect, separated by commas.', 'posts-to-posts' ); ?></p>
90
+ </div>
91
+
92
+ <?php // TODO: move to footer, to avoid $_POST polution ?>
93
+ <div style="display:none" class="connection-template">
94
+ <?php $this->connection_template(); ?>
95
+ </div>
96
+ </div>
97
+ <?php
98
+ }
99
+
100
+ function connection_template( $post_id = 0, $p2p_id = 0 ) {
101
+ if ( $post_id ) {
102
+ $post_title = get_the_title( $post_id );
103
+ } else {
104
+ $post_id = '%post_id%';
105
+ $post_title = '%post_title%';
106
+ }
107
+
108
+ ?>
109
+ <li>
110
+ <label>
111
+ <input type="checkbox" checked="checked" name="<?php echo $this->input_name( array( 'enabled', '' ) ); ?>" value="<?php echo $p2p_id; ?>">
112
+ <?php echo $post_title; ?>
113
+ </label>
114
+ </li>
115
+ <?php
116
+ }
117
+
118
+ protected function get_connected_ids( $post_id ) {
119
+ $connected_posts = p2p_get_connected( $post_id, $this->direction );
120
+
121
+ if ( empty( $connected_posts ) )
122
+ return array();
123
+
124
+ $args = array(
125
+ 'post__in' => $connected_posts,
126
+ 'post_type'=> $this->to,
127
+ 'post_status' => 'any',
128
+ 'nopaging' => true,
129
+ 'suppress_filters' => false,
130
+ );
131
+
132
+ $post_ids = scbUtil::array_pluck( get_posts($args), 'ID' );
133
+
134
+ return array_intersect( $connected_posts, $post_ids ); // to preserve p2p_id keys
135
+ }
136
+
137
+ function ajax_search() {
138
+ $post_type_name = $_GET['post_type'];
139
+
140
+ add_filter( 'posts_search', array( __CLASS__, 'only_search_by_title' ) );
141
+
142
+ $args = array(
143
+ 's' => $_GET['q'],
144
+ 'post_type' => $post_type_name,
145
+ 'post_status' => 'any',
146
+ 'posts_per_page' => 5,
147
+ 'order' => 'ASC',
148
+ 'orderby' => 'title',
149
+ 'suppress_filters' => false,
150
+ 'update_post_term_cache' => false,
151
+ 'update_post_meta_cache' => false
152
+ );
153
+
154
+ $posts = get_posts( $args );
155
+
156
+ $results = array();
157
+ foreach ( $posts as $post )
158
+ $results[ $post->ID ] = $post->post_title;
159
+
160
+ die( json_encode( $results ) );
161
+ }
162
+
163
+ function only_search_by_title( $sql ) {
164
+ remove_filter( current_filter(), array( __CLASS__, __FUNCTION__ ) );
165
+
166
+ list( $sql ) = explode( ' OR ', $sql, 2 );
167
+
168
+ return $sql . '))';
169
+ }
170
+ }
171
+
ui/ui.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery(document).ready(function($) {
2
+ $('.p2p_results').delegate('a', 'click', function() {
3
+ var $self = $(this);
4
+ $metabox = $self.parents('.p2p_metabox'),
5
+ $list = $metabox.find('.p2p_connected'),
6
+ post_id = $self.attr('name');
7
+
8
+ $list.append(
9
+ $metabox.find('.connection-template').html()
10
+ .replace( '%post_id%', post_id )
11
+ .replace( '%post_title%', $self.html() )
12
+ );
13
+
14
+ $metabox.find('.p2p_connected .howto').remove();
15
+
16
+ var $connected = $metabox.find('.p2p_to_connect');
17
+
18
+ $connected.val( $connected.val() + post_id + ',' );
19
+
20
+ return false;
21
+ });
22
+
23
+ var delayed = undefined;
24
+
25
+ $('.p2p_search :text').keyup(function() {
26
+
27
+ if ( delayed != undefined )
28
+ clearTimeout(delayed);
29
+
30
+ var $self = $(this);
31
+ $metabox = $self.parents('.p2p_metabox'),
32
+ $results = $metabox.find('.p2p_results'),
33
+ post_type = $self.attr('name').replace('p2p_search_', ''),
34
+ old_value = '',
35
+ $spinner = $metabox.find('.waiting');
36
+
37
+ var delayed = setTimeout(function() {
38
+ if ( !$self.val().length ) {
39
+ $results.html('');
40
+ return;
41
+ }
42
+
43
+ if ( $self.val() == old_value )
44
+ return;
45
+ old_value = $self.val();
46
+
47
+ $spinner.show();
48
+ $.getJSON(ajaxurl, {action: 'p2p_search', q: $self.val(), post_type: post_type}, function(data) {
49
+ $spinner.hide();
50
+
51
+ $results.html('');
52
+
53
+ $.each(data, function(id, title) {
54
+ $results.append('<li><a href="#" name="' + id + '">' + title + '</a></li>');
55
+ });
56
+ });
57
+ }, 400);
58
+ });
59
+ });
60
+
ui/ui.php ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ abstract class P2P_Box {
4
+
5
+ protected $reversed;
6
+ protected $direction;
7
+
8
+ private $box_id;
9
+ private $input;
10
+
11
+ abstract function save( $post_id, $data );
12
+ abstract function box( $post_id );
13
+
14
+ protected function input_name( $name ) {
15
+ return $this->input->get_name( $name );
16
+ }
17
+
18
+
19
+ // Internal stuff
20
+
21
+
22
+ public function __construct( $args, $reversed, $box_id ) {
23
+ foreach ( $args as $key => $value )
24
+ $this->$key = $value;
25
+
26
+ $this->box_id = $box_id;
27
+ $this->reversed = $reversed;
28
+
29
+ $this->input = new p2pInput( array( 'p2p', $box_id ) );
30
+
31
+ $this->direction = $this->reversed ? 'to' : 'from';
32
+
33
+ if ( $this->reversed )
34
+ list( $this->to, $this->from ) = array( $this->from, $this->to );
35
+ }
36
+
37
+ function _register( $from ) {
38
+ $title = $this->title;
39
+
40
+ if ( empty( $title ) )
41
+ $title = get_post_type_object( $this->to )->labels->name;
42
+
43
+ add_meta_box(
44
+ 'p2p-connections-' . $this->box_id,
45
+ $title,
46
+ array( $this, '_box' ),
47
+ $from,
48
+ 'side',
49
+ 'default'
50
+ );
51
+ }
52
+
53
+ function _save( $post_id ) {
54
+ $data = $this->input->extract( $_POST );
55
+
56
+ if ( is_null( $data ) )
57
+ return;
58
+
59
+ $this->save( $post_id, $data );
60
+ }
61
+
62
+ function _box( $post ) {
63
+ $this->box( $post->ID );
64
+ }
65
+ }
66
+
67
+
68
+ class p2pInput {
69
+
70
+ private $prefix;
71
+
72
+ function __construct( $prefix = array() ) {
73
+ $this->prefix = $prefix;
74
+ }
75
+
76
+ function get_name( $suffix ) {
77
+ $name_a = array_merge( $this->prefix, (array) $suffix );
78
+
79
+ $name = array_shift( $name_a );
80
+ foreach ( $name_a as $key )
81
+ $name .= '[' . esc_attr( $key ) . ']';
82
+
83
+ return $name;
84
+ }
85
+
86
+ function extract( $value, $suffix = array() ) {
87
+ $name_a = array_merge( $this->prefix, (array) $suffix );
88
+
89
+ foreach ( $name_a as $key ) {
90
+ if ( !isset( $value[ $key ] ) )
91
+ return null;
92
+
93
+ $value = $value[$key];
94
+ }
95
+
96
+ return $value;
97
+ }
98
+ }
99
+
100
+
101
+ class P2P_Connection_Types {
102
+
103
+ private static $ctype_id = 0;
104
+ private static $ctypes = array();
105
+
106
+ static public function register( $args ) {
107
+ $args = wp_parse_args( $args, array(
108
+ 'from' => '',
109
+ 'to' => '',
110
+ 'box' => 'P2P_Box_Multiple',
111
+ 'title' => '',
112
+ 'reciprocal' => false
113
+ ) );
114
+
115
+ self::$ctypes[] = $args;
116
+ }
117
+
118
+ static function init() {
119
+ add_action( 'add_meta_boxes', array( __CLASS__, '_register' ) );
120
+ add_action( 'save_post', array( __CLASS__, '_save' ), 10 );
121
+ }
122
+
123
+ static function _register( $from ) {
124
+ foreach ( self::filter_ctypes( $from ) as $ctype ) {
125
+ $ctype->_register( $from );
126
+ }
127
+ }
128
+
129
+ static function _save( $post_id ) {
130
+ $from = get_post_type( $post_id );
131
+ if ( defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) || empty( $_POST ) || 'revision' == $from )
132
+ return;
133
+
134
+ foreach ( self::filter_ctypes( $from ) as $ctype ) {
135
+ $ctype->_save( $post_id );
136
+ }
137
+ }
138
+
139
+ private static function filter_ctypes( $post_type ) {
140
+ $r = array();
141
+ $i = 0;
142
+ foreach ( self::$ctypes as $args ) {
143
+ if ( $post_type == $args['from'] ) {
144
+ $reversed = false;
145
+ } elseif ( $args['reciprocal'] && $post_type == $args['to'] ) {
146
+ $reversed = true;
147
+ } else {
148
+ continue;
149
+ }
150
+
151
+ $r[] = new $args['box']($args, $reversed, $i++);
152
+ }
153
+
154
+ return $r;
155
+ }
156
+ }
157
+