Plugin Directory

Changeset 995609


Ignore:
Timestamp:
09/23/2014 06:34:23 PM (12 years ago)
Author:
stenberg.me
Message:

New AJAX importer. Pagination in post list.

Location:
content-staging/trunk
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • content-staging/trunk/assets/js/content-staging.js

    r990417 r995609  
    11jQuery( document ).ready(function($) {
    22
    3     var batchImporterId = $('#sme-batch-importer-id').html();
    4     var nbrOfPrintedMsg = 0;
    5 
    6     // Check if a batch importer ID has been found.
    7     if (batchImporterId) {
    8         var data = {
    9             'action': 'sme_batch_import_status',
    10             'importer_id': batchImporterId
    11         };
    12         getBatchImporterStatus();
    13     }
    14 
    15     function getBatchImporterStatus() {
    16         setTimeout(function () {
    17 
    18             /*
    19              * Since WordPress 2.8 ajaxurl is always defined in the admin header and
    20              * points to admin-ajax.php.
    21              */
     3    /**
     4     * Since WordPress 2.8 'ajaxurl' is always defined in the admin header
     5     * and points to admin-ajax.php.
     6     */
     7    var app = {
     8
     9        /**
     10         * Init method of this application. Contains a simple router.
     11         */
     12        init: function() {
     13
     14            // Check if global variable 'adminpage' is defined.
     15            if (typeof adminpage !== 'undefined') {
     16
     17                /*
     18                 * Make sure posts in batch cookie is emptied if this is not the edit
     19                 * batch page.
     20                 */
     21                if (adminpage !== 'admin_page_sme-edit-batch') {
     22                    document.cookie = 'wp-sme-bpl=';
     23                }
     24
     25                // Simple router.
     26                switch (adminpage) {
     27                    case 'admin_page_sme-edit-batch':
     28                        this.editBatch();
     29                        break;
     30                    case 'admin_page_sme-send-batch':
     31                        this.deployBatch();
     32                        break;
     33                }
     34            }
     35        },
     36
     37        /**
     38         * User is currently on the Edit Batch page.
     39         *
     40         * If this is a new batch we would only have data from cookie. If user
     41         * goes to next page of posts all posts selected on the previous page will
     42         * be stored in cookie and inserted into HTML form.
     43         *
     44         * If user visit any other (content staging) page, cookie should be
     45         * cleared.
     46         */
     47        editBatch: function() {
     48            var self       = this;
     49            var batchId    = $('#sme-batch-id').html();
     50            var posts      = $('.sme-select-post');
     51            var postIdsObj = $('input[name="post_ids"]');
     52            var postIds    = [];
     53            var selectAll  = $('[id^=cb-select-all-]');
     54            var cookie;
     55            var batch;
     56
     57            // Get value from cookie.
     58            cookie = document.cookie.replace(/(?:(?:^|.*;\s*)wp-sme-bpl\s*\=\s*([^;]*).*$)|^.*$/, '$1');
     59
     60            if (cookie === '') {
     61                /*
     62                 * Cookie is empty, use post IDs from HTML form as selected posts.
     63                 */
     64                postIds = postIdsObj.val().split(',');
     65            } else {
     66                /*
     67                 * Cookie has been populated. Use post IDs from cookie as selected posts.
     68                 */
     69
     70                // Split batch and posts.
     71                batch = cookie.split(':');
     72
     73                /*
     74                 * We are not editing the same batch as cookie is referring to, reset
     75                 * cookie.
     76                 */
     77                if (batch[0] !== batchId) {
     78                    document.cookie = 'wp-sme-bpl=';
     79                } else {
     80                    // Add posts to array.
     81                    postIds = batch[1].split(',');
     82                }
     83            }
     84
     85            // Convert all post IDs to integers.
     86            postIds = this.arrayValuesToIntegers(postIds);
     87
     88            // Add currently selected post IDs to HTML form.
     89            postIdsObj.val(postIds.join());
     90
     91            // Go through all posts and determine which should be selected.
     92            posts.each(function() {
     93                if (postIds.indexOf(parseInt($(this).val())) > -1) {
     94                    $(this).prop('checked', true);
     95                } else {
     96                    $(this).prop('checked', false);
     97                }
     98            });
     99
     100            // User has selected/unselected a post.
     101            posts.click(function() {
     102                var postObj = $(this);
     103
     104                // Add post ID to array of post IDs.
     105                self.selectPost(postIds, parseInt(postObj.val()), postObj.prop('checked'));
     106
     107                // Update selected posts.
     108                self.updateSelectedPosts(batchId, postIds, postIdsObj);
     109            });
     110
     111            // User has selected/unselected all posts.
     112            selectAll.click(function() {
     113                var isChecked = $(this).prop('checked');
     114
     115                posts.each(function() {
     116                    self.selectPost(postIds, parseInt($(this).val()), isChecked);
     117                });
     118
     119                // Update selected posts.
     120                self.updateSelectedPosts(batchId, postIds, postIdsObj);
     121            });
     122        },
     123
     124        /**
     125         * Select a post in Edit Batch view.
     126         *
     127         * @param {Array} postIds
     128         * @param {int} postId
     129         * @param {bool} checked
     130         */
     131        selectPost: function(postIds, postId, checked) {
     132            var i;
     133
     134            // Remove post ID from array of post IDs.
     135            for (i = 0; i < postIds.length; i++) {
     136                if (postIds[i] === postId) {
     137                    postIds.splice(i, 1);
     138                }
     139            }
     140
     141            // Add post ID to array of post IDs.
     142            if (checked) {
     143                postIds.push(parseInt(postId));
     144            }
     145        },
     146
     147        /**
     148         * Update cookie and HTML form with currently selected posts.
     149         *
     150         * @param {int} batchId
     151         * @param {Array} postIds
     152         * @param {Object} postIdsObj
     153         */
     154        updateSelectedPosts: function(batchId, postIds, postIdsObj) {
     155            var str = postIds.join();
     156
     157            // Add post IDs to HTML form.
     158            postIdsObj.val(str);
     159
     160            // Add post IDs to cookie.
     161            document.cookie = 'wp-sme-bpl=' + batchId + ':' + str;
     162        },
     163
     164        /**
     165         * User is currently on the Deploy Batch page.
     166         */
     167        deployBatch: function() {
     168
     169            var data = {
     170                action: 'sme_import_request',
     171                job_id: $('#sme-batch-import-job-id').html(),
     172                importer: $('#sme-batch-importer-type').html()
     173            };
     174
     175            var printed = $('.sme-cs-message').length;
     176
     177            // Check if a batch importer ID has been found.
     178            if (data.job_id && data.importer) {
     179                this.deployStatus(data, printed);
     180            }
     181        },
     182
     183        /**
     184         * Get batch import status.
     185         *
     186         * @param data
     187         * @param printed Number of messages that has been printed.
     188         */
     189        deployStatus: function(data, printed) {
     190
     191            var self = this;
     192
    22193            $.post(ajaxurl, data, function(response) {
    23 
    24                 console.log(response);
    25194
    26195                // Number of messages in this response.
     
    28197
    29198                // Only print messages we haven't printed before.
    30                 for (var i = nbrOfPrintedMsg; i < nbrOfMsg; i++) {
     199                for (var i = printed; i < nbrOfMsg; i++) {
    31200                    $('.wrap').append('<div class="sme-cs-message sme-cs-' + response.messages[i].level + '"><p>' + response.messages[i].message + '</p></div>');
    32                     nbrOfPrintedMsg++;
    33                 }
    34 
     201                    printed++;
     202                }
     203
     204                // If import is not completed, select import method.
    35205                if (response.status < 2) {
    36                     getBatchImporterStatus();
    37                 }
    38             });
    39         }, 3000);
    40     }
     206                    switch (data.importer) {
     207                        case 'ajax':
     208                            self.ajaxImport(data, printed);
     209                            break;
     210                        case 'background':
     211                            self.backgroundImport(data, printed);
     212                            break;
     213                    }
     214                }
     215            });
     216        },
     217
     218        ajaxImport: function(data, printed) {
     219            this.deployStatus(data, printed);
     220        },
     221
     222        backgroundImport: function(data, printed) {
     223            var self = this;
     224            setTimeout(function() {
     225                self.deployStatus(data, printed);
     226            }, 3000);
     227        },
     228
     229        /**
     230         * Convert array values to integers and sort out any values that are not
     231         * a number.
     232         *
     233         * @param {Array} array
     234         * @return {Array}
     235         */
     236        arrayValuesToIntegers: function(array) {
     237            var i;
     238            var int;
     239            var newArray = [];
     240
     241            for (i = 0; i < array.length; i++) {
     242                int = parseInt(array[i]);
     243                if ( ! isNaN(int)) {
     244                    newArray[i] = int;
     245                }
     246            }
     247
     248            return newArray;
     249        }
     250    };
     251
     252    // Initialize application.
     253    app.init();
    41254
    42255});
  • content-staging/trunk/classes/class-import-batch.php

    r990417 r995609  
    22namespace Me\Stenberg\Content\Staging;
    33
    4 use Me\Stenberg\Content\Staging\DB\Batch_Importer_DAO;
     4use Me\Stenberg\Content\Staging\DB\Batch_Import_Job_DAO;
    55use Me\Stenberg\Content\Staging\DB\Post_DAO;
    66use Me\Stenberg\Content\Staging\DB\Postmeta_DAO;
    77use Me\Stenberg\Content\Staging\DB\Term_DAO;
    88use Me\Stenberg\Content\Staging\DB\User_DAO;
    9 use Me\Stenberg\Content\Staging\Models\Batch_Importer;
     9use Me\Stenberg\Content\Staging\Models\Batch_Import_Job;
    1010use Me\Stenberg\Content\Staging\Models\Post;
    1111use Me\Stenberg\Content\Staging\Models\Relationships\Post_Taxonomy;
     
    1919 *
    2020 * @todo Consider moving 'import_*' methods in this class to the
    21  * Batch_Importer model. Might want an import per import type though,
     21 * Batch_Import_Job model. Might want an import per import type though,
    2222 * e.g. a Post_Importer etc.
    2323 */
    2424class Import_Batch {
    2525
    26     private $batch_importer_dao;
     26    private $import_job_dao;
    2727    private $post_dao;
    2828    private $postmeta_dao;
     
    8080     * Construct object, dependencies are injected.
    8181     *
    82      * @param Batch_Importer_DAO $batch_importer_dao
     82     * @param Batch_Import_Job_DAO $import_job_dao
    8383     * @param Post_DAO $post_dao
    8484     * @param Postmeta_DAO $postmeta_dao
     
    8686     * @param User_DAO $user_dao
    8787     */
    88     public function __construct( Batch_Importer_DAO $batch_importer_dao, Post_DAO $post_dao,
    89                                  Postmeta_DAO $postmeta_dao, Term_DAO $term_dao, User_DAO $user_dao ) {
    90         $this->batch_importer_dao    = $batch_importer_dao;
     88    public function __construct( Batch_Import_Job_DAO $import_job_dao, Post_DAO $post_dao, Postmeta_DAO $postmeta_dao,
     89                                 Term_DAO $term_dao, User_DAO $user_dao ) {
     90        $this->import_job_dao        = $import_job_dao;
    9191        $this->post_dao              = $post_dao;
    9292        $this->postmeta_dao          = $postmeta_dao;
     
    106106
    107107        // Make sure an importer ID has been provided.
    108         if ( ! isset( $_GET['sme_batch_importer_id'] ) || ! $_GET['sme_batch_importer_id'] ) {
     108        if ( ! isset( $_GET['sme_batch_import_job_id'] ) || ! $_GET['sme_batch_import_job_id'] ) {
    109109            return;
    110110        }
     
    115115        }
    116116
    117         $importer_id = intval( $_GET['sme_batch_importer_id'] );
     117        $importer_id = intval( $_GET['sme_batch_import_job_id'] );
    118118        $import_key  = $_GET['sme_import_batch_key'];
    119119
    120120        // Get batch importer from database.
    121         $importer = $this->batch_importer_dao->get_importer_by_id( $importer_id );
     121        $importer = $this->import_job_dao->get_job_by_id( $importer_id );
    122122
    123123        // No importer found, error.
     
    134134
    135135        // Import running.
    136         $importer->set_status( 1 );
    137136        $importer->generate_key();
    138         $this->batch_importer_dao->update_importer( $importer );
     137        $this->import_job_dao->update_job( $importer );
    139138
    140139        // Get the batch.
     
    157156        // Import postmeta.
    158157        foreach ( $batch->get_posts() as $post ) {
    159             $this->import_postmeta( $post->get_meta() );
     158            $this->import_postmeta( $post );
    160159        }
    161160
     
    173172        $importer->add_message( 'Batch has been successfully imported!', 'success' );
    174173        $importer->set_status( 3 );
    175         $this->batch_importer_dao->update_importer( $importer );
     174        $this->import_job_dao->update_job( $importer );
    176175
    177176        /*
     
    180179         * us the status of the import even when import has finished.
    181180         */
    182         $this->batch_importer_dao->delete_importer( $importer );
     181        $this->import_job_dao->delete_job( $importer );
    183182    }
    184183
     
    289288            // This post exists on production, update it.
    290289            $this->post_dao->update_post( $post );
    291             $this->postmeta_dao->delete_postmeta( array( 'post_id' => $post->get_id() ), array( '%d' ) );
    292290        }
    293291
     
    309307
    310308    /**
    311      * Import postmeta.
     309     * Import postmeta for a specific post.
    312310     *
    313311     * Never call before all posts has been imported! In case you do
     
    320318     * array and the production post ID is used as value.
    321319     *
    322      * @param array $postmeta
    323      */
    324     private function import_postmeta( array $postmeta ) {
    325 
    326         foreach ( $postmeta as $meta ) {
    327             if ( in_array( $meta['meta_key'], $this->postmeta_keys ) ) {
     320     * @param Post $post
     321     */
     322    private function import_postmeta( Post $post ) {
     323
     324        $meta = $post->get_meta();
     325
     326        for ( $i = 0; $i < count($meta); $i++ ) {
     327            if ( in_array( $meta[$i]['meta_key'], $this->postmeta_keys ) ) {
    328328
    329329                /*
    330330                 * The meta value must be an integer pointing at the ID of the post
    331                  * that the post whose postmeta we are currently importing has a
     331                 * that the post whose post meta we are currently importing has a
    332332                 * relationship to.
    333333                 */
    334                 if ( isset( $this->post_relations[$meta['meta_value']] ) ) {
    335                     $meta['meta_value'] = $this->post_relations[$meta['meta_value']];
     334                if ( isset( $this->post_relations[$meta[$i]['meta_value']] ) ) {
     335                    $meta[$i]['meta_value'] = $this->post_relations[$meta[$i]['meta_value']];
    336336                } else {
    337                     error_log( 'Trying to update dependency between posts. Relationship is defined in postmeta (post_id: ' . $this->post_relations[$meta['post_id']] . ', meta_key: ' . $meta['meta_key'] . ', meta_value: ' . $meta['meta_value'] . ') where post_id is the post ID that has a relationship to the post defined in meta_value. If meta_value does not contain a valid post ID relationship between posts cannot be maintained.' );
     337                    error_log( 'Trying to update dependency between posts. Relationship is defined in postmeta (post_id: ' . $this->post_relations[$meta[$i]['post_id']] . ', meta_key: ' . $meta[$i]['meta_key'] . ', meta_value: ' . $meta[$i]['meta_value'] . ') where post_id is the post ID that has a relationship to the post defined in meta_value. If meta_value does not contain a valid post ID relationship between posts cannot be maintained.' );
    338338                }
    339339            }
    340340
    341             $meta['post_id'] = $this->post_relations[$meta['post_id']];
    342             $this->postmeta_dao->insert_postmeta( $meta );
    343         }
     341            $meta[$i]['post_id'] = $this->post_relations[$meta[$i]['post_id']];
     342        }
     343
     344        $this->postmeta_dao->update_postmeta_by_post( $post->get_id(), $meta );
    344345    }
    345346
     
    347348     * Import attachments.
    348349     *
    349      * @param Batch_Importer $importer
    350      */
    351     private function import_attachments( Batch_Importer $importer ) {
     350     * @param Batch_Import_Job $importer
     351     */
     352    private function import_attachments( Batch_Import_Job $importer ) {
    352353
    353354        $attachments = $importer->get_batch()->get_attachments();
     
    465466     * Import data added by a third-party.
    466467     *
    467      * @param Batch_Importer $importer
    468      */
    469     private function import_custom_data( Batch_Importer $importer ) {
     468     * @param Batch_Import_Job $importer
     469     */
     470    private function import_custom_data( Batch_Import_Job $importer ) {
    470471        foreach ( $importer->get_batch()->get_custom_data() as $addon => $data ) {
    471472            do_action( 'sme_import_' . $addon, $data, $importer );
  • content-staging/trunk/classes/class-setup.php

    r990417 r995609  
    2727         * dependencies.
    2828         */
    29         wp_register_script( 'content-staging', $this->plugin_url . '/assets/js/content-staging.js', array( 'jquery' ), '1.0', false );
     29        wp_register_script( 'content-staging', $this->plugin_url . '/assets/js/content-staging.js', array( 'jquery' ), '1.1', false );
    3030
    3131        // Register CSS stylesheet files for later use with wp_enqueue_style().
     
    7070
    7171        // Arguments for batch importer post type
    72         $importer = array(
    73             'label'  => __( 'Batch Importers', 'sme-content-staging' ),
     72        $import_job = array(
     73            'label'  => __( 'Batch Import Jobs', 'sme-content-staging' ),
    7474            'labels' => array(
    75                 'singular_name'      => __( 'Batch Importers', 'sme-content-staging' ),
    76                 'add_new_item'       => __( 'Add New Batch Importer', 'sme-content-staging' ),
    77                 'edit_item'          => __( 'Edit Batch Importer', 'sme-content-staging' ),
    78                 'new_item'           => __( 'New Batch Importer', 'sme-content-staging' ),
    79                 'view_item'          => __( 'View Batch Importer', 'sme-content-staging' ),
    80                 'search_items'       => __( 'Search Batch Importers', 'sme-content-staging' ),
    81                 'not_found'          => __( 'No Batch Importers found', 'sme-content-staging' ),
    82                 'not_found_in_trash' => __( 'No Batch Importers found in Trash', 'sme-content-staging' )
     75                'singular_name'      => __( 'Batch Import Jobs', 'sme-content-staging' ),
     76                'add_new_item'       => __( 'Add New Batch Import Job', 'sme-content-staging' ),
     77                'edit_item'          => __( 'Edit Batch Import Job', 'sme-content-staging' ),
     78                'new_item'           => __( 'New Batch Import Job', 'sme-content-staging' ),
     79                'view_item'          => __( 'View Batch Import Job', 'sme-content-staging' ),
     80                'search_items'       => __( 'Search Batch Import Jobs', 'sme-content-staging' ),
     81                'not_found'          => __( 'No Batch Import Jobs found', 'sme-content-staging' ),
     82                'not_found_in_trash' => __( 'No Batch Import Jobs found in Trash', 'sme-content-staging' )
    8383            ),
    84             'description' => __( 'Batches are imported by Batch Importers.', 'sme-content-staging' ),
     84            'description' => __( 'Batches are packaged in Batch Import Jobs that in turn is imported by Batch Importers.', 'sme-content-staging' ),
    8585            'public'      => false,
    8686            'supports'    => array( 'editor' ),
     
    8888
    8989        register_post_type( 'sme_content_batch', $batch );
    90         register_post_type( 'sme_batch_importer', $importer );
     90        register_post_type( 'sme_batch_import_job', $import_job );
    9191
    9292
     
    9797        add_submenu_page( null, 'Edit Batch', 'Edit', 'manage_options', 'sme-edit-batch', array( $this->batch_ctrl, 'edit_batch' ) );
    9898        add_submenu_page( null, 'Delete Batch', 'Delete', 'manage_options', 'sme-delete-batch', array( $this->batch_ctrl, 'confirm_delete_batch' ) );
    99         add_submenu_page( null, 'Quick Deploy Batch', 'Quick Deploy', 'manage_options', 'sme-quick-deploy-batch', array( $this->batch_ctrl, 'quick_deploy_batch' ) );
    100         add_submenu_page( null, 'Pre-Flight Batch', 'Pre-Flight', 'manage_options', 'sme-preflight-batch', array( $this->batch_ctrl, 'preflight_batch' ) );
    101         add_submenu_page( null, 'Deploy Batch', 'Deploy', 'manage_options', 'sme-send-batch', array( $this->batch_ctrl, 'deploy_batch' ) );
     99        add_submenu_page( null, 'Pre-Flight Batch', 'Pre-Flight', 'manage_options', 'sme-preflight-batch', array( $this->batch_ctrl, 'prepare' ) );
     100        add_submenu_page( null, 'Quick Deploy Batch', 'Quick Deploy', 'manage_options', 'sme-quick-deploy-batch', array( $this->batch_ctrl, 'quick_deploy' ) );
     101        add_submenu_page( null, 'Deploy Batch', 'Deploy', 'manage_options', 'sme-send-batch', array( $this->batch_ctrl, 'deploy' ) );
    102102    }
    103103
     
    123123    public function register_xmlrpc_methods( $methods ) {
    124124
    125         $methods['smeContentStaging.preflight']    = array( $this->batch_ctrl, 'preflight' );
    126         $methods['smeContentStaging.deploy']       = array( $this->batch_ctrl, 'deploy' );
    127         $methods['smeContentStaging.deployStatus'] = array( $this->batch_ctrl, 'deploy_status' );
     125        $methods['smeContentStaging.verify'] = array( $this->batch_ctrl, 'verify' );
     126        $methods['smeContentStaging.import'] = array( $this->batch_ctrl, 'import' );
    128127
    129128        return $methods;
  • content-staging/trunk/classes/controllers/class-batch-ctrl.php

    r990417 r995609  
    44use Me\Stenberg\Content\Staging\Background_Process;
    55use Me\Stenberg\Content\Staging\DB\Batch_DAO;
    6 use Me\Stenberg\Content\Staging\DB\Batch_Importer_DAO;
     6use Me\Stenberg\Content\Staging\DB\Batch_Import_Job_DAO;
     7use Me\Stenberg\Content\Staging\Importers\Batch_Importer_Factory;
    78use Me\Stenberg\Content\Staging\Managers\Batch_Mgr;
    89use Me\Stenberg\Content\Staging\Models\Batch;
    9 use Me\Stenberg\Content\Staging\Models\Batch_Importer;
     10use Me\Stenberg\Content\Staging\Models\Batch_Import_Job;
    1011use Me\Stenberg\Content\Staging\View\Batch_Table;
    1112use Me\Stenberg\Content\Staging\DB\Post_DAO;
     
    1920    private $batch_mgr;
    2021    private $xmlrpc_client;
    21     private $batch_importer_dao;
     22    private $importer_factory;
     23    private $batch_import_job_dao;
    2224    private $batch_dao;
    2325    private $post_dao;
    2426
    2527    public function __construct( Template $template, Batch_Mgr $batch_mgr, Client $xmlrpc_client,
    26                                  Batch_Importer_DAO $batch_importer_dao, Batch_DAO $batch_dao, Post_DAO $post_dao ) {
    27         $this->template           = $template;
    28         $this->batch_mgr          = $batch_mgr;
    29         $this->xmlrpc_client      = $xmlrpc_client;
    30         $this->batch_importer_dao = $batch_importer_dao;
    31         $this->batch_dao          = $batch_dao;
    32         $this->post_dao           = $post_dao;
     28                                 Batch_Importer_Factory $importer_factory, Batch_Import_Job_DAO $batch_import_job_dao,
     29                                 Batch_DAO $batch_dao, Post_DAO $post_dao ) {
     30        $this->template             = $template;
     31        $this->batch_mgr            = $batch_mgr;
     32        $this->xmlrpc_client        = $xmlrpc_client;
     33        $this->importer_factory     = $importer_factory;
     34        $this->batch_import_job_dao = $batch_import_job_dao;
     35        $this->batch_dao            = $batch_dao;
     36        $this->post_dao             = $post_dao;
    3337    }
    3438
     
    138142        }
    139143
     144        $total_posts = $this->post_dao->get_published_posts_count();
     145
    140146        // Create and prepare table of posts.
    141147        $table        = new Post_Table( $batch, $post_ids );
    142148        $table->items = $posts;
     149        $table->set_pagination_args(
     150            array(
     151                'total_items' => $total_posts,
     152                'per_page'    => $per_page,
     153            )
     154        );
    143155        $table->prepare_items();
    144156
    145157        $data = array(
    146             'batch' => $batch,
    147             'table' => $table,
     158            'batch'    => $batch,
     159            'table'    => $table,
     160            'post_ids' => implode( ',', $post_ids ),
    148161        );
    149162
     
    172185
    173186    /**
    174      * Pre-flight batch.
     187     * Prepare batch for pre-flight.
    175188     *
    176189     * Send batch from content staging environment to production. Production
     
    187200     * @param Batch $batch
    188201     */
    189     public function preflight_batch( $batch = null ) {
     202    public function prepare( $batch = null ) {
    190203
    191204        // Make sure a query param ID exists in current URL.
     
    210223        );
    211224
    212         $this->xmlrpc_client->query( 'smeContentStaging.preflight', $request );
     225        $this->xmlrpc_client->query( 'smeContentStaging.verify', $request );
    213226        $response = $this->xmlrpc_client->get_response_data();
    214227
     
    240253     * @return string
    241254     */
    242     public function preflight( array $args ) {
     255    public function verify( array $args ) {
    243256
    244257        $this->xmlrpc_client->handle_request( $args );
     
    256269
    257270        // Create importer.
    258         $importer = new Batch_Importer();
     271        $importer = new Batch_Import_Job();
    259272        $importer->set_batch( $batch );
    260273
     
    307320     * Send post directly to production.
    308321     */
    309     public function quick_deploy_batch() {
     322    public function quick_deploy() {
    310323
    311324        // Make sure a query param 'post_id' exists in current URL.
     
    343356     * would more closely resemble how we handle e.g. editing a batch.
    344357     */
    345     public function deploy_batch() {
     358    public function deploy() {
    346359
    347360        // Check that the current request carries a valid nonce.
     
    362375        );
    363376
    364         $this->xmlrpc_client->query( 'smeContentStaging.deploy', $request );
     377        $this->xmlrpc_client->query( 'smeContentStaging.import', $request );
    365378        $response = $this->xmlrpc_client->get_response_data();
    366379
     
    373386
    374387        $data = array(
    375             'response' => $response,
     388            'messages' => $response['messages'],
    376389        );
    377390
     
    381394    /**
    382395     * Runs on production when a deploy request has been received.
    383      *
    384      * @todo Checking if a batch has been provided is duplicated from
    385      * pre-flight, fix!
    386      *
    387      * Store background process ID.
    388396     *
    389397     * @param array $args
    390398     * @return string
    391399     */
    392     public function deploy( array $args ) {
    393 
     400    public function import( array $args ) {
     401
     402        $job           = null;
     403        $importer_type = null;
    394404        $this->xmlrpc_client->handle_request( $args );
    395405        $result = $this->xmlrpc_client->get_request_data();
    396406
    397         // ----- Duplicated -----
     407        if ( isset( $result['job_id'] ) ) {
     408            $job = $this->batch_import_job_dao->get_job_by_id( intval( $result['job_id'] ) );
     409        }
     410
     411        if ( ! $job ) {
     412            $job = $this->create_import_job( $result );
     413        }
     414
     415        if ( $job->get_status() !== 2 ) {
     416
     417            if ( isset( $result['importer'] ) ) {
     418                $importer_type = $result['importer'];
     419            }
     420
     421            $importer = $this->importer_factory->get_importer( $job, $importer_type );
     422
     423            if ( $job->get_status() === 0 ) {
     424                $job->add_message(
     425                    sprintf(
     426                        'Starting batch import...<span id="sme-batch-importer-type" class="hidden">%s</span>',
     427                        $importer->get_type()
     428                    ),
     429                    'info'
     430                );
     431                $this->batch_import_job_dao->update_job( $job );
     432            }
     433
     434            $importer->run();
     435        }
     436
     437        $response = array(
     438            'status'   => $job->get_status(),
     439            'messages' => $job->get_messages(),
     440        );
     441
     442        // Prepare and return the XML-RPC response data.
     443        return $this->xmlrpc_client->prepare_response( $response );
     444    }
     445
     446    /**
     447     * Output the status of an import job together with any messages
     448     * generated during import.
     449     *
     450     * Triggered by an AJAX call.
     451     *
     452     * Runs on staging environment.
     453     */
     454    public  function import_request() {
     455
     456        $request = array(
     457            'job_id'   => intval( $_POST['job_id'] ),
     458            'importer' => $_POST['importer'],
     459        );
     460
     461        $this->xmlrpc_client->query( 'smeContentStaging.import', $request );
     462        $response = $this->xmlrpc_client->get_response_data();
     463
     464        header( 'Content-Type: application/json' );
     465        echo json_encode( $response );
     466
     467        die(); // Required to return a proper result.
     468    }
     469
     470    /**
     471     * Runs on production when an import status request has been received.
     472     *
     473     * @param array $result
     474     * @return Batch_Import_Job
     475     */
     476    private function create_import_job( $result ) {
     477
     478        $job = new Batch_Import_Job();
    398479
    399480        // Check if a batch has been provided.
    400481        if ( ! isset( $result['batch'] ) || ! ( $result['batch'] instanceof Batch ) ) {
    401             return $this->xmlrpc_client->prepare_response(
    402                 array( 'error' => array( 'Invalid batch!' ) )
    403             );
     482            $job->add_message( 'Failed creating import job.', 'error' );
     483            $job->set_status( 2 );
     484            return $job;
     485        }
     486
     487        $job->set_batch( $result['batch'] );
     488        $this->batch_import_job_dao->insert_job( $job );
     489        $job->add_message(
     490            sprintf(
     491                'Created import job ID: <span id="sme-batch-import-job-id">%s</span>',
     492                $job->get_id()
     493            ),
     494            'info'
     495        );
     496        return $job;
     497    }
     498
     499    /**
     500     * Add a post ID to batch.
     501     *
     502     * Triggered by an AJAX call.
     503     */
     504    public function include_post() {
     505
     506        if ( ! isset( $_POST['include'] ) || ! isset( $_POST['batch_id'] ) || ! isset( $_POST['post_id'] ) ) {
     507            die();
     508        }
     509
     510        $batch_id    = null;
     511        $post_id     = intval( $_POST['post_id'] );
     512        $is_selected = false;
     513
     514        if ( $_POST['batch_id'] ) {
     515            $batch_id = intval( $_POST['batch_id'] );
     516        }
     517
     518        if ( $_POST['include'] === 'true' ) {
     519            $is_selected = true;
    404520        }
    405521
    406522        // Get batch.
    407         $batch = $result['batch'];
    408 
    409         // ----- Duplicated -----
    410 
    411         $importer = new Batch_Importer();
    412         $importer->set_batch( $batch );
    413         $this->batch_importer_dao->insert_importer( $importer );
    414 
    415         // Default site path.
    416         $site_path = '/';
    417 
    418         // Site path in multi-site setup.
    419         if ( is_multisite() ) {
    420             $site      = get_blog_details();
    421             $site_path = $site->path;
    422         }
    423 
    424         // Trigger import script.
    425         $import_script = dirname( dirname( dirname( __FILE__ ) ) ) . '/scripts/import-batch.php';
    426         $background_process = new Background_Process(
    427             'php ' . $import_script . ' ' . ABSPATH . ' ' . get_site_url() . ' ' . $importer->get_id() . ' ' . $site_path . ' ' . $importer->get_key()
    428         );
    429 
    430         if ( file_exists( $import_script ) ) {
    431             $background_process->run();
    432         }
    433 
    434         // @todo store background process ID: $background_process->get_pid();
    435 
    436         $response = array(
    437             'info' => array(
    438                 'Import of batch has been started. Importer ID: <span id="sme-batch-importer-id">' . $importer->get_id() . '</span>',
    439             )
    440         );
    441 
    442         // Prepare and return the XML-RPC response data.
    443         return $this->xmlrpc_client->prepare_response( $response );
    444     }
    445 
    446     /**
    447      * Triggered by an AJAX call. Returns the status of the import together
    448      * with any messages generated during import.
    449      */
    450     public function get_import_status() {
    451 
    452         $importer_id = intval( $_POST['importer_id'] );
    453 
    454         $request = array(
    455             'importer_id' => $importer_id,
    456         );
    457 
    458         $this->xmlrpc_client->query( 'smeContentStaging.deployStatus', $request );
    459         $response = $this->xmlrpc_client->get_response_data();
     523        $batch = $this->batch_mgr->get_batch( $batch_id, true );
     524
     525        // Create new batch if needed.
     526        if ( ! $batch->get_id() ) {
     527            $this->batch_dao->insert_batch( $batch );
     528        }
     529
     530        // Get IDs of posts already included in the batch.
     531        $post_ids = $this->batch_dao->get_post_meta( $batch->get_id(), 'sme_selected_post_ids', true );
     532
     533        if ( ! $post_ids ) {
     534            $post_ids = array();
     535        }
     536
     537        if ( $is_selected ) {
     538            // Add post ID.
     539            $post_ids[] = $post_id;
     540        } else {
     541            // Remove post ID.
     542            if ( ( $key = array_search( $post_id, $post_ids ) ) !== false ) {
     543                unset( $post_ids[$key] );
     544            }
     545        }
     546
     547        $post_ids = array_unique( $post_ids );
     548
     549        // Update batch meta with IDs of posts user selected to include in batch.
     550        $this->batch_dao->update_post_meta( $batch->get_id(), 'sme_selected_post_ids', $post_ids );
    460551
    461552        header( 'Content-Type: application/json' );
    462         echo json_encode( $response );
     553        echo json_encode( array( 'batchId' => $batch->get_id() ) );
    463554
    464555        die(); // Required to return a proper result.
    465     }
    466 
    467     /**
    468      * Runs on production when a deploy status request has been received.
    469      *
    470      * @param array $args
    471      * @return string
    472      */
    473     public function deploy_status( array $args ) {
    474 
    475         $response = array(
    476             'status'   => 0,
    477             'messages' => array(),
    478         );
    479 
    480         $this->xmlrpc_client->handle_request( $args );
    481         $result = $this->xmlrpc_client->get_request_data();
    482 
    483         // Check if a batch has been provided.
    484         if ( ! isset( $result['importer_id'] ) ) {
    485             $response['messages']['error'] = array( 'No batch importer has been provided!' );
    486             return $this->xmlrpc_client->prepare_response( $response );
    487         }
    488 
    489         // Get batch importer ID.
    490         $importer_id = intval( $result['importer_id'] );
    491 
    492         // Get batch importer.
    493         $importer = $this->batch_importer_dao->get_importer_by_id( $importer_id );
    494 
    495         // Create response.
    496         $response['status']   = $importer->get_status();
    497         $response['messages'] = $importer->get_messages();
    498 
    499         // Prepare and return the XML-RPC response data.
    500         return $this->xmlrpc_client->prepare_response( $response );
    501556    }
    502557
     
    586641    private function handle_edit_batch_form_data( Batch $batch, $request_data ) {
    587642
    588         // IDs of posts user has selected to include in this batch.
    589         $post_ids = array();
    590 
    591643        // Check if a title has been set.
    592644        if ( isset( $request_data['batch_title'] ) ) {
    593645            $batch->set_title( $request_data['batch_title'] );
    594         }
    595 
    596         // Check if any posts to include in batch has been selected.
    597         if ( isset( $request_data['posts'] ) && is_array( $request_data['posts'] ) ) {
    598             $post_ids = $request_data['posts'];
    599646        }
    600647
     
    605652            // Update existing batch.
    606653            $this->batch_dao->update_batch( $batch );
     654        }
     655
     656        // IDs of posts user has selected to include in this batch.
     657        $post_ids = array();
     658
     659        // Check if any posts to include in batch has been selected.
     660        if ( isset( $request_data['post_ids'] ) && $request_data['post_ids'] ) {
     661            $post_ids = explode( ',', $request_data['post_ids'] );
    607662        }
    608663
  • content-staging/trunk/classes/db/class-postmeta-dao.php

    r990417 r995609  
    88    }
    99
     10    /**
     11     * Get all post meta records for a specific post.
     12     *
     13     * @param int $post_id
     14     * @return array
     15     */
    1016    public function get_postmetas_by_post_id( $post_id ) {
    1117        $query = $this->wpdb->prepare(
     
    3440    }
    3541
     42    /**
     43     * Update post meta.
     44     *
     45     * @param array $record
     46     */
     47    public function update_postmeta( $record ) {
     48        $this->wpdb->update(
     49            $this->wpdb->postmeta,
     50            array(
     51                'post_id'    => $record['post_id'],
     52                'meta_key'   => $record['meta_key'],
     53                'meta_value' => $record['meta_value'],
     54            ),
     55            array( 'meta_id' => $record['meta_id'] ),
     56            array( '%d', '%s', '%s' ),
     57            array( '%d' )
     58        );
     59    }
     60
    3661    public function delete_postmeta( $where, $where_format ) {
    3762        $this->wpdb->delete( $this->wpdb->postmeta, $where, $where_format );
     63    }
     64
     65    /**
     66     * Update all post meta for a post.
     67     *
     68     * @param int $post_id
     69     * @param array $stage_records
     70     */
     71    public function update_postmeta_by_post( $post_id, array $stage_records ) {
     72
     73        $insert_keys = array();
     74        $stage_keys  = array();
     75
     76        $insert = array();
     77        $update = array();
     78        $delete = array();
     79
     80        $prod_records = $this->get_postmetas_by_post_id( $post_id );
     81
     82        /*
     83         * Go through each meta record we got from stage. If a meta_key exists
     84         * more then once, then we will not try to update any records with that
     85         * meta key.
     86         */
     87        foreach ( $stage_records as $key => $prod_record ) {
     88            if ( in_array( $prod_record['meta_key'], $stage_keys ) ) {
     89                $insert[]      = $prod_record;
     90                $insert_keys[] = $prod_record['meta_key'];
     91                unset( $stage_records[$key] );
     92            } else {
     93                $stage_keys[] = $prod_record['meta_key'];
     94            }
     95        }
     96
     97        /*
     98         * Go through each meta record we got from production. If a meta_key
     99         * exist that is already part of the keys scheduled for insertion or if a
     100         * key that is found that is not part of the keys from stage, then
     101         * schedule that record for deletion.
     102         *
     103         * Records left in $stage_records is candidates for being updated. Go
     104         * through them and see if they already exist in $prod_records.
     105         */
     106        foreach ( $prod_records as $prod_key => $prod_record ) {
     107            if ( ! in_array( $prod_record['meta_key'], $stage_keys ) || in_array( $prod_record['meta_key'], $insert_keys ) ) {
     108                $delete[] = $prod_record;
     109                unset( $prod_records[$prod_key] );
     110            } else {
     111                foreach ( $stage_records as $stage_key => $stage_record ) {
     112                    if ( $stage_record['meta_key'] == $prod_record['meta_key'] ) {
     113                        $stage_record['meta_id'] = $prod_record['meta_id'];
     114                        $update[] = $stage_record;
     115                        unset( $stage_records[$stage_key] );
     116                        unset( $prod_records[$prod_key] );
     117                    }
     118                }
     119            }
     120        }
     121
     122        // Records left in $stage_records should be inserted.
     123        foreach ( $stage_records as $record ) {
     124            $insert[] = $record;
     125        }
     126
     127        // Records left in $prod_records should be deleted.
     128        foreach ( $prod_records as $record ) {
     129            $delete[] = $record;
     130        }
     131
     132        foreach( $delete as $record ) {
     133            $this->delete_postmeta(
     134                array( 'meta_id' => $record['meta_id'] ),
     135                array( '%d' )
     136            );
     137        }
     138
     139        foreach ( $insert as $record ) {
     140            $this->insert_postmeta( $record );
     141        }
     142
     143        foreach( $update as $record ) {
     144            $this->update_postmeta( $record );
     145        }
    38146    }
    39147
  • content-staging/trunk/classes/view/class-post-table.php

    r990417 r995609  
    7575     */
    7676    public function column_cb( Post $post ) {
    77 
    78         $checked = '';
    79 
    80         /*
    81          * Set property 'checked' that will either be an empty string if the post
    82          * is not part of this batch or to a string indicating that the post
    83          * should be checked in the HTML form.
    84          */
    85         if ( in_array( $post->get_id(), $this->post_ids ) ) {
    86             $checked = 'checked="checked" ';
    87         }
    88 
    8977        return sprintf(
    90             '<input type="checkbox" name="%s[]" value="%s" %s/>',
     78            '<input type="checkbox" class="sme-select-post" name="%s[]" value="%s"/>',
    9179            $this->_args['plural'],
    92             $post->get_id(),
    93             $checked
     80            $post->get_id()
    9481        );
    9582    }
  • content-staging/trunk/content-staging.php

    r990417 r995609  
    3333require_once( 'classes/controllers/class-batch-ctrl.php' );
    3434require_once( 'classes/db/mappers/class-mapper.php' );
    35 require_once( 'classes/db/mappers/class-batch-importer-mapper.php' );
     35require_once( 'classes/db/mappers/class-batch-import-job-mapper.php' );
    3636require_once( 'classes/db/mappers/class-batch-mapper.php' );
    3737require_once( 'classes/db/mappers/class-post-mapper.php' );
     
    4040require_once( 'classes/db/class-dao.php' );
    4141require_once( 'classes/db/class-batch-dao.php' );
    42 require_once( 'classes/db/class-batch-importer-dao.php' );
     42require_once( 'classes/db/class-batch-import-job-dao.php' );
    4343require_once( 'classes/db/class-post-dao.php' );
    4444require_once( 'classes/db/class-postmeta-dao.php' );
    4545require_once( 'classes/db/class-term-dao.php' );
    4646require_once( 'classes/db/class-user-dao.php' );
     47require_once( 'classes/importers/class-batch-importer.php' );
     48require_once( 'classes/importers/class-batch-ajax-importer.php' );
     49require_once( 'classes/importers/class-batch-background-importer.php' );
     50require_once( 'classes/importers/class-batch-importer-factory.php' );
    4751require_once( 'classes/managers/class-batch-mgr.php' );
    4852require_once( 'classes/models/class-batch.php' );
    49 require_once( 'classes/models/class-batch-importer.php' );
     53require_once( 'classes/models/class-batch-import-job.php' );
    5054require_once( 'classes/models/class-post.php' );
    5155require_once( 'classes/models/class-taxonomy.php' );
     
    6771 */
    6872use Me\Stenberg\Content\Staging\API;
    69 use Me\Stenberg\Content\Staging\DB\Batch_Importer_DAO;
    70 use Me\Stenberg\Content\Staging\DB\Mappers\Batch_Importer_Mapper;
     73use Me\Stenberg\Content\Staging\DB\Batch_Import_Job_DAO;
     74use Me\Stenberg\Content\Staging\DB\Mappers\Batch_Import_Job_Mapper;
    7175use Me\Stenberg\Content\Staging\Import_Batch;
    7276use Me\Stenberg\Content\Staging\Setup;
     
    8286use Me\Stenberg\Content\Staging\DB\Term_DAO;
    8387use Me\Stenberg\Content\Staging\DB\User_DAO;
     88use Me\Stenberg\Content\Staging\Importers\Batch_Importer_Factory;
    8489use Me\Stenberg\Content\Staging\Managers\Batch_Mgr;
    8590use Me\Stenberg\Content\Staging\XMLRPC\Client;
     
    118123
    119124        // Database mappers
    120         $batch_importer_mapper = new Batch_Importer_Mapper();
     125        $batch_importer_mapper = new Batch_Import_Job_Mapper();
    121126        $batch_mapper          = new Batch_Mapper();
    122127        $post_mapper           = new Post_Mapper();
     
    125130
    126131        // Data access objects.
    127         $batch_dao          = new Batch_DAO( $wpdb, $batch_mapper );
    128         $batch_importer_dao = new Batch_Importer_DAO( $wpdb, $batch_importer_mapper );
    129         $post_dao           = new Post_DAO( $wpdb, $post_mapper );
    130         $postmeta_dao       = new Postmeta_DAO( $wpdb );
    131         $term_dao           = new Term_DAO( $wpdb, $term_mapper );
    132         $user_dao           = new User_DAO( $wpdb, $user_mapper );
     132        $batch_dao    = new Batch_DAO( $wpdb, $batch_mapper );
     133        $job_dao      = new Batch_Import_Job_DAO( $wpdb, $batch_importer_mapper );
     134        $post_dao     = new Post_DAO( $wpdb, $post_mapper );
     135        $postmeta_dao = new Postmeta_DAO( $wpdb );
     136        $term_dao     = new Term_DAO( $wpdb, $term_mapper );
     137        $user_dao     = new User_DAO( $wpdb, $user_mapper );
    133138
    134139        // XML-RPC client.
     
    136141
    137142        // Managers.
    138         $batch_mgr = new Batch_Mgr( $batch_dao, $post_dao, $postmeta_dao, $term_dao, $user_dao );
     143        $batch_mgr        = new Batch_Mgr( $batch_dao, $post_dao, $postmeta_dao, $term_dao, $user_dao );
     144        $importer_factory = new Batch_Importer_Factory( $job_dao, $post_dao, $postmeta_dao, $term_dao, $user_dao );
    139145
    140146        // Template engine.
     
    143149        // Controllers.
    144150        $batch_ctrl = new Batch_Ctrl(
    145             $template, $batch_mgr, $xmlrpc_client, $batch_importer_dao, $batch_dao, $post_dao
     151            $template, $batch_mgr, $xmlrpc_client, $importer_factory, $job_dao, $batch_dao, $post_dao
    146152        );
    147153
    148154        // APIs.
    149155        $sme_content_staging_api = new API( $post_dao, $postmeta_dao );
    150 
    151         // Controller responsible for importing a batch to production.
    152         $import_batch = new Import_Batch( $batch_importer_dao, $post_dao, $postmeta_dao, $term_dao, $user_dao );
    153156
    154157        // Plugin setup.
     
    157160        // Actions.
    158161        add_action( 'init', array( $setup, 'register_post_types' ) );
    159         add_action( 'init', array( $import_batch, 'init' ) );
     162        add_action( 'init', array( $importer_factory, 'run_background_import' ) );
    160163        add_action( 'admin_menu', array( $setup, 'register_menu_pages' ) );
    161164        add_action( 'admin_notices', array( $setup, 'quick_deploy_batch' ) );
    162165        add_action( 'admin_enqueue_scripts', array( $setup, 'load_assets' ) );
    163166        add_action( 'admin_post_sme-save-batch', array( $batch_ctrl, 'save_batch' ) );
    164         add_action( 'admin_post_sme-quick-deploy-batch', array( $batch_ctrl, 'quick_deploy_batch' ) );
     167        add_action( 'admin_post_sme-quick-deploy-batch', array( $batch_ctrl, 'quick_deploy' ) );
    165168        add_action( 'admin_post_sme-delete-batch', array( $batch_ctrl, 'delete_batch' ) );
    166         add_action( 'wp_ajax_sme_batch_import_status', array( $batch_ctrl, 'get_import_status' ) );
     169        add_action( 'wp_ajax_sme_include_post', array( $batch_ctrl, 'include_post' ) );
     170        add_action( 'wp_ajax_sme_import_request', array( $batch_ctrl, 'import_request' ) );
    167171
    168172        // Filters.
  • content-staging/trunk/readme.txt

    r990417 r995609  
    63633. Pre-Flight your batch to make sure it is ready for deployment.
    64644. Deploy your batch from staging environment to your live site.
     65
     66== Changelog ==
     67
     68= 1.1 =
     69* New AJAX importer to use when Background importer is not an option.
     70* Pagination on Edit Batch page.
  • content-staging/trunk/scripts/import-batch.php

    r990417 r995609  
    5353// Set up GET parameters.
    5454$_GET = array(
    55     'sme_batch_importer_id' => $argv[3],
    56     'sme_import_batch_key'  => $argv[5],
     55    'sme_background_import'   => true,
     56    'sme_batch_import_job_id' => $argv[3],
     57    'sme_import_batch_key'    => $argv[5],
    5758);
    5859
  • content-staging/trunk/templates/deploy-batch.php

    r990417 r995609  
    22    <h2>Deploying Batch</h2>
    33
    4     <?php foreach ( $response as $level => $messages ) { ?>
    5         <div class="sme-cs-message sme-cs-<?php echo $level; ?>">
     4    <?php foreach ( $messages as $message ) { ?>
     5        <div class="sme-cs-message sme-cs-<?php echo $message['level']; ?>">
    66            <ul>
    7                 <?php foreach ( $messages as $message ) { ?>
    8                     <li><?php echo $message; ?></li>
    9                 <?php } ?>
     7                <li><?php echo $message['message']; ?></li>
    108            </ul>
    119        </div>
  • content-staging/trunk/templates/edit-batch.php

    r990417 r995609  
    11<div class="wrap">
     2
     3    <!-- Enable JavaScript to pick up the batch ID. -->
     4    <span id="sme-batch-id" class="hidden"><?php echo $batch->get_id();?></span>
    25
    36    <?php if ( isset( $_GET['updated'] ) ) { ?>
     
    1013
    1114        <?php wp_nonce_field( 'sme-save-batch','sme_save_batch_nonce' ); ?>
     15        <input type="hidden" name="post_ids" value="<?php echo $post_ids; ?>">
    1216
    1317        <input type="text" name="batch_title" size="30" value="<?php echo $batch->get_title(); ?>" class="sme-input-text" placeholder="Batch Title" autocomplete="off">
Note: See TracChangeset for help on using the changeset viewer.