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

Change subject: Choose banner on client
......................................................................


Choose banner on client

This is part of a series of changes to shift banner selection partly
to the client. This change only modifies CN functionality when
serving banners if $wgCentralNoticeChooseBannerOnClient is set to
true.

Introduces a new global:

$wgCentralSelectedBannerDispatcher: URL for fetching a banner that
  is chosen on the client. This is the new banner-choosing
  mechanism's version of $wgCentralBannerDispatcher.

Change-Id: I04e2d4bfdd43529a83d32998e20d4bd885b9033d
---
M CentralNotice.hooks.php
M CentralNotice.php
M modules/ext.centralNotice.bannerController/bannerController.js
M modules/ext.centralNotice.bannerController/bannerController.lib.js
M special/SpecialBannerLoader.php
M special/SpecialCentralNotice.php
6 files changed, 342 insertions(+), 29 deletions(-)

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



diff --git a/CentralNotice.hooks.php b/CentralNotice.hooks.php
index 30667ac..9bef6f9 100644
--- a/CentralNotice.hooks.php
+++ b/CentralNotice.hooks.php
@@ -309,13 +309,17 @@
                   $wgNoticeInfrastructure, $wgNoticeCloseButton, 
$wgCentralBannerDispatcher,
                   $wgCentralBannerRecorder, $wgNoticeNumberOfBuckets, 
$wgNoticeBucketExpiry,
                   $wgNoticeNumberOfControllerBuckets, 
$wgNoticeCookieDurations, $wgScript,
-                  $wgNoticeHideUrls, $wgNoticeOldCookieApocalypse;
+                  $wgNoticeHideUrls, $wgNoticeOldCookieApocalypse,
+                  $wgCentralNoticeChooseBannerOnClient, 
$wgCentralSelectedBannerDispatcher;
 
        // Making these calls too soon will causes issues with the namespace 
localisation cache. This seems
        // to be just right. We require them at all because MW will 302 page 
requests made to non localised
        // namespaces which results in wasteful extra calls.
        if ( !$wgCentralBannerDispatcher ) {
                $wgCentralBannerDispatcher = SpecialPage::getTitleFor( 
'BannerRandom' )->getLocalUrl();
+       }
+       if ( !$wgCentralSelectedBannerDispatcher ) {
+               $wgCentralSelectedBannerDispatcher = SpecialPage::getTitleFor( 
'BannerLoader' )->getLocalUrl();
        }
        if ( !$wgCentralBannerRecorder ) {
                $wgCentralBannerRecorder = SpecialPage::getTitleFor( 
'RecordImpression' )->getLocalUrl();
@@ -345,6 +349,8 @@
        $vars[ 'wgNoticeCookieDurations' ] = $wgNoticeCookieDurations;
        $vars[ 'wgNoticeHideUrls' ] = $wgNoticeHideUrls;
        $vars[ 'wgNoticeOldCookieApocalypse' ] = $wgNoticeOldCookieApocalypse;
+       $vars[ 'wgCentralNoticeChooseBannerOnClient' ] = 
$wgCentralNoticeChooseBannerOnClient;
+       $vars[ 'wgCentralSelectedBannerDispatcher' ] = 
$wgCentralSelectedBannerDispatcher;
 
        if ( $wgNoticeInfrastructure ) {
                $vars[ 'wgNoticeCloseButton' ] = $wgNoticeCloseButton;
diff --git a/CentralNotice.php b/CentralNotice.php
index fb177e7..550e139 100644
--- a/CentralNotice.php
+++ b/CentralNotice.php
@@ -62,10 +62,8 @@
 // The name of the database which hosts the centralized campaign data
 $wgCentralDBname = false;
 
-// URL where BannerRandom is hosted, where FALSE will default to the
-// Special:BannerRandom on the machine serving ResourceLoader requests (meta
-// in the case of WMF).  To use our reverse proxy, for example, set this
-// variable to 'http://banners.wikimedia.org/banner_load'.
+// URL where BannerRandom is hosted, where false will default to the
+// Special:BannerRandom on the machine serving ResourceLoader requests.
 $wgCentralBannerDispatcher = false;
 
 // URL which is hit after a banner is loaded, for compatibility with analytics.
@@ -98,6 +96,13 @@
 // Enable the new mechanism for making the banner selection on the client
 $wgCentralNoticeChooseBannerOnClient = false;
 
+// URL for BannerLoader, for requests to fetch a banner that is already
+// known (using the banner URL param). If false, it will default to
+// Special:BannerLoader on the machine serving ResourceLoader requests. This
+// value is used when for fetching banners that are chosen on the client (i.e.,
+// when $wgCentralNoticeChooseBannerOnClient is set to true).
+$wgCentralSelectedBannerDispatcher = false;
+
 // Enable the loader itself
 // Allows to control the loader visibility, without destroying infrastructure
 // for cached content
diff --git a/modules/ext.centralNotice.bannerController/bannerController.js 
b/modules/ext.centralNotice.bannerController/bannerController.js
index 8aca1f2..1069335 100644
--- a/modules/ext.centralNotice.bannerController/bannerController.js
+++ b/modules/ext.centralNotice.bannerController/bannerController.js
@@ -140,6 +140,8 @@
                                debug: mw.centralNotice.data.getVars.debug
                        };
 
+                       // TODO use the new $wgCentralSelectedBannerDispatcher 
here instead
+
                        $.ajax({
                                url: mw.config.get( 'wgCentralPagePath' ) + '?' 
+ $.param( bannerPageQuery ),
                                dataType: 'script',
@@ -147,25 +149,69 @@
                        });
                },
                loadRandomBanner: function () {
-                       var RAND_MAX = 30;
-                       var bannerDispatchQuery = {
-                               uselang: mw.config.get( 'wgUserLanguage' ),
-                               sitename: mw.config.get( 'wgSiteName' ),
-                               project: mw.config.get( 'wgNoticeProject' ),
-                               anonymous: mw.config.get( 'wgUserName' ) === 
null,
-                               bucket: mw.centralNotice.data.bucket,
-                               country: mw.centralNotice.data.country,
-                               device: mw.centralNotice.data.device,
-                               slot: Math.floor( Math.random() * RAND_MAX ) + 
1,
-                               debug: mw.centralNotice.data.getVars.debug
-                       };
-                       var scriptUrl = mw.config.get( 
'wgCentralBannerDispatcher' ) + '?' + $.param( bannerDispatchQuery );
 
-                       $.ajax({
-                               url: scriptUrl,
-                               dataType: 'script',
-                               cache: true
-                       });
+                       var fetchBannerQueryParams = {
+                                       uselang: mw.config.get( 
'wgUserLanguage' ),
+                                       project: mw.config.get( 
'wgNoticeProject' ),
+                                       anonymous: mw.config.get( 'wgUserName' 
) === null,
+                                       bucket: mw.centralNotice.data.bucket,
+                                       country: mw.centralNotice.data.country,
+                                       device: mw.centralNotice.data.device,
+                                       debug: 
mw.centralNotice.data.getVars.debug
+                               },
+                               scriptUrl;
+
+                       // Check if we're configured to get choose banners on 
the client,
+                       // and do a few sanity checks.
+                       if ( mw.config.get( 
'wgCentralNoticeChooseBannerOnClient' ) &&
+                               mw.cnBannerControllerLib &&
+                               mw.cnBannerControllerLib.choiceData !== null ) {
+
+                               // Filter choice data and calculate allocations
+                               mw.cnBannerControllerLib.filterChoiceData();
+                               
mw.cnBannerControllerLib.calculateBannerAllocations();
+
+                               // Get a random seed or use the random= 
parameter from the URL,
+                               // and choose the banner.
+                               var random = 
mw.centralNotice.data.getVars.random || Math.random();
+                               mw.cnBannerControllerLib.chooseBanner( random );
+
+                               // Only fetch a banner if we need to :)
+                               if ( mw.centralNotice.data.banner ) {
+                                       fetchBannerQueryParams.banner = 
mw.centralNotice.data.banner;
+                                       fetchBannerQueryParams.campaign = 
mw.centralNotice.data.campaign;
+
+                                       scriptUrl = mw.config.get( 
'wgCentralSelectedBannerDispatcher' ) +
+                                               '?' + $.param( 
fetchBannerQueryParams );
+
+                                       // This will call insertBanner() after 
the banner is retrieved
+                                       $.ajax( {
+                                               url: scriptUrl,
+                                               dataType: 'script',
+                                               cache: true
+                                       } );
+
+                               } else {
+                                       // Call insertBanner to trigger a call 
to
+                                       // Special:RecordImpression to register 
the empty result.
+                                       // TODO Refactor and register that the 
banner wasn't even
+                                       // fetched.
+                                       mw.centralNotice.insertBanner( false );
+                               }
+
+                       } else {
+                               var RAND_MAX = 30;
+                               fetchBannerQueryParams.slot = Math.floor( 
Math.random() * RAND_MAX ) + 1;
+
+                               scriptUrl = mw.config.get( 
'wgCentralBannerDispatcher' ) +
+                                       '?' + $.param( fetchBannerQueryParams );
+
+                               $.ajax( {
+                                       url: scriptUrl,
+                                       dataType: 'script',
+                                       cache: true
+                               } );
+                       }
                },
                // TODO: move function definitions once controller cache has 
cleared
                insertBanner: function( bannerJson ) {
diff --git a/modules/ext.centralNotice.bannerController/bannerController.lib.js 
b/modules/ext.centralNotice.bannerController/bannerController.lib.js
index bd53bfb..8e7fcaa 100644
--- a/modules/ext.centralNotice.bannerController/bannerController.lib.js
+++ b/modules/ext.centralNotice.bannerController/bannerController.lib.js
@@ -8,11 +8,267 @@
                 * Set possible campaign and banner choices. Called by
                 * ext.centralNotice.bannerChoices.
                 */
-               'setChoiceData': function ( choices ) {
+               setChoiceData: function ( choices ) {
                        this.choiceData = choices;
                },
 
-               'choiceData': null
+               choiceData: null,
+
+               possibleBanners: null,
+
+               /**
+                * Filter the choice data and create a flat list of possible 
banners
+                * to chose from. Add some additional data on to each banner 
entry. The
+                * result is placed in possibleBanners.
+                *
+                * Note that if the same actual banner is assigned to more than 
one
+                * campaign it can have more than one entry in that list. 
That's the
+                * desired result; here "banners" would be more accurately 
called
+                * "banner assignments".
+                *
+                * The procedure followed here closely resembles legacy PHP 
code in
+                * BannerChooser and current PHP code in 
BannerAllocationCalculator.
+                *
+                * FIXME Re-organize code in all those places to make it easier 
to
+                * understand.
+                */
+               filterChoiceData: function() {
+
+                       var i, campaign, j, banner;
+                       this.possibleBanners = [];
+
+                       for ( i = 0; i < this.choiceData.length; i++ ) {
+                               campaign = this.choiceData[i];
+
+                               // Filter for country if geotargetted
+                               if ( campaign.geotargetted &&
+                                       ( $.inArray(
+                                       mw.centralNotice.data.country, 
campaign.countries )
+                                       === -1 ) ) {
+
+                                       continue;
+                               }
+
+                               // Now filter by banner properties
+                               for ( j = 0; j < campaign.banners.length; j++ ) 
{
+                                       banner = campaign.banners[j];
+
+                                       // Device
+                                       if ( $.inArray(
+                                               mw.centralNotice.data.device, 
banner.devices ) === -1 ) {
+                                               continue;
+                                       }
+
+                                       // Bucket
+                                       if ( parseInt( 
mw.centralNotice.data.bucket, 10) %
+                                               campaign.bucket_count !== 
banner.bucket ) {
+                                               continue;
+                                       }
+
+                                       // Add in data about the campaign the 
banner is part of.
+                                       // This will be used in the 
calculateBannerAllocations(),
+                                       // the next step in choosing a banner.
+                                       banner.campaignName = campaign.name;
+                                       banner.campaignThrottle = 
campaign.throttle;
+                                       banner.campaignZIndex = 
campaign.preferred;
+
+                                       this.possibleBanners.push( banner );
+                               }
+                       }
+               },
+
+               /**
+                * Calculate the allocation of possible banners (i.e., the 
required
+                * relative distribution among users that meet the same 
criteria as
+                * this user). This calculation will be used to randomly select 
a
+                * banner. This method operates on the values of 
this.possibleBanners.
+                *
+                * The procedure followed here closely resembles legacy PHP 
code in
+                * BannerChooser and current PHP code in 
BannerAllocationCalculator.
+                *
+                * FIXME Re-organize code in all those places to make it easier 
to
+                * understand.
+                */
+               calculateBannerAllocations: function() {
+
+                       var campaignTotalWeights = [],
+                               priorityTotalAllocations = [],
+                               bannersByPriority = [],
+                               priorities =  [],
+                               remainingAllocation = 1,
+                               i, j,
+                               banner, campaignName, campaignZIndex, priority,
+                               bannersAtThisPriority, 
totalAllocationAtThisPriority, scaling;
+
+                       // Calculate the sum of the weight properties for 
banners in each
+                       // campaign. This will be used to calculate their 
proportional
+                       // weight within each campaign and normalize weights 
across
+                       // campaigns.
+                       for ( i = 0; i < this.possibleBanners.length ; i ++ ) {
+
+                               banner = this.possibleBanners[i];
+                               campaignName = banner.campaignName;
+
+                               if ( !campaignTotalWeights[campaignName] ) {
+                                       campaignTotalWeights[campaignName] = 0;
+                               }
+
+                               campaignTotalWeights[campaignName] += 
banner.weight;
+                       }
+
+                       // Calculate the normalized maximum allocation of each 
banner
+                       // within the campaign it's assigned to. First we 
normalize the
+                       // banner's weight, then scale down as necessary if the 
campaign
+                       // is throttled. This is the maximum allocation because 
it may
+                       // be scaled down further in subsequent steps.
+                       for ( i = 0; i < this.possibleBanners.length ; i ++ ) {
+                               banner = this.possibleBanners[i];
+                               banner.maxAllocation =
+                                       ( banner.weight / 
campaignTotalWeights[banner.campaignName] )
+                                       * ( banner.campaignThrottle / 100 );
+                       }
+
+                       // Make an index of banners by priority level, and find 
the sum of
+                       // all maximum allocations for each priority level.
+
+                       // Note that here we are using a variety of terms for 
the same thing.
+                       // Priority level = priority = preferred (DB column) = 
z-index
+                       // (as copied in filterChoiceData()). This needs to be 
fixed, but
+                       // it's being left as-is for the transition to choosing 
banners on
+                       // the client, to make it easier to compare legacy and 
new code.
+
+                       // Note also that the actual values of priority levels 
are integers,
+                       // and higher integers represent higher priority. These 
values are
+                       // defined by class constants in the CentralNotice PHP 
class.
+
+                       for ( i = 0; i < this.possibleBanners.length ; i ++ ) {
+
+                               banner = this.possibleBanners[i];
+                               campaignZIndex = banner.campaignZIndex;
+
+                               // Initialize index vars the first time we hit 
this priority
+                               // level/zIndex
+                               if ( !bannersByPriority[campaignZIndex] ) {
+                                       bannersByPriority[campaignZIndex] = [];
+                                       
priorityTotalAllocations[campaignZIndex] = 0;
+                               }
+
+                               bannersByPriority[campaignZIndex].push( banner 
);
+                               priorityTotalAllocations[campaignZIndex] += 
banner.maxAllocation;
+                       }
+
+                       // Dole out chunks of allocation to create the final 
allocation
+                       // values. Full allocation is 1 and no allocation is 0; 
this is
+                       // tracked by the remainingAllocation variable.
+
+                       // First, make an array of priority levels and sort in 
descending
+                       // order.
+                       for ( priority in bannersByPriority ) {
+                               priorities.push( priority );
+                       }
+                       priorities.sort();
+                       priorities.reverse();
+
+                       // Now go through the priority levels from highest to 
lowest. If
+                       // campaigns are not throttled, then campaigns with a 
higher
+                       // priority level will eclipse all campaigns with lower 
priority.
+                       // Only if some campaigns are throttled will they allow 
some space
+                       // for campaigns at the next level down.
+
+                       // Also note that since priority and throttling are set 
at the
+                       // campaign level, there will never be banners from a 
single
+                       // campaign at more than one priority level. (This is 
important for
+                       // the per-campaign normalizations performed above to 
be useful
+                       // here.)
+
+                       for ( i = 0; i < priorities.length; i++ ) {
+
+                               bannersAtThisPriority = 
bannersByPriority[priorities[i]];
+                               totalAllocationAtThisPriority =
+                                       priorityTotalAllocations[priorities[i]];
+
+                               // If we fully allocated at a previous level, 
set allocations
+                               // at this level to zero. (We check with 0.01 
instead of 0 in
+                               // case of issues due to finite precision.)
+                               if ( remainingAllocation <= 0.01 ) {
+                                       for ( j = 0; j < 
bannersAtThisPriority.length; j++ ) {
+                                               
bannersAtThisPriority[j].allocation = 0;
+                                       }
+                                       continue;
+                               }
+
+                               // If we are here, there is some allocation 
remaining.
+
+                               // First see if the total allocation for this 
level is greater
+                               // than the remaining allocation. This can 
happen in two
+                               // circumstances: (1) if there is more than one 
campaign
+                               // at this level, and (2) if we are using up 
some remaining
+                               // allocation left over from a higher priority 
level. In both
+                               // cases, we'll scale the allocation of banners 
at this level to
+                               // exactly fill the remaining allocation.
+                               if ( totalAllocationAtThisPriority > 
remainingAllocation ) {
+                                       scaling = remainingAllocation / 
totalAllocationAtThisPriority;
+                                       remainingAllocation = 0;
+
+                               } else {
+                                       // If we are here, it means that 
whatever allocations there
+                                       // are at this level will either not 
fully take up or will
+                                       // exactly take up the remaining 
allocation. The former
+                                       // case means some campaigns are 
throttled, so we don't want
+                                       // to scale up, but rather leave some 
allocation tidbits for
+                                       // the next level. In the latter case, 
also, no scaling
+                                       // is needed. So we set scaling to 1, 
and take the chunk
+                                       // we are due from remainingAllocation.
+
+                                       scaling = 1;
+                                       remainingAllocation -= 
totalAllocationAtThisPriority;
+                               }
+
+                               // Set the allocation property of all the 
banners at this level,
+                               // scaling the previously set maxAllocation 
property as required.
+                               for ( j = 0; j < bannersAtThisPriority.length; 
j++ ) {
+                                       banner = bannersAtThisPriority[j];
+                                       banner.allocation = 
banner.maxAllocation * scaling;
+                               }
+                       }
+               },
+
+               /**
+                * Choose a banner (or choose not to show one) as determined by 
random
+                * and the allocations in this.possibleBanners. If a banner is 
chosen,
+                * set the banner's name in mw.centralNotice.data.banner and the
+                * campaign it's associated with in 
mw.centralNotice.data.campaign. If
+                * no banner is chosen, set mw.centralNotice.data.banner to 
null.
+                *
+                * @param random float A random number, greater or equal to 0  
and less
+                * than 1, to use in choosing a banner.
+                */
+               chooseBanner: function( random ) {
+                       var blockStart = 0,
+                               i, banner, blockEnd;
+
+                       // Cycle through possible banners, calculating which 
piece of
+                       // the allocation pie they should get. When random is 
in the piece,
+                       // choose the banner. Note that the order of the 
contents of
+                       // possibleBanners is not guaranteed to be consistent 
between
+                       // requests, but that shouldn't matter.
+                       for ( i = 0; i < this.possibleBanners.length; i ++ ) {
+                               banner = this.possibleBanners[i];
+                               blockEnd = blockStart + banner.allocation;
+
+                               if ( ( random >= blockStart ) && ( random < 
blockEnd ) ) {
+                                       mw.centralNotice.data.banner = 
banner.name;
+                                       mw.centralNotice.data.campaign = 
banner.campaignName;
+                                       return;
+                               }
+
+                               blockStart = blockEnd;
+                       }
+
+                       // We get here if there is less than full allocation 
(including no
+                       // allocation).
+                       mw.centralNotice.data.banner = null;
+               }
        };
 
 } )( jQuery, mediaWiki );
\ No newline at end of file
diff --git a/special/SpecialBannerLoader.php b/special/SpecialBannerLoader.php
index 82cea9e..692a90b 100644
--- a/special/SpecialBannerLoader.php
+++ b/special/SpecialBannerLoader.php
@@ -42,11 +42,8 @@
                $bucket = intval( $this->getSanitized( 'bucket', 
ApiCentralNoticeAllocations::BUCKET_FILTER ) );
                $device = $this->getSanitized( 'device', 
ApiCentralNoticeAllocations::DEVICE_NAME_FILTER );
 
-               $this->siteName = $request->getText( 'sitename' );
-
                $required_values = array(
-                       $project, $language, $country, $anonymous, $bucket, 
$device,
-                       $this->siteName,
+                       $project, $language, $country, $anonymous, $bucket, 
$device
                );
                foreach ( $required_values as $value ) {
                        if ( is_null( $value ) ) {
diff --git a/special/SpecialCentralNotice.php b/special/SpecialCentralNotice.php
index 3b8a2f7..893bacb 100644
--- a/special/SpecialCentralNotice.php
+++ b/special/SpecialCentralNotice.php
@@ -1,6 +1,9 @@
 <?php
 
 class CentralNotice extends SpecialPage {
+
+       // Note: These values are not arbitrary. Higher priority is indicated 
by a
+       // higher value.
        const LOW_PRIORITY = 0;
        const NORMAL_PRIORITY = 1;
        const HIGH_PRIORITY = 2;

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I04e2d4bfdd43529a83d32998e20d4bd885b9033d
Gerrit-PatchSet: 15
Gerrit-Project: mediawiki/extensions/CentralNotice
Gerrit-Branch: master
Gerrit-Owner: AndyRussG <[email protected]>
Gerrit-Reviewer: AndyRussG <[email protected]>
Gerrit-Reviewer: Awight <[email protected]>
Gerrit-Reviewer: Ejegg <[email protected]>
Gerrit-Reviewer: Katie Horn <[email protected]>
Gerrit-Reviewer: Mwalker <[email protected]>
Gerrit-Reviewer: Ssmith <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to