Making WordPress.org

Changeset 1786


Ignore:
Timestamp:
07/26/2015 04:42:45 PM (9 years ago)
Author:
ocean90
Message:

Rosetta: Update roles plugin to support sub projects.

props folletto for the design idea.
see #1101.

Location:
sites/trunk/global.wordpress.org/public_html/wp-content/mu-plugins/roles
Files:
2 added
4 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/global.wordpress.org/public_html/wp-content/mu-plugins/roles/class-translation-editors-list-table.php

    r1437 r1786  
    3434     */
    3535    public $projects;
     36
     37    /**
     38     * Holds the list of a project tree.
     39     *
     40     * @var array
     41     */
     42    public $project_tree;
    3643
    3744    /**
     
    5259        $this->project_access_meta_key = $args['project_access_meta_key'];
    5360        $this->projects = $args['projects'];
     61        $this->project_tree = $args['project_tree'];
    5462        $this->user_can_promote = current_user_can( 'promote_users' );
    5563    }
     
    226234        foreach ( $project_access_list as $project_id ) {
    227235            if ( $this->projects[ $project_id ] ) {
    228                 $projects[] = esc_html( $this->projects[ $project_id ]->name );
     236                $parent = $this->get_parent_project( $this->project_tree, $project_id );
     237                if ( $parent->id != $project_id ) {
     238                    $name = sprintf(
     239                        '%s → %s',
     240                        esc_html( $parent->name ),
     241                        esc_html( $this->projects[ $project_id ]->name )
     242                    );
     243                } else {
     244                    $name = esc_html( $this->projects[ $project_id ]->name );
     245                }
     246                $projects[] = $name;
    229247            }
    230248        }
     
    232250        echo implode( '<br>', $projects );
    233251    }
     252
     253    /**
     254     * Returns the parent project for a sub project.
     255     *
     256     * @param array $tree The project tree.
     257     * @param int $child_id The project tree.
     258     * @return object The parent project.
     259     */
     260    private function get_parent_project( $tree, $child_id ) {
     261        $parent = null;
     262        foreach ( $tree as $project ) {
     263            if ( $project->id == $child_id ) {
     264                $parent = $project;
     265                break;
     266            }
     267
     268            if ( isset( $project->sub_projects ) ) {
     269                $parent = $this->get_parent_project( $project->sub_projects, $child_id );
     270                if ( $parent ) {
     271                    $parent = $project;
     272                    break;
     273                }
     274            }
     275        }
     276
     277        return $parent;
     278    }
    234279}
  • sites/trunk/global.wordpress.org/public_html/wp-content/mu-plugins/roles/js/rosetta-roles.js

    r1419 r1786  
    11( function( $ ) {
    22
    3     $(function() {
    4         var $projects = $( 'input.project' );
    5 
    6         if ( $projects.length ) {
    7             var $allProjects = $( '#project-all' ),
    8                 checked = [];
    9 
    10             // Deselect "All" if a project is checked.
    11             $projects.on( 'change', function() {
    12                 $allProjects.prop( 'checked', false );
    13                 checked = [];
    14             } );
    15 
    16             // (De)select projects if "All" is (de)selected.
    17             $allProjects.on( 'change', function() {
    18                 if ( this.checked ) {
    19                     $projects.each( function( index, checkbox ) {
    20                         var $cb = $( checkbox );
    21                         if ( $cb.prop( 'checked' ) ) {
    22                             checked.push( $cb.attr( 'id' ) );
    23                             $cb.prop( 'checked', false );
    24                         }
    25                     } );
    26                 } else {
    27                     for ( i = 0; i < checked.length; i++ ) {
    28                         $( '#' +  checked[ i ] ).prop( 'checked', true );
    29                     }
    30                     checked = [];
     3    var projects, l10n, $window = $(window);
     4
     5    projects = { model: {}, view: {}, controller: {} };
     6
     7    projects.settings = window._rosettaProjectsSettings || {};
     8    l10n = projects.settings.l10n || {};
     9    delete projects.settings.l10n;
     10
     11    /**
     12     * MODELS
     13     */
     14    projects.model.Project = Backbone.Model.extend({
     15        defaults: {
     16            id: 0,
     17            name: '',
     18            checked: false,
     19            isActive: false,
     20            checkedSubProjects: false
     21        },
     22
     23        initialize: function() {
     24            var subProjects = this.get( 'sub_projects' );
     25            this.unset( 'sub_projects' );
     26
     27            this.set( 'subProjects', new projects.model.subProjects( subProjects ) );
     28            this.set( 'checked', _.contains( projects.settings.accessList, parseInt( this.get( 'id' ), 10 ) ) );
     29
     30            this.listenTo( this.get( 'subProjects' ), 'change:checked', this.updateChecked );
     31
     32            this.checkForCheckedSubProjects();
     33        },
     34
     35        updateChecked: function( model ) {
     36            if ( model.get( 'checked' ) ) {
     37                this.set( 'checked', false );
     38            }
     39
     40            this.checkForCheckedSubProjects();
     41        },
     42
     43        checkForCheckedSubProjects: function() {
     44            var checked = this.get( 'subProjects' ).findWhere({ 'checked': true });
     45            if ( checked ) {
     46                this.set( 'checkedSubProjects', true );
     47            } else {
     48                this.set( 'checkedSubProjects', false );
     49            }
     50        }
     51    });
     52
     53    projects.model.Projects = Backbone.Collection.extend({
     54        model: projects.model.Project,
     55
     56        initialize: function() {
     57            _.bindAll( this, 'disableActiveStates' );
     58            this.on( 'change:isActive', this.toggleActiveStates );
     59            $window.on( 'deactivate-all-projects.rosetta', this.disableActiveStates );
     60        },
     61
     62        toggleActiveStates: function( model ) {
     63            if ( ! model.get( 'isActive' ) ) {
     64                return;
     65            }
     66            this.each( function( project ) {
     67                if ( project.get( 'id' ) != model.get( 'id' ) ) {
     68                    project.set( 'isActive', false );
    3169                }
    32             } );
    33 
    34             // Deselect all checkboxes.
    35             $( '#clear-all' ).on( 'click', function( event ) {
     70            });
     71
     72            $window.trigger( 'deactivate-other-projects.rosetta' );
     73        },
     74
     75        disableActiveStates: function() {
     76            this.each( function( project ) {
     77                project.set( 'isActive', false );
     78            });
     79        }
     80    });
     81
     82    projects.model.subProject = Backbone.Model.extend({
     83        defaults: {
     84            id: 0,
     85            name: '',
     86            checked: false,
     87            isVisible: true
     88        },
     89
     90        initialize: function() {
     91            this.set( 'checked', _.contains( projects.settings.accessList, parseInt( this.get( 'id' ), 10 ) ) );
     92        }
     93    });
     94
     95    projects.model.subProjects = Backbone.Collection.extend({
     96        model: projects.model.subProject,
     97
     98        // Search terms
     99        terms: '',
     100
     101        initialize: function() {
     102            this.on( 'uncheckall', this.uncheckall );
     103        },
     104
     105        uncheckall: function() {
     106            this.each( function( project ) {
     107                project.set( 'checked', false );
     108            });
     109        },
     110
     111        doSearch: function( value ) {
     112            // Don't do anything if we've already done this search
     113            if ( this.terms === value ) {
     114                return;
     115            }
     116
     117            this.terms = value;
     118
     119            if ( this.terms.length > 0 ) {
     120                this.search( this.terms );
     121            }
     122
     123            if ( this.terms === '' ) {
     124                this.each( function( project ) {
     125                    project.set( 'isVisible', true );
     126                });
     127            }
     128        },
     129
     130        // Performs a search within the collection
     131        // @uses RegExp
     132        search: function( term ) {
     133            var match, name;
     134
     135            // Escape the term string for RegExp meta characters
     136            term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
     137
     138            // Consider spaces as word delimiters and match the whole string
     139            // so matching terms can be combined
     140            term = term.replace( / /g, ')(?=.*' );
     141            match = new RegExp( '^(?=.*' + term + ').+', 'i' );
     142
     143            // Find results
     144            this.each( function( project ) {
     145                name = project.get( 'name' );
     146                project.set( 'isVisible', match.test( name ) );
     147            });
     148        },
     149    });
     150
     151    /**
     152     * VIEWS
     153     */
     154    projects.view.Frame = wp.Backbone.View.extend({
     155        el: '#projects-list',
     156
     157        render: function() {
     158            var view = this;
     159            wp.Backbone.View.prototype.render.apply( this, arguments );
     160
     161            this.collection.each( function( project ) {
     162                var projectView = new projects.view.Project({
     163                    model: project
     164                });
     165
     166                projectView.render();
     167                view.$el.append( projectView.el );
     168            });
     169
     170            this.views.ready();
     171
     172            return this;
     173        }
     174    });
     175
     176    projects.view.Checkbox = wp.Backbone.View.extend({
     177        className: 'project-checkbox',
     178        template: wp.template( 'project-checkbox' ),
     179
     180        initialize: function() {
     181            this.listenTo( this.model, 'change', this.render );
     182        },
     183
     184        events: {
     185            'click .input-checkbox': 'updateChecked',
     186            'click .input-radio': 'setChecked'
     187        },
     188
     189        prepare: function() {
     190            return _.pick( this.model.toJSON(), 'id', 'name', 'checked', 'checkedSubProjects' );
     191        },
     192
     193        updateChecked: function() {
     194            this.model.set( 'checked', this.$el.find( 'input' ).prop( 'checked' ) );
     195        },
     196
     197        setChecked: function() {
     198            this.model.set( 'checked', true );
     199        }
     200    });
     201
     202    projects.view.Project = wp.Backbone.View.extend({
     203        tagName: 'li',
     204
     205        events: {
     206            'click': 'updateIsActive'
     207        },
     208
     209        initialize: function() {
     210            this.listenTo( this.model, 'change:checked', this.propagateChange );
     211            this.listenTo( this.model, 'change:isActive', this.toggleActiveState );
     212
     213            this.views.add( new projects.view.Checkbox({
     214                model: this.model
     215            }) );
     216
     217            this.views.add( new projects.view.SubProjects({
     218                model: this.model
     219            }) );
     220
     221            this.toggleActiveState();
     222
     223            this.views.ready();
     224        },
     225
     226        propagateChange: function() {
     227            if ( this.model.get( 'checked' ) ) {
     228                this.model.get( 'subProjects' ).trigger( 'uncheckall' );
     229            }
     230        },
     231
     232        toggleActiveState: function() {
     233            if ( this.model.get( 'isActive' ) ) {
     234                this.$el.addClass( 'active' );
     235            } else {
     236                this.$el.removeClass( 'active' );
     237            }
     238        },
     239
     240        updateIsActive: function() {
     241            this.model.set( 'isActive', true );
     242        },
     243    });
     244
     245    projects.view.SubProjects = wp.Backbone.View.extend({
     246        tagName: 'div',
     247        className: 'sub-projects-wrapper',
     248
     249        initialize: function() {
     250            var collection = this.model.get( 'subProjects' );
     251
     252            if ( collection.length > 5 ) {
     253                this.views.add( new projects.view.Search({
     254                    collection: collection
     255                }) );
     256            }
     257
     258            this.views.add( new projects.view.SubProjectsList({
     259                collection: collection
     260            }) );
     261        },
     262    });
     263
     264    projects.view.SubProjectsList = wp.Backbone.View.extend({
     265        tagName: 'ul',
     266        className: 'sub-projects-list',
     267
     268        initialize: function() {
     269            var view = this;
     270            this.collection.each( function( project ) {
     271                view.views.add( new projects.view.SubProject({
     272                    model: project
     273                }) );
     274            });
     275        }
     276    });
     277
     278    projects.view.SubProject = wp.Backbone.View.extend({
     279        tagName: 'li',
     280
     281        initialize: function() {
     282            this.views.add( new projects.view.Checkbox({
     283                model: this.model
     284            }) );
     285
     286            this.listenTo( this.model, 'change:isVisible', this.changeVisibility );
     287        },
     288
     289        changeVisibility: function() {
     290            if ( this.model.get( 'isVisible' ) ) {
     291                this.$el.removeClass( 'hidden' );
     292            } else {
     293                this.$el.addClass( 'hidden' );
     294            }
     295        }
     296    });
     297
     298    projects.view.Search = wp.Backbone.View.extend({
     299        tagName: 'input',
     300        className: 'sub-projects-search',
     301
     302        attributes: {
     303            placeholder: l10n.searchPlaceholder,
     304            type: 'search',
     305        },
     306
     307        events: {
     308            'input': 'search',
     309            'keyup': 'search',
     310            'keydown': 'search',
     311        },
     312
     313        search: function( event ) {
     314            // Prevent form submit
     315            if ( event.type === 'keydown' && event.which === 13 ) {
    36316                event.preventDefault();
    37 
    38                 checked = [];
    39                 $allProjects.prop( 'checked', false );
    40                 $projects.prop( 'checked', false );
    41             } );
    42         }
     317            } else if ( event.type === 'keydown' ) {
     318                return;
     319            }
     320
     321            // Clear on escape.
     322            if ( event.type === 'keyup' && event.which === 27 ) {
     323                event.target.value = '';
     324            }
     325
     326            this.doSearch( event );
     327        },
     328
     329        doSearch: _.debounce( function( event ) {
     330            this.collection.doSearch( event.target.value );
     331        }, 200 )
     332    });
     333
     334    projects.init = function() {
     335        projects.view.frame = new projects.view.Frame({
     336            collection: new projects.model.Projects( projects.settings.data )
     337        }).render();
     338    };
     339
     340    $( projects.init );
     341
     342    $( '#project-all' ).on( 'click', function() {
     343        var $el = $( this );
     344
     345        if ( $el.hasClass( 'active' ) ) {
     346            return;
     347        }
     348
     349        $el.addClass( 'active' );
     350        $window.trigger( 'deactivate-all-projects.rosetta' );
     351    });
     352
     353    $window.on( 'deactivate-other-projects.rosetta', function() {
     354        $( '#project-all' ).removeClass( 'active' );
    43355    } );
    44356} )( jQuery );
  • sites/trunk/global.wordpress.org/public_html/wp-content/mu-plugins/roles/rosetta-roles.php

    r1741 r1786  
    211211        add_action( 'load-' . $this->translation_editors_page, array( $this, 'load_translation_editors_page' ) );
    212212        add_action( 'admin_print_scripts-' . $this->translation_editors_page, array( $this, 'enqueue_scripts' ) );
     213        add_action( 'admin_footer-' . $this->translation_editors_page, array( $this, 'print_js_templates' ) );
     214        add_action( 'admin_print_styles-' . $this->translation_editors_page, array( $this, 'enqueue_styles' ) );
    213215    }
    214216
     
    217219     */
    218220    public function enqueue_scripts() {
    219         wp_enqueue_script( 'rosetta-roles', plugins_url( '/js/rosetta-roles.js', __FILE__ ), array( 'jquery' ), '1', true );
     221        wp_enqueue_script( 'rosetta-roles', plugins_url( '/js/rosetta-roles.js', __FILE__ ), array( 'jquery', 'wp-backbone' ), '2', true );
     222    }
     223
     224    /**
     225     * Enqueues styles.
     226     */
     227    public function enqueue_styles() {
     228        wp_enqueue_style( 'rosetta-roles', plugins_url( '/css/rosetta-roles.css', __FILE__ ), array(), '2' );
     229    }
     230
     231    /**
     232     * Prints JavaScript templates.
     233     */
     234    public function print_js_templates() {
     235        ?>
     236        <script id="tmpl-project-checkbox" type="text/html">
     237            <# if ( ! data.checkedSubProjects ) {
     238                #>
     239                <label>
     240                    <input type="checkbox" class="input-checkbox" name="projects[]" value="{{data.id}}"
     241                    <#
     242                    if ( data.checked ) {
     243                        #> checked="checked"<#
     244                    }
     245                    #>
     246                    />
     247                    {{data.name}}
     248                </label>
     249            <# } else { #>
     250                <label>
     251                    <input type="radio" class="input-radio" checked="checked" /> {{data.name}}
     252                </label>
     253            <# } #>
     254        </script>
     255        <?php
    220256    }
    221257
     
    290326                    $this->notify_translation_editor_update( $user_details->ID, 'add' );
    291327
     328                    $meta_key = $wpdb->get_blog_prefix() . $this->project_access_meta_key;
     329
    292330                    $projects = empty( $_REQUEST['projects'] ) ? '' : $_REQUEST['projects'];
    293331                    if ( 'custom' === $projects ) {
     332                        update_user_meta( $user_details->ID, $meta_key, array() );
    294333                        $redirect = add_query_arg( 'user_id', $user_details->ID, $redirect );
    295334                        wp_redirect( add_query_arg( array( 'update' => 'user-added-custom-projects' ), $redirect ) );
     
    297336                    }
    298337
    299                     $meta_key = $wpdb->get_blog_prefix() . $this->project_access_meta_key;
    300338                    update_user_meta( $user_details->ID, $meta_key, array( 'all' ) );
    301339
     
    393431                $redirect = add_query_arg( 'user_id', $user_details->ID, $redirect );
    394432
    395                 $all_projects = $this->get_translate_top_level_projects();
     433                $all_projects = $this->get_translate_projects();
    396434                $all_projects = wp_list_pluck( $all_projects, 'id' );
    397435                $all_projects = array_map( 'intval', $all_projects );
     
    431469        global $wpdb;
    432470
    433         $projects = $this->get_translate_top_level_projects();
     471        $projects = $this->get_translate_projects();
     472        $project_tree = $this->get_project_tree( $projects, 0, 1 );
     473
     474        // Sort the tree and remove array keys.
     475        usort( $project_tree, array( $this, '_sort_name_callback' ) );
     476        foreach ( $project_tree as $key => $project ) {
     477            if ( $project->sub_projects ) {
     478                usort( $project->sub_projects, array( $this, '_sort_name_callback' ) );
     479            }
     480            $project->sub_projects = array_values( $project->sub_projects );
     481        }
     482        $project_tree = array_values( $project_tree );
    434483
    435484        $meta_key = $wpdb->get_blog_prefix() . $this->project_access_meta_key;
     
    438487            $project_access_list = array();
    439488        }
     489
     490        wp_localize_script( 'rosetta-roles', '_rosettaProjectsSettings', array(
     491            'l10n' => array(
     492                'searchPlaceholder' => esc_attr__( 'Search...', 'rosetta' )
     493            ),
     494            'data' => $project_tree,
     495            'accessList' => $project_access_list
     496        ) );
    440497
    441498        $feedback_message = $this->get_feedback_message();
     
    504561        }
    505562
     563        $projects = $this->get_translate_projects();
     564        $project_tree = $this->get_project_tree( $projects, 0, 1 );
     565
    506566        $args = array(
    507567            'user_role'               => $this->translation_editor_role,
    508             'projects'                => $this->get_translate_top_level_projects(),
     568            'projects'                => $projects,
     569            'project_tree'            => $project_tree,
    509570            'project_access_meta_key' => $wpdb->get_blog_prefix() . $this->project_access_meta_key,
    510571        );
     
    535596
    536597    /**
    537      * Fetches all top level projects from translate.wordpress.org.
     598     * Fetches all projects from translate.wordpress.org.
    538599     *
    539600     * @return array List of projects.
    540601     */
    541     private function get_translate_top_level_projects() {
     602    private function get_translate_projects() {
    542603        global $wpdb;
    543 
    544         $cache = get_site_transient( 'translate-top-level-projects' );
    545         if ( false !== $cache ) {
    546             return $cache;
     604        static $projects = null;
     605
     606        if ( null !== $projects ) {
     607            return $projects;
    547608        }
    548609
    549610        $_projects = $wpdb->get_results( "
    550             SELECT id, name
     611            SELECT id, name, parent_project_id
    551612            FROM translate_projects
    552             WHERE parent_project_id IS NULL
    553             ORDER BY name ASC
     613            ORDER BY id ASC
    554614        " );
    555615
     
    559619        }
    560620
    561         set_site_transient( 'translate-top-level-projects', $projects, DAY_IN_SECONDS );
    562 
    563621        return $projects;
    564622    }
     623
     624    /**
     625     * Transforms a flat array to a hierarchy tree.
     626     *
     627     * @param array $projects  The projects.
     628     * @param int   $parent_id Optional. Parent ID. Default 0.
     629     * @param int   $max_depth Optional. Max depth to avoid endless recursion. Default 5.
     630     * @return array The project tree.
     631     */
     632    public function get_project_tree( $projects, $parent_id = 0, $max_depth = 5 ) {
     633        if ( $max_depth < 0 ) { // Avoid an endless recursion.
     634            return;
     635        }
     636
     637        $tree = array();
     638        foreach ( $projects as $project ) {
     639            if ( $project->parent_project_id == $parent_id ) {
     640                $sub_projects = $this->get_project_tree( $projects, $project->id, $max_depth - 1 );
     641                if ( $sub_projects ) {
     642                    $project->sub_projects = $sub_projects;
     643                }
     644
     645                $tree[ $project->id ] = $project;
     646            }
     647        }
     648        return $tree;
     649    }
     650
     651    private function _sort_name_callback( $a, $b ) {
     652        return strnatcasecmp( $a->name, $b->name );
     653    }
    565654}
  • sites/trunk/global.wordpress.org/public_html/wp-content/mu-plugins/roles/views/edit-translation-editor.php

    r1419 r1786  
    1010                    <th scope="row">
    1111                        <?php _e( 'Add editor access for:', 'rosetta' ); ?><br>
    12                         <small style="font-weight:normal"><a href="#clear-all" id="clear-all"><?php _ex( 'Clear All', 'Deselects all checkboxes', 'rosetta' ); ?></a></small>
    1312                    </th>
    1413                    <td>
    15                         <fieldset>
     14                        <fieldset id="projects">
    1615                            <legend class="screen-reader-text"><span><?php _e( 'Add editor access for:', 'rosetta' ); ?></span></legend>
    17                             <p>
    18                                 <label for="project-all">
    19                                     <input name="projects[]" id="project-all" value="all" type="checkbox"<?php checked( in_array( 'all', $project_access_list ) ); ?>> <?php _e( 'All projects &ndash; If selected, translation editor will have validation permissions for all projects, including newly-added projects.', 'rosetta' ); ?>
    20                                 </label>
    21                             </p>
    22                             <?php
    23                             foreach ( $projects as $project ) {
    24                                 $project_id = esc_attr( $project->id );
    25                                 printf(
    26                                     '<p><label for="project-%d"><input name="projects[]" id="project-%d" class="project" value="%d" type="checkbox"%s> %s</label></p>',
    27                                     $project_id,
    28                                     $project_id,
    29                                     $project_id,
    30                                     checked( in_array( $project->id, $project_access_list ), true, false ),
    31                                     esc_html( $project->name )
    32                                 );
    33                             }
    34                             ?>
    35                             <p class="description"><?php _e( 'Each project includes sub projects and newly-added sub projects.', 'rosetta' ); ?></p>
     16
     17                            <ul id="projects-list" class="projects-list">
     18                                <li id="project-all" class="active">
     19                                    <label>
     20                                        <input name="projects[]"value="all" type="checkbox"<?php checked( in_array( 'all', $project_access_list ) ); ?>> <?php _e( 'All projects', 'rosetta' ); ?>
     21                                    </label>
     22                                    <div class="sub-projects-wrapper">
     23                                        <?php _e( 'The translation editor has validation permissions for all projects, including newly-added projects.', 'rosetta' ); ?>
     24                                    </div>
     25                                </li>
     26                            </ul>
    3627                        </fieldset>
     28                        <p class="description"><?php _e( 'Each project includes sub projects and newly-added sub projects.', 'rosetta' ); ?></p>
    3729                    </td>
    3830                </tr>
Note: See TracChangeset for help on using the changeset viewer.