WordPress.org

Plugin Directory

Changeset 662236


Ignore:
Timestamp:
02/01/13 17:55:19 (15 months ago)
Author:
scribu
Message:

deploy from git

Location:
posts-to-posts/trunk
Files:
31 added
7 deleted
17 edited

Legend:

Unmodified
Added
Removed
  • posts-to-posts/trunk/admin/box-factory.php

    r630742 r662236  
    156156} 
    157157 
    158 new P2P_Box_Factory; 
    159  
  • posts-to-posts/trunk/admin/box.js

    r577329 r662236  
    11(function() { 
     2  var Candidates, CandidatesView, Connections, ConnectionsView, CreatePostView, MetaboxView, get_mustache_template, remove_row, row_wait; 
     3 
     4  row_wait = function($td) { 
     5    return $td.find('.p2p-icon').css('background-image', 'url(' + P2PAdmin.spinner + ')'); 
     6  }; 
     7 
     8  remove_row = function($td) { 
     9    var $table; 
     10    $table = $td.closest('table'); 
     11    $td.closest('tr').remove(); 
     12    if (!$table.find('tbody tr').length) { 
     13      return $table.hide(); 
     14    } 
     15  }; 
     16 
     17  get_mustache_template = function(name) { 
     18    return jQuery('#p2p-template-' + name).html(); 
     19  }; 
     20 
     21  Candidates = Backbone.Model.extend({ 
     22    sync: function() { 
     23      var params, 
     24        _this = this; 
     25      params = _.extend({}, this.attributes, { 
     26        subaction: 'search' 
     27      }); 
     28      return this.ajax_request(params, function(response) { 
     29        var _ref; 
     30        _this.total_pages = ((_ref = response.navigation) != null ? _ref['total-pages-raw'] : void 0) || 1; 
     31        return _this.trigger('sync', response); 
     32      }); 
     33    }, 
     34    validate: function(attrs) { 
     35      var _ref; 
     36      if ((0 < (_ref = attrs['paged']) && _ref <= this.total_pages)) { 
     37        return null; 
     38      } 
     39      return 'invalid page'; 
     40    } 
     41  }); 
     42 
     43  Connections = Backbone.Model.extend({ 
     44    createItemAndConnect: function(title) { 
     45      var data, 
     46        _this = this; 
     47      data = { 
     48        subaction: 'create_post', 
     49        post_title: title 
     50      }; 
     51      return this.ajax_request(data, function(response) { 
     52        return _this.trigger('create:from_new_item', response); 
     53      }); 
     54    }, 
     55    create: function($td) { 
     56      var data, 
     57        _this = this; 
     58      data = { 
     59        subaction: 'connect', 
     60        to: $td.find('div').data('item-id') 
     61      }; 
     62      return this.ajax_request(data, function(response) { 
     63        return _this.trigger('create', response, $td); 
     64      }); 
     65    }, 
     66    "delete": function($td) { 
     67      var data, 
     68        _this = this; 
     69      data = { 
     70        subaction: 'disconnect', 
     71        p2p_id: $td.find('input').val() 
     72      }; 
     73      return this.ajax_request(data, function(response) { 
     74        return _this.trigger('delete', response, $td); 
     75      }); 
     76    }, 
     77    clear: function() { 
     78      var data, 
     79        _this = this; 
     80      data = { 
     81        subaction: 'clear_connections' 
     82      }; 
     83      return this.ajax_request(data, function(response) { 
     84        return _this.trigger('clear', response); 
     85      }); 
     86    } 
     87  }); 
     88 
     89  ConnectionsView = Backbone.View.extend({ 
     90    events: { 
     91      'click th.p2p-col-delete .p2p-icon': 'clear', 
     92      'click td.p2p-col-delete .p2p-icon': 'delete' 
     93    }, 
     94    initialize: function(options) { 
     95      this.ajax_request = options.ajax_request; 
     96      this.maybe_make_sortable(); 
     97      this.collection.on('create', this.afterCreate, this); 
     98      this.collection.on('create:from_new_item', this.afterCreate, this); 
     99      this.collection.on('delete', this.afterDelete, this); 
     100      this.collection.on('clear', this.afterClear, this); 
     101      return options.candidates.on('promote', this.create, this); 
     102    }, 
     103    maybe_make_sortable: function() { 
     104      if (this.$('th.p2p-col-order').length) { 
     105        return this.$('tbody').sortable({ 
     106          handle: 'td.p2p-col-order', 
     107          helper: function(e, ui) { 
     108            ui.children().each(function() { 
     109              var $this; 
     110              $this = jQuery(this); 
     111              return $this.width($this.width()); 
     112            }); 
     113            return ui; 
     114          } 
     115        }); 
     116      } 
     117    }, 
     118    clear: function(ev) { 
     119      var $td; 
     120      ev.preventDefault(); 
     121      if (!confirm(P2PAdmin.deleteConfirmMessage)) { 
     122        return; 
     123      } 
     124      $td = jQuery(ev.target).closest('td'); 
     125      row_wait($td); 
     126      return this.collection.clear(); 
     127    }, 
     128    afterClear: function() { 
     129      return this.$el.hide().find('tbody').html(''); 
     130    }, 
     131    "delete": function(ev) { 
     132      var $td; 
     133      ev.preventDefault(); 
     134      $td = jQuery(ev.target).closest('td'); 
     135      row_wait($td); 
     136      this.collection["delete"]($td); 
     137      return null; 
     138    }, 
     139    afterDelete: function(response, $td) { 
     140      return remove_row($td); 
     141    }, 
     142    create: function($td) { 
     143      this.collection.create($td); 
     144      return null; 
     145    }, 
     146    afterCreate: function(response) { 
     147      this.$el.show().find('tbody').append(response.row); 
     148      return this.collection.trigger('append', response); 
     149    } 
     150  }); 
     151 
     152  CandidatesView = Backbone.View.extend({ 
     153    template: Mustache.compile(get_mustache_template('tab-list')), 
     154    events: { 
     155      'keypress :text': 'handleReturn', 
     156      'keyup :text': 'handleSearch', 
     157      'click .p2p-prev, .p2p-next': 'changePage', 
     158      'click td.p2p-col-create div': 'promote' 
     159    }, 
     160    initialize: function(options) { 
     161      this.spinner = options.spinner; 
     162      options.connections.on('create', this.afterConnectionCreated, this); 
     163      options.connections.on('delete', this.refreshCandidates, this); 
     164      options.connections.on('clear', this.refreshCandidates, this); 
     165      this.collection.on('sync', this.refreshCandidates, this); 
     166      this.collection.on('error', this.afterInvalid, this); 
     167      return this.collection.on('invalid', this.afterInvalid, this); 
     168    }, 
     169    afterConnectionCreated: function(response, $td) { 
     170      if (this.options.duplicate_connections) { 
     171        return $td.find('.p2p-icon').css('background-image', ''); 
     172      } else { 
     173        return remove_row($td); 
     174      } 
     175    }, 
     176    promote: function(ev) { 
     177      var $td; 
     178      $td = jQuery(ev.target).closest('td'); 
     179      row_wait($td); 
     180      this.collection.trigger('promote', $td); 
     181      return false; 
     182    }, 
     183    handleReturn: function(ev) { 
     184      if (ev.keyCode === 13) { 
     185        ev.preventDefault(); 
     186      } 
     187      return null; 
     188    }, 
     189    handleSearch: function(ev) { 
     190      var $searchInput, delayed, 
     191        _this = this; 
     192      if (delayed !== void 0) { 
     193        clearTimeout(delayed); 
     194      } 
     195      $searchInput = jQuery(ev.target); 
     196      delayed = setTimeout(function() { 
     197        var searchStr; 
     198        searchStr = $searchInput.val(); 
     199        if (searchStr === _this.collection.get('s')) { 
     200          return; 
     201        } 
     202        _this.spinner.insertAfter(_this.searchInput).show(); 
     203        return _this.collection.save({ 
     204          's': searchStr, 
     205          'paged': 1 
     206        }); 
     207      }, 400); 
     208      return null; 
     209    }, 
     210    changePage: function(ev) { 
     211      var $navButton, new_page; 
     212      $navButton = jQuery(ev.currentTarget); 
     213      new_page = this.collection.get('paged'); 
     214      if ($navButton.hasClass('p2p-prev')) { 
     215        new_page--; 
     216      } else { 
     217        new_page++; 
     218      } 
     219      this.spinner.appendTo(this.$('.p2p-navigation')); 
     220      return this.collection.save('paged', new_page); 
     221    }, 
     222    refreshCandidates: function(response) { 
     223      this.spinner.remove(); 
     224      this.$('button, .p2p-results, .p2p-navigation, .p2p-notice').remove(); 
     225      return this.$el.append(this.template(response)); 
     226    }, 
     227    afterInvalid: function() { 
     228      return this.spinner.remove(); 
     229    } 
     230  }); 
     231 
     232  CreatePostView = Backbone.View.extend({ 
     233    events: { 
     234      'click button': 'createItem', 
     235      'keypress :text': 'handleReturn' 
     236    }, 
     237    initialize: function(options) { 
     238      this.ajax_request = options.ajax_request; 
     239      this.createButton = this.$('button'); 
     240      this.createInput = this.$(':text'); 
     241      return this.collection.on('create:from_new_item', this.afterItemCreated, this); 
     242    }, 
     243    handleReturn: function(ev) { 
     244      if (ev.keyCode === 13) { 
     245        this.createButton.click(); 
     246        ev.preventDefault(); 
     247      } 
     248      return null; 
     249    }, 
     250    createItem: function(ev) { 
     251      var title; 
     252      ev.preventDefault(); 
     253      if (this.createButton.hasClass('inactive')) { 
     254        return false; 
     255      } 
     256      title = this.createInput.val(); 
     257      if (title === '') { 
     258        this.createInput.focus(); 
     259        return; 
     260      } 
     261      this.createButton.addClass('inactive'); 
     262      this.collection.createItemAndConnect(title); 
     263      return null; 
     264    }, 
     265    afterItemCreated: function() { 
     266      this.createInput.val(''); 
     267      return this.createButton.removeClass('inactive'); 
     268    } 
     269  }); 
     270 
     271  MetaboxView = Backbone.View.extend({ 
     272    events: { 
     273      'click .p2p-toggle-tabs': 'toggleTabs', 
     274      'click .wp-tab-bar li': 'setActiveTab' 
     275    }, 
     276    initialize: function(options) { 
     277      this.spinner = options.spinner; 
     278      this.initializedCandidates = false; 
     279      options.connections.on('append', this.afterConnectionAppended, this); 
     280      options.connections.on('clear', this.afterConnectionDeleted, this); 
     281      return options.connections.on('delete', this.afterConnectionDeleted, this); 
     282    }, 
     283    toggleTabs: function(ev) { 
     284      var $tabs; 
     285      ev.preventDefault(); 
     286      $tabs = this.$('.p2p-create-connections-tabs'); 
     287      $tabs.toggle(); 
     288      if (!this.initializedCandidates && $tabs.is(':visible')) { 
     289        this.options.candidates.sync(); 
     290        this.initializedCandidates = true; 
     291      } 
     292      return null; 
     293    }, 
     294    setActiveTab: function(ev) { 
     295      var $tab; 
     296      ev.preventDefault(); 
     297      $tab = jQuery(ev.currentTarget); 
     298      this.$('.wp-tab-bar li').removeClass('wp-tab-active'); 
     299      $tab.addClass('wp-tab-active'); 
     300      return this.$el.find('.tabs-panel').hide().end().find($tab.data('ref')).show().find(':text').focus(); 
     301    }, 
     302    afterConnectionAppended: function(response) { 
     303      if ('one' === this.options.cardinality) { 
     304        return this.$('.p2p-create-connections').hide(); 
     305      } 
     306    }, 
     307    afterConnectionDeleted: function(response) { 
     308      if ('one' === this.options.cardinality) { 
     309        return this.$('.p2p-create-connections').show(); 
     310      } 
     311    } 
     312  }); 
    2313 
    3314  jQuery(function() { 
     
    24335      jQuery('.p2p-search input[placeholder]').each(setVal).focus(clearVal).blur(setVal); 
    25336    } 
     337    Mustache.compilePartial('table-row', get_mustache_template('table-row')); 
    26338    return jQuery('.p2p-box').each(function() { 
    27       var $connections, $createButton, $createInput, $metabox, $searchInput, $spinner, PostsTab, ajax_request, append_connection, clear_connections, create_connection, delete_connection, refresh_candidates, remove_row, row_ajax_request, searchTab, switch_to_tab, toggle_tabs; 
     339      var $metabox, $spinner, ajax_request, candidates, candidatesView, connections, connectionsView, createPostView, ctype, metaboxView; 
    28340      $metabox = jQuery(this); 
    29       $connections = $metabox.find('.p2p-connections'); 
    30341      $spinner = jQuery('<img>', { 
    31342        'src': P2PAdmin.spinner, 
    32343        'class': 'p2p-spinner' 
    33344      }); 
    34       ajax_request = function(data, callback, type) { 
    35         var handler; 
    36         if (type == null) { 
    37           type = 'POST'; 
    38         } 
    39         jQuery.extend(data, { 
     345      candidates = new Candidates({ 
     346        's': '', 
     347        'paged': 1 
     348      }); 
     349      candidates.total_pages = $metabox.find('.p2p-total').data('num') || 1; 
     350      ctype = { 
     351        p2p_type: $metabox.data('p2p_type'), 
     352        direction: $metabox.data('direction'), 
     353        from: jQuery('#post_ID').val() 
     354      }; 
     355      ajax_request = function(options, callback) { 
     356        var params; 
     357        params = _.extend({}, options, candidates.attributes, ctype, { 
    40358          action: 'p2p_box', 
    41           nonce: P2PAdmin.nonce, 
    42           p2p_type: $metabox.data('p2p_type'), 
    43           direction: $metabox.data('direction'), 
    44           from: jQuery('#post_ID').val(), 
    45           s: searchTab.params.s, 
    46           paged: searchTab.params.paged 
     359          nonce: P2PAdmin.nonce 
    47360        }); 
    48         handler = function(response) { 
     361        return jQuery.post(ajaxurl, params, function(response) { 
    49362          try { 
    50363            response = jQuery.parseJSON(response); 
    51             if (response.error) { 
    52               return alert(response.error); 
    53             } else { 
    54               return callback(response); 
     364          } catch (e) { 
     365            if (typeof console !== "undefined" && console !== null) { 
     366              console.error('Malformed response', response); 
    55367            } 
    56           } catch (e) { 
    57             return typeof console !== "undefined" && console !== null ? console.error('Malformed response', response) : void 0; 
    58           } 
    59         }; 
    60         return jQuery.ajax({ 
    61           type: type, 
    62           url: ajaxurl, 
    63           data: data, 
    64           success: handler 
    65         }); 
    66       }; 
    67       PostsTab = (function() { 
    68  
    69         function PostsTab(selector) { 
    70           var _this = this; 
    71           this.tab = $metabox.find(selector); 
    72           this.params = { 
    73             subaction: 'search', 
    74             s: '' 
    75           }; 
    76           this.init_pagination_data(); 
    77           this.tab.delegate('.p2p-prev, .p2p-next', 'click', function(ev) { 
    78             return _this.change_page(ev.target); 
    79           }); 
    80         } 
    81  
    82         PostsTab.prototype.init_pagination_data = function() { 
    83           this.params.paged = this.tab.find('.p2p-current').data('num') || 1; 
    84           return this.total_pages = this.tab.find('.p2p-total').data('num') || 1; 
    85         }; 
    86  
    87         PostsTab.prototype.change_page = function(button) { 
    88           var $navButton, new_page; 
    89           $navButton = jQuery(button); 
    90           new_page = this.params.paged; 
    91           if ($navButton.hasClass('inactive')) { 
    92368            return; 
    93369          } 
    94           if ($navButton.hasClass('p2p-prev')) { 
    95             new_page--; 
     370          if (response.error) { 
     371            return alert(response.error); 
    96372          } else { 
    97             new_page++; 
    98           } 
    99           $spinner.appendTo(this.tab.find('.p2p-navigation')); 
    100           return this.find_posts(new_page); 
    101         }; 
    102  
    103         PostsTab.prototype.find_posts = function(new_page) { 
    104           var _this = this; 
    105           if ((0 < new_page && new_page <= this.total_pages)) { 
    106             this.params.paged = new_page; 
    107           } 
    108           return ajax_request(this.params, function(response) { 
    109             return _this.update_rows(response); 
    110           }, 'GET'); 
    111         }; 
    112  
    113         PostsTab.prototype.update_rows = function(response) { 
    114           $spinner.remove(); 
    115           this.tab.find('button, .p2p-results, .p2p-navigation, .p2p-notice').remove(); 
    116           this.tab.append(response.rows); 
    117           return this.init_pagination_data(); 
    118         }; 
    119  
    120         return PostsTab; 
    121  
    122       })(); 
    123       searchTab = new PostsTab('.p2p-tab-search'); 
    124       row_ajax_request = function($td, data, callback) { 
    125         $td.find('.p2p-icon').css('background-image', 'url(' + P2PAdmin.spinner + ')'); 
    126         return ajax_request(data, callback); 
    127       }; 
    128       remove_row = function($td) { 
    129         var $table; 
    130         $table = $td.closest('table'); 
    131         $td.closest('tr').remove(); 
    132         if (!$table.find('tbody tr').length) { 
    133           return $table.hide(); 
    134         } 
    135       }; 
    136       append_connection = function(response) { 
    137         $connections.show().find('tbody').append(response.row); 
    138         if ('one' === $metabox.data('cardinality')) { 
    139           return $metabox.find('.p2p-create-connections').hide(); 
    140         } 
    141       }; 
    142       refresh_candidates = function(results) { 
    143         $metabox.find('.p2p-create-connections').show(); 
    144         return searchTab.update_rows(results); 
    145       }; 
    146       clear_connections = function(ev) { 
    147         var $td, data, 
    148           _this = this; 
    149         ev.preventDefault(); 
    150         if (!confirm(P2PAdmin.deleteConfirmMessage)) { 
    151           return; 
    152         } 
    153         $td = jQuery(ev.target).closest('td'); 
    154         data = { 
    155           subaction: 'clear_connections' 
    156         }; 
    157         row_ajax_request($td, data, function(response) { 
    158           $connections.hide().find('tbody').html(''); 
    159           return refresh_candidates(response); 
    160         }); 
    161         return null; 
    162       }; 
    163       delete_connection = function(ev) { 
    164         var $td, data, 
    165           _this = this; 
    166         ev.preventDefault(); 
    167         $td = jQuery(ev.target).closest('td'); 
    168         data = { 
    169           subaction: 'disconnect', 
    170           p2p_id: $td.find('input').val() 
    171         }; 
    172         row_ajax_request($td, data, function(response) { 
    173           remove_row($td); 
    174           return refresh_candidates(response); 
    175         }); 
    176         return null; 
    177       }; 
    178       create_connection = function(ev) { 
    179         var $td, data, 
    180           _this = this; 
    181         ev.preventDefault(); 
    182         $td = jQuery(ev.target).closest('td'); 
    183         data = { 
    184           subaction: 'connect', 
    185           to: $td.find('div').data('item-id') 
    186         }; 
    187         row_ajax_request($td, data, function(response) { 
    188           append_connection(response); 
    189           if ($metabox.data('duplicate_connections')) { 
    190             return $td.find('.p2p-icon').css('background-image', ''); 
    191           } else { 
    192             return remove_row($td); 
     373            return callback(response); 
    193374          } 
    194375        }); 
    195         return null; 
    196       }; 
    197       toggle_tabs = function(ev) { 
    198         ev.preventDefault(); 
    199         $metabox.find('.p2p-create-connections-tabs').toggle(); 
    200         return null; 
    201       }; 
    202       switch_to_tab = function(ev) { 
    203         var $tab; 
    204         ev.preventDefault(); 
    205         $tab = jQuery(this); 
    206         $metabox.find('.wp-tab-bar li').removeClass('wp-tab-active'); 
    207         $tab.addClass('wp-tab-active'); 
    208         return $metabox.find('.tabs-panel').hide().end().find($tab.data('ref')).show().find(':text').focus(); 
    209       }; 
    210       $metabox.delegate('th.p2p-col-delete .p2p-icon', 'click', clear_connections).delegate('td.p2p-col-delete .p2p-icon', 'click', delete_connection).delegate('td.p2p-col-create div', 'click', create_connection).delegate('.p2p-toggle-tabs', 'click', toggle_tabs).delegate('.wp-tab-bar li', 'click', switch_to_tab); 
    211       if ($connections.find('th.p2p-col-order').length) { 
    212         $connections.find('tbody').sortable({ 
    213           handle: 'td.p2p-col-order', 
    214           helper: function(e, ui) { 
    215             ui.children().each(function() { 
    216               var $this; 
    217               $this = jQuery(this); 
    218               return $this.width($this.width()); 
    219             }); 
    220             return ui; 
    221           } 
    222         }); 
    223       } 
    224       $searchInput = $metabox.find('.p2p-tab-search :text'); 
    225       $searchInput.keypress(function(ev) { 
    226         if (ev.keyCode === 13) { 
    227           ev.preventDefault(); 
    228         } 
    229         return null; 
    230       }).keyup(function(ev) { 
    231         var delayed; 
    232         if (delayed !== void 0) { 
    233           clearTimeout(delayed); 
    234         } 
    235         delayed = setTimeout(function() { 
    236           var searchStr; 
    237           searchStr = $searchInput.val(); 
    238           if (searchStr === searchTab.params.s) { 
    239             return; 
    240           } 
    241           searchTab.params.s = searchStr; 
    242           $spinner.insertAfter($searchInput).show(); 
    243           return searchTab.find_posts(1); 
    244         }, 400); 
    245         return null; 
    246       }); 
    247       $createButton = $metabox.find('.p2p-tab-create-post button'); 
    248       $createInput = $metabox.find('.p2p-tab-create-post :text'); 
    249       $createButton.click(function(ev) { 
    250         var $button, data, title; 
    251         ev.preventDefault(); 
    252         $button = jQuery(this); 
    253         if ($button.hasClass('inactive')) { 
    254           return; 
    255         } 
    256         title = $createInput.val(); 
    257         if (title === '') { 
    258           $createInput.focus(); 
    259           return; 
    260         } 
    261         $button.addClass('inactive'); 
    262         data = { 
    263           subaction: 'create_post', 
    264           post_title: title 
    265         }; 
    266         ajax_request(data, function(response) { 
    267           append_connection(response); 
    268           $createInput.val(''); 
    269           return $button.removeClass('inactive'); 
    270         }); 
    271         return null; 
    272       }); 
    273       return $createInput.keypress(function(ev) { 
    274         if (13 === ev.keyCode) { 
    275           $createButton.click(); 
    276           ev.preventDefault(); 
    277         } 
    278         return null; 
     376      }; 
     377      candidates.ajax_request = ajax_request; 
     378      connections = new Connections; 
     379      connections.ajax_request = ajax_request; 
     380      connectionsView = new ConnectionsView({ 
     381        el: $metabox.find('.p2p-connections'), 
     382        collection: connections, 
     383        candidates: candidates 
     384      }); 
     385      candidatesView = new CandidatesView({ 
     386        el: $metabox.find('.p2p-tab-search'), 
     387        collection: candidates, 
     388        connections: connections, 
     389        spinner: $spinner, 
     390        duplicate_connections: $metabox.data('duplicate_connections') 
     391      }); 
     392      createPostView = new CreatePostView({ 
     393        el: $metabox.find('.p2p-tab-create-post'), 
     394        collection: connections 
     395      }); 
     396      return metaboxView = new MetaboxView({ 
     397        el: $metabox, 
     398        spinner: $spinner, 
     399        cardinality: $metabox.data('cardinality'), 
     400        candidates: candidates, 
     401        connections: connections 
    279402      }); 
    280403    }); 
  • posts-to-posts/trunk/admin/box.php

    r629142 r662236  
    11<?php 
    2  
    3 interface P2P_Field { 
    4     function get_title(); 
    5     function render( $p2p_id, $item ); 
    6 } 
    72 
    83class P2P_Box { 
     
    3631            return; 
    3732 
    38         wp_enqueue_style( 'p2p-box', plugins_url( 'box.css', __FILE__ ), array(), P2P_PLUGIN_VERSION ); 
    39  
    40         wp_enqueue_script( 'p2p-box', plugins_url( 'box.js', __FILE__ ), array( 'jquery' ), P2P_PLUGIN_VERSION, true ); 
     33        wp_enqueue_style( 'p2p-box', plugins_url( 'box.css', __FILE__ ), 
     34            array(), P2P_PLUGIN_VERSION ); 
     35 
     36        wp_register_script( 'mustache', plugins_url( 'mustache.js', __FILE__ ), 
     37            array(), '0.7.2', true ); 
     38 
     39        wp_enqueue_script( 'p2p-box', plugins_url( 'box.js', __FILE__ ), 
     40            array( 'backbone', 'mustache' ), P2P_PLUGIN_VERSION, true ); 
     41 
    4142        wp_localize_script( 'p2p-box', 'P2PAdmin', array( 
    4243            'nonce' => wp_create_nonce( P2P_BOX_NONCE ), 
     
    4748        self::$enqueued_scripts = true; 
    4849 
     50        add_action( 'admin_footer', array( __CLASS__, 'add_templates' ) ); 
     51    } 
     52 
     53    static function add_templates() { 
     54        self::add_template( 'tab-list' ); 
     55        self::add_template( 'table-row' ); 
     56    } 
     57 
     58    private static function add_template( $slug ) { 
     59        echo html( 'script', array( 
     60            'type' => 'text/html', 
     61            'id' => "p2p-template-$slug" 
     62        ), file_get_contents( dirname( __FILE__ ) . "/templates/$slug.html" ) ); 
    4963    } 
    5064 
     
    116130        $tab_content = P2P_Mustache::render( 'tab-search', array( 
    117131            'placeholder' => $this->labels->search_items, 
    118             'candidates' => $this->post_rows( $post->ID ) 
    119132        ) ); 
    120133 
     
    166179    protected function post_rows( $current_post_id, $page = 1, $search = '' ) { 
    167180        $extra_qv = array_merge( self::$admin_box_qv, array( 
     181            'p2p:context' => 'admin_box_candidates', 
    168182            'p2p:search' => $search, 
    169183            'p2p:page' => $page, 
     
    192206                'total-pages' => number_format_i18n( $candidate->total_pages ), 
    193207 
    194                 'current-page-raw' => $candidate->current_page, 
    195208                'total-pages-raw' => $candidate->total_pages, 
    196209 
     
    204217        } 
    205218 
    206         return P2P_Mustache::render( 'tab-list', $data ); 
     219        return $data; 
    207220    } 
    208221 
     
    281294 
    282295    private function refresh_candidates() { 
    283         $rows = $this->post_rows( $_REQUEST['from'], $_REQUEST['paged'], $_REQUEST['s'] ); 
    284  
    285         $results = compact( 'rows' ); 
    286  
    287         die( json_encode( $results ) ); 
     296        die( json_encode( $this->post_rows( 
     297            $_REQUEST['from'], $_REQUEST['paged'], $_REQUEST['s'] ) ) ); 
    288298    } 
    289299 
  • posts-to-posts/trunk/admin/column-factory.php

    r630742 r662236  
    2020} 
    2121 
    22 new P2P_Column_Factory; 
    23  
  • posts-to-posts/trunk/admin/column.php

    r646162 r662236  
    7979} 
    8080 
    81  
    82 class P2P_Column_Post extends P2P_Column { 
    83  
    84     function __construct( $directed ) { 
    85         parent::__construct( $directed ); 
    86  
    87         $screen = get_current_screen(); 
    88  
    89         add_action( "manage_{$screen->post_type}_posts_custom_column", array( $this, 'display_column' ), 10, 2 ); 
    90     } 
    91  
    92     protected function get_items() { 
    93         global $wp_query; 
    94  
    95         return $wp_query->posts; 
    96     } 
    97  
    98     function get_admin_link( $item ) { 
    99         $args = array( 
    100             'connected_type' => $this->ctype->name, 
    101             'connected_direction' => $this->ctype->flip_direction()->get_direction(), 
    102             'connected_items' => $item->get_id(), 
    103             'post_type' => get_current_screen()->post_type 
    104         ); 
    105  
    106         return add_query_arg( $args, admin_url( 'edit.php' ) ); 
    107     } 
    108  
    109     function display_column( $column, $item_id ) { 
    110         echo parent::render_column( $column, $item_id ); 
    111     } 
    112 } 
    113  
    114  
    115 class P2P_Column_User extends P2P_Column { 
    116  
    117     function __construct( $directed ) { 
    118         parent::__construct( $directed ); 
    119  
    120         add_action( 'pre_user_query', array( __CLASS__, 'user_query' ), 9 ); 
    121  
    122         add_filter( 'manage_users_custom_column', array( $this, 'display_column' ), 10, 3 ); 
    123     } 
    124  
    125     protected function get_items() { 
    126         global $wp_list_table; 
    127  
    128         return $wp_list_table->items; 
    129     } 
    130  
    131     // Add the query vars to the global user query (on the user admin screen) 
    132     static function user_query( $query ) { 
    133         if ( isset( $query->_p2p_capture ) ) 
    134             return; 
    135  
    136         // Don't overwrite existing P2P query 
    137         if ( isset( $query->query_vars['connected_type'] ) ) 
    138             return; 
    139  
    140         _p2p_append( $query->query_vars, wp_array_slice_assoc( $_GET, 
    141             P2P_URL_Query::get_custom_qv() ) ); 
    142     } 
    143  
    144     function get_admin_link( $item ) { 
    145         $args = array( 
    146             'connected_type' => $this->ctype->name, 
    147             'connected_direction' => $this->ctype->flip_direction()->get_direction(), 
    148             'connected_items' => $item->get_id(), 
    149         ); 
    150  
    151         return add_query_arg( $args, admin_url( 'users.php' ) ); 
    152     } 
    153  
    154     function display_column( $content, $column, $item_id ) { 
    155         return parent::render_column( $column, $item_id ); 
    156     } 
    157 } 
    158  
  • posts-to-posts/trunk/admin/dropdown-factory.php

    r630742 r662236  
    1818} 
    1919 
    20 new P2P_Dropdown_Factory; 
    21  
  • posts-to-posts/trunk/admin/dropdown.php

    r630742 r662236  
    6060} 
    6161 
    62  
    63 class P2P_Dropdown_Post extends P2P_Dropdown { 
    64  
    65     function __construct( $directed, $title ) { 
    66         parent::__construct( $directed, $title ); 
    67  
    68         add_filter( 'request', array( __CLASS__, 'massage_query' ) ); 
    69  
    70         add_action( 'restrict_manage_posts', array( $this, 'show_dropdown' ) ); 
    71     } 
    72  
    73     static function massage_query( $request ) { 
    74         return array_merge( $request, self::get_qv() ); 
    75     } 
    76 } 
    77  
    78  
    79 class P2P_Dropdown_User extends P2P_Dropdown_Post { 
    80  
    81     function __construct( $directed, $title ) { 
    82         parent::__construct( $directed, $title ); 
    83  
    84         add_action( 'pre_user_query', array( __CLASS__, 'massage_query' ), 9 ); 
    85  
    86         add_action( 'restrict_manage_users', array( $this, 'show_dropdown' ) ); 
    87     } 
    88  
    89     static function massage_query( $query ) { 
    90         if ( isset( $query->_p2p_capture ) ) 
    91             return; 
    92  
    93         // Don't overwrite existing P2P query 
    94         if ( isset( $query->query_vars['connected_type'] ) ) 
    95             return; 
    96  
    97         _p2p_append( $query->query_vars, self::get_qv() ); 
    98     } 
    99  
    100     protected function render_dropdown() { 
    101         return html( 'div', array( 
    102             'style' => 'float: right; margin-left: 16px' 
    103         ), 
    104             parent::render_dropdown(), 
    105             html( 'input', array( 
    106                 'type' => 'submit', 
    107                 'class' => 'button', 
    108                 'value' => __( 'Filter', P2P_TEXTDOMAIN ) 
    109             ) ) 
    110         ); 
    111     } 
    112 } 
    113  
  • posts-to-posts/trunk/admin/templates/tab-list.html

    r446436 r662236  
    1111    <div class="p2p-prev button {{prev-inactive}}" title="{{prev-label}}">&lsaquo;</div> 
    1212    <div> 
    13         <span class="p2p-current" data-num="{{current-page-raw}}">{{current-page}}</span> 
     13        <span class="p2p-current">{{current-page}}</span> 
    1414        {{of-label}} 
    1515        <span class="p2p-total" data-num="{{total-pages-raw}}">{{total-pages}}</span> 
  • posts-to-posts/trunk/command.php

    r646162 r662236  
    3333                $assoc_args = array( 'post_type' => $ptype ); 
    3434 
    35                 WP_CLI::launch( 'wp post generate' . \WP_CLI\Utils\compose_assoc_args( $assoc_args ) ); 
     35                WP_CLI::launch( 'wp post generate' . \WP_CLI\Utils\assoc_args_to_str( $assoc_args ) ); 
    3636            } 
    3737        } 
  • posts-to-posts/trunk/core/item.php

    r599761 r662236  
    3333} 
    3434 
    35  
    36 class P2P_Item_Any extends P2P_Item { 
    37  
    38     function __construct() {} 
    39  
    40     function get_permalink() {} 
    41  
    42     function get_title() {} 
    43  
    44     function get_object() { 
    45         return 'any'; 
    46     } 
    47  
    48     function get_id() { 
    49         return false; 
    50     } 
    51 } 
    52  
    53  
    54 class P2P_Item_Post extends P2P_Item { 
    55  
    56     function get_title() { 
    57         return get_the_title( $this->item ); 
    58     } 
    59  
    60     function get_permalink() { 
    61         return get_permalink( $this->item ); 
    62     } 
    63  
    64     function get_editlink() { 
    65         return get_edit_post_link( $this->item ); 
    66     } 
    67 } 
    68  
    69  
    70 class P2P_Item_Attachment extends P2P_Item_Post { 
    71  
    72     function get_title() { 
    73         return wp_get_attachment_image( $this->item->ID, 'thumbnail', false ); 
    74     } 
    75 } 
    76  
    77  
    78 class P2P_Item_User extends P2P_Item { 
    79  
    80     function get_title() { 
    81         return $this->item->display_name; 
    82     } 
    83  
    84     function get_permalink() { 
    85         return get_author_posts_url( $this->item->ID ); 
    86     } 
    87  
    88     function get_editlink() { 
    89         return get_edit_user_link( $this->item->ID ); 
    90     } 
    91 } 
    92  
    93  
    94 // WP < 3.5 
    95 if ( !function_exists( 'get_edit_user_link' ) ) : 
    96 function get_edit_user_link( $user_id = null ) { 
    97     if ( ! $user_id ) 
    98         $user_id = get_current_user_id(); 
    99  
    100     if ( empty( $user_id ) || ! current_user_can( 'edit_user', $user_id ) ) 
    101         return ''; 
    102  
    103     $user = new WP_User( $user_id ); 
    104  
    105     if ( ! $user->exists() ) 
    106         return ''; 
    107  
    108     if ( get_current_user_id() == $user->ID ) 
    109         $link = get_edit_profile_url( $user->ID ); 
    110     else 
    111         $link = add_query_arg( 'user_id', $user->ID, self_admin_url( 'user-edit.php' ) ); 
    112  
    113     return apply_filters( 'get_edit_user_link', $link, $user->ID ); 
    114 } 
    115 endif; 
    116  
  • posts-to-posts/trunk/core/query-post.php

    r577329 r662236  
    11<?php 
    22 
    3 class P2P_Post_Query { 
     3class P2P_Query_Post { 
    44 
    55    static function init() { 
     
    6060} 
    6161 
    62 P2P_Post_Query::init(); 
    63  
  • posts-to-posts/trunk/core/query-user.php

    r568375 r662236  
    11<?php 
    22 
    3 class P2P_User_Query { 
     3class P2P_Query_User { 
    44 
    55    static function init() { 
     
    4444} 
    4545 
    46 P2P_User_Query::init(); 
    47  
  • posts-to-posts/trunk/core/side.php

    r646162 r662236  
    5353} 
    5454 
    55  
    56 class P2P_Side_Post extends P2P_Side { 
    57  
    58     protected $item_type = 'P2P_Item_Post'; 
    59  
    60     function __construct( $query_vars ) { 
    61         $this->query_vars = $query_vars; 
    62     } 
    63  
    64     public function get_object_type() { 
    65         return 'post'; 
    66     } 
    67  
    68     public function first_post_type() { 
    69         return $this->query_vars['post_type'][0]; 
    70     } 
    71  
    72     private function get_ptype() { 
    73         return get_post_type_object( $this->first_post_type() ); 
    74     } 
    75  
    76     function get_base_qv( $q ) { 
    77         if ( isset( $q['post_type'] ) && 'any' != $q['post_type'] ) { 
    78             $common = array_intersect( $this->query_vars['post_type'], (array) $q['post_type'] ); 
    79  
    80             if ( !$common ) 
    81                 unset( $q['post_type'] ); 
    82         } 
    83  
    84         return array_merge( $this->query_vars, $q, array( 
    85             'suppress_filters' => false, 
    86             'ignore_sticky_posts' => true, 
    87         ) ); 
    88     } 
    89  
    90     function get_desc() { 
    91         return implode( ', ', array_map( array( $this, 'post_type_label' ), $this->query_vars['post_type'] ) ); 
    92     } 
    93  
    94     private function post_type_label( $post_type ) { 
    95         $cpt = get_post_type_object( $post_type ); 
    96         return $cpt ? $cpt->label : $post_type; 
    97     } 
    98  
    99     function get_title() { 
    100         return $this->get_ptype()->labels->name; 
    101     } 
    102  
    103     function get_labels() { 
    104         return $this->get_ptype()->labels; 
    105     } 
    106  
    107     function can_edit_connections() { 
    108         return current_user_can( $this->get_ptype()->cap->edit_posts ); 
    109     } 
    110  
    111     function can_create_item() { 
    112         if ( count( $this->query_vars['post_type'] ) > 1 ) 
    113             return false; 
    114  
    115         if ( count( $this->query_vars ) > 1 ) 
    116             return false; 
    117  
    118         return true; 
    119     } 
    120  
    121     function translate_qv( $qv ) { 
    122         $map = array( 
    123             'include' => 'post__in', 
    124             'exclude' => 'post__not_in', 
    125             'search' => 's', 
    126             'page' => 'paged', 
    127             'per_page' => 'posts_per_page' 
    128         ); 
    129  
    130         foreach ( $map as $old => $new ) 
    131             if ( isset( $qv["p2p:$old"] ) ) 
    132                 $qv[$new] = _p2p_pluck( $qv, "p2p:$old" ); 
    133  
    134         return $qv; 
    135     } 
    136  
    137     function do_query( $args ) { 
    138         return new WP_Query( $args ); 
    139     } 
    140  
    141     function capture_query( $args ) { 
    142         $q = new WP_Query; 
    143         $q->_p2p_capture = true; 
    144  
    145         $q->query( $args ); 
    146  
    147         return $q->_p2p_sql; 
    148     } 
    149  
    150     function get_list( $wp_query ) { 
    151         $list = new P2P_List( $wp_query->posts, $this->item_type ); 
    152  
    153         $list->current_page = max( 1, $wp_query->get('paged') ); 
    154         $list->total_pages = $wp_query->max_num_pages; 
    155  
    156         return $list; 
    157     } 
    158  
    159     function is_indeterminate( $side ) { 
    160         $common = array_intersect( 
    161             $this->query_vars['post_type'], 
    162             $side->query_vars['post_type'] 
    163         ); 
    164  
    165         return !empty( $common ); 
    166     } 
    167  
    168     protected function recognize( $arg ) { 
    169         if ( is_object( $arg ) && !isset( $arg->post_type ) ) 
    170             return false; 
    171  
    172         $post = get_post( $arg ); 
    173  
    174         if ( !is_object( $post ) ) 
    175             return false; 
    176  
    177         if ( !$this->recognize_post_type( $post->post_type ) ) 
    178             return false; 
    179  
    180         return $post; 
    181     } 
    182  
    183     public function recognize_post_type( $post_type ) { 
    184         if ( !post_type_exists( $post_type ) ) 
    185             return false; 
    186  
    187         return in_array( $post_type, $this->query_vars['post_type'] ); 
    188     } 
    189 } 
    190  
    191  
    192 class P2P_Side_Attachment extends P2P_Side_Post { 
    193  
    194     protected $item_type = 'P2P_Item_Attachment'; 
    195  
    196     function __construct( $query_vars ) { 
    197         $this->query_vars = $query_vars; 
    198  
    199         $this->query_vars['post_type'] = array( 'attachment' ); 
    200     } 
    201  
    202     function can_create_item() { 
    203         return false; 
    204     } 
    205  
    206     function get_base_qv( $q ) { 
    207         return array_merge( parent::get_base_qv( $q ), array( 
    208             'post_status' => 'inherit' 
    209         ) ); 
    210     } 
    211 } 
    212  
    213  
    214 class P2P_Side_User extends P2P_Side { 
    215  
    216     protected $item_type = 'P2P_Item_User'; 
    217  
    218     function __construct( $query_vars ) { 
    219         $this->query_vars = $query_vars; 
    220     } 
    221  
    222     function get_object_type() { 
    223         return 'user'; 
    224     } 
    225  
    226     function get_desc() { 
    227         return __( 'Users', P2P_TEXTDOMAIN ); 
    228     } 
    229  
    230     function get_title() { 
    231         return $this->get_desc(); 
    232     } 
    233  
    234     function get_labels() { 
    235         return (object) array( 
    236             'singular_name' => __( 'User', P2P_TEXTDOMAIN ), 
    237             'search_items' => __( 'Search Users', P2P_TEXTDOMAIN ), 
    238             'not_found' => __( 'No users found.', P2P_TEXTDOMAIN ), 
    239         ); 
    240     } 
    241  
    242     function can_edit_connections() { 
    243         return current_user_can( 'list_users' ); 
    244     } 
    245  
    246     function can_create_item() { 
    247         return false; 
    248     } 
    249  
    250     function translate_qv( $qv ) { 
    251         if ( isset( $qv['p2p:include'] ) ) 
    252             $qv['include'] = _p2p_pluck( $qv, 'p2p:include' ); 
    253  
    254         if ( isset( $qv['p2p:exclude'] ) ) 
    255             $qv['exclude'] = _p2p_pluck( $qv, 'p2p:exclude' ); 
    256  
    257         if ( isset( $qv['p2p:search'] ) && $qv['p2p:search'] ) 
    258             $qv['search'] = '*' . _p2p_pluck( $qv, 'p2p:search' ) . '*'; 
    259  
    260         if ( isset( $qv['p2p:page'] ) && $qv['p2p:page'] > 0 ) { 
    261             if ( isset( $qv['p2p:per_page'] ) && $qv['p2p:per_page'] > 0 ) { 
    262                 $qv['number'] = $qv['p2p:per_page']; 
    263                 $qv['offset'] = $qv['p2p:per_page'] * ( $qv['p2p:page'] - 1 ); 
    264             } 
    265         } 
    266  
    267         return $qv; 
    268     } 
    269  
    270     function do_query( $args ) { 
    271         return new WP_User_Query( $args ); 
    272     } 
    273  
    274     function capture_query( $args ) { 
    275         $args['count_total'] = false; 
    276  
    277         $uq = new WP_User_Query; 
    278         $uq->_p2p_capture = true; // needed by P2P_URL_Query 
    279  
    280         // see http://core.trac.wordpress.org/ticket/21119 
    281         $uq->query_vars = wp_parse_args( $args, array( 
    282             'blog_id' => $GLOBALS['blog_id'], 
    283             'role' => '', 
    284             'meta_key' => '', 
    285             'meta_value' => '', 
    286             'meta_compare' => '', 
    287             'include' => array(), 
    288             'exclude' => array(), 
    289             'search' => '', 
    290             'search_columns' => array(), 
    291             'orderby' => 'login', 
    292             'order' => 'ASC', 
    293             'offset' => '', 
    294             'number' => '', 
    295             'count_total' => true, 
    296             'fields' => 'all', 
    297             'who' => '' 
    298         ) ); 
    299  
    300         $uq->prepare_query(); 
    301  
    302         return "SELECT $uq->query_fields $uq->query_from $uq->query_where $uq->query_orderby $uq->query_limit"; 
    303     } 
    304  
    305     function get_list( $query ) { 
    306         $list = new P2P_List( $query->get_results(), $this->item_type ); 
    307  
    308         $qv = $query->query_vars; 
    309  
    310         if ( isset( $qv['p2p:page'] ) ) { 
    311             $list->current_page = $qv['p2p:page']; 
    312             $list->total_pages = ceil( $query->get_total() / $qv['p2p:per_page'] ); 
    313         } 
    314  
    315         return $list; 
    316     } 
    317  
    318     function is_indeterminate( $side ) { 
    319         return true; 
    320     } 
    321  
    322     function get_base_qv( $q ) { 
    323         return array_merge( $this->query_vars, $q ); 
    324     } 
    325  
    326     protected function recognize( $arg ) { 
    327         if ( is_a( $arg, 'WP_User' ) ) 
    328             return $arg; 
    329  
    330         return get_user_by( 'id', $arg ); 
    331     } 
    332 } 
    333  
  • posts-to-posts/trunk/core/storage.php

    r598601 r662236  
    6161} 
    6262 
    63 P2P_Storage::init(); 
    64  
  • posts-to-posts/trunk/core/util.php

    r632792 r662236  
    134134} 
    135135 
     136/** @internal */ 
     137function _p2p_get_list( $args ) { 
     138    extract( $args ); 
     139 
     140    $ctype = p2p_type( $ctype ); 
     141    if ( !$ctype ) { 
     142        trigger_error( sprintf( "Unregistered connection type '%s'.", $ctype ), E_USER_WARNING ); 
     143        return ''; 
     144    } 
     145 
     146    $directed = $ctype->find_direction( $item ); 
     147    if ( !$directed ) 
     148        return ''; 
     149 
     150    $extra_qv = array( 
     151        'p2p:per_page' => -1, 
     152        'p2p:context' => $context 
     153    ); 
     154 
     155    $connected = $directed->$method( $item, $extra_qv, 'abstract' ); 
     156 
     157    switch ( $mode ) { 
     158    case 'inline': 
     159        $args = array( 
     160            'separator' => ', ' 
     161        ); 
     162        break; 
     163 
     164    case 'ol': 
     165        $args = array( 
     166            'before_list' => '<ol id="' . $ctype->name . '_list">', 
     167            'after_list' => '</ol>', 
     168        ); 
     169        break; 
     170 
     171    case 'ul': 
     172    default: 
     173        $args = array( 
     174            'before_list' => '<ul id="' . $ctype->name . '_list">', 
     175            'after_list' => '</ul>', 
     176        ); 
     177        break; 
     178    } 
     179 
     180    $args['echo'] = false; 
     181 
     182    return apply_filters( "p2p_{$context}_html", $connected->render( $args ), $connected, $directed, $mode ); 
     183} 
     184 
  • posts-to-posts/trunk/posts-to-posts.php

    r632792 r662236  
    33Plugin Name: Posts 2 Posts 
    44Description: Create many-to-many relationships between all types of posts. 
    5 Version: 1.5-alpha 
     5Version: 1.5-alpha2 
    66Author: scribu 
    77Author URI: http://scribu.net/ 
     
    1111*/ 
    1212 
    13 define( 'P2P_PLUGIN_VERSION', '1.4.3' ); 
     13define( 'P2P_PLUGIN_VERSION', '1.5-alpha2' ); 
    1414 
    1515define( 'P2P_TEXTDOMAIN', 'posts-to-posts' ); 
     
    2222    load_plugin_textdomain( P2P_TEXTDOMAIN, '', basename( $base ) . '/lang' ); 
    2323 
    24     _p2p_load_files( "$base/core", array( 
    25         'storage', 'query', 'query-post', 'query-user', 'url-query', 
    26         'util', 'item', 'list', 'side', 
    27         'type-factory', 'type', 'directed-type', 'indeterminate-type', 
    28         'api', 'extra' 
    29     ) ); 
     24    require $base . '/core/util.php'; 
     25    require $base . '/core/api.php'; 
     26    require $base . '/autoload.php'; 
     27 
     28    P2P_Autoload::register( 'P2P_', $base . '/core' ); 
     29 
     30    P2P_Storage::init(); 
     31 
     32    P2P_Query_Post::init(); 
     33    P2P_Query_User::init(); 
    3034 
    3135    P2P_Widget::init(); 
     
    4044 
    4145function _p2p_load_admin() { 
    42     _p2p_load_files( dirname(__FILE__) . '/admin', array( 
    43         'mustache', 'factory', 
    44         'box-factory', 'box', 'fields', 
    45         'column-factory', 'column', 
    46         'dropdown-factory', 'dropdown', 
    47         'tools' 
    48     ) ); 
     46    P2P_Autoload::register( 'P2P_', dirname( __FILE__ ) . '/admin' ); 
     47 
     48    new P2P_Box_Factory; 
     49    new P2P_Column_Factory; 
     50    new P2P_Dropdown_Factory; 
     51 
     52    new P2P_Tools_Page; 
    4953} 
    5054 
     
    5559add_action( 'wp_loaded', '_p2p_init' ); 
    5660 
    57 function _p2p_load_files( $dir, $files ) { 
    58     foreach ( $files as $file ) 
    59         require_once "$dir/$file.php"; 
    60 } 
    61  
  • posts-to-posts/trunk/readme.txt

    r630742 r662236  
    5454== Changelog == 
    5555 
    56 = 1.5 = 
     56= 1.5 (next) = 
    5757* added admin dropdowns 
     58* fixed SQL error related to user connections 
     59* fixed 'labels' handling and added 'column_title' subkey 
    5860 
    5961= 1.4.3 = 
Note: See TracChangeset for help on using the changeset viewer.