From 53fa809a625c141cb395d41ddcd91b383189afa5 Mon Sep 17 00:00:00 2001 From: Geoffrey Mon Date: Mon, 12 Dec 2016 09:23:54 -0500 Subject: [PATCH] Pager class for filtering by date range 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 | 1 + includes/pager/RangeChronologicalPager.php | 121 ++++++++++++++++++ includes/pager/ReverseChronologicalPager.php | 83 +++++++----- languages/i18n/en.json | 2 + languages/i18n/qqq.json | 2 + .../pager/RangeChronologicalPagerTest.php | 96 ++++++++++++++ 6 files changed, 271 insertions(+), 34 deletions(-) create mode 100644 includes/pager/RangeChronologicalPager.php create mode 100644 tests/phpunit/includes/pager/RangeChronologicalPagerTest.php diff --git a/autoload.php b/autoload.php index 36773746b0..fbdee83ba8 100644 --- a/autoload.php +++ b/autoload.php @@ -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 index 0000000000..901d576df9 --- /dev/null +++ b/includes/pager/RangeChronologicalPager.php @@ -0,0 +1,121 @@ +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 ]; + } +} diff --git a/includes/pager/ReverseChronologicalPager.php b/includes/pager/ReverseChronologicalPager.php index 76f347023e..9eef728a93 100644 --- a/includes/pager/ReverseChronologicalPager.php +++ b/includes/pager/ReverseChronologicalPager.php @@ -1,7 +1,5 @@ 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" ); } } diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 05164df522..15e82b6153 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -4180,6 +4180,8 @@ "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", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 0234d24759..cd42665a33 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -4368,6 +4368,8 @@ "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 index 0000000000..3374f4ab93 --- /dev/null +++ b/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php @@ -0,0 +1,96 @@ + + */ +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', ], + ]; + } + +} -- 2.20.1