Merge "resourceloader: Simplify validateScriptFile() with getWithSetCallback"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 13 May 2017 00:32:10 +0000 (00:32 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 13 May 2017 00:32:10 +0000 (00:32 +0000)
13 files changed:
includes/api/ApiQueryRevisions.php
languages/i18n/af.json
languages/i18n/az.json
languages/i18n/azb.json
languages/i18n/be-tarask.json
languages/i18n/bs.json
languages/i18n/csb.json
languages/i18n/fi.json
languages/i18n/he.json
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.js
tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js

index b0a8468..a4f0315 100644 (file)
@@ -219,26 +219,26 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
                        }
 
                        // Convert startid/endid to timestamps (T163532)
-                       if ( $params['startid'] !== null || $params['endid'] !== null ) {
-                               $ids = [
-                                       (int)$params['startid'] => true,
-                                       (int)$params['endid'] => true,
-                               ];
-                               unset( $ids[0] ); // null
-                               $ids = array_keys( $ids );
-
+                       $revids = [];
+                       if ( $params['startid'] !== null ) {
+                               $revids[] = (int)$params['startid'];
+                       }
+                       if ( $params['endid'] !== null ) {
+                               $revids[] = (int)$params['endid'];
+                       }
+                       if ( $revids ) {
                                $db = $this->getDB();
                                $sql = $db->unionQueries( [
                                        $db->selectSQLText(
                                                'revision',
                                                [ 'id' => 'rev_id', 'ts' => 'rev_timestamp' ],
-                                               [ 'rev_id' => $ids ],
+                                               [ 'rev_id' => $revids ],
                                                __METHOD__
                                        ),
                                        $db->selectSQLText(
                                                'archive',
                                                [ 'id' => 'ar_rev_id', 'ts' => 'ar_timestamp' ],
-                                               [ 'ar_rev_id' => $ids ],
+                                               [ 'ar_rev_id' => $revids ],
                                                __METHOD__
                                        ),
                                ], false );
index 40cad08..99b135d 100644 (file)
        "sourcefilename": "Oorspronklike lêernaam:",
        "sourceurl": "Bron-URL:",
        "destfilename": "Teikenlêernaam:",
-       "upload-maxfilesize": "Maksimum lêer grootte: $1",
+       "upload-maxfilesize": "Maksimum-lêergrootte: $1",
        "upload-description": "Lêerbeskrywing",
        "upload-options": "Oplaai-opsies",
        "watchthisupload": "Hou die lêer dop",
index ef53cca..8911474 100644 (file)
        "confirmemail_body_set": "Kimsə, ehtimal ki, siz özünüz, $1 IP adresindən \n{{SITENAME}} layihəsindəki \"$2\" adlı istifadəçi hesabı üçün e-poçt adresi olaraq bu adresi göstərib.\n\nBu hesabın həqiqətən sizə aid olduğunu təsdiq etmək və {{SITENAME}} saytındakı\ne-poçt əməliyyatlarını aktivləşdirmək üçün aşağıdakı linki brauzerinizdə açın:\n\n$3\n\nƏgər hesab sizə aid *deyilsə*, e-poçt adresi təsdiqlənməsini ləğv etmək\nüçün aşağıdakı linkə daxil olun:\n\n$5\n\nBu təsdiq kodu $4 tarixinə qədər aktiv olacaqdır.",
        "confirmemail_invalidated": "E-mail təsdiqlənməsi dayandırıldı",
        "invalidateemail": "E-mail təsdiqlənməsindən imtina",
+       "notificationemail_body_changed": "Kimsə, ehtimal ki, siz özünüz, $1 IP adresindən {{SITENAME}} layihəsindəki \n\"$2\" adlı istifadəçi hesabının e-poçt adresini \"$3\" ilə əvəz edib.\n\nƏgər bunu siz etməmisinizsə, o zaman dərhal saytın administratoru ilə əlaqə saxlayın.",
+       "notificationemail_body_removed": "Kimsə, ehtimal ki, siz özünüz, $1 IP adresindən {{SITENAME}} layihəsindəki \n\"$2\" adlı istifadəçi hesabının e-poçt adresini silib.\n\nƏgər bunu siz etməmisinizsə, o zaman dərhal saytın administratoru ilə əlaqə saxlayın.",
        "scarytranscludedisabled": "[«Interwiki transcluding»dən çıxılmışdır]",
        "scarytranscludetoolong": "[URL uzundur]",
        "deletedwhileediting": "'''Diqqət!''' Bu səhifə siz redaktə etməyə başladıqdan sonra silinmişdir!",
index 5e23f36..a65a7d1 100644 (file)
        "redirectedfrom": "($1-دن يوْل‌لاندیریلمیش)",
        "redirectpagesub": "یوْللاندیرما صفحه‌سی",
        "redirectto": "مسیزپرین دَییشیب:",
-       "lastmodifiedat": "بۇ صفحه‌‌ سوْن دفعه $1، $2 تاریخینده دَییشیلمیشدیر.",
+       "lastmodifiedat": "بۇ صفحه‌‌ سوْن دفعه $1، $2 تاریخینده دَییشدیریلمیشدیر.",
        "viewcount": "بۇ صحیفه {{PLURAL:$1|بیر|$1}} دفعه گؤرولوبدور.",
        "protectedpage": "قوْرونموش صفحه",
        "jumpto": "آتیل:",
index b846b50..1709efc 100644 (file)
        "rcfilters-activefilters": "Актыўныя фільтры",
        "rcfilters-quickfilters": "Хуткія спасылкі",
        "rcfilters-savedqueries-defaultlabel": "Захаваныя фільтры",
+       "rcfilters-savedqueries-rename": "Перайменаваць",
        "rcfilters-restore-default-filters": "Аднавіць фільтры па змоўчаньні",
        "rcfilters-clear-all-filters": "Ачысьціць усе фільтры",
        "rcfilters-search-placeholder": "Фільтар апошніх зьменаў (праглядзець або пачніце друкаваць)",
index f36f73e..1ddde02 100644 (file)
        "search-file-match": "(podudara se sadržaj datoteke)",
        "search-suggest": "Jeste li mislili: $1",
        "search-rewritten": "Prikazujem rezultate za $1. Umjesto toga potraži $2.",
-       "search-interwiki-caption": "Srodni projekti",
+       "search-interwiki-caption": "Rezultati s bratskih projekata",
        "search-interwiki-default": "$1 rezultati:",
        "search-interwiki-more": "(više)",
        "search-interwiki-more-results": "više rezultata",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} ([[Special:NewPages|spisak novih stranica]])",
        "recentchanges-submit": "Prikaži",
        "rcfilters-activefilters": "Aktivni filteri",
+       "rcfilters-quickfilters": "Brzi linkovi",
+       "rcfilters-savedqueries-defaultlabel": "Sačuvani filteri",
+       "rcfilters-savedqueries-rename": "Preimenuj",
+       "rcfilters-savedqueries-setdefault": "Postavi kao predodređeno",
+       "rcfilters-savedqueries-remove": "Ukloni",
+       "rcfilters-savedqueries-new-name-label": "Naziv",
+       "rcfilters-savedqueries-apply-label": "Napravi brzi link",
+       "rcfilters-savedqueries-cancel-label": "Otkaži",
        "rcfilters-restore-default-filters": "Vrati predodređene filtere",
        "rcfilters-clear-all-filters": "Ukloni sve filtere",
        "rcfilters-search-placeholder": "Filtriraj nedavne izmjene (prelistajte mogućnosti ili počnite kucati)",
        "rcfilters-filter-user-experience-level-newcomer-label": "Novajlije",
        "rcfilters-filter-user-experience-level-newcomer-description": "Manje od 10 izmjena i 4 dana aktivnosti.",
        "rcfilters-filter-user-experience-level-learner-label": "Učenici",
-       "rcfilters-filter-user-experience-level-learner-description": "Više dana aktivnosti i izmjena od \"novajlija\", ali manje od \"iskusnih korisnika\".",
+       "rcfilters-filter-user-experience-level-learner-description": "Više iskustva od \"novajlija\", ali manje od \"iskusnih korisnika\".",
        "rcfilters-filter-user-experience-level-experienced-label": "Iskusni korisnici",
        "rcfilters-filter-user-experience-level-experienced-description": "Preko 30 dana aktivnosti i 500 izmjena.",
        "rcfilters-filtergroup-automated": "Automatski doprinosi",
        "rcfilters-filter-minor-description": "Izmjene koje je njihov autor označio manjim.",
        "rcfilters-filter-major-label": "Obične izmjene",
        "rcfilters-filter-major-description": "Izmjene koje nisu označene manjim.",
+       "rcfilters-filtergroup-watchlist": "Stranice na spisku praćenja",
        "rcfilters-filter-watchlist-watched-label": "Na spisku praćenja",
+       "rcfilters-filter-watchlist-watched-description": "Izmjene na stranicama na Vašem spisku praćenja.",
+       "rcfilters-filter-watchlist-watchednew-label": "Nove izmjene na spisku praćenja",
+       "rcfilters-filter-watchlist-watchednew-description": "Izmjene na stranicama koje se nalaze na spisku praćenja, a koje još niste posjetili otkako su izmijenjene.",
+       "rcfilters-filter-watchlist-notwatched-label": "Nije na spisku praćenja",
+       "rcfilters-filter-watchlist-notwatched-description": "Sve osim izmjena na stranicama koje pratite.",
        "rcfilters-filtergroup-changetype": "Vrsta izmjene",
        "rcfilters-filter-pageedits-label": "Izmjene stranica",
        "rcfilters-filter-pageedits-description": "Izmjene wiki sadržaja, rasprava, opisa kategorija....",
        "rcfilters-typeofchange-conflicts-hideminor": "Ovaj filter za vrstu izmjene u sukobu je s filterom za \"manje izmjene\". Izvjesne vrste izmjena ne mogu se označiti kao \"manje\".",
        "rcfilters-filtergroup-lastRevision": "Posljednja izmjena",
        "rcfilters-filter-lastrevision-label": "Posljednja izmjena",
+       "rcfilters-filter-lastrevision-description": "Najnovija izmjena na stranici.",
        "rcfilters-filter-previousrevision-label": "Ranije izmjene",
+       "rcfilters-filter-previousrevision-description": "Sve izmjene koje nisu najnovije na stranici.",
        "rcnotefrom": "Ispod {{PLURAL:$5|je izmjena|su izmjene}} od <strong>$3, $4</strong> (do <strong>$1</strong> prikazano).",
        "rclistfromreset": "Resetiraj izbor datuma",
        "rclistfrom": "Prikaži nove izmjene počev od $3 u $2",
        "apihelp": "API pomoć",
        "apihelp-no-such-module": "Modul \"$1\" nije pronađen.",
        "apisandbox-unfullscreen": "Prikaži stranicu",
+       "apisandbox-submit": "Postavi zahtjev",
        "apisandbox-reset": "Očisti",
        "apisandbox-retry": "Pokušaj ponovo",
        "apisandbox-loading": "Učitavam podatke o API modulu \"$1\"...",
        "tags-source-extension": "Definirano softverom",
        "tags-source-manual": "Ručno postavili korisnici ili botovi",
        "tags-source-none": "Više se ne koristi",
-       "tags-edit": "uređivanje",
+       "tags-edit": "uredi",
        "tags-delete": "obriši",
        "tags-activate": "aktiviraj",
        "tags-deactivate": "dekativiraj",
index 4057941..a4f1d8f 100644 (file)
        "confirm_purge_button": "Jo!",
        "imgmultigo": "Biéj!",
        "autoredircomment": "Przeczérowanié do [[$1]]",
+       "autosumm-new": "Pòwsta nowô starna:",
        "watchlisttools-view": "Òbaczë wôżnészé zmianë",
        "watchlisttools-edit": "Òbaczë a editëjë lëstã ùzérónëch artiklów",
        "watchlisttools-raw": "Editëjë sërą lëstã",
index c82fffe..37a0abc 100644 (file)
        "rcfilters-filter-patrolled-label": "Tarkastetut",
        "rcfilters-filter-patrolled-description": "Tarkastetut muokkaukset",
        "rcfilters-filter-unpatrolled-label": "Ei tarkastetut",
-       "rcfilters-filter-unpatrolled-description": "Muutokset joita ei ole tarkastettu",
+       "rcfilters-filter-unpatrolled-description": "Muutokset, joita ei ole tarkastettu",
        "rcfilters-filtergroup-significance": "Merkitys",
        "rcfilters-filter-minor-label": "Pienet muutokset",
        "rcfilters-filter-minor-description": "Muokkaukset, jotka on merkitty pieniksi.",
        "rcfilters-filter-major-label": "Ei-pienet muutokset",
-       "rcfilters-filter-major-description": "Muokkaukset joita ei ole merkitty pieniksi.",
+       "rcfilters-filter-major-description": "Muokkaukset, joita ei ole merkitty pieniksi.",
        "rcfilters-filtergroup-watchlist": "Tarkkailulistalla olevat sivut",
        "rcfilters-filter-watchlist-watched-label": "Tarkkailulistalla",
        "rcfilters-filter-watchlist-watched-description": "Muutokset tarkkailulistalla oleviin sivuihin.",
        "rcfilters-filter-pageedits-label": "Sivun muokkaukset",
        "rcfilters-filter-pageedits-description": "Muokkaukset wikin sisältöön, keskusteluihin, luokkakuvauksiin....",
        "rcfilters-filter-newpages-label": "Sivujen luonnit",
-       "rcfilters-filter-newpages-description": "Muokkaukset joilla on luotu uusia sivuja.",
+       "rcfilters-filter-newpages-description": "Muokkaukset, joilla on luotu uusia sivuja.",
        "rcfilters-filter-categorization-label": "Luokkamuutokset",
        "rcfilters-filter-categorization-description": "Tulokset sivuista, joita on lisätty tai poistettu luokista.",
        "rcfilters-filter-logactions-label": "Kirjatut toimet",
index d326bd2..4dcf647 100644 (file)
        "recentchanges-submit": "הצגה",
        "rcfilters-activefilters": "מסננים פעילים",
        "rcfilters-quickfilters": "קישורים מהירים",
+       "rcfilters-quickfilters-placeholder": "שמירת ההגדרות המועדפות שלך לשימוש בעתיד.",
        "rcfilters-savedqueries-defaultlabel": "מסננים שמורים",
        "rcfilters-savedqueries-rename": "שינוי שם",
        "rcfilters-savedqueries-setdefault": "הגדרה כברירת מחדל",
        "mw-widgets-titleinput-description-redirect": "הפניה ל{{GRAMMAR:תחילית|$1}}",
        "mw-widgets-categoryselector-add-category-placeholder": "הוספת קטגוריה...",
        "mw-widgets-usersmultiselect-placeholder": "הוספת עוד...",
+       "date-range-from": "מתאריך:",
+       "date-range-to": "עד תאריך:",
        "sessionmanager-tie": "לא ניתן לצרף מספר סוגי אימות זהות: $1.",
        "sessionprovider-generic": "התחברויות של $1",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "התחברויות המבוססות על עוגיות",
index 63e13fd..22b2619 100644 (file)
@@ -38,6 +38,7 @@
                this.whatsThis = config.whatsThis || {};
 
                this.conflicts = config.conflicts || {};
+               this.defaultParams = {};
 
                this.aggregate( { update: 'filterItemUpdate' } );
                this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
 
        /* Methods */
 
+       /**
+        * Initialize the group and create its filter items
+        *
+        * @param {Object} filterDefinition Filter definition for this group
+        * @param {string|Object} [groupDefault] Definition of the group default
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
+               var supersetMap = {},
+                       model = this,
+                       items = [];
+
+               filterDefinition.forEach( function ( filter ) {
+                       // Instantiate an item
+                       var subsetNames = [],
+                               filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, {
+                                       group: model.getName(),
+                                       label: mw.msg( filter.label ),
+                                       description: mw.msg( filter.description ),
+                                       cssClass: filter.cssClass
+                               } );
+
+                       filter.subset = filter.subset || [];
+                       filter.subset = filter.subset.map( function ( el ) {
+                               return el.filter;
+                       } );
+
+                       if ( filter.subset ) {
+                               subsetNames = [];
+                               filter.subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
+                                       // Subsets (unlike conflicts) are always inside the same group
+                                       // We can re-map the names of the filters we are getting from
+                                       // the subsets with the group prefix
+                                       var subsetName = model.getPrefixedName( subsetFilterName );
+                                       // For convenience, we should store each filter's "supersets" -- these are
+                                       // the filters that have that item in their subset list. This will just
+                                       // make it easier to go through whether the item has any other items
+                                       // that affect it (and are selected) at any given time
+                                       supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
+                                       mw.rcfilters.utils.addArrayElementsUnique(
+                                               supersetMap[ subsetName ],
+                                               filterItem.getName()
+                                       );
+
+                                       // Translate subset param name to add the group name, so we
+                                       // get consistent naming. We know that subsets are only within
+                                       // the same group
+                                       subsetNames.push( subsetName );
+                               } );
+
+                               // Set translated subset
+                               filterItem.setSubset( subsetNames );
+                       }
+
+                       items.push( filterItem );
+
+                       // Store default parameter state; in this case, default is defined per filter
+                       if ( model.getType() === 'send_unselected_if_any' ) {
+                               // Store the default parameter state
+                               // For this group type, parameter values are direct
+                               model.defaultParams[ filter.name ] = Number( !!filter.default );
+                       }
+               } );
+
+               // Add items
+               this.addItems( items );
+
+               // Now that we have all items, we can apply the superset map
+               this.getItems().forEach( function ( filterItem ) {
+                       filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
+               } );
+
+               // Store default parameter state; in this case, default is defined per the
+               // entire group, given by groupDefault method parameter
+               if ( this.getType() === 'string_options' ) {
+                       // Store the default parameter group state
+                       // For this group, the parameter is group name and value is the names
+                       // of selected items
+                       this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
+                               // Current values
+                               groupDefault ?
+                                       groupDefault.split( this.getSeparator() ) :
+                                       [],
+                               // Legal values
+                               this.getItems().map( function ( item ) {
+                                       return item.getParamName();
+                               } )
+                       ).join( this.getSeparator() );
+               }
+       };
+
        /**
         * Respond to filterItem update event
         *
                return this.name;
        };
 
+       /**
+        * Get the default param state of this group
+        *
+        * @return {Object} Default param state
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getDefaultParams = function () {
+               return this.defaultParams;
+       };
+
        /**
         * Get the messags defining the 'whats this' popup for this group
         *
                this.conflicts = conflicts;
        };
 
+       /**
+        * Set conflicts for each filter item in the group based on the
+        * given conflict map
+        *
+        * @param {Object} conflicts Object representing the conflict map,
+        *  keyed by the item name, where its value is an object for all its conflicts
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.setFilterConflicts = function ( conflicts ) {
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( conflicts[ filterItem.getName() ] ) {
+                               filterItem.setConflicts( conflicts[ filterItem.getName() ] );
+                       }
+               } );
+       };
+
        /**
         * Check whether this item has a potential conflict with the given item
         *
                return result;
        };
 
+       /**
+        * Get the filter representation this group would provide
+        * based on given parameter states.
+        *
+        * @param {Object|string} [paramRepresentation] An object defining a parameter
+        *  state to translate the filter state from. If not given, an object
+        *  representing all filters as falsey is returned; same as if the parameter
+        *  given were an empty object, or had some of the filters missing.
+        * @return {Object} Filter representation
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
+               var areAnySelected, paramValues,
+                       model = this,
+                       paramToFilterMap = {},
+                       result = {};
+
+               if ( this.getType() === 'send_unselected_if_any' ) {
+                       paramRepresentation = paramRepresentation || {};
+                       // Expand param representation to include all filters in the group
+                       this.getItems().forEach( function ( filterItem ) {
+                               paramRepresentation[ filterItem.getParamName() ] = !!paramRepresentation[ filterItem.getParamName() ];
+                               paramToFilterMap[ filterItem.getParamName() ] = filterItem;
+
+                               if ( paramRepresentation[ filterItem.getParamName() ] ) {
+                                       areAnySelected = true;
+                               }
+                       } );
+
+                       $.each( paramRepresentation, function ( paramName, paramValue ) {
+                               var filterItem = paramToFilterMap[ paramName ];
+
+                               result[ filterItem.getName() ] = areAnySelected ?
+                                       // Flip the definition between the parameter
+                                       // state and the filter state
+                                       // This is what the 'toggleSelected' value of the filter is
+                                       !Number( paramValue ) :
+                                       // Otherwise, there are no selected items in the
+                                       // group, which means the state is false
+                                       false;
+                       } );
+               } else if ( this.getType() === 'string_options' ) {
+                       paramRepresentation = paramRepresentation || '';
+
+                       // Normalize the given parameter values
+                       paramValues = mw.rcfilters.utils.normalizeParamOptions(
+                               // Given
+                               paramRepresentation.split(
+                                       this.getSeparator()
+                               ),
+                               // Allowed values
+                               this.getItems().map( function ( filterItem ) {
+                                       return filterItem.getParamName();
+                               } )
+                       );
+                       // Translate the parameter values into a filter selection state
+                       this.getItems().forEach( function ( filterItem ) {
+                               result[ filterItem.getName() ] = (
+                                               // If it is the word 'all'
+                                               paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
+                                               // All values are written
+                                               paramValues.length === model.getItemCount()
+                                       ) ?
+                                       // All true (either because all values are written or the term 'all' is written)
+                                       // is the same as all filters set to true
+                                       true :
+                                       // Otherwise, the filter is selected only if it appears in the parameter values
+                                       paramValues.indexOf( filterItem.getParamName() ) > -1;
+                       } );
+               }
+
+               // Go over result and make sure all filters are represented.
+               // If any filters are missing, they will get a falsey value
+               this.getItems().forEach( function ( filterItem ) {
+                       result[ filterItem.getName() ] = !!result[ filterItem.getName() ];
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get item by its parameter name
+        *
+        * @param {string} paramName Parameter name
+        * @return {mw.rcfilters.dm.FilterItem} Filter item
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) {
+               return this.getItems().filter( function ( item ) {
+                       return item.getParamName() === paramName;
+               } )[ 0 ];
+       };
+
        /**
         * Get group type
         *
        };
 
        /**
-        * Get the prefix used for the filter names inside this group
+        * Get the prefix used for the filter names inside this group.
         *
+        * @param {string} [name] Filter name to prefix
         * @return {string} Group prefix
         */
        mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
                return this.getName() + '__';
        };
 
+       /**
+        * Get a filter name with the prefix used for the filter names inside this group.
+        *
+        * @param {string} name Filter name to prefix
+        * @return {string} Group prefix
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getPrefixedName = function ( name ) {
+               return this.getNamePrefix() + name;
+       };
+
        /**
         * Get group's title
         *
index 9054fe4..88ce33c 100644 (file)
         * @param {Array} filters Filter group definition
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
-               var i, filterItem, filterConflictResult, groupConflictResult, subsetNames,
+               var filterItem, filterConflictResult, groupConflictResult,
                        model = this,
                        items = [],
-                       supersetMap = {},
                        groupConflictMap = {},
                        filterConflictMap = {},
-                       addArrayElementsUnique = function ( arr, elements ) {
-                               elements = Array.isArray( elements ) ? elements : [ elements ];
-
-                               elements.forEach( function ( element ) {
-                                       if ( arr.indexOf( element ) === -1 ) {
-                                               arr.push( element );
-                                       }
-                               } );
-
-                               return arr;
-                       },
+                       /*!
+                        * Expand a conflict definition from group name to
+                        * the list of all included filters in that group.
+                        * We do this so that the direct relationship in the
+                        * models are consistently item->items rather than
+                        * mixing item->group with item->item.
+                        *
+                        * @param {Object} obj Conflict definition
+                        * @return {Object} Expanded conflict definition
+                        */
                        expandConflictDefinitions = function ( obj ) {
                                var result = {};
 
                                                var filter;
 
                                                if ( conflict.filter ) {
-                                                       filterName = model.groups[ conflict.group ].getNamePrefix() + conflict.filter;
+                                                       filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter );
                                                        filter = model.getItemByName( filterName );
 
                                                        // Rename
                this.groups = {};
 
                filters.forEach( function ( data ) {
-                       var group = data.name;
+                       var i,
+                               group = data.name;
 
                        if ( !model.groups[ group ] ) {
                                model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, {
                                        }
                                } );
                        }
+                       model.groups[ group ].initializeFilters( data.filters, data.default );
+                       items = items.concat( model.groups[ group ].getItems() );
 
+                       // Prepare conflicts
                        if ( data.conflicts ) {
+                               // Group conflicts
                                groupConflictMap[ group ] = data.conflicts;
                        }
 
                        for ( i = 0; i < data.filters.length; i++ ) {
-                               data.filters[ i ].subset = data.filters[ i ].subset || [];
-                               data.filters[ i ].subset = data.filters[ i ].subset.map( function ( el ) {
-                                       return el.filter;
-                               } );
-
-                               filterItem = new mw.rcfilters.dm.FilterItem( data.filters[ i ].name, model.groups[ group ], {
-                                       group: group,
-                                       label: mw.msg( data.filters[ i ].label ),
-                                       description: mw.msg( data.filters[ i ].description ),
-                                       cssClass: data.filters[ i ].cssClass
-                               } );
-
-                               if ( data.filters[ i ].subset ) {
-                                       subsetNames = [];
-                                       data.filters[ i ].subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
-                                               var subsetName = model.groups[ group ].getNamePrefix() + subsetFilterName;
-                                               // For convenience, we should store each filter's "supersets" -- these are
-                                               // the filters that have that item in their subset list. This will just
-                                               // make it easier to go through whether the item has any other items
-                                               // that affect it (and are selected) at any given time
-                                               supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
-                                               addArrayElementsUnique(
-                                                       supersetMap[ subsetName ],
-                                                       filterItem.getName()
-                                               );
-
-                                               // Translate subset param name to add the group name, so we
-                                               // get consistent naming. We know that subsets are only within
-                                               // the same group
-                                               subsetNames.push( subsetName );
-                                       } );
-
-                                       // Set translated subset
-                                       filterItem.setSubset( subsetNames );
-                               }
-
-                               // Store conflicts
+                               // Filter conflicts
                                if ( data.filters[ i ].conflicts ) {
+                                       filterItem = model.groups[ group ].getItemByParamName( data.filters[ i ].name );
                                        filterConflictMap[ filterItem.getName() ] = data.filters[ i ].conflicts;
                                }
-
-                               if ( data.type === 'send_unselected_if_any' ) {
-                                       // Store the default parameter state
-                                       // For this group type, parameter values are direct
-                                       model.defaultParams[ data.filters[ i ].name ] = Number( !!data.filters[ i ].default );
-                               }
-
-                               model.groups[ group ].addItems( filterItem );
-                               items.push( filterItem );
-                       }
-
-                       if ( data.type === 'string_options' ) {
-                               // Store the default parameter group state
-                               // For this group, the parameter is group name and value is the names
-                               // of selected items
-                               model.defaultParams[ group ] = model.sanitizeStringOptionGroup(
-                                       group,
-                                       data.default ?
-                                               data.default.split( model.groups[ group ].getSeparator() ) :
-                                               []
-                               ).join( model.groups[ group ].getSeparator() );
                        }
                } );
 
-               // Add items to the model
+               // Add item references to the model, for lookup
                this.addItems( items );
 
                // Expand conflicts
                        model.groups[ group ].setConflicts( conflicts );
                } );
 
-               items.forEach( function ( filterItem ) {
-                       // Apply the superset map
-                       filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
-
-                       // set conflicts for item
-                       if ( filterConflictResult[ filterItem.getName() ] ) {
-                               filterItem.setConflicts( filterConflictResult[ filterItem.getName() ] );
-                       }
+               // Set conflicts for items
+               $.each( filterConflictResult, function ( filterName, conflicts ) {
+                       var filterItem = model.getItemByName( filterName );
+                       // set conflicts for items in the group
+                       filterItem.setConflicts( conflicts );
                } );
 
                // Create a map between known parameters and their models
                        }
                } );
 
+               // Finish initialization
                this.emit( 'initialize' );
        };
 
        };
 
        /**
-        * Get the default parameters object
+        * Get an object representing default parameters state
         *
         * @return {Object} Default parameter values
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () {
-               return this.defaultParams;
+               var result = {};
+
+               $.each( this.groups, function ( name, model ) {
+                       result = $.extend( true, {}, result, model.getDefaultParams() );
+               } );
+
+               return result;
        };
 
        /**
                return result;
        };
 
+       /**
+        * This is the opposite of the #getParametersFromFilters method; this goes over
+        * the given parameters and translates into a selected/unselected value in the filters.
+        *
+        * @param {Object} params Parameters query object
+        * @return {Object} Filter state object
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
+               var groupMap = {},
+                       model = this,
+                       result = {};
+
+               // Go over the given parameters, break apart to groupings
+               // The resulting object represents the group with its parameter
+               // values. For example:
+               // {
+               //    group1: {
+               //       param1: "1",
+               //       param2: "0",
+               //       param3: "1"
+               //    },
+               //    group2: "param4|param5"
+               // }
+               $.each( params, function ( paramName, paramValue ) {
+                       var itemOrGroup = model.parameterMap[ paramName ];
+
+                       if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) {
+                               groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {};
+                               groupMap[ itemOrGroup.getGroupName() ][ itemOrGroup.getParamName() ] = paramValue;
+                       } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) {
+                               // This parameter represents a group (values are the filters)
+                               // this is equivalent to checking if the group is 'string_options'
+                               groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {};
+                               groupMap[ itemOrGroup.getName() ] = paramValue;
+                       }
+               } );
+
+               // Go over all groups, so we make sure we get the complete output
+               // even if the parameters don't include a certain group
+               $.each( this.groups, function ( groupName, groupModel ) {
+                       result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) );
+               } );
+
+               return result;
+       };
+
        /**
         * Get the highlight parameters based on current filter configuration
         *
         * @return {string[]} Array of valid values
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
-               var result = [],
-                       validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
-                               return filterItem.getParamName();
-                       } );
-
-               if ( valueArray.indexOf( 'all' ) > -1 ) {
-                       // If anywhere in the values there's 'all', we
-                       // treat it as if only 'all' was selected.
-                       // Example: param=valid1,valid2,all
-                       // Result: param=all
-                       return [ 'all' ];
-               }
-
-               // Get rid of any dupe and invalid parameter, only output
-               // valid ones
-               // Example: param=valid1,valid2,invalid1,valid1
-               // Result: param=valid1,valid2
-               valueArray.forEach( function ( value ) {
-                       if (
-                               validNames.indexOf( value ) > -1 &&
-                               result.indexOf( value ) === -1
-                       ) {
-                               result.push( value );
-                       }
+               var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
+                       return filterItem.getParamName();
                } );
 
-               return result;
+               return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames );
        };
 
        /**
                if ( this.defaultFiltersEmpty !== null ) {
                        // We only need to do this test once,
                        // because defaults are set once per session
-                       defaultFilters = this.getFiltersFromParameters();
+                       defaultFilters = this.getFiltersFromParameters( this.getDefaultParams() );
                        this.defaultFiltersEmpty = Object.keys( defaultFilters ).every( function ( filterName ) {
                                return !defaultFilters[ filterName ];
                        } );
                return this.defaultFiltersEmpty;
        };
 
-       /**
-        * This is the opposite of the #getParametersFromFilters method; this goes over
-        * the given parameters and translates into a selected/unselected value in the filters.
-        *
-        * @param {Object} params Parameters query object
-        * @return {Object} Filter state object
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
-               var i,
-                       groupMap = {},
-                       model = this,
-                       base = this.getDefaultParams(),
-                       result = {};
-
-               params = $.extend( {}, base, params );
-
-               // Go over the given parameters
-               $.each( params, function ( paramName, paramValue ) {
-                       var itemOrGroup = model.parameterMap[ paramName ];
-
-                       if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) {
-                               // Mark the group if it has any items that are selected
-                               groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {};
-                               groupMap[ itemOrGroup.getGroupName() ].hasSelected = (
-                                       groupMap[ itemOrGroup.getGroupName() ].hasSelected ||
-                                       !!Number( paramValue )
-                               );
-
-                               // Add filters
-                               groupMap[ itemOrGroup.getGroupName() ].filters = groupMap[ itemOrGroup.getGroupName() ].filters || [];
-                               groupMap[ itemOrGroup.getGroupName() ].filters.push( itemOrGroup );
-                       } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) {
-                               groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {};
-                               // This parameter represents a group (values are the filters)
-                               // this is equivalent to checking if the group is 'string_options'
-                               groupMap[ itemOrGroup.getName() ].filters = itemOrGroup.getItems();
-                       }
-               } );
-
-               // Now that we know the groups' selection states, we need to go over
-               // the filters in the groups and mark their selected states appropriately
-               $.each( groupMap, function ( group, data ) {
-                       var paramValues, filterItem,
-                               allItemsInGroup = data.filters;
-
-                       if ( model.groups[ group ].getType() === 'send_unselected_if_any' ) {
-                               for ( i = 0; i < allItemsInGroup.length; i++ ) {
-                                       filterItem = allItemsInGroup[ i ];
-
-                                       result[ filterItem.getName() ] = groupMap[ filterItem.getGroupName() ].hasSelected ?
-                                               // Flip the definition between the parameter
-                                               // state and the filter state
-                                               // This is what the 'toggleSelected' value of the filter is
-                                               !Number( params[ filterItem.getParamName() ] ) :
-                                               // Otherwise, there are no selected items in the
-                                               // group, which means the state is false
-                                               false;
-                               }
-                       } else if ( model.groups[ group ].getType() === 'string_options' ) {
-                               paramValues = model.sanitizeStringOptionGroup(
-                                       group,
-                                       params[ group ].split(
-                                               model.groups[ group ].getSeparator()
-                                       )
-                               );
-
-                               for ( i = 0; i < allItemsInGroup.length; i++ ) {
-                                       filterItem = allItemsInGroup[ i ];
-
-                                       result[ filterItem.getName() ] = (
-                                                       // If it is the word 'all'
-                                                       paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
-                                                       // All values are written
-                                                       paramValues.length === model.groups[ group ].getItemCount()
-                                               ) ?
-                                               // All true (either because all values are written or the term 'all' is written)
-                                               // is the same as all filters set to true
-                                               true :
-                                               // Otherwise, the filter is selected only if it appears in the parameter values
-                                               paramValues.indexOf( filterItem.getParamName() ) > -1;
-                               }
-                       }
-               } );
-
-               return result;
-       };
-
        /**
         * Get the item that matches the given name
         *
index 3ddb5a0..8cea27e 100644 (file)
@@ -1,3 +1,45 @@
 ( function ( mw ) {
-       mw.rcfilters = { dm: {}, ui: {} };
+       mw.rcfilters = {
+               dm: {},
+               ui: {},
+               utils: {
+                       addArrayElementsUnique: function ( arr, elements ) {
+                               elements = Array.isArray( elements ) ? elements : [ elements ];
+
+                               elements.forEach( function ( element ) {
+                                       if ( arr.indexOf( element ) === -1 ) {
+                                               arr.push( element );
+                                       }
+                               } );
+
+                               return arr;
+                       },
+                       normalizeParamOptions: function ( givenOptions, legalOptions ) {
+                               var result = [];
+
+                               if ( givenOptions.indexOf( 'all' ) > -1 ) {
+                                       // If anywhere in the values there's 'all', we
+                                       // treat it as if only 'all' was selected.
+                                       // Example: param=valid1,valid2,all
+                                       // Result: param=all
+                                       return [ 'all' ];
+                               }
+
+                               // Get rid of any dupe and invalid parameter, only output
+                               // valid ones
+                               // Example: param=valid1,valid2,invalid1,valid1
+                               // Result: param=valid1,valid2
+                               givenOptions.forEach( function ( value ) {
+                                       if (
+                                               legalOptions.indexOf( value ) > -1 &&
+                                               result.indexOf( value ) === -1
+                                       ) {
+                                               result.push( value );
+                                       }
+                               } );
+
+                               return result;
+                       }
+               }
+       };
 }( mediaWiki ) );
index bc266fb..c61c288 100644 (file)
                );
        } );
 
+       QUnit.test( 'Default filters', function ( assert ) {
+               var definition = [ {
+                               name: 'group1',
+                               title: 'Group 1',
+                               type: 'send_unselected_if_any',
+                               filters: [
+                                       {
+                                               name: 'hidefilter1',
+                                               label: 'Show filter 1',
+                                               description: 'Description of Filter 1 in Group 1',
+                                               default: true
+                                       },
+                                       {
+                                               name: 'hidefilter2',
+                                               label: 'Show filter 2',
+                                               description: 'Description of Filter 2 in Group 1'
+                                       },
+                                       {
+                                               name: 'hidefilter3',
+                                               label: 'Show filter 3',
+                                               description: 'Description of Filter 3 in Group 1',
+                                               default: true
+                                       }
+                               ]
+                       }, {
+                               name: 'group2',
+                               title: 'Group 2',
+                               type: 'send_unselected_if_any',
+                               filters: [
+                                       {
+                                               name: 'hidefilter4',
+                                               label: 'Show filter 4',
+                                               description: 'Description of Filter 1 in Group 2'
+                                       },
+                                       {
+                                               name: 'hidefilter5',
+                                               label: 'Show filter 5',
+                                               description: 'Description of Filter 2 in Group 2',
+                                               default: true
+                                       },
+                                       {
+                                               name: 'hidefilter6',
+                                               label: 'Show filter 6',
+                                               description: 'Description of Filter 3 in Group 2'
+                                       }
+                               ]
+                       }, {
+
+                               name: 'group3',
+                               title: 'Group 3',
+                               type: 'string_options',
+                               separator: ',',
+                               default: 'filter8',
+                               filters: [
+                                       {
+                                               name: 'filter7',
+                                               label: 'Group 3: Filter 1',
+                                               description: 'Description of Filter 1 in Group 3'
+                                       },
+                                       {
+                                               name: 'filter8',
+                                               label: 'Group 3: Filter 2',
+                                               description: 'Description of Filter 2 in Group 3'
+                                       },
+                                       {
+                                               name: 'filter9',
+                                               label: 'Group 3: Filter 3',
+                                               description: 'Description of Filter 3 in Group 3'
+                                       }
+                               ]
+                       } ],
+                       model = new mw.rcfilters.dm.FiltersViewModel();
+
+               model.initializeFilters( definition );
+
+               // Empty query = only default values
+               assert.deepEqual(
+                       model.getDefaultParams(),
+                       {
+                               hidefilter1: 1,
+                               hidefilter2: 0,
+                               hidefilter3: 1,
+                               hidefilter4: 0,
+                               hidefilter5: 1,
+                               hidefilter6: 0,
+                               group3: 'filter8'
+                       },
+                       'Default parameters are stored properly per filter and group'
+               );
+       } );
+
        QUnit.test( 'Finding matching filters', function ( assert ) {
                var matches,
                        definition = [ {
                                        }
                                ]
                        } ],
-                       defaultFilterRepresentation = {
-                               // Group 1 and 2, "send_unselected_if_any", the values of the filters are "flipped" from the values of the parameters
+                       baseFilterRepresentation = {
                                group1__hidefilter1: false,
-                               group1__hidefilter2: true,
+                               group1__hidefilter2: false,
                                group1__hidefilter3: false,
-                               group2__hidefilter4: true,
+                               group2__hidefilter4: false,
                                group2__hidefilter5: false,
-                               group2__hidefilter6: true,
-                               // Group 3, "string_options", default values correspond to parameters and filters
+                               group2__hidefilter6: false,
                                group3__filter7: false,
-                               group3__filter8: true,
+                               group3__filter8: false,
                                group3__filter9: false
                        },
                        model = new mw.rcfilters.dm.FiltersViewModel();
                // Empty query = only default values
                assert.deepEqual(
                        model.getFiltersFromParameters( {} ),
-                       defaultFilterRepresentation,
-                       'Empty parameter query results in filters in initial default state'
+                       baseFilterRepresentation,
+                       'Empty parameter query results in an object representing all filters set to false'
                );
 
                assert.deepEqual(
                        model.getFiltersFromParameters( {
                                hidefilter2: '1'
                        } ),
-                       $.extend( {}, defaultFilterRepresentation, {
-                               group1__hidefilter1: false, // The text is "show filter 1"
+                       $.extend( {}, baseFilterRepresentation, {
+                               group1__hidefilter1: true, // The text is "show filter 1"
                                group1__hidefilter2: false, // The text is "show filter 2"
-                               group1__hidefilter3: false // The text is "show filter 3"
+                               group1__hidefilter3: true // The text is "show filter 3"
                        } ),
                        'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
                );
                                hidefilter2: '1',
                                hidefilter3: '1'
                        } ),
-                       $.extend( {}, defaultFilterRepresentation, {
+                       $.extend( {}, baseFilterRepresentation, {
                                group1__hidefilter1: false, // The text is "show filter 1"
                                group1__hidefilter2: false, // The text is "show filter 2"
                                group1__hidefilter3: false // The text is "show filter 3"
                );
 
                // The result here is ignoring the first toggleFiltersSelected call
-               // We should receive default values + hidefilter6 as false
                assert.deepEqual(
                        model.getSelectedState(),
-                       $.extend( {}, defaultFilterRepresentation, {
-                               group2__hidefilter5: false,
+                       $.extend( {}, baseFilterRepresentation, {
+                               group2__hidefilter4: true,
+                               group2__hidefilter5: true,
                                group2__hidefilter6: false
                        } ),
                        'getFiltersFromParameters does not care about previous or existing state.'
                model = new mw.rcfilters.dm.FiltersViewModel();
                model.initializeFilters( definition );
 
-               model.toggleFiltersSelected(
-                       model.getFiltersFromParameters( {
-                               hidefilter1: '0'
-                       } )
-               );
-               model.toggleFiltersSelected(
-                       model.getFiltersFromParameters( {
-                               hidefilter1: '1'
-                       } )
-               );
-
-               // Simulates minor edits being hidden in preferences, then unhidden via URL
-               // override.
-               assert.deepEqual(
-                       model.getSelectedState(),
-                       defaultFilterRepresentation,
-                       'After checking and then unchecking a \'send_unselected_if_any\' filter (without touching other filters in that group), results are default'
-               );
-
                model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7'
                );
                assert.deepEqual(
                        model.getSelectedState(),
-                       $.extend( {}, defaultFilterRepresentation, {
+                       $.extend( {}, baseFilterRepresentation, {
                                group3__filter7: true,
                                group3__filter8: false,
                                group3__filter9: false
                );
                assert.deepEqual(
                        model.getSelectedState(),
-                       $.extend( {}, defaultFilterRepresentation, {
+                       $.extend( {}, baseFilterRepresentation, {
                                group3__filter7: true,
                                group3__filter8: true,
                                group3__filter9: false
                );
                assert.deepEqual(
                        model.getSelectedState(),
-                       $.extend( {}, defaultFilterRepresentation, {
+                       $.extend( {}, baseFilterRepresentation, {
                                group3__filter7: true,
                                group3__filter8: true,
                                group3__filter9: true
                );
                assert.deepEqual(
                        model.getSelectedState(),
-                       $.extend( {}, defaultFilterRepresentation, {
+                       $.extend( {}, baseFilterRepresentation, {
                                group3__filter7: true,
                                group3__filter8: true,
                                group3__filter9: true
                );
                assert.deepEqual(
                        model.getSelectedState(),
-                       $.extend( {}, defaultFilterRepresentation, {
+                       $.extend( {}, baseFilterRepresentation, {
                                group3__filter7: true,
                                group3__filter8: false,
                                group3__filter9: true