Making WordPress.org

Ticket #1112: 1112.2.diff

File 1112.2.diff, 35.8 KB (added by prettyboymp, 9 years ago)

Updated patch with new wp_cron for updating sites and added search filters

  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php
    index 8628d2c..88bc3d1 100644
    a b namespace WordCamp\Site_Cloner; 
    55defined( 'WPINC' ) or die();
    66
    77/**
    8  * Custom Customizer Control for a WordCamp site
     8 * Custom Customizer Control for Search WordCamp sites to clone
    99 */
    1010class Site_Control extends \WP_Customize_Control {
    11         public $site_id, $site_name, $screenshot_url, $theme_slug;
    12         public $settings = 'wcsc_source_site_id';
    13         public $section  = 'wcsc_sites';
     11
     12        public function __construct( $manager, $id, $args = [ ] ) {
     13                parent::__construct( $manager, $id, $args );
     14
     15                $this->capability = 'edit_theme_options';
     16                $this->section = 'wcsc_sites';
     17        }
    1418
    1519        /**
    1620         * Enqueue scripts and styles
    class Site_Control extends \WP_Customize_Control { 
    1822        public function enqueue() {
    1923                wp_enqueue_style(  'wordcamp-site-cloner' );
    2024                wp_enqueue_script( 'wordcamp-site-cloner' );
     25                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_view_templates' ) );
    2126        }
    2227
    23         /**
    24          * Render the control's content
    25          */
    2628        public function render_content() {
    27                 $preview_url = add_query_arg(
    28                         array(
    29                                 'theme'               => rawurlencode( $this->theme_slug ),
    30                                 'wcsc_source_site_id' => rawurlencode( $this->site_id ),
    31                         ),
    32                         admin_url( 'customize.php' )
    33                 );
    34 
    3529                require( dirname( __DIR__ ) . '/templates/site-control.php' );
    3630        }
     31
     32        public function print_view_templates() {
     33                ?>
     34                <script id="tmpl-wcsc-site-option" type="text/html">
     35                        <?php include dirname( __DIR__ ) . '/templates/site-option.php'; ?>
     36                </script>
     37                <script id="tmpl-wcsc-site-filters" type="text/html">
     38                        <?php include dirname( __DIR__ ) . '/templates/site-filters.php'; ?>
     39                </script>
     40                <?php
     41        }
    3742}
  • deleted file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php
    deleted file mode 100644
    index 69b85e4..0000000
    + -  
    1 <?php
    2 
    3 namespace WordCamp\Site_Cloner;
    4 
    5 defined( 'WPINC' ) or die();
    6 
    7 /**
    8  * Custom Customizer Section for WordCamp sites
    9  */
    10 class Sites_Section extends \WP_Customize_Section {
    11         public $type = 'wcsc-sites';
    12 
    13         /**
    14          * Render the section's content
    15          */
    16         protected function render() {
    17                 require_once( dirname( __DIR__ ) . '/templates/sites-section.php' );
    18         }
    19 }
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php
    index 952a134..9b60dba 100644
    a b  
    1 <?php defined( 'WPINC' ) or die(); ?>
     1<?php
     2defined( 'WPINC' ) or die();
    23
    3 <div id="wcsc-site-<?php echo esc_attr( $this->site_id ); ?>" class="wcscSite" data-preview-url="<?php echo esc_url( $preview_url ); ?>">
    4         <div class="wcsc-site-screenshot">
    5                 <img src="<?php echo esc_url( $this->screenshot_url ); ?>" alt="<?php echo esc_attr( $this->site_name ); ?>" />
    6         </div>
    7 
    8         <h3 class="wcsc-site-name">
    9                 <?php echo esc_html( $this->site_name ); ?>
     4/**
     5 *  Top level template for the output of the Site Cloner Customizer Control
     6 */
     7?>
     8<div id="wcsc-cloner">
     9        <h3>
     10                <?php esc_html_e( 'WordCamp Sites', 'wordcamporg' ); ?>
     11                <span id="wcsc-sites-count" class="title-count wcsc-sites-count"></span>
    1012        </h3>
     13        <div class="filters">
     14        </div>
    1115
    12         <span id="live-preview-label-<?php echo esc_attr( $this->site_id ); ?>" class="wcsc-live-preview-label">
    13                 <?php _e( 'Live Preview', 'wordcamporg' ); ?>
    14         </span>
    15 </div>
     16        <div class="wcsc-search">
     17                <ul id="wcsc-results">
     18                </ul>
     19        </div>
     20</div>
     21 No newline at end of file
  • new file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php
    new file mode 100644
    index 0000000..d68edc3
    - +  
     1<?php
     2/**
     3 *  Template for the search and drop down filters
     4 */
     5
     6defined( 'WPINC' ) or die();
     7
     8?>
     9        <div class="wcsc-filter">
     10                <label for="wcsc-filter-search-input">
     11                        <span class="customize-control-title"><?php _e( 'Search', 'wordcamporg' ); ?></span>
     12                        <div class="customize-control-content">
     13                                <input type="search" id="wcsc-filter-search-input" class="wcsc-filter-search"/>
     14                        </div>
     15                </label>
     16        </div>
     17
     18        <div class="wcsc-filter">
     19                <label for="wcsc-filter-theme_slug">
     20                        <span class="customize-control-title"><?php _e( 'Theme', 'wordcamporg' ); ?></span>
     21                        <div class="customize-control-content">
     22                                <select id="wcsc-filter-theme_slug" data-filter="theme_slug">
     23                                        <option value="">Any</option>
     24                                        <# _.each(data.themeOptions, function(themeOption) { #>
     25                                                <option value="{{themeOption}}">{{themeOption}}</option>
     26                                                <# }); #>
     27                                </select>
     28                        </div>
     29                </label>
     30        </div>
     31
     32        <div class="wcsc-filter">
     33                <label for="wcsc-filter-year">
     34                        <span class="customize-control-title"><?php _e( 'WordCamp Year', 'wordcamporg' ); ?></span>
     35                        <div class="customize-control-content">
     36                                <select id="wcsc-filter-year" data-filter="year">
     37                                        <option value="">Any</option>
     38                                        <# _.each(data.yearOptions, function(yearOption) { #>
     39                                                <option value="{{yearOption}}">{{yearOption}}</option>
     40                                                <# }); #>
     41                                </select>
     42                        </div>
     43                </label>
     44        </div>
     45
     46        <div class="wcsc-filter">
     47                <label for="wcsc-filter-css_preprocessor">
     48                        <span class="customize-control-title"><?php _e( 'CSS Preprocessor', 'wordcamporg' ); ?></span>
     49                        <div class="customize-control-content">
     50                                <select id="wcsc-filter-css_preprocessor" data-filter="css_preprocessor">
     51                                        <option value="">Any</option>
     52                                        <# _.each(data.preprocessorOptions, function(preprocessorOption) { #>
     53                                                <option value="{{preprocessorOption}}">{{preprocessorOption}}</option>
     54                                                <# }); #>
     55                                </select>
     56                        </div>
     57                </label>
     58        </div>
     59<?php
     60 No newline at end of file
  • new file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php
    new file mode 100644
    index 0000000..453a407
    - +  
     1<?php
     2/**
     3 *  Template for a single Site representation within the Site Cloner Control
     4 */
     5
     6defined( 'WPINC' ) or die();
     7
     8?>
     9        <div class="wcsc-site-screenshot">
     10                <img src="{{ data.screenshot_url }}" alt="{{ data.name }}"/>
     11        </div>
     12
     13        <h3 class="wcsc-site-name">
     14                {{ data.name }}
     15        </h3>
     16
     17        <span id="live-preview-label-{{ data.site_id }}" class="wcsc-live-preview-label">
     18                <?php _e( 'Live Preview', 'wordcamporg' ); ?>
     19        </span>
     20<?php
     21 No newline at end of file
  • deleted file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php
    deleted file mode 100644
    index a05272c..0000000
    + -  
    1 <?php defined( 'WPINC' ) or die(); ?>
    2 
    3 <li id="section-<?php echo esc_attr( $this->id ); ?>" class="accordion-section control-section control-section-<?php echo esc_attr( $this->type ); ?>">
    4         <h3>
    5                 <?php esc_html_e( 'WordCamp Sites' ); ?>
    6 
    7                 <span class="title-count wcsc-sites-count">
    8                         <?php echo count( $this->controls ); ?>
    9                 </span>
    10         </h3>
    11 
    12         <div class="wcsc-sites-section-content">
    13                 <ul id="wcsc-sites"></ul>
    14         </div>
    15 </li>
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css
    index 0a33876..afde3c1 100644
    a b  
    66                overflow: auto;
    77        }
    88
    9                 .wcscSite {
     9                .wcsc-site {
    1010                        position: relative;
    1111                        cursor: pointer;
    1212                        border: 1px solid #DEDEDE;
    1313                        box-shadow: 0 1px 1px -1px rgba( 0, 0, 0, 0.1 );
     14                        margin-top: 5px;
    1415                }
    1516
    1617                        .wcsc-site-screenshot {
    1718                                transition: opacity 0.2s ease-in-out 0s;
    1819                        }
    1920
    20                                 .wcscSite:hover .wcsc-site-screenshot {
     21                                .wcsc-site:hover .wcsc-site-screenshot {
    2122                                        opacity: 0.4;
    2223                                }
    2324
     
    3839                                transition: opacity 0.1s ease-in-out 0s;
    3940                        }
    4041
    41                                 .wcscSite:hover .wcsc-live-preview-label {
     42                                .wcsc-site:hover .wcsc-live-preview-label {
    4243                                        opacity: 1;
    4344                                }
     45
     46div.wcsc-filter {
     47        margin-bottom: 10px;
     48}
     49 No newline at end of file
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js
    index 5c05f82..0dcec81 100644
    a b  
    1 ( function( wp, $ ) {
     1(function ( wp, $, Backbone, win, settings ) {
    22        'use strict';
    33
    4         if ( ! wp || ! wp.customize ) {
     4        if ( !wp || !wp.customize ) {
    55                return;
    66        }
    77
    8         var api = wp.customize;
     8        var api  = wp.customize,
     9                        wcsc = wp.wcSiteCloner = { models: {}, views: {}, collections: {}, routers: {}, settings: {} };
    910
    10         /**
    11          * The Clone Another WordCamp panel
    12          */
    13         api.panelConstructor.wcscPanel = api.Panel.extend( {
    14                 /**
    15                  * Initialize the panel after it's loaded
    16                  *
    17                  * Ideally, the Previewer would be set to the requested site ID during the initial PHP request, rather than
    18                  * loading the host site in the Previewer, and then refreshing it to use the requested site. That became a
    19                  * rabbit hole, though, so it's done this way instead.
    20                  */
    21                 ready : function() {
    22                         var urlParams = getUrlParams( window.location.href );
     11        wcsc.settings = settings || {};
    2312
    24                         if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) {
    25                                 this.expand();
    26                                 api( 'wcsc_source_site_id' ).set( urlParams.wcsc_source_site_id );
     13        // Model for a single site
     14        wcsc.models.Site = Backbone.Model.extend( {
     15                idAttribute: 'site_id'
     16        } );
     17
     18        wcsc.models.SearchFilter = Backbone.Model.extend( {
     19                's': '',
     20                'theme_slug': '',
     21                'year': '',
     22                'css_preprocessor': ''
     23        } );
     24
     25        // Top level view for the Site Cloner Control
     26        wcsc.views.SiteSearch = Backbone.View.extend( {
     27                el: '#wcsc-cloner .wcsc-search',
     28
     29                // index of the currently viewed page of results
     30                page: 0,
     31
     32                initialize: function ( options ) {
     33
     34                        // update scroller position
     35                        _.bindAll( this, 'scroller' );
     36
     37                        // container that will be scrolled within
     38                        this.$container = $( '#wcsc-cloner' ).parents( 'ul.accordion-section-content' );
     39                        // bind scrolling within the container to check for infinite scroll
     40                        this.$container.bind( 'scroll', _.throttle( this.scroller, 300 ) );
     41
     42                        // the model and view for filtering the site results
     43                        this.filter = options.filter;
     44                        this.filterView = new wcsc.views.SearchFilters( {
     45                                model: this.filter,
     46                                parent: this
     47                        } );
     48
     49                        // View for listing the matching sites
     50                        this.resultsView = new wcsc.views.SearchResults( {
     51                                collection: this.collection,
     52                                parent: this
     53                        } );
     54                },
     55
     56                render: function () {
     57                        this.filterView.render();
     58
     59                        this.resultsView.render();
     60
     61                        this.$el.empty().append( this.resultsView.el );
     62                },
     63
     64                // Checks if a user has reached the bottom of the list
     65                // and triggers a scroll event to show more sites if needed
     66                scroller: function () {
     67                        var visibleBottom, threshold, elementHeight, containerHeight, scrollTop;
     68
     69                        scrollTop = this.$container.scrollTop();
     70                        containerHeight = this.$container.innerHeight();
     71                        elementHeight = this.$container.get( 0 ).scrollHeight;
     72
     73                        visibleBottom = scrollTop + containerHeight;
     74                        threshold = Math.round( elementHeight * 0.9 );
     75
     76                        if ( visibleBottom > threshold ) {
     77                                this.trigger( 'wcsc:scroll' );
    2778                        }
    2879                }
     80
    2981        } );
    3082
    31         /**
    32          * Custom control representing a site that can be previewed/imported
    33          */
    34         api.controlConstructor.wcscSite = api.Control.extend( {
    35                 /**
    36                  * Initialize the control after it's loaded
    37                  */
    38                 ready : function() {
    39                         this.container.on( 'click', '.wcscSite', this.previewSite );
    40                 },
    41 
    42                 /**
    43                  * Preview the selected site
    44                  *
    45                  * If the site is using a different theme, then reload the entire Customizer with the theme URL parameter
    46                  * set, so that the Theme Switcher will handle previewing the new theme for us. Otherwise just set the ID
    47                  * to refresh the Previewer with the current theme and the new site's CSS, etc.
    48                  *
    49                  * @param {object} event
    50                  */
    51                 previewSite : function( event ) {
    52                         var previewUrl       = $( this ).data( 'preview-url' ),
    53                                 previewUrlParams = getUrlParams( previewUrl );
    54 
    55                         if ( api( 'wcsc_source_site_id' ).get() == previewUrlParams.wcsc_source_site_id ) {
     83        // Collection representing the list of cloneable sites
     84        wcsc.collections.Sites = Backbone.Collection.extend( {
     85                model: wcsc.models.Site,
     86                url: wcsc.settings.apiUrl,
     87
     88                initialize: function ( options ) {
     89                        this.searchFilter = options.searchFilter || {};
     90
     91                        this.listenTo( this.searchFilter, 'change', this.applyFilter );
     92                },
     93
     94                applyFilter: function () {
     95                        var filters       = this.searchFilter.toJSON(),
     96                                        activeFilters = _.pick( filters, _.identity ),
     97                                        term          = '',
     98                                        sites;
     99
     100                        // nothing actually changed, so don't update the collection
     101                        if ( _.isEmpty( this.searchFilter.changedAttributes() ) ) {
    56102                                return;
    57103                        }
    58104
    59                         if ( api.settings.theme.stylesheet === previewUrlParams.theme ) {
    60                                 api( 'wcsc_source_site_id' ).set( previewUrlParams.wcsc_source_site_id );
     105                        //no active filters.  Reset to the full list and bail
     106                        if ( _.isEmpty( activeFilters ) ) {
     107                                this.resetCanonical();
     108                                return;
     109                        }
     110
     111                        this.resetCanonical( { silent: true } );
     112
     113                        if ( activeFilters.s ) {
     114                                term = activeFilters.s;
     115                                delete activeFilters.s;
     116                        }
     117
     118                        sites = this.where( activeFilters );
     119
     120                        if ( term ) {
     121                                sites = this.filterBySearch( sites, term );
     122                        }
     123
     124                        this.reset( sites );
     125
     126                },
     127
     128                filterBySearch: function ( sites, term ) {
     129                        var match, name;
     130
     131                        // Escape the term string for RegExp meta characters
     132                        term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
     133
     134                        // Consider spaces as word delimiters and match the whole string
     135                        // so matching terms can be combined
     136                        term = term.replace( / /g, ')(?=.*' );
     137                        match = new RegExp( '^(?=.*' + term + ').+', 'i' );
     138
     139                        return _.filter( sites, function ( site ) {
     140                                name = site.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
     141
     142                                return match.test( name );
     143                        } );
     144                },
     145
     146                paginate: function ( pageIndex ) {
     147                        var collection = this;
     148                        pageIndex = pageIndex || 0;
     149
     150                        collection = _( collection.rest( 20 * pageIndex ) );
     151                        collection = _( collection.first( 20 ) );
     152                        return collection;
     153                },
     154
     155                // Resets the site collection dataset to the canonical list originally pulled from the api
     156                resetCanonical: function ( options ) {
     157                        options = options || {};
     158                        this.reset( wcsc.settings.siteData, options );
     159                }
     160        } );
     161
     162        // View for a single site
     163        wcsc.views.Site = Backbone.View.extend( {
     164                className: 'wcsc-site',
     165
     166                attributes: function () {
     167                        return {
     168                                'id': 'wcsc-site-' + this.model.get( 'site_id' ),
     169                                'data-site-id': this.model.get( 'site_id' )
     170                        }
     171                },
     172
     173                html: wp.template( 'wcsc-site-option' ),
     174
     175                touchDrag: false,
     176
     177                events: {
     178                        'click': 'preview',
     179                        'keydown': 'preview',
     180                        'touchend': 'preview',
     181                        'touchmove': 'preventPreview'
     182                },
     183
     184                initialize: function ( options ) {
     185                        this.parent = options.parent;
     186
     187                        this.render();
     188                },
     189
     190                render: function () {
     191                        var data = this.model.toJSON();
     192
     193                        this.$el.html( this.html( data ) );
     194                },
     195
     196                preventPreview: function () {
     197                        this.touchDrag = true;
     198                },
     199
     200                preview: function ( event ) {
     201                        var current, preview;
     202
     203                        event = event || window.event;
     204
     205                        //ignore touches caused by scrolling
     206                        if ( this.touchDrag === true ) {
     207                                this.touchDrag = false;
     208                        }
     209
     210                        event.preventDefault();
     211
     212                        this.$el.trigger( 'wcsc:previewSite', this.model );
     213                }
     214
     215        } );
     216
     217        wcsc.views.SearchResults = Backbone.View.extend( {
     218                className: 'wcsc-results',
     219
     220                liveSiteCount: 0,
     221
     222                initialize: function ( options ) {
     223                        var self = this;
     224
     225                        this.parent = options.parent;
     226
     227                        this.$siteCount = $( '#wcsc-sites-count' );
     228
     229                        // Rerender the view whenever a collection change is complete
     230                        this.listenTo( this.collection, 'reset', function () {
     231                                //reset pagination
     232                                self.parent.page = 0;
     233                                self.render( this );
     234                        } );
     235
     236                        this.listenTo( this.parent, 'wcsc:scroll', function () {
     237                                self.renderSites( self.parent.page );
     238                        } );
     239                },
     240
     241                render: function () {
     242                        this.$el.empty();
     243
     244                        this.renderSites( this.parent.page );
     245
     246                        this.$siteCount.text( this.collection.length );
     247                },
     248
     249                renderSites: function ( page ) {
     250                        var self = this;
     251
     252                        // get a collection of just the requested page
     253                        this.instance = this.collection.paginate( page );
     254
     255                        if ( this.instance.size() === 0 ) {
     256                                this.parent.trigger( 'wcsc:end' );
     257                                return;
     258                        }
     259
     260                        this.instance.each( function ( site ) {
     261                                var siteView = new wcsc.views.Site( {
     262                                        model: site,
     263                                        parent: self
     264                                } );
     265
     266                                siteView.render();
     267
     268                                self.$el.append( siteView.el );
     269                        } );
     270
     271                        this.parent.page++;
     272                }
     273
     274        } );
     275
     276        wcsc.views.SearchFilters = Backbone.View.extend( {
     277                el: '#wcsc-cloner .filters',
     278                className: 'wscs-filters',
     279                html: wp.template( 'wcsc-site-filters' ),
     280                events: {
     281                        "input #wcsc-filter-search-input": "search",
     282                        "keyup #wcsc-filter-search-input": "search",
     283                        "change select": "applyFilter"
     284                },
     285                initialize: function ( options ) {
     286                        this.parent = options.parent;
     287
     288                        this.$searchInput = $( '#wcsc-filter-search-input' );
     289                        //this.listenTo( this.$searchInput, 'input keyup', this.search );
     290
     291                        this.$themeFilter = $( '#wcsc-filter-theme' );
     292                        //this.listenTo( this.$themeFilter, 'change', this.applyFilter );
     293
     294                        this.$yearFilter = $( '#wcsc-filter-year' );
     295                        //this.listenTo( this.$yearFilter, 'change', this.applyFilter );
     296
     297                        this.$preprocessorFilter = $( '#wcsc-filter-preprocessor' );
     298                        //this.listenTo( this.$preprocessorFilter, 'change', this.applyFilter );
     299
     300                },
     301
     302                render: function () {
     303                        var data = {};
     304
     305                        data.themeOptions = _.uniq( this.parent.collection.pluck( 'theme_slug' ) ).sort();
     306                        data.yearOptions = _.uniq( this.parent.collection.pluck( 'year' ) ).sort();
     307                        data.preprocessorOptions = _.uniq( this.parent.collection.pluck( 'css_preprocessor' ) ).sort();
     308
     309                        this.$el.html( this.html( data ) );
     310                },
     311
     312                search: function ( event ) {
     313                        // Clear on escape.
     314                        if ( event.type === 'keyup' && event.which === 27 ) {
     315                                event.target.value = '';
     316                        }
     317
     318                        /**
     319                         * Since doSearch is debounced, it will only run when user input comes to a rest
     320                         */
     321                        this.doSearch( event );
     322                },
     323
     324                doSearch: _.debounce( function ( event ) {
     325                        var options = {};
     326
     327                        this.model.set( 's', event.target.value );
     328
     329                }, 500 ),
     330
     331                applyFilter: function ( event ) {
     332                        var $target = $( event.target ),
     333                                        value   = $target.val(),
     334                                        filter  = $target.data( 'filter' );
     335                        this.model.set( filter, value );
     336                }
     337
     338        } );
     339
     340        // Sets up listener to store the user's selected filters and search so a user's position can be
     341        // restored as well as possible after a theme changes causes a full refresh
     342        wcsc.routers.FilterState = Backbone.Router.extend( {
     343                routes: {
     344                        'wcsc?*filters': 'applyFilters'
     345                },
     346                intialize: function ( options ) {
     347                        this.parent = options.parent;
     348                },
     349                applyFilters: function ( queryString ) {
     350                        var filters = deserializeQueryString( queryString );
     351                        if ( filters.s ) {
     352                                this.parent.filterView.$searchInput.val( filters.s );
     353                        }
     354
     355                        this.parent.view.trigger( 'change:filters' )
     356                }
     357        } );
     358
     359        api.controlConstructor.wcscSearch = api.Control.extend( {
     360                ready: function () {
     361                        var control        = this,
     362                                        urlParams      = getUrlParams( win.location.href ),
     363                                        filter         = new wcsc.models.SearchFilter(),
     364                                        siteCollection = new wcsc.collections.Sites( { searchFilter: filter } );
     365
     366                        //setup the backbone view for the control
     367                        //@todo would be nice to delay the fetch until this section is loaded
     368                        siteCollection.fetch( {
     369                                success: function ( collection ) {
     370                                        wcsc.settings.siteData = collection.toJSON();
     371
     372                                        control.view = new wcsc.views.SiteSearch( {
     373                                                parent: this,
     374                                                collection: collection,
     375                                                filter: filter
     376                                        } );
     377
     378                                        control.renderSearch();
     379                                        wcsc.router = new wcsc.routers.FilterState( { parent: control } );
     380                                        Backbone.history.start();
     381                                }
     382                        } );
     383
     384
     385                        // if the wcsc_source_site_id is set, its most likely from a user previewing a site, so bring them back
     386                        if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) {
     387                                api.section( this.section() ).expand();
     388                                api( 'wcsc_source_site_id' ).set( urlParams.wcsc_source_site_id );
     389                        }
     390
     391                        $( '#wcsc-cloner' ).on( 'wcsc:previewSite', '.wcsc-site', function ( event, site ) {
     392                                control.previewSite( site );
     393                        } );
     394                },
     395
     396                previewSite: function ( site ) {
     397
     398                        if ( api( 'wcsc_source_site_id' ).get() == site.get( 'site_id' ) ) {
     399                                //we're already previewing this site
     400                                return;
     401                        }
     402
     403                        if ( api.settings.theme.stylesheet === site.get( 'theme_slug' ) ) {
     404                                api( 'wcsc_source_site_id' ).set( site.get( 'site_id' ) );
    61405                        } else {
    62                                 window.parent.location = previewUrl;
     406                                win.parent.location = wcsc.settings.customizerUrl + '?' + $.param( {
     407                                                'theme': site.get( 'theme_slug' ),
     408                                                'wcsc_source_site_id': site.get( 'site_id' )
     409                                        } );
    63410                        }
     411                },
     412
     413                renderSearch: function () {
     414                        this.view.render();
    64415                }
     416
    65417        } );
    66418
     419
    67420        /**
    68421         * Parse the URL parameters
    69422         *
     
    74427         * @returns {object}
    75428         */
    76429        function getUrlParams( url ) {
    77                 var match, questionMarkIndex, query,
    78                         urlParams = {},
    79                         pl        = /\+/g,  // Regex for replacing addition symbol with a space
    80                         search    = /([^&=]+)=?([^&]*)/g,
    81                         decode    = function ( s ) {
    82                                 return decodeURIComponent( s.replace( pl, " " ) );
    83                         };
     430                var questionMarkIndex, query,
     431                                pl     = /\+/g,  // Regex for replacing addition symbol with a space
     432                                search = /([^&=]+)=?([^&]*)/g,
     433                                decode = function ( s ) {
     434                                        return decodeURIComponent( s.replace( pl, " " ) );
     435                                };
    84436
    85437                questionMarkIndex = url.indexOf( '?' );
    86438
    87439                if ( -1 === questionMarkIndex ) {
    88                         return urlParams;
     440                        return {};
    89441                } else {
    90442                        query = url.substring( questionMarkIndex + 1 );
    91443                }
    92444
    93                 while ( match = search.exec( query ) ) {
     445                return deserializeQueryString( query );
     446        }
     447
     448        /**
     449         * Deserialize a query string into an object
     450         *
     451         * @param queryString
     452         * @returns {{}}
     453         */
     454        function deserializeQueryString( queryString ) {
     455                var match,
     456                                urlParams = {},
     457                                pl        = /\+/g,  // Regex for replacing addition symbol with a space
     458                                search    = /([^&=]+)=?([^&]*)/g,
     459                                decode    = function ( s ) {
     460                                        return decodeURIComponent( s.replace( pl, " " ) );
     461                                };
     462
     463                while ( match = search.exec( queryString ) ) {
    94464                        urlParams[ decode( match[ 1 ] ) ] = decode( match[ 2 ] );
    95465                }
    96466
    97467                return urlParams;
    98468        }
    99 } )( window.wp, jQuery );
     469})( wp, jQuery, Backbone, window, _wcscSettings );
     470 No newline at end of file
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php
    index c398534..51e46c5 100755
    a b defined( 'WPINC' ) or die(); 
    77/*
    88Plugin Name: WordCamp Site Cloner
    99Description: Allows organizers to clone another WordCamp's theme and custom CSS as a starting point for their site.
    10 Version:     0.1
     10Version:     0.2.0-alpha
    1111Author:      WordCamp.org
    1212Author URI:  http://wordcamp.org
    1313License:     GPLv2 or later
    License: GPLv2 or later 
    1515
    1616// todo if Jetpack_Custom_CSS:get_css is callable, register these, otherwise fatal errors
    1717
    18 add_action( 'plugins_loaded',        __NAMESPACE__ . '\get_wordcamp_sites' );
    1918add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts' );
    2019add_action( 'admin_menu',            __NAMESPACE__ . '\add_submenu_page' );
    2120add_action( 'customize_register',    __NAMESPACE__ . '\register_customizer_components' );
     21add_action( 'rest_api_init',         __NAMESPACE__ . '\initialize_site_list_API' );
     22
     23// Setup a daily cron to run on the main central.wordcamp.org site to keep the site list up to date.
     24if ( is_main_site() ) {
     25        $hook = 'wcsc_prime_sites';
     26        if ( ! wp_next_scheduled( $hook ) ) {
     27                wp_schedule_event( time(), 'daily', $hook );
     28        }
     29
     30        add_action( $hook, __NAMESPACE__ . '\prime_wordcamp_sites' );
     31}
    2232
    2333/**
    2434 * Register scripts and styles
    function register_scripts() { 
    2838                'wordcamp-site-cloner',
    2939                plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.css',
    3040                array(),
    31                 1
     41                '0.2.0-alpha'
    3242        );
    3343
    3444        wp_register_script(
    3545                'wordcamp-site-cloner',
    3646                plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.js',
    37                 array( 'jquery', 'customize-controls' ),
    38                 1,
     47                array( 'jquery', 'customize-controls', 'wp-backbone' ),
     48                '0.2.0-alpha',
    3949                true
    4050        );
     51
     52        wp_localize_script( 'wordcamp-site-cloner', '_wcscSettings', array(
     53                'apiUrl'        => get_rest_url( null, '/wordcamp-site-cloner/v1/sites/' ),
     54                'customizerUrl' => admin_url( 'customize.php' ),
     55                'l10n'          => array(
     56                        'search' => __( 'Search', 'wordcamporg' )
     57                )
     58        ) );
     59
    4160}
    4261
    4362/**
    function add_submenu_page() { 
    5271                __( 'Clone Another WordCamp', 'wordcamporg' ),
    5372                __( 'Clone Another WordCamp', 'wordcamporg' ),
    5473                'switch_themes',
    55                 'customize.php?autofocus[panel]=wordcamp_site_cloner'
     74                'customize.php?autofocus[section]=wcsc_sites'
    5675        );
    5776}
    5877
    function add_submenu_page() { 
    6382 */
    6483function register_customizer_components( $wp_customize ) {
    6584        require_once( __DIR__ . '/includes/source-site-id-setting.php' );
    66         require_once( __DIR__ . '/includes/sites-section.php' );
    6785        require_once( __DIR__ . '/includes/site-control.php' );
    6886
    69         $wp_customize->register_control_type( __NAMESPACE__ . '\Site_Control' );
    70 
    7187        $wp_customize->add_setting( new Source_Site_ID_Setting(
    7288                $wp_customize,
    7389                'wcsc_source_site_id',
    74                 array()
     90                array(
     91                        'capability' => 'switch_themes'
     92                )
    7593        ) );
    7694
    77         $wp_customize->add_panel(
    78                 'wordcamp_site_cloner',
    79                 array(
    80                         'type'        => 'wcscPanel',
    81                         'title'       => __( 'Clone Another WordCamp', 'wordcamporg' ),
    82                         'description' => __( "Clone another WordCamp's theme and custom CSS as a starting point for your site.", 'wordcamporg' ),
     95        $wp_customize->add_section( 'wcsc_sites', array(
     96                        'title'      => __( 'Clone Another WordCamp', 'wordcamporg' ),
     97                        'capability' => 'switch_themes'
    8398                )
    8499        );
    85100
    86         $wp_customize->add_section( new Sites_Section(
     101        $wp_customize->add_control( new Site_Control(
    87102                $wp_customize,
    88                 'wcsc_sites',
     103                'wcsc_site_search',
    89104                array(
    90                         'panel' => 'wordcamp_site_cloner',
    91                         'title' => __( 'WordCamp Sites', 'wordcamporg' ),
     105                        'type'     => 'wcscSearch',
     106                        'label'    => __( 'Search', 'wordcamporg' ),
     107                        'settings' => 'wcsc_source_site_id',
     108                        'section'  => 'wcsc_sites'
    92109                )
    93110        ) );
     111}
    94112
    95         foreach( get_wordcamp_sites() as $wordcamp ) {
    96                 if ( get_current_blog_id() == $wordcamp['site_id'] ) {
    97                         continue;
    98                 }
    99 
    100                 $wp_customize->add_control( new Site_Control(
    101                         $wp_customize,
    102                         'wcsc_site_id_' . $wordcamp['site_id'],
    103                         array(
    104                                 'type'           => 'wcscSite',                      // todo should be able to set this in control instead of here, but if do that then control contents aren't rendered
    105                                 'site_id'        => $wordcamp['site_id'],
    106                                 'site_name'      => $wordcamp['name'],
    107                                 'theme_slug'     => $wordcamp['theme_slug'],
    108                                 'screenshot_url' => $wordcamp['screenshot_url'],
    109                         )
    110                 ) );
    111         }
     113function initialize_site_list_API() {
     114        register_rest_route( 'wordcamp-site-cloner/v1', '/sites', array(
     115                'methods'  => 'GET',
     116                'callback' => __NAMESPACE__ . '\get_wordcamp_sites'
     117        ) );
    112118}
    113119
    114120/**
    function register_customizer_components( $wp_customize ) { 
    122128 * @return array
    123129 */
    124130function get_wordcamp_sites() {
    125         require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' );
     131        $transient_key = 'wcsc_sites';
     132
     133        if ( false == ( $sites = get_site_transient( $transient_key ) ) ) {
     134                $sites = _get_wordcamp_sites();
    126135
    127         // plugins_loaded is runs on every screen, but we only need this when loading the Customizer and Previewer
    128         if ( 'customize.php' != basename( $_SERVER['SCRIPT_NAME'] ) && empty( $_REQUEST['wp_customize'] ) ) {
    129                 return array();
     136                //set the transient longer than needed to be sure it doesn't expire before the cron runs again.
     137                set_site_transient( $transient_key, $sites, DAY_IN_SECONDS * 2 );
    130138        }
    131139
    132         $transient_key = 'wcsc_sites';
     140        // remove the current site from the result set
     141        unset( $sites[ get_current_blog_id() ] );
    133142
    134         if ( $sites = get_site_transient( $transient_key ) ) {
    135                 return $sites;
    136         }
     143        return array_values( $sites );
     144}
    137145
    138         switch_to_blog( BLOG_ID_CURRENT_SITE ); // central.wordcamp.org
     146/**
     147 * Queries and filters the available sites to clone by a single page.  If more than one page of WordCamp posts exists,
     148 * a cron will be scheduled in 10 minutes to update the next page's worth of sites in the cache. *
     149 *
     150 * @internal
     151 *
     152 * @param array $args {
     153 *     An array of elements that make up a post to update or insert.
     154 *
     155 *     @type int    $page                  The page of results to select. Default 1.
     156 *     @type int    $posts_per_page        The number of results to return in the initial query. Default 500
     157 *     @type bool   $schedule_cron         Whether to schedule a cron job to prime the next page of results.  Default true
     158 * }
     159 *
     160 * @return array WordCamp Sites array.
     161 */
     162function _get_wordcamp_sites($args = array()) {
    139163
     164        $options = wp_parse_args( $args, array(
     165                'page'           => 1,
     166                'posts_per_page' => 500,
     167                'schedule_cron'  => true
     168        ) );
     169
     170        $page = absint($options['page']);
     171
     172        require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' );
     173        if ( ! \Jetpack::is_module_active( 'custom-css' ) ) {
     174                \Jetpack::activate_module( 'custom-css', false, false );
     175        }
     176        switch_to_blog( get_current_site()->blog_id );
    140177        $sites = array();
    141         $wordcamps = get_posts( array(
     178        $wordcamp_query = new \WP_Query( array(
    142179                'post_type'      => 'wordcamp',
    143180                'post_status'    => \WordCamp_Loader::get_public_post_statuses(),
    144                 'posts_per_page' => 125, // todo temporary workaround until able to add filters to make hundreds of sites manageable
     181                'posts_per_page' => $options[ 'posts_per_page' ],
    145182                'meta_key'       => 'Start Date (YYYY-mm-dd)',
    146183                'orderby'        => 'meta_value_num',
    147 
    148                 'meta_query' => array(
     184                'order'          => 'DESC',
     185                'paged'          => $page,
     186                'meta_query'     => array(
    149187                        array(
    150188                                'key'     => 'Start Date (YYYY-mm-dd)',
    151189                                'value'   => strtotime( 'now - 1 month' ),
    152190                                'compare' => '<'
    153191                        ),
     192                        array(
     193                                'key'     => '_site_id',
     194                                'compare' => 'EXISTS'
     195                        )
    154196                ),
    155197        ) );
    156198
    157         foreach( $wordcamps as $wordcamp ) {
    158                 $site_id  = get_wordcamp_site_id( $wordcamp );
     199        //If there are more pages to process, reschedule the cron to run again with the next page
     200        if ( $options[ 'schedule_cron' ] && $wordcamp_query->max_num_pages > $page && ! wp_next_scheduled( 'wcsc_prime_sites', [ $page + 1 ] ) ) {
     201                wp_schedule_single_event( time() + 10 * MINUTE_IN_SECONDS, 'wcsc_prime_sites', [ $page + 1 ] );
     202        }
     203
     204        $wordcamps = $wordcamp_query->get_posts();
     205
     206        $coming_soon_settings_handler = false;
     207        if(class_exists('\WCCSP_Settings')) {
     208                $coming_soon_settings_handler = isset( $GLOBALS[ 'WCCSP_Settings' ] ) ? $GLOBALS[ 'WCCSP_Settings' ] : new \WCCSP_Settings();
     209        }
     210
     211        foreach ( $wordcamps as $wordcamp ) {
     212                $site_id = get_wordcamp_site_id( $wordcamp );
    159213                $site_url = get_post_meta( $wordcamp->ID, 'URL', true );
     214                $start_date = get_post_meta( $wordcamp->ID, 'Start Date (YYYY-mm-dd)', true );
    160215
    161                 if ( ! $site_id || ! $site_url ) {
     216                if ( ! $site_id || ! $site_url || ! $start_date ) {
    162217                        continue;
    163218                }
    164219
    165220                switch_to_blog( $site_id );
    166221
    167                 $sites[] = array(
    168                         'site_id'        => $site_id,
    169                         'name'           => get_wordcamp_name(),
    170                         'theme_slug'     => get_stylesheet(),
    171                         'screenshot_url' => get_screenshot_url( $site_url ),
     222                //skip any sites with the coming soon enabled.
     223                if ( $coming_soon_settings_handler ) {
     224                        $coming_soon_settings = $coming_soon_settings_handler->get_settings();
     225                        if ( isset( $coming_soon_settings[ 'enabled' ] ) && $coming_soon_settings[ 'enabled' ] === 'on' ) {
     226                                restore_current_blog();
     227                                continue;
     228                        }
     229                }
     230
     231
     232                $preprocessor = \Jetpack_Custom_CSS::get_preprocessor();
     233
     234                $sites[ $site_id ] = array(
     235                        'site_id'          => $site_id,
     236                        'name'             => get_wordcamp_name(),
     237                        'theme_slug'       => get_stylesheet(),
     238                        'screenshot_url'   => get_screenshot_url( $site_url ),
     239                        'year'             => date( 'Y', $start_date ),
     240                        'css_preprocessor' => is_array( $preprocessor ) && isset( $preprocessor[ 'name' ] ) ? $preprocessor[ 'name' ] : 'none'
    172241                );
    173242
    174243                restore_current_blog();
    function get_wordcamp_sites() { 
    176245
    177246        restore_current_blog();
    178247
    179         set_site_transient( $transient_key, $sites, DAY_IN_SECONDS );
    180 
    181248        return $sites;
    182249}
    183250
    184251/**
     252 * Daily cron job callback to keep the site list transient valid.  This is only run on the main site.
     253 *
     254 * Since it can be expensive to loop through 100's of sites with switch_to_blog, we're breaking the cron up
     255 * into smaller gorups and updating a set at a time.  This means that users getting the initial load may not
     256 * see EVERY site,but since they are ordered by date descending, most applicable sites should be available
     257 * immediately if the transient gets flushed.
     258 *
     259 * The current logic assumes that no sites that have previously made the list of sites will end up getting
     260 * removed.
     261 *
     262 */
     263function prime_wordcamp_sites($page = 1) {
     264        $transient_key = 'wcsc_sites';
     265
     266        $sites = _get_wordcamp_sites( compact( 'page' ) );
     267
     268        $stale_sites = get_site_transient( $transient_key );
     269
     270        if ( ! empty( $stale_sites ) ) {
     271                //merge the fresh sites with the previously stored sites, overriding old copies with the new
     272                $sites = array_merge( $stale_sites, $sites );
     273
     274                //sort the sites in descending order by year.  More exact dates can be added if the rough order causes confusion.
     275                uasort( $sites, function( $site_a, $site_b ) {
     276                        if ( $site_a[ 'year' ] === $site_b[ 'year' ] ) {
     277                                return 0;
     278                        }
     279                        return ( $site_a[ 'year' ] < $site_b[ 'year' ] ? 1 : -1 );
     280                } );
     281        }
     282
     283        //set the transient longer than needed to be sure it doesn't expire before the cron runs again.
     284        set_site_transient( 'wcsc_sites', $sites, DAY_IN_SECONDS * 2 );
     285}
     286
     287/**
    185288 * Get the mShot URL for the given site URL
    186289 *
    187290 * Allow it to be filtered so that production URLs can be changed to match development URLs in local environments.
    function get_screenshot_url( $site_url ) { 
    194297        $screenshot_url = add_query_arg( 'w', 275, 'https://www.wordpress.com/mshots/v1/' . rawurlencode( $site_url ) );
    195298
    196299        return apply_filters( 'wcsc_site_screenshot_url', $screenshot_url );
    197 }
     300}
     301 No newline at end of file