Pager class for filtering by date range
authorGeoffrey Mon <geofbot@gmail.com>
Mon, 12 Dec 2016 14:23:54 +0000 (09:23 -0500)
committerMatthias Mullie <git@mullie.eu>
Fri, 12 May 2017 12:55:54 +0000 (14:55 +0200)
New abstract class RangeChronologicalPager to provide shared date range
filtering capability (with unit tests)

I18n msgs to use as common labels for date range inputs

Expose some ReverseChronologicalParser::getDateCond logic so we can
convert year/month date filters to datestamp date filters

Bug: T120733
Change-Id: I65fdc00368f406f5fa2492600e95e07ce442c165

autoload.php
includes/pager/RangeChronologicalPager.php [new file with mode: 0644]
includes/pager/ReverseChronologicalPager.php
languages/i18n/en.json
languages/i18n/qqq.json
tests/phpunit/includes/pager/RangeChronologicalPagerTest.php [new file with mode: 0644]

index 3677374..fbdee83 100644 (file)
@@ -1165,6 +1165,7 @@ $wgAutoloadLocalClasses = [
        'RESTBagOStuff' => __DIR__ . '/includes/libs/objectcache/RESTBagOStuff.php',
        'RSSFeed' => __DIR__ . '/includes/Feed.php',
        'RandomPage' => __DIR__ . '/includes/specials/SpecialRandompage.php',
+       'RangeChronologicalPager' => __DIR__ . '/includes/pager/RangeChronologicalPager.php',
        'RangeDifference' => __DIR__ . '/includes/diff/DiffEngine.php',
        'RawAction' => __DIR__ . '/includes/actions/RawAction.php',
        'RawMessage' => __DIR__ . '/includes/Message.php',
diff --git a/includes/pager/RangeChronologicalPager.php b/includes/pager/RangeChronologicalPager.php
new file mode 100644 (file)
index 0000000..901d576
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Pager
+ */
+use Wikimedia\Timestamp\TimestampException;
+
+/**
+ * Pager for filtering by a range of dates.
+ * @ingroup Pager
+ */
+abstract class RangeChronologicalPager extends ReverseChronologicalPager {
+
+       protected $rangeConds = [];
+
+       /**
+        * Set and return a date range condition using timestamps provided by the user.
+        * We want the revisions between the two timestamps.
+        * Also supports only having a start or end timestamp.
+        * Assumes that the start timestamp comes before the end timestamp.
+        *
+        * @param string $startStamp Timestamp of the beginning of the date range (or empty)
+        * @param string $endStamp Timestamp of the end of the date range (or empty)
+        * @return array|null Database conditions to satisfy the specified date range
+        *     or null if dates are invalid
+        */
+       public function getDateRangeCond( $startStamp, $endStamp ) {
+               $this->rangeConds = [];
+
+               try {
+                       if ( $startStamp !== '' ) {
+                               $startTimestamp = MWTimestamp::getInstance( $startStamp );
+                               $startTimestamp->setTimezone( $this->getConfig()->get( 'Localtimezone' ) );
+                               $startOffset = $this->mDb->timestamp( $startTimestamp->getTimestamp() );
+                               $this->rangeConds[] = $this->mIndexField . '>=' . $this->mDb->addQuotes( $startOffset );
+                       }
+
+                       if ( $endStamp !== '' ) {
+                               $endTimestamp = MWTimestamp::getInstance( $endStamp );
+                               $endTimestamp->setTimezone( $this->getConfig()->get( 'Localtimezone' ) );
+                               $endOffset = $this->mDb->timestamp( $endTimestamp->getTimestamp() );
+                               $this->rangeConds[] = $this->mIndexField . '<=' . $this->mDb->addQuotes( $endOffset );
+
+                               // populate existing variables for compatibility with parent
+                               $this->mYear = (int)$endTimestamp->format( 'Y' );
+                               $this->mMonth = (int)$endTimestamp->format( 'm' );
+                               $this->mDay = (int)$endTimestamp->format( 'd' );
+                               $this->mOffset = $endOffset;
+                       }
+               } catch ( TimestampException $ex ) {
+                       return null;
+               }
+
+               return $this->rangeConds;
+       }
+
+       /**
+        * Takes ReverseChronologicalPager::getDateCond parameters and repurposes
+        * them to work with timestamp-based getDateRangeCond.
+        *
+        * @param int $year Year up to which we want revisions
+        * @param int $month Month up to which we want revisions
+        * @param int $day [optional] Day up to which we want revisions. Default is end of month.
+        * @return string|null Timestamp or null if year and month are false/invalid
+        */
+       public function getDateCond( $year, $month, $day = -1 ) {
+               // run through getDateRangeCond so rangeConds, mOffset, ... are set
+               $legacyTimestamp = self::getOffsetDate( $year, $month, $day );
+               // ReverseChronologicalPager uses strict inequality for the end date ('<'),
+               // but this class uses '<=' and expects extending classes to handle modifying the end date.
+               // Therefore, we need to subtract one second from the output of getOffsetDate to make it
+               // work with the '<=' inequality used in this class.
+               $legacyTimestamp->timestamp = $legacyTimestamp->timestamp->modify( '-1 second' );
+               $this->getDateRangeCond( '', $legacyTimestamp->getTimestamp( TS_MW ) );
+               return $this->mOffset;
+       }
+
+       /**
+        * Build variables to use by the database wrapper.
+        *
+        * @param string $offset Index offset, inclusive
+        * @param int $limit Exact query limit
+        * @param bool $descending Query direction, false for ascending, true for descending
+        * @return array
+        */
+       protected function buildQueryInfo( $offset, $limit, $descending ) {
+               if ( count( $this->rangeConds ) > 0 ) {
+                       // If range conditions are set, $offset is not used.
+                       // However, if range conditions aren't set, (such as when using paging links)
+                       // use the provided offset to get the proper query.
+                       $offset = '';
+               }
+
+               list( $tables, $fields, $conds, $fname, $options, $join_conds ) = parent::buildQueryInfo(
+                       $offset,
+                       $limit,
+                       $descending
+               );
+
+               if ( $this->rangeConds ) {
+                       $conds = array_merge( $conds, $this->rangeConds );
+               }
+
+               return [ $tables, $fields, $conds, $fname, $options, $join_conds ];
+       }
+}
index 76f3470..9eef728 100644 (file)
@@ -1,7 +1,5 @@
 <?php
 /**
- * Efficient paging for SQL queries.
- *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or
@@ -23,7 +21,8 @@
 use Wikimedia\Timestamp\TimestampException;
 
 /**
- * IndexPager with a formatted navigation bar
+ * Efficient paging for SQL queries.
+ * IndexPager with a formatted navigation bar.
  * @ingroup Pager
  */
 abstract class ReverseChronologicalPager extends IndexPager {
@@ -32,7 +31,7 @@ abstract class ReverseChronologicalPager extends IndexPager {
        public $mMonth;
        public $mDay;
 
-       function getNavigationBar() {
+       public function getNavigationBar() {
                if ( !$this->isNavigationBarShown() ) {
                        return '';
                }
@@ -65,52 +64,79 @@ abstract class ReverseChronologicalPager extends IndexPager {
        /**
         * Set and return the mOffset timestamp such that we can get all revisions with
         * a timestamp up to the specified parameters.
+        *
         * @param int $year Year up to which we want revisions
         * @param int $month Month up to which we want revisions
         * @param int $day [optional] Day up to which we want revisions. Default is end of month.
         * @return string|null Timestamp or null if year and month are false/invalid
         */
-       function getDateCond( $year, $month, $day = -1 ) {
-               $year = intval( $year );
-               $month = intval( $month );
-               $day = intval( $day );
+       public function getDateCond( $year, $month, $day = -1 ) {
+               $year = (int)$year;
+               $month = (int)$month;
+               $day = (int)$day;
 
                // Basic validity checks for year and month
-               $this->mYear = $year > 0 ? $year : false;
-               $this->mMonth = ( $month > 0 && $month < 13 ) ? $month : false;
+               // If year and month are invalid, don't update the mOffset
+               if ( $year <= 0 && ( $month <= 0 || $month >= 13 ) ) {
+                       return null;
+               }
 
-               // If year and month are false, don't update the mOffset
-               if ( !$this->mYear && !$this->mMonth ) {
+               // Treat the given time in the wiki timezone and get a UTC timestamp for the database lookup
+               $timestamp = self::getOffsetDate( $year, $month, $day );
+               $timestamp->setTimezone( $this->getConfig()->get( 'Localtimezone' ) );
+
+               try {
+                       $this->mYear = (int)$timestamp->format( 'Y' );
+                       $this->mMonth = (int)$timestamp->format( 'm' );
+                       $this->mDay = (int)$timestamp->format( 'd' );
+                       $this->mOffset = $this->mDb->timestamp( $timestamp->getTimestamp() );
+               } catch ( TimestampException $e ) {
+                       // Invalid user provided timestamp (T149257)
                        return null;
                }
 
+               return $this->mOffset;
+       }
+
+       /**
+        * Core logic of determining the mOffset timestamp such that we can get all items with
+        * a timestamp up to the specified parameters. Given parameters for a day up to which to get
+        * items, this function finds the timestamp of the day just after the end of the range for use
+        * in an database strict inequality filter.
+        *
+        * This is separate from getDateCond so we can use this logic in other places, such as in
+        * RangeChronologicalPager, where this function is used to convert year/month/day filter options
+        * into a timestamp.
+        *
+        * @param int $year Year up to which we want revisions
+        * @param int $month Month up to which we want revisions
+        * @param int $day [optional] Day up to which we want revisions. Default is end of month.
+        * @return MWTimestamp Timestamp or null if year and month are false/invalid
+        */
+       public static function getOffsetDate( $year, $month, $day = -1 ) {
                // Given an optional year, month, and day, we need to generate a timestamp
                // to use as "WHERE rev_timestamp <= result"
                // Examples: year = 2006      equals < 20070101 (+000000)
                // year=2005, month=1         equals < 20050201
                // year=2005, month=12        equals < 20060101
                // year=2005, month=12, day=5 equals < 20051206
-               if ( $this->mYear ) {
-                       $year = $this->mYear;
-               } else {
+               if ( $year <= 0 ) {
                        // If no year given, assume the current one
                        $timestamp = MWTimestamp::getInstance();
                        $year = $timestamp->format( 'Y' );
                        // If this month hasn't happened yet this year, go back to last year's month
-                       if ( $this->mMonth > $timestamp->format( 'n' ) ) {
+                       if ( $month > $timestamp->format( 'n' ) ) {
                                $year--;
                        }
                }
 
-               if ( $this->mMonth ) {
-                       $month = $this->mMonth;
-
+               if ( $month && $month > 0 && $month < 13 ) {
                        // Day validity check after we have month and year checked
-                       $this->mDay = checkdate( $month, $day, $year ) ? $day : false;
+                       $day = checkdate( $month, $day, $year ) ? $day : false;
 
-                       if ( $this->mDay ) {
+                       if ( $day && $day > 0 ) {
                                // If we have a day, we want up to the day immediately afterward
-                               $day = $this->mDay + 1;
+                               $day++;
 
                                // Did we overflow the current month?
                                if ( !checkdate( $month, $day, $year ) ) {
@@ -147,17 +173,6 @@ abstract class ReverseChronologicalPager extends IndexPager {
                        $ymd = 20320101;
                }
 
-               // Treat the given time in the wiki timezone and get a UTC timestamp for the database lookup
-               $timestamp = MWTimestamp::getInstance( "${ymd}000000" );
-               $timestamp->setTimezone( $this->getConfig()->get( 'Localtimezone' ) );
-
-               try {
-                       $this->mOffset = $this->mDb->timestamp( $timestamp->getTimestamp() );
-               } catch ( TimestampException $e ) {
-                       // Invalid user provided timestamp (T149257)
-                       return null;
-               }
-
-               return $this->mOffset;
+               return MWTimestamp::getInstance( "${ymd}000000" );
        }
 }
index 05164df..15e82b6 100644 (file)
        "mw-widgets-titleinput-description-redirect": "redirect to $1",
        "mw-widgets-categoryselector-add-category-placeholder": "Add a category...",
        "mw-widgets-usersmultiselect-placeholder": "Add more...",
+       "date-range-from": "From date:",
+       "date-range-to": "To date:",
        "sessionmanager-tie": "Cannot combine multiple request authentication types: $1.",
        "sessionprovider-generic": "$1 sessions",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "cookie-based sessions",
index 0234d24..cd42665 100644 (file)
        "mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.",
        "mw-widgets-categoryselector-add-category-placeholder": "Placeholder displayed in the category selector widget after the capsules of already added categories.",
        "mw-widgets-usersmultiselect-placeholder": "Placeholder displayed in the input field, where new usernames are entered",
+       "date-range-from": "Label for an input field that specifies the start date of a date range filter.",
+       "date-range-to": " Label for an input field that specifies the end date of a date range filter.",
        "sessionmanager-tie": "Used as an error message when multiple session sources are tied in priority.\n\nParameters:\n* $1 - List of dession type descriptions, from messages like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
        "sessionprovider-generic": "Used to create a generic session type description when one isn't provided via the proper message. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.\n\nParameters:\n* $1 - PHP classname.",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "Description of the sessions provided by the CookieSessionProvider class, which use HTTP cookies. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.",
diff --git a/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php b/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php
new file mode 100644 (file)
index 0000000..3374f4a
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * Test class for RangeChronologicalPagerTest logic.
+ *
+ * @group Pager
+ *
+ * @author Geoffrey Mon <geofbot@gmail.com>
+ */
+class RangeChronologicalPagerTest extends MediaWikiLangTestCase {
+
+       /**
+        * @covers       RangeChronologicalPager::getDateCond
+        * @dataProvider getDateCondProvider
+        */
+       public function testGetDateCond( $inputYear, $inputMonth, $inputDay, $expected ) {
+               $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' );
+               $this->assertEquals( $expected, $pager->getDateCond( $inputYear, $inputMonth, $inputDay ) );
+       }
+
+       /**
+        * Data provider in [ input year, input month, input day, expected timestamp output ] format
+        */
+       public function getDateCondProvider() {
+               return [
+                       [ 2016, 12, 5, '20161205235959' ],
+                       [ 2016, 12, 31, '20161231235959' ],
+                       [ 2016, 12, 1337, '20161231235959' ],
+                       [ 2016, 1337, 1337, '20161231235959' ],
+                       [ 2016, 1337, -1, '20161231235959' ],
+                       [ 2016, 12, 32, '20161231235959' ],
+                       [ 2016, 12, -1, '20161231235959' ],
+                       [ 2016, -1, -1, '20161231235959' ],
+               ];
+       }
+
+       /**
+        * @covers       RangeChronologicalPager::getDateRangeCond
+        * @dataProvider getDateRangeCondProvider
+        */
+       public function testGetDateRangeCond( $start, $end, $expected ) {
+               $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' );
+               $this->assertArrayEquals( $expected, $pager->getDateRangeCond( $start, $end ) );
+       }
+
+       /**
+        * Data provider in [ start, end, [ expected output has start condition, has end cond ] ] format
+        */
+       public function getDateRangeCondProvider() {
+               $db = wfGetDB( DB_MASTER );
+
+               return [
+                       [
+                               '20161201000000',
+                               '20161203000000',
+                               [
+                                       '>=' . $db->addQuotes( $db->timestamp( '20161201000000' ) ),
+                                       '<=' . $db->addQuotes( $db->timestamp( '20161203000000' ) ),
+                               ],
+                       ],
+                       [
+                               '',
+                               '20161203000000',
+                               [
+                                       '<=' . $db->addQuotes( $db->timestamp( '20161203000000' ) ),
+                               ],
+                       ],
+                       [
+                               '20161201000000',
+                               '',
+                               [
+                                       '>=' . $db->addQuotes( $db->timestamp( '20161201000000' ) ),
+                               ],
+                       ],
+                       [ '', '', [] ],
+               ];
+       }
+
+       /**
+        * @covers       RangeChronologicalPager::getDateRangeCond
+        * @dataProvider getDateRangeCondInvalidProvider
+        */
+       public function testGetDateRangeCondInvalid( $start, $end ) {
+               $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' );
+               $this->assertEquals( null, $pager->getDateRangeCond( $start, $end ) );
+       }
+
+       public function getDateRangeCondInvalidProvider() {
+               return [
+                       [ '-2016-12-01', '2017-12-01', ],
+                       [ '2016-12-01', '-2017-12-01', ],
+                       [ 'abcdefghij', 'klmnopqrstu', ],
+               ];
+       }
+
+}