Merge "Follow-up 0b3ea6e1ab: use more flexible regex for timestamp in ChangesListSpec...
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 29 Jul 2017 01:14:17 +0000 (01:14 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 29 Jul 2017 01:14:17 +0000 (01:14 +0000)
20 files changed:
composer.json
includes/Preferences.php
includes/installer/MssqlUpdater.php
includes/installer/MysqlUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
maintenance/mssql/archives/patch-add-3d.sql [new file with mode: 0644]
maintenance/mssql/tables.sql
maintenance/postgres/archives/patch-add-3d.sql [new file with mode: 0644]
maintenance/postgres/tables.sql
maintenance/sqlite/archives/patch-add-3d.sql [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitButtonWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DateButtonWidget.js
tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js

index d04e9f5..aefc158 100644 (file)
@@ -37,7 +37,7 @@
                "wikimedia/html-formatter": "1.0.1",
                "wikimedia/ip-set": "1.1.0",
                "wikimedia/php-session-serializer": "1.0.4",
-               "wikimedia/relpath": "1.0.3",
+               "wikimedia/relpath": "2.0.0",
                "wikimedia/remex-html": "1.0.1",
                "wikimedia/running-stat": "1.1.0",
                "wikimedia/scoped-callback": "1.0.0",
index 7efbef1..de6d681 100644 (file)
@@ -918,6 +918,9 @@ class Preferences {
                $defaultPreferences['rcfilters-saved-queries'] = [
                        'type' => 'api',
                ];
+               $defaultPreferences['rcfilters-rclimit'] = [
+                       'type' => 'api',
+               ];
 
                if ( $config->get( 'RCWatchCategoryMembership' ) ) {
                        $defaultPreferences['hidecategorization'] = [
index 1a9915d..b4de44d 100644 (file)
@@ -101,6 +101,9 @@ class MssqlUpdater extends DatabaseUpdater {
                        [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ],
                        [ 'dropIndex', 'oldimage', 'oi_name_archive_name',
                                'patch-alter-table-oldimage.sql' ],
+
+                       // 1.30
+                       [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
                ];
        }
 
index adfe2f6..b4ae1dd 100644 (file)
@@ -301,6 +301,8 @@ class MysqlUpdater extends DatabaseUpdater {
                        [ 'dropIndex', 'user_groups', 'ug_user_group', 'patch-user_groups-primary-key.sql' ],
                        [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ],
                        [ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ],
+
+                       // 1.30
                        [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
                ];
        }
index 0172f1a..d8db6a2 100644 (file)
@@ -452,6 +452,9 @@ class PostgresUpdater extends DatabaseUpdater {
                        [ 'addPgIndex', 'externallinks', 'el_from_index_60', '( el_from, el_index_60, el_id )' ],
                        [ 'addPgField', 'user_groups', 'ug_expiry', "TIMESTAMPTZ NULL" ],
                        [ 'addPgIndex', 'user_groups', 'user_groups_expiry', '( ug_expiry )' ],
+
+                       // 1.30
+                       [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
                ];
        }
 
index 9c90283..46e3e7e 100644 (file)
@@ -165,6 +165,9 @@ class SqliteUpdater extends DatabaseUpdater {
                        [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ],
                        [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ],
                        [ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ],
+
+                       // 1.30
+                       [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
                ];
        }
 
diff --git a/maintenance/mssql/archives/patch-add-3d.sql b/maintenance/mssql/archives/patch-add-3d.sql
new file mode 100644 (file)
index 0000000..51d2775
--- /dev/null
@@ -0,0 +1,27 @@
+ALTER TABLE /*$wgDBprefix*/image
+       DROP CONSTRAINT img_media_type_ckc;
+
+ALTER TABLE /*$wgDBprefix*/image
+       ADD CONSTRAINT img_media_type_ckc
+       CHECK (img_media_type IN("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D"));
+
+ALTER TABLE /*$wgDBprefix*/oldimage
+       DROP CONSTRAINT oi_media_type_ckc;
+
+ALTER TABLE /*$wgDBprefix*/oldimage
+       ADD CONSTRAINT oi_media_type_ckc
+       CHECK (oi_media_type IN("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D"));
+
+ALTER TABLE /*$wgDBprefix*/filearchive
+       DROP CONSTRAINT fa_media_type_ckc;
+
+ALTER TABLE /*$wgDBprefix*/filearchive
+       ADD CONSTRAINT fa_media_type_ckc
+       CHECK (fa_media_type IN("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D"));
+
+ALTER TABLE /*$wgDBprefix*/uploadstash
+       DROP CONSTRAINT us_media_type_ckc;
+
+ALTER TABLE /*$wgDBprefix*/uploadstash
+       ADD CONSTRAINT us_media_type_ckc
+       CHECK (us_media_type IN("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D"));
index 3babb39..2a67294 100644 (file)
@@ -610,7 +610,7 @@ CREATE TABLE /*_*/image (
   img_sha1 nvarchar(32) NOT NULL default '',
 
   CONSTRAINT img_major_mime_ckc check (img_major_mime IN('unknown', 'application', 'audio', 'image', 'text', 'video', 'message', 'model', 'multipart', 'chemical')),
-  CONSTRAINT img_media_type_ckc check (img_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE'))
+  CONSTRAINT img_media_type_ckc check (img_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE','3D'))
 );
 
 CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
@@ -656,7 +656,7 @@ CREATE TABLE /*_*/oldimage (
   oi_sha1 nvarchar(32) NOT NULL default '',
 
   CONSTRAINT oi_major_mime_ckc check (oi_major_mime IN('unknown', 'application', 'audio', 'image', 'text', 'video', 'message', 'model', 'multipart', 'chemical')),
-  CONSTRAINT oi_media_type_ckc check (oi_media_type IN('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE'))
+  CONSTRAINT oi_media_type_ckc check (oi_media_type IN('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE','3D'))
 );
 
 CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
@@ -715,7 +715,7 @@ CREATE TABLE /*_*/filearchive (
   fa_sha1 nvarchar(32) NOT NULL default '',
 
   CONSTRAINT fa_major_mime_ckc check (fa_major_mime in('unknown', 'application', 'audio', 'image', 'text', 'video', 'message', 'model', 'multipart', 'chemical')),
-  CONSTRAINT fa_media_type_ckc check (fa_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE'))
+  CONSTRAINT fa_media_type_ckc check (fa_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE','3D'))
 );
 
 -- pick out by image name
@@ -776,7 +776,7 @@ CREATE TABLE /*_*/uploadstash (
   us_image_height int,
   us_image_bits smallint,
 
-  CONSTRAINT us_media_type_ckc check (us_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE'))
+  CONSTRAINT us_media_type_ckc check (us_media_type in('UNKNOWN', 'BITMAP', 'DRAWING', 'AUDIO', 'VIDEO', 'MULTIMEDIA', 'OFFICE', 'TEXT', 'EXECUTABLE', 'ARCHIVE', '3D'))
 );
 
 -- sometimes there's a delete for all of a user's stuff.
diff --git a/maintenance/postgres/archives/patch-add-3d.sql b/maintenance/postgres/archives/patch-add-3d.sql
new file mode 100644 (file)
index 0000000..f892755
--- /dev/null
@@ -0,0 +1 @@
+ALTER TYPE media_type ADD VALUE '3D';
index e19c447..03fd03a 100644 (file)
@@ -393,7 +393,7 @@ CREATE INDEX fa_nouser    ON filearchive (fa_deleted_user);
 CREATE INDEX fa_sha1      ON filearchive (fa_sha1);
 
 CREATE SEQUENCE uploadstash_us_id_seq;
-CREATE TYPE media_type AS ENUM ('UNKNOWN','BITMAP','DRAWING','AUDIO','VIDEO','MULTIMEDIA','OFFICE','TEXT','EXECUTABLE','ARCHIVE');
+CREATE TYPE media_type AS ENUM ('UNKNOWN','BITMAP','DRAWING','AUDIO','VIDEO','MULTIMEDIA','OFFICE','TEXT','EXECUTABLE','ARCHIVE','3D');
 
 CREATE TABLE uploadstash (
   us_id           INTEGER PRIMARY KEY NOT NULL DEFAULT nextval('uploadstash_us_id_seq'),
diff --git a/maintenance/sqlite/archives/patch-add-3d.sql b/maintenance/sqlite/archives/patch-add-3d.sql
new file mode 100644 (file)
index 0000000..10d74fb
--- /dev/null
@@ -0,0 +1,249 @@
+-- image
+
+CREATE TABLE /*_*/image_tmp (
+  -- Filename.
+  -- This is also the title of the associated description page,
+  -- which will be in namespace 6 (NS_FILE).
+  img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+
+  -- File size in bytes.
+  img_size int unsigned NOT NULL default 0,
+
+  -- For images, size in pixels.
+  img_width int NOT NULL default 0,
+  img_height int NOT NULL default 0,
+
+  -- Extracted Exif metadata stored as a serialized PHP array.
+  img_metadata mediumblob NOT NULL,
+
+  -- For images, bits per pixel if known.
+  img_bits int NOT NULL default 0,
+
+  -- Media type as defined by the MEDIATYPE_xxx constants
+  img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+
+  -- major part of a MIME media type as defined by IANA
+  -- see https://www.iana.org/assignments/media-types/
+  -- for "chemical" cf. http://dx.doi.org/10.1021/ci9803233 by the ACS
+  img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+
+  -- minor part of a MIME media type as defined by IANA
+  -- the minor parts are not required to adher to any standard
+  -- but should be consistent throughout the database
+  -- see https://www.iana.org/assignments/media-types/
+  img_minor_mime varbinary(100) NOT NULL default "unknown",
+
+  -- Description field as entered by the uploader.
+  -- This is displayed in image upload history and logs.
+  img_description varbinary(767) NOT NULL,
+
+  -- user_id and user_name of uploader.
+  img_user int unsigned NOT NULL default 0,
+  img_user_text varchar(255) binary NOT NULL,
+
+  -- Time of the upload.
+  img_timestamp varbinary(14) NOT NULL default '',
+
+  -- SHA-1 content hash in base-36
+  img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT INTO /*_*/image_tmp
+       SELECT img_name, img_size, img_width, img_height, img_metadata, img_bits,
+       img_media_type, img_major_mime, img_minor_mime, img_description,
+       img_user, img_user_text, img_timestamp, img_sha1
+               FROM /*_*/image;
+
+DROP TABLE /*_*/image;
+
+ALTER TABLE /*_*/image_tmp RENAME TO /*_*/image;
+
+-- Used by Special:Newimages and ApiQueryAllImages
+CREATE INDEX /*i*/img_user_timestamp ON /*_*/image (img_user,img_timestamp);
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+-- Used by Special:ListFiles for sort-by-size
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+-- Used by Special:Newimages and Special:ListFiles
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+-- Used in API and duplicate search
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10));
+-- Used to get media of one type
+CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime);
+
+-- oldimage
+
+CREATE TABLE /*_*/oldimage_tmp (
+  -- Base filename: key to image.img_name
+  oi_name varchar(255) binary NOT NULL default '',
+
+  -- Filename of the archived file.
+  -- This is generally a timestamp and '!' prepended to the base name.
+  oi_archive_name varchar(255) binary NOT NULL default '',
+
+  -- Other fields as in image...
+  oi_size int unsigned NOT NULL default 0,
+  oi_width int NOT NULL default 0,
+  oi_height int NOT NULL default 0,
+  oi_bits int NOT NULL default 0,
+  oi_description varbinary(767) NOT NULL,
+  oi_user int unsigned NOT NULL default 0,
+  oi_user_text varchar(255) binary NOT NULL,
+  oi_timestamp binary(14) NOT NULL default '',
+
+  oi_metadata mediumblob NOT NULL,
+  oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+  oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+  oi_minor_mime varbinary(100) NOT NULL default "unknown",
+  oi_deleted tinyint unsigned NOT NULL default 0,
+  oi_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT INTO /*_*/oldimage_tmp
+       SELECT oi_name, oi_archive_name, oi_size, oi_width, oi_height, oi_bits,
+       oi_description, oi_user, oi_user_text, oi_timestamp, oi_metadata,
+       oi_media_type, oi_major_mime, oi_minor_mime, oi_deleted, oi_sha1
+               FROM /*_*/oldimage;
+
+DROP TABLE /*_*/oldimage;
+
+ALTER TABLE oldimage_tmp RENAME TO /*_*/oldimage;
+
+CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
+-- oi_archive_name truncated to 14 to avoid key length overflow
+CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
+CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10));
+
+-- filearchive
+
+CREATE TABLE /*_*/filearchive_tmp (
+  -- Unique row id
+  fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+
+  -- Original base filename; key to image.img_name, page.page_title, etc
+  fa_name varchar(255) binary NOT NULL default '',
+
+  -- Filename of archived file, if an old revision
+  fa_archive_name varchar(255) binary default '',
+
+  -- Which storage bin (directory tree or object store) the file data
+  -- is stored in. Should be 'deleted' for files that have been deleted;
+  -- any other bin is not yet in use.
+  fa_storage_group varbinary(16),
+
+  -- SHA-1 of the file contents plus extension, used as a key for storage.
+  -- eg 8f8a562add37052a1848ff7771a2c515db94baa9.jpg
+  --
+  -- If NULL, the file was missing at deletion time or has been purged
+  -- from the archival storage.
+  fa_storage_key varbinary(64) default '',
+
+  -- Deletion information, if this file is deleted.
+  fa_deleted_user int,
+  fa_deleted_timestamp binary(14) default '',
+  fa_deleted_reason varbinary(767) default '',
+
+  -- Duped fields from image
+  fa_size int unsigned default 0,
+  fa_width int default 0,
+  fa_height int default 0,
+  fa_metadata mediumblob,
+  fa_bits int default 0,
+  fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+  fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") default "unknown",
+  fa_minor_mime varbinary(100) default "unknown",
+  fa_description varbinary(767),
+  fa_user int unsigned default 0,
+  fa_user_text varchar(255) binary,
+  fa_timestamp binary(14) default '',
+
+  -- Visibility of deleted revisions, bitfield
+  fa_deleted tinyint unsigned NOT NULL default 0,
+
+  -- sha1 hash of file content
+  fa_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT INTO /*_*/filearchive_tmp
+       SELECT fa_id, fa_name, fa_archive_name, fa_storage_group, fa_storage_key, fa_deleted_user, fa_deleted_timestamp,
+       fa_deleted_reason, fa_size, fa_width, fa_height, fa_metadata, fa_bits, fa_media_type, fa_major_mime,
+       fa_minor_mime, fa_description, fa_user, fa_user_text, fa_timestamp, fa_deleted, fa_sha1
+               FROM /*_*/filearchive;
+
+DROP TABLE /*_*/filearchive;
+
+ALTER TABLE /*_*/filearchive_tmp RENAME TO /*_*/filearchive;
+
+-- pick out by image name
+CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp);
+-- pick out dupe files
+CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key);
+-- sort by deletion time
+CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
+-- sort by uploader
+CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+-- find file by sha1, 10 bytes will be enough for hashes to be indexed
+CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10));
+
+-- uploadstash
+
+CREATE TABLE /*_*/uploadstash_tmp (
+  us_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+
+  -- the user who uploaded the file.
+  us_user int unsigned NOT NULL,
+
+  -- file key. this is how applications actually search for the file.
+  -- this might go away, or become the primary key.
+  us_key varchar(255) NOT NULL,
+
+  -- the original path
+  us_orig_path varchar(255) NOT NULL,
+
+  -- the temporary path at which the file is actually stored
+  us_path varchar(255) NOT NULL,
+
+  -- which type of upload the file came from (sometimes)
+  us_source_type varchar(50),
+
+  -- the date/time on which the file was added
+  us_timestamp varbinary(14) NOT NULL,
+
+  us_status varchar(50) NOT NULL,
+
+  -- chunk counter starts at 0, current offset is stored in us_size
+  us_chunk_inx int unsigned NULL,
+
+  -- Serialized file properties from FSFile::getProps()
+  us_props blob,
+
+  -- file size in bytes
+  us_size int unsigned NOT NULL,
+  -- this hash comes from FSFile::getSha1Base36(), and is 31 characters
+  us_sha1 varchar(31) NOT NULL,
+  us_mime varchar(255),
+  -- Media type as defined by the MEDIATYPE_xxx constants, should duplicate definition in the image table
+  us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+  -- image-specific properties
+  us_image_width int unsigned,
+  us_image_height int unsigned,
+  us_image_bits smallint unsigned
+
+) /*$wgDBTableOptions*/;
+
+INSERT INTO /*_*/uploadstash_tmp
+       SELECT us_id, us_user, us_key, us_orig_path, us_path, us_source_type,
+       us_timestamp, us_status, us_chunk_inx, us_props, us_size, us_sha1, us_mime,
+       us_media_type, us_image_width, us_image_height, us_image_bits
+               FROM /*_*/uploadstash;
+
+DROP TABLE uploadstash;
+
+ALTER TABLE /*_*/uploadstash_tmp RENAME TO /*_*/uploadstash;
+
+-- sometimes there's a delete for all of a user's stuff.
+CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user);
+-- pick out files by key, enforce key uniqueness
+CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key);
+-- the abandoned upload cleanup script needs this
+CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_timestamp);
index 7849cc2..5cca5d8 100644 (file)
@@ -11,6 +11,8 @@
         * @cfg {string} [type='send_unselected_if_any'] Group type
         * @cfg {string} [view='default'] Name of the display group this group
         *  is a part of.
+        * @cfg {boolean} [isSticky] This group is using a 'sticky' default; meaning
+        *  that every time a value is changed, it becomes the new default
         * @cfg {string} [title] Group title
         * @cfg {boolean} [hidden] This group is hidden from the regular menu views
         * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
@@ -38,6 +40,7 @@
                this.name = name;
                this.type = config.type || 'send_unselected_if_any';
                this.view = config.view || 'default';
+               this.sticky = !!config.isSticky;
                this.title = config.title || name;
                this.hidden = !!config.hidden;
                this.allowArbitrary = !!config.allowArbitrary;
@@ -90,7 +93,6 @@
                        var subsetNames = [],
                                filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, {
                                        group: model.getName(),
-                                       useDefaultAsBaseValue: !!filter.useDefaultAsBaseValue,
                                        label: filter.label || filter.name,
                                        description: filter.description || '',
                                        labelPrefixKey: model.labelPrefixKey,
                        // For this group, the parameter is the group name,
                        // and a single item can be selected: default or first item
                        this.defaultParams[ this.getName() ] = defaultParam;
-
-                       // Single option means there must be a single option
-                       // selected, so we have to either select the default
-                       // or select the first option
-                       this.selectItemByParamName( defaultParam );
                }
 
                // Store default filter state based on default params
                this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
 
                // Check for filters that should be initially selected by their default value
-               this.getItems().forEach( function ( item ) {
-                       if (
-                               item.isUsingDefaultAsBaseValue() &&
-                               (
-                                       // This setting can only be applied to these groups
-                                       // the other groups are way too complex for that
-                                       model.getType() === 'single_option' ||
-                                       model.getType() === 'boolean'
-                               )
-                       ) {
-                               // Apply selection
-                               item.toggleSelected( !!model.defaultFilters[ item.getName() ] );
-                       }
-               } );
+               if ( this.isSticky() ) {
+                       $.each( this.defaultFilters, function ( filterName, filterValue ) {
+                               model.getItemByName( filterName ).toggleSelected( filterValue );
+                       } );
+               }
+
+               // Verify that single_option group has at least one item selected
+               if (
+                       this.getType() === 'single_option' &&
+                       this.getSelectedItems().length === 0
+               ) {
+                       defaultParam = groupDefault !== undefined ?
+                               groupDefault : this.getItems()[ 0 ].getParamName();
+
+                       // Single option means there must be a single option
+                       // selected, so we have to either select the default
+                       // or select the first option
+                       this.selectItemByParamName( defaultParam );
+               }
        };
 
        /**
                        // Single option means there must be a single option
                        // selected, so we have to either select the default
                        // or select the first option
-                       this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] );
+                       this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
+                               this.getItems()[ 0 ];
                        this.currSelected.toggleSelected( true );
                        changed = true;
                }
                        this.active !== active ||
                        this.currSelected !== item
                ) {
+                       if ( this.isSticky() ) {
+                               // If this group is sticky, then change the default according to the
+                               // current selection.
+                               this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
+                       }
+
                        this.active = active;
                        this.currSelected = item;
 
                                // This means we have not been given a filter representation
                                // so we are building one based on current state
                                filterRepresentation[ item.getName() ] = item.isSelected();
-                       } else if ( !filterRepresentation[ item.getName() ] ) {
+                       } else if ( filterRepresentation[ item.getName() ] === undefined ) {
                                // We are given a filter representation, but we have to make
                                // sure that we fill in the missing filters if there are any
-                               // we will assume they are all falsey, unless they have
-                               // isUsingDefaultAsBaseValue, in which case they get their
-                               // default state
-                               if (
-                                       item.isUsingDefaultAsBaseValue() &&
-                                       (
-                                               // This setting can only be applied to these groups
-                                               // the other groups are way too complex for that
-                                               model.getType() === 'single_option' ||
-                                               model.getType() === 'boolean'
-                                       )
-                               ) {
+                               // we will assume they are all falsey
+                               if ( model.isSticky() ) {
                                        filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
                                } else {
                                        filterRepresentation[ item.getName() ] = false;
         * @return {Object} Filter representation
         */
        mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
-               var areAnySelected, paramValues, defaultValue, item, currentValue,
+               var areAnySelected, paramValues, item, currentValue,
                        oneWasSelected = false,
                        defaultParams = this.getDefaultParams(),
-                       defaultFilters = this.getDefaultFilters(),
                        expandedParams = $.extend( true, {}, paramRepresentation ),
                        model = this,
                        paramToFilterMap = {},
                        result = {};
 
+               if ( this.isSticky() ) {
+                       // If the group is sticky, check if all parameters are represented
+                       // and for those that aren't represented, add them with their default
+                       // values
+                       paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
+               }
+
                paramRepresentation = paramRepresentation || {};
                if (
                        this.getType() === 'send_unselected_if_any' ||
                        } );
 
                        $.each( expandedParams, function ( paramName, paramValue ) {
-                               var value = paramValue,
-                                       filterItem = paramToFilterMap[ paramName ];
+                               var filterItem = paramToFilterMap[ paramName ];
 
                                if ( model.getType() === 'send_unselected_if_any' ) {
                                        // Flip the definition between the parameter
                                                false;
                                } else if ( model.getType() === 'boolean' ) {
                                        // Straight-forward definition of state
-                                       if (
-                                               filterItem.isUsingDefaultAsBaseValue() &&
-                                               paramRepresentation[ filterItem.getParamName() ] === undefined
-                                       ) {
-                                               value = defaultParams[ filterItem.getParamName() ];
-                                       }
-                                       result[ filterItem.getName() ] = !!Number( value );
+                                       result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
                                }
                        } );
                } else if ( this.getType() === 'string_options' ) {
                } else if ( this.getType() === 'single_option' ) {
                        // There is parameter that fits a single filter and if not, get the default
                        this.getItems().forEach( function ( filterItem ) {
-                               var selected = false;
+                               var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
 
-                               if (
-                                       filterItem.isUsingDefaultAsBaseValue() &&
-                                       paramRepresentation[ model.getName() ] === undefined
-                               ) {
-                                       selected = !!Number( paramRepresentation[ model.getName() ] );
-                               } else {
-                                       selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
-                               }
                                result[ filterItem.getName() ] = selected;
                                oneWasSelected = oneWasSelected || selected;
                        } );
                // 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 ) {
-                       if (
-                               (
-                                       // This setting can only be applied to these groups
-                                       // the other groups are way too complex for that
-                                       model.getType() === 'single_option' ||
-                                       model.getType() === 'boolean'
-                               ) &&
-                               result[ filterItem.getName() ] === undefined &&
-                               filterItem.isUsingDefaultAsBaseValue()
-                       ) {
-                               result[ filterItem.getName() ] = !!defaultFilters[ filterItem.getName() ];
+                       if ( result[ filterItem.getName() ] === undefined ) {
+                               result[ filterItem.getName() ] = false;
                        }
-                       oneWasSelected = oneWasSelected || !!result[ filterItem.getName() ];
                } );
 
                // Make sure that at least one option is selected in
                        this.getType() === 'single_option' &&
                        !oneWasSelected
                ) {
-                       defaultValue = this.getDefaultParams();
-                       item = this.getItemByParamName( defaultValue[ this.getName() ] );
+                       if ( defaultParams[ this.getName() ] ) {
+                               item = this.getItemByParamName( defaultParams[ this.getName() ] );
+                       } else {
+                               item = this.getItems()[ 0 ];
+                       }
                        result[ item.getName() ] = true;
                }
 
                return result;
        };
 
+       /**
+        * Get current selected state of all filter items in this group
+        *
+        * @return {Object} Selected state
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getSelectedState = function () {
+               var state = {};
+
+               this.getItems().forEach( function ( filterItem ) {
+                       state[ filterItem.getName() ] = filterItem.isSelected();
+               } );
+
+               return state;
+       };
+
        /**
         * Get item by its filter name
         *
        mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
                return this.fullCoverage;
        };
+
+       /**
+        * Check whether the group is defined as sticky default
+        *
+        * @return {boolean} Group is sticky default
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.isSticky = function () {
+               return this.sticky;
+       };
 }( mediaWiki ) );
index 57e618c..a6a22ef 100644 (file)
                return result;
        };
 
+       /**
+        * Get a parameter representation of all sticky parameters
+        *
+        * @return {Object} Sticky parameter values
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParams = function () {
+               var result = {};
+
+               $.each( this.groups, function ( name, model ) {
+                       if ( model.isSticky() ) {
+                               $.extend( true, result, model.getDefaultParams() );
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get a filter representation of all sticky parameters
+        *
+        * @return {Object} Sticky filters values
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getStickyFiltersState = function () {
+               var result = {};
+
+               $.each( this.groups, function ( name, model ) {
+                       if ( model.isSticky() ) {
+                               $.extend( true, result, model.getSelectedState() );
+                       }
+               } );
+
+               return result;
+       };
+
        /**
         * Analyze the groups and their filters and output an object representing
         * the state of the parameters they represent.
index 54a4dbe..aa82e21 100644 (file)
@@ -32,7 +32,6 @@
                this.namePrefix = config.namePrefix || 'item_';
                this.name = this.namePrefix + param;
 
-               this.useDefaultAsBaseValue = !!config.useDefaultAsBaseValue;
                this.label = config.label || this.name;
                this.labelPrefixKey = config.labelPrefixKey;
                this.description = config.description || '';
                return this.identifiers;
        };
 
-       /**
-        * Check whether the item uses its default state as a base value
-        *
-        * @return {boolean} Use default as base value
-        */
-       mw.rcfilters.dm.ItemModel.prototype.isUsingDefaultAsBaseValue = function () {
-               return this.useDefaultAsBaseValue;
-       };
-
        /**
         * Toggle the highlight feature on and off for this filter.
         * It only works if highlight is supported for this filter.
index d6dda1e..a7f3d23 100644 (file)
         *  the above structure.
         * @param {Object} [baseState] An object representing the base state
         *  so we can normalize the data
+        * @param {string[]} [ignoreFilters] Filters to ignore and remove from
+        *  the data
         * @fires initialize
         */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries, baseState ) {
+       mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries, baseState, ignoreFilters ) {
                var items = [],
                        defaultItem = null;
 
                savedQueries = savedQueries || {};
+               ignoreFilters = ignoreFilters || {};
 
                this.baseState = baseState;
 
                        // for existing users, who are only betalabs users at the moment.
                        normalizedData.highlights.highlight = !!Number( normalizedData.highlights.highlight );
 
+                       // Backwards-compat fix: Remove sticky parameters from the 'ignoreFilters' list
+                       ignoreFilters.forEach( function ( name ) {
+                               delete normalizedData.filters[ name ];
+                       } );
+
                        item = new mw.rcfilters.dm.SavedQueryItemModel(
                                id,
                                obj.label,
index 8cee0a8..73ff165 100644 (file)
@@ -32,7 +32,7 @@
         * @param {Object} [tagList] Tag definition
         */
        mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) {
-               var parsedSavedQueries,
+               var parsedSavedQueries, limitDefault,
                        controller = this,
                        views = {},
                        items = [],
                        };
                }
 
+               // Convert the default from the old preference
+               // since the limit preference actually affects more
+               // than just the RecentChanges page
+               limitDefault = Number( mw.user.options.get( 'rcfilters-rclimit', mw.user.options.get( 'rclimit', '50' ) ) );
+
                // Add parameter range operations
                views.range = {
                        groups: [
                                        allowArbitrary: true,
                                        validate: $.isNumeric,
                                        sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
-                                       'default': mw.user.options.get( 'rclimit' ),
+                                       'default': String( limitDefault ),
+                                       isSticky: true,
                                        filters: [ 50, 100, 250, 500 ].map( function ( num ) {
                                                return controller._createFilterDataFromNumber( num, num );
                                        } )
                                                        ( Number( i ) * 24 ).toFixed( 2 ) :
                                                        Number( i );
                                        },
-                                       'default': mw.user.options.get( 'rcdays' ),
+                                       'default': mw.user.options.get( 'rcdays', '30' ),
+                                       isSticky: true,
                                        filters: [
                                                // Hours (1, 2, 6, 12)
                                                0.04166, 0.0833, 0.25, 0.5,
                // can normalize them per each query item
                this.savedQueriesModel.initialize(
                        parsedSavedQueries,
-                       this._getBaseFilterState()
+                       this._getBaseFilterState(),
+                       // This is for backwards compatibility - delete all sticky filter states
+                       Object.keys( this.filtersModel.getStickyFiltersState() )
                );
 
                // Check whether we need to load defaults.
         */
        mw.rcfilters.Controller.prototype.resetToDefaults = function () {
                this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() );
+
                this.updateChangesList();
        };
 
        mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
                var queryID,
                        highlightedItems = {},
-                       highlightEnabled = this.filtersModel.isHighlightEnabled();
+                       highlightEnabled = this.filtersModel.isHighlightEnabled(),
+                       selectedState = this.filtersModel.getSelectedState();
 
                // Prepare highlights
                this.filtersModel.getHighlightedItems().forEach( function ( item ) {
                // These are filter states; highlight is stored as boolean
                highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
 
+               // Delete all sticky filters
+               this._deleteStickyValuesFromFilterState( selectedState );
+
                // Add item
                queryID = this.savedQueriesModel.addNewQuery(
                        label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
                        {
-                               filters: this.filtersModel.getSelectedState(),
+                               filters: selectedState,
                                highlights: highlightedItems,
                                invert: this.filtersModel.areNamespacesInverted()
                        }
                        highlights.highlight = highlights.highlights || highlights.highlight;
 
                        // Update model state from filters
-                       this.filtersModel.toggleFiltersSelected( data.filters );
+                       this.filtersModel.toggleFiltersSelected(
+                               // Merge filters with sticky values
+                               $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
+                       );
 
                        // Update namespace inverted property
                        this.filtersModel.toggleInvertedNamespaces( !!Number( data.invert ) );
         * @return {boolean} Query exists
         */
        mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () {
-               var highlightedItems = {};
+               var highlightedItems = {},
+                       selectedState = this.filtersModel.getSelectedState();
 
                // Prepare highlights of the current query
                this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
                } );
                highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
 
+               // Remove sticky filters
+               this._deleteStickyValuesFromFilterState( selectedState );
+
                return this.savedQueriesModel.findMatchingQuery(
                        {
-                               filters: this.filtersModel.getSelectedState(),
+                               filters: selectedState,
                                highlights: highlightedItems,
                                invert: this.filtersModel.areNamespacesInverted()
                        }
                );
        };
 
+       /**
+        * Delete sticky filters from given object
+        *
+        * @param {Object} filterState Filter state
+        */
+       mw.rcfilters.Controller.prototype._deleteStickyValuesFromFilterState = function ( filterState ) {
+               // Remove sticky filters
+               $.each( this.filtersModel.getStickyFiltersState(), function ( filterName ) {
+                       delete filterState[ filterName ];
+               } );
+       };
+
        /**
         * Get an object representing the base state of parameters
         * and highlights.
                mw.user.options.set( 'rcfilters-saved-queries', stringified );
        };
 
+       /**
+        * Update sticky preferences with current model state
+        */
+       mw.rcfilters.Controller.prototype.updateStickyPreferences = function () {
+               // Update default sticky values with selected, whether they came from
+               // the initial defaults or from the URL value that is being normalized
+               this.updateDaysDefault( this.filtersModel.getGroup( 'days' ).getSelectedItems()[ 0 ].getParamName() );
+               this.updateLimitDefault( this.filtersModel.getGroup( 'limit' ).getSelectedItems()[ 0 ].getParamName() );
+       };
+
+       /**
+        * Update the limit default value
+        *
+        * @param {number} newValue New value
+        */
+       mw.rcfilters.Controller.prototype.updateLimitDefault = function ( newValue ) {
+               if ( !$.isNumeric( newValue ) ) {
+                       return;
+               }
+
+               newValue = Number( newValue );
+
+               if ( mw.user.options.get( 'rcfilters-rclimit' ) !== newValue ) {
+                       // Save the preference
+                       new mw.Api().saveOption( 'rcfilters-rclimit', newValue );
+                       // Update the preference for this session
+                       mw.user.options.set( 'rcfilters-rclimit', newValue );
+               }
+       };
+
+       /**
+        * Update the days default value
+        *
+        * @param {number} newValue New value
+        */
+       mw.rcfilters.Controller.prototype.updateDaysDefault = function ( newValue ) {
+               if ( !$.isNumeric( newValue ) ) {
+                       return;
+               }
+
+               newValue = Number( newValue );
+
+               if ( mw.user.options.get( 'rcdays' ) !== newValue ) {
+                       // Save the preference
+                       new mw.Api().saveOption( 'rcdays', newValue );
+                       // Update the preference for this session
+                       mw.user.options.set( 'rcdays', newValue );
+               }
+       };
+
        /**
         * Synchronize the URL with the current state of the filters
         * without adding an history entry.
 
                this.uriProcessor.updateModelBasedOnQuery( new mw.Uri().query );
 
+               // Update the sticky preferences, in case we received a value
+               // from the URL
+               this.updateStickyPreferences();
+
                // Only update and fetch new results if it is requested
                if ( fetchChangesList ) {
                        this.updateChangesList();
                        data = defaultSavedQueryItem.getData();
 
                        queryHighlights = data.highlights || {};
-                       savedParams = this.filtersModel.getParametersFromFilters( data.filters || {} );
+                       savedParams = this.filtersModel.getParametersFromFilters(
+                               // Merge filters with sticky values
+                               $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
+                       );
 
                        // Translate highlights to parameters
                        savedHighlights.highlight = String( Number( queryHighlights.highlight ) );
         */
        mw.rcfilters.Controller.prototype._fetchChangesList = function () {
                var uri = this._getUpdatedUri(),
+                       stickyParams = this.filtersModel.getStickyParams(),
                        requestId = ++this.requestCounter,
                        latestRequest = function () {
                                return requestId === this.requestCounter;
                        }.bind( this );
 
+               // Sticky parameters override the URL params
+               // this is to make sure that whether we represent
+               // the sticky params in the URL or not (they may
+               // be normalized out) the sticky parameters are
+               // always being sent to the server with their
+               // current/default values
+               uri.extend( stickyParams );
+
                return $.ajax( uri.toString(), { contentType: 'html' } )
                        .then(
                                // Success
index b4ea8af..ba61ba9 100644 (file)
         * @return {Object} Empty parameter state
         */
        mw.rcfilters.UriProcessor.prototype._getEmptyParameterState = function () {
-               return this.emptyParameterState;
+               // Override empty parameter state with the sticky parameter values
+               return $.extend( true, {}, this.emptyParameterState, this.filtersModel.getStickyParams() );
        };
 
        /**
index 32d2097..1e5bfc3 100644 (file)
         * @param {string} filterName Chosen filter name
         */
        mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onPopupLimit = function ( filterName ) {
+               var item = this.limitGroupModel.getItemByName( filterName );
+
                this.controller.toggleFilterSelect( filterName, true );
+               this.controller.updateLimitDefault( item.getParamName() );
                this.button.popup.toggle( false );
        };
 
index af81883..82607f1 100644 (file)
         * @param {string} filterName Chosen filter name
         */
        mw.rcfilters.ui.DateButtonWidget.prototype.onPopupDays = function ( filterName ) {
+               var item = this.daysGroupModel.getItemByName( filterName );
+
                this.controller.toggleFilterSelect( filterName, true );
+               this.controller.updateDaysDefault( item.getParamName() );
                this.button.popup.toggle( false );
        };
 
index da7bafd..58e4d29 100644 (file)
                }, {
                        name: 'group6',
                        type: 'boolean',
+                       isSticky: true,
                        filters: [
-                               { name: 'group6option1', label: 'group6option1-label', description: 'group5option1-desc' },
-                               { name: 'group6option2', label: 'group6option2-label', description: 'group5option2-desc', default: true, useDefaultAsBaseValue: true },
-                               { name: 'group6option3', label: 'group6option3-label', description: 'group5option3-desc', default: true }
+                               { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc' },
+                               { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true },
+                               { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true }
+                       ]
+               }, {
+                       name: 'group7',
+                       type: 'single_option',
+                       isSticky: true,
+                       default: 'group7option2',
+                       filters: [
+                               { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc' },
+                               { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc' },
+                               { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc' }
                        ]
                } ],
                viewsDefinition = {
                        group6option1: '0',
                        group6option2: '1',
                        group6option3: '1',
+                       group7: 'group7option2',
                        namespace: ''
                },
                baseParamRepresentation = {
                        group5: 'option1',
                        group6option1: '0',
                        group6option2: '1',
-                       group6option3: '0',
+                       group6option3: '1',
+                       group7: 'group7option2',
                        namespace: ''
                },
                baseFilterRepresentation = {
                        group5__option3: false,
                        group6__group6option1: false,
                        group6__group6option2: true,
-                       group6__group6option3: false,
+                       group6__group6option3: true,
+                       group7__group7option1: false,
+                       group7__group7option2: true,
+                       group7__group7option3: false,
                        namespace__0: false,
                        namespace__1: false,
                        namespace__2: false,
                        group5__option3: { selected: false, conflicted: false, included: false },
                        group6__group6option1: { selected: false, conflicted: false, included: false },
                        group6__group6option2: { selected: true, conflicted: false, included: false },
-                       group6__group6option3: { selected: false, conflicted: false, included: false },
+                       group6__group6option3: { selected: true, conflicted: false, included: false },
+                       group7__group7option1: { selected: false, conflicted: false, included: false },
+                       group7__group7option2: { selected: true, conflicted: false, included: false },
+                       group7__group7option3: { selected: false, conflicted: false, included: false },
                        namespace__0: { selected: false, conflicted: false, included: false },
                        namespace__1: { selected: false, conflicted: false, included: false },
                        namespace__2: { selected: false, conflicted: false, included: false },
                        defaultParameters,
                        'Default parameters are stored properly per filter and group'
                );
+
+               // Change sticky filter
+               model.toggleFiltersSelected( {
+                       group7__group7option1: true
+               } );
+
+               // Make sure defaults have changed
+               assert.deepEqual(
+                       model.getDefaultParams(),
+                       $.extend( true, {}, defaultParameters, {
+                               group7: 'group7option1'
+                       } ),
+                       'Default parameters are stored properly per filter and group'
+               );
        } );
 
        QUnit.test( 'Finding matching filters', function ( assert ) {