jenkins-bot has submitted this change and it was merged.

Change subject: Introduce {{#transcludelist:<page name>|options...}}
......................................................................


Introduce {{#transcludelist:<page name>|options...}}

This also changes normal transclusion to transclude the entire list.

Syntax is {{#transcludelist:page name of list
  |includeDesc=true
  |maxItems=<number>
  |offset=<number>
  |defaultSort=[natural|random]
  |tags=tag1+tag2
  |tags=tag3+tag4
}}
(Where each tags argument is AND-ed together, and the + acts as an OR)

Bug: T135088
Change-Id: Ib4acc39a76dd34aa4701daa2a4b17915cc4d2246
---
A CollaborationKit.i18n.magic.php
M extension.json
M i18n/en.json
M i18n/qqq.json
M includes/CollaborationKit.php
M includes/content/CollaborationListContent.php
6 files changed, 240 insertions(+), 22 deletions(-)

Approvals:
  Brian Wolff: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/CollaborationKit.i18n.magic.php b/CollaborationKit.i18n.magic.php
new file mode 100644
index 0000000..0019658
--- /dev/null
+++ b/CollaborationKit.i18n.magic.php
@@ -0,0 +1,7 @@
+<?php
+
+$magicWords = [];
+
+$magicWords['en'] = [
+       'transcludelist' => [ 0, 'transcludelist' ],
+];
diff --git a/extension.json b/extension.json
index d5b354e..dfd776e 100644
--- a/extension.json
+++ b/extension.json
@@ -12,7 +12,8 @@
                ]
        },
        "ExtensionMessagesFiles": {
-               "CollaborationKitAlias": "CollaborationKit.alias.php"
+               "CollaborationKitAlias": "CollaborationKit.alias.php",
+               "CollaborationKitMagic": "CollaborationKit.i18n.magic.php"
        },
        "AutoloadClasses": {
                "ApiEditCollaborationHub": 
"includes/ApiEditCollaborationHub.php",
@@ -39,6 +40,9 @@
                ],
                "UnitTestsList": [
                        "CollaborationKitHooks::onUnitTestsList"
+               ],
+               "ParserFirstCallInit": [
+                       "CollaborationKitHooks::onParserFirstCallInit"
                ]
        },
        "ResourceModules": {
diff --git a/i18n/en.json b/i18n/en.json
index 4ce1daf..2a17571 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -38,5 +38,7 @@
        "collaborationhub-edit-apierror": "Editing the Collaboration Hub 
through the API failed with error code $1.",
        "collaborationhub-edit-tojsonerror": "Error creating json out of 
provided junk",
        "collaborationkit-taglist": "'''{{PLURAL:$2|Tagged:|Tags:}}''' $1 ",
-       "collaborationkit-listempty": "This list has no items in it."
+       "collaborationkit-listempty": "This list has no items in it.",
+       "collaborationkit-listcontent-toomanytags": "You are not allowed to 
specify more than {{PLURAL:$1|one tag|$1 tags}}",
+       "collaborationkit-listcontent-notlist": "[[$1]] is not a 
CollaborationKit list page"
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 72a07bd..5dd3ddb 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -38,5 +38,7 @@
        "collaborationhub-edit-apierror": "Error message shown when a request 
to the edit API to update a collaboration hub failed.\n\nParameters:\n* $1 - 
error code string",
        "collaborationhub-edit-tojsonerror": "Error message shown when the 
input could not be successfully encoded back into the collaboration hub json",
        "collaborationkit-taglist": "Box for showing tags a specific item has 
on a list. $1 = comma separated list of tags. $2 = number of tags",
-       "collaborationkit-listempty": "Shown on lists that are empty 
immediately after the description"
+       "collaborationkit-listempty": "Shown on lists that are empty 
immediately after the description",
+       "collaborationkit-listcontent-toomanytags": "Error if 
<nowiki>{{#transcludelist:...}}</nowiki> is given more than the allowed number 
of tag arguments. $1 - number of tags allowed. $2 - number of tags given.",
+       "collaborationkit-listcontent-notlist": "Error when 
<nowiki>{{#transcludelist:page}}</nowiki> is given a page that is not a 
CollaborationKit list page. $1 Name of page given."
 }
diff --git a/includes/CollaborationKit.php b/includes/CollaborationKit.php
index 09dd39a..ce1f4ad 100644
--- a/includes/CollaborationKit.php
+++ b/includes/CollaborationKit.php
@@ -76,4 +76,8 @@
 
                $files = array_merge( $files, $ourFiles );
        }
+
+       public static function onParserFirstCallInit( $parser ) {
+               $parser->setFunctionHook( 'transcludelist', 
'CollaborationListContent::transcludeHook' );
+       }
 }
diff --git a/includes/content/CollaborationListContent.php 
b/includes/content/CollaborationListContent.php
index 4447bb4..1cf1698 100644
--- a/includes/content/CollaborationListContent.php
+++ b/includes/content/CollaborationListContent.php
@@ -1,6 +1,8 @@
 <?php
 
 /**
+ * Important design assumption: This class assumes lists are small
+ * (e.g. Average case < 500 items, outliers < 2000)
  *
  * Json structure is as follows:
  * {
@@ -16,7 +18,7 @@
  *             ...
  *     ],
  *     "options": {
- *             "sortcriteria": {
+ *             "sortcriteria": { //unimplemented
  *                     // FIXME, not sure if this actually meets with usecase
  *                     "criteria name": {
  *                             "order": "numeric", // or potentially 
collation??
@@ -33,6 +35,8 @@
 class CollaborationListContent extends JsonContent {
 
        const MAX_LIST_SIZE = 2000; // Maybe should be a config option.
+       const RANDOM_CACHE_EXPIRY = 28800; // 8 hours
+       const MAX_TAGS = 50;
 
        /** @var $decoded boolean Have we decoded the data yet */
        private $decoded = false;
@@ -197,6 +201,26 @@
                return true;
        }
 
+       private static function validateOption( $name, &$value ) {
+               switch ( $name ) {
+               case 'defaultSort':
+                       if ( in_array( $value, [ 'natural', 'random' ] ) ) {
+                               return true;
+                       }
+               case 'maxItems':
+               case 'offset':
+                       if ( is_numeric( $value ) ) {
+                               $value = (int)$value;
+                               return true;
+                       }
+               case 'includeDesc':
+                       $value = (bool)$value;
+                       return true;
+               default:
+                       return false;
+               }
+       }
+
        /**
         * Decode the JSON contents and populate protected variables.
         */
@@ -242,34 +266,57 @@
                if ( !$lang ) {
                        $lang = $title->getPageLanguage();
                }
-               $text = $this->convertToWikitext( $lang, true );
+               $text = $this->convertToWikitext( $lang, 
$this->getFullRenderListOptions() );
                $output = $wgParser->parse( $text, $title, $options, true, 
true, $revId );
+       }
+
+       private function getFullRenderListOptions() {
+               return $listOptions = [
+                       'includeDesc' => true,
+                       'maxItems' => false,
+                       'defaultSort' => 'natural'
+               ];
        }
 
        /**
         * Convert the JSON to wikitext.
         *
         * @param $lang Language The (content) language to render the page in.
-        * @param $includeDesc boolean Include the description
+        * @param $options Array Options to override the default transclude 
options
         * @return string The wikitext
         */
-       private function convertToWikitext( Language $lang, $includeDesc = 
false, $maxItems = false ) {
+       private function convertToWikitext( Language $lang, $options = [] ) {
                $this->decode();
+               $options = $options + $this->getDefaultOptions();
+               $maxItems = $options['maxItems'];
+               $includeDesc = $options['includeDesc'];
 
                $text = "__NOEDITSECTION__\n__NOTOC__";
-               if ( includeDesc ) {
+               if ( $includeDesc ) {
                        $text .= $this->getDescription() . "\n";
                }
                if ( count( $this->items ) === 0 ) {
-                       $text .= "<hr>\n" . wfMessage( 
'collaborationkit-listempty' )
-                               ->inLanguage( $lang )
-                               ->plain() . "\n";
+                       $text .= 
"<hr>\n{{mediawiki:collaborationkit-listempty}}\n";
                }
                $curItem = 0;
-               foreach ( $this->items as $item ) {
+               $offset = $options['defaultSort'] === 'random' ? 0 : 
$options['offset'];
+
+               $sortedItems = $this->items;
+               $this->sortList( $sortedItems, $options['defaultSort'] );
+
+               foreach ( $sortedItems as $item ) {
+                       if ( $offset !== 0 ) {
+                               $offset--;
+                               continue;
+                       }
                        $curItem++;
                        if ( $maxItems !== false && $maxItems < $curItem ) {
                                break;
+                       }
+
+                       $itemTags = $item->tags ? $item->tags : [];
+                       if ( !$this->matchesTag( $options['tags'], $itemTags ) 
) {
+                               continue;
                        }
 
                        $titleForItem = null;
@@ -314,7 +361,7 @@
                                                                array_map( 
'wfEscapeWikiText', $item->tags )
                                                        )
                                                )->numParams( count( 
$item->tags ) )
-                                               ->plain() .
+                                               ->text() .
                                        "</div>\n";
                        }
                        if ( $image ) {
@@ -330,7 +377,8 @@
                        // using wgContLang is kind of icky. Maybe we should 
transclude
                        // from mediawiki namespace, or give up on not 
splitting the
                        // parser cache and just use {{int:... (?)
-                       $text = $this->convertToWikitext( $wgContLang, true );
+                       $renderOpts = $this->getFullRenderListOptions();
+                       $text = $this->convertToWikitext( $wgContLang, 
$renderOpts );
                        return ContentHandler::makeContent( $text, null, 
$toModel );
                } elseif ( $toModel === CONTENT_MODEL_JSON ) {
                        return ContentHandler::makeContent( 
$this->getNativeData(), null, $toModel );
@@ -338,13 +386,164 @@
                return parent::convert( $toModel, $lossy );
        }
 
-       public function getWikitextForTransclusion() {
-               global $wgContLang;
-               // FIXME Unclear if we should really do this as a transclusion, 
or
-               // introduce a parser function. Too bad we don't have access to
-               // transclusion parameters from this interface w/o doing 
something
-               // insane:(
-               $text = $this->convertToWikitext( $wgContLang, false, 5 );
-               return $text;
+       public function getDefaultOptions() {
+               // FIXME implement
+               return [
+                       'includeDesc' => false,
+                       'maxItems' => 5,
+                       'defaultSort' => 'random',
+                       'offset' => 0,
+                       'tags' => [],
+               ];
+       }
+
+
+       /**
+        * Sort a list
+        *
+        * @param &$items Array List to sort (sorted in-place)
+        * @param $mode String sort method
+        * @return Array sorted list
+        * @throws UnexpectedValueException on unrecognized mode
+        */
+       private function sortList( &$items, $mode ) {
+               switch ( $mode ) {
+                       case 'random':
+                               return $this->sortRandomly( $items );
+                       case 'natural':
+                               return $items;
+                       default:
+                               throw new UnexpectedValueException( "invalid 
sort mode" );
+               }
+       }
+
+       /**
+        * Sort an array pseudo-randomly using an affine transform
+        *
+        * @param Array $items Stuff to sort (sorted in-place)
+        * @return Array
+        */
+       private function sortRandomly( &$items ) {
+               $totItems = count( $items );
+               $rand1 = mt_rand( 1, $totItems - 1 );
+               $rand2 = mt_rand( 0, $totItems - 1 );
+
+               while ( $rand1 < $totItems - 1 && $rand1 % $totItems === 0 ) {
+                       // Make rand1 relatively prime to $totItems.
+                       $rand1++;
+               }
+               uksort( $items, function ( $a, $b ) use( $rand1, $rand2, 
$totItems ) {
+                       $a2 = ( $a * $rand1 + $rand2 ) % $totItems;
+                       $b2 = ( $b * $rand1 + $rand2 ) % $totItems;
+                       if ( $a2 === $b2 ) {
+                               // Really should not happen
+                               return 0;
+                       }
+                       return $a2 > $b2 ? 1 : -1;
+               } );
+               return $items;
+       }
+
+
+       /**
+        * Determine if an item matches a set of tags
+        *
+        * $tagSpecifier is a 2D array describing an AND of OR conditions
+        * e.g. $tagSpecifier = [ [ 'a', 'b' ], ['b', 'd'] ]
+        * means that any item must have the tags (a&&b) || (b&&d).
+        *
+        * @param $tagSpecifier Array What tags to check (aka $options['tags'])
+        * @param $itemTags Array What tags is this item tagged with
+        * @return boolean If the item matches
+        */
+       private function matchesTag( Array $tagSpecifier, Array $itemTags ) {
+               if ( !$tagSpecifier ) {
+                       return true;
+               }
+               $matchesAllGroups = true;
+               foreach ( $tagSpecifier as $tagGroups ) {
+                       foreach ( $tagGroups as $tagAlt ) {
+                               $matchesOneAlternative = false;
+                               $itemTags;
+                               if ( in_array( $tagAlt, $itemTags ) ) {
+                                       $matchesOneAlternative = true;
+                                       break;
+                               }
+                       }
+                       if ( !$matchesOneAlternative ) {
+                               $matchesAllGroups = false;
+                               break;
+                       }
+               }
+               return $matchesAllGroups;
+       }
+
+       /**
+        * Function to handle {{#trancludelist:Page name|options...}} calls
+        */
+       public static function transcludeHook( $parser, $pageName = '' ) {
+               $args = array_splice( func_get_args(), 2 );
+               $options = [];
+               $title = Title::newFromText( $pageName );
+               $lang = $parser->getFunctionLang();
+
+               if ( !$title || $title->getContentModel() !== __CLASS__ ) {
+                       // This is interpreted as wikitext, so is safe.
+                       return Html::rawElement( 'div', [ 'class' => 'error' ],
+                               wfMessage( 
'collaborationkit-listcontent-notlist' )
+                                       ->params( $title->getPrefixedText() )
+                                       ->inLanguage( $lang )
+                                       ->text()
+                       );
+               }
+               $tagCount = 0;
+               foreach ( $args as $argument ) {
+                       if ( strpos( $argument, '=' ) === false ) {
+                               continue;
+                       }
+                       // If we need everything i18n-ized, this could be
+                       // replaced with magic words.
+                       list( $name, $value ) = explode( '=', $argument, 2 );
+                       if ( $name === 'tags' ) {
+                               $tagList = explode( '+', $value );
+                               if ( !isset( $options['tags'] ) ) {
+                                       $options['tags'] = [];
+                               }
+                               $options['tags'][] = $tagList;
+                               $tagCount += count( $tagList );
+                       } elseif ( self::validateOption( $name, $value ) ) {
+                               $options[$name] = $value;
+                       }
+               }
+
+               if ( $tagCount > self::MAX_TAGS ) {
+                       return Html::rawElement( 'div', [ 'class' => 'error' ],
+                               wfMessage( 
'collaborationkit-listcontent-toomanytags' )
+                                       ->numParams( self::MAX_TAGS, $tagCount )
+                                       ->inLanguage( $lang )
+                                       ->text()
+                       );
+               }
+
+               $wikipage = WikiPage::Factory( $title );
+               $content = $wikipage->getContent();
+               if ( !$content instanceof CollaborationListContent ) {
+                       // We already checked this, so this should not happen...
+                       return Html::rawElement( 'div', [ 'class' => 'error' ],
+                               wfMessage( 
'collaborationkit-listcontent-notlist' )
+                                       ->params( $title->getPrefixedText() )
+                                       ->inLanguage( $lang )
+                                       ->text()
+                       );
+               }
+
+               if ( ( isset( $options['defaultSort'] )
+                       && $options['defaultSort'] === 'random' )
+                       || $content->getDefaultOptions()['defaultSort'] === 
'random'
+               ) {
+                       $parser->getOutput()->updateCacheExpiry( 
self::RANDOM_CACHE_EXPIRY );
+               }
+               $parser->getOutput()->addTemplate( $title, $wikipage->getId(), 
$wikipage->getLatest() );
+               return $content->convertToWikitext( $lang, $options );
        }
 }

-- 
To view, visit https://gerrit.wikimedia.org/r/292830
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ib4acc39a76dd34aa4701daa2a4b17915cc4d2246
Gerrit-PatchSet: 2
Gerrit-Project: mediawiki/extensions/CollaborationKit
Gerrit-Branch: master
Gerrit-Owner: Brian Wolff <bawolff...@gmail.com>
Gerrit-Reviewer: Brian Wolff <bawolff...@gmail.com>
Gerrit-Reviewer: Siebrand <siebr...@kitano.nl>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to