AndyRussG has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/205193

Change subject: Allocation: choose campaign first, fix throttling bug
......................................................................

Allocation: choose campaign first, fix throttling bug

This patch rewrites allocation client-side allocation code and
its PHP mirror used by the admin interface. Instead of allocating
from a flat list of banners from all campaigns targeting the user,
we choose a campaign first, then choose a banner within that
campaign. Along the way, we fix a bug in how throttling works.

Bug: T96194
Change-Id: I42985cf4de2dfeba9244b8d82f63b3d9d8319c81
---
M includes/BannerAllocationCalculator.php
M includes/BannerChoiceDataProvider.php
M modules/ext.centralNotice.bannerController/bannerController.js
M modules/ext.centralNotice.bannerController/bannerController.lib.js
M special/SpecialBannerAllocation.php
M 
tests/qunit/ext.centralNotice.bannerController.lib/bannerController.lib.tests.js
6 files changed, 654 insertions(+), 462 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/CentralNotice 
refs/changes/93/205193/1

diff --git a/includes/BannerAllocationCalculator.php 
b/includes/BannerAllocationCalculator.php
index 871319f..b6798a5 100644
--- a/includes/BannerAllocationCalculator.php
+++ b/includes/BannerAllocationCalculator.php
@@ -96,62 +96,231 @@
        }
 
        /**
-        * Allocation helper. Maps an array of campaigns with banners to a 
flattened
-        * list of banners, omitting those not available for the specified 
logged-in
-        * status, device and bucket.
+        * Filter an array in the format output by
+        * BannerCHoiceDataProvider::getChoices(), based on country, logged-in
+        * status and device. This method is the server-side equivalent of
+        * mw.cnBannerControllerLib.filterChoiceData(). (However, this method 
does
+        * not perform campaign freshness checks.)
         *
-        * @param array $campaigns campaigns with banners as returned by
+        * @param array $choiceData Campaigns with banners as returned by
         *   @see BannerChoiceDataProvider::getChoicesForCountry
+        *
+        * @param string $country Country of interest
         *
         * @param integer $status A status constant defined by this class (i.e.,
         *   BannerAllocationCalculator::ANONYMOUS or
         *   BannerAllocationCalculator::LOGGED_IN).
         *
         * @param string $device target device code
-        * @param integer $bucket target bucket number
         *
-        * @return array banners with properties suitable for
-        *   @see BannerAllocationCalculator::calculateAllocations
+        * @return array A filtered copy of $choiceData
         */
-       static function filterAndTransformBanners(
-               $campaigns, $status, $device, $bucket ) {
+       static function filterChoiceData( &$choiceData, $country, $status, 
$device ) {
 
-               // Set which property we need to check to filter logged-in 
status
-               switch ( $status ) {
-                       case self::ANONYMOUS:
-                               $status_prop = 'display_anon';
-                               break;
+               $filteredChoiceData = array();
 
-                       case self::LOGGED_IN:
-                               $status_prop = 'display_account';
-                               break;
+               foreach ( $choiceData as $campaign ) {
 
-                       default:
-                               throw new MWException( $this->status . 'is not 
a valid status '
-                                               . 'for 
BannerAllocationsCalculator.' );
-               }
+                       $keepCampaign = false;
 
-               $banners = array();
-               foreach( $campaigns as $campaign ) {
+                       // Filter for country if geotargeted
+                       if ( $campaign['geotargeted'] &&
+                               !in_array( $country, $campaign['countries'] ) ) 
{
+
+                               continue;
+                       }
+
+                       // Now filter by banner logged-in status and device
                        foreach ( $campaign['banners'] as $banner ) {
-                               if ( !$banner[$status_prop] ) {
+
+                               // Logged-in status
+                               if ( $status === 
BannerAllocationCalculator::ANONYMOUS &&
+                                       !$banner['display_anon'] ) {
                                        continue;
                                }
+                               if ( $status === 
BannerAllocationCalculator::LOGGED_IN &&
+                                       !$banner['display_account'] ) {
+                                       continue;
+                               }
+
+                               // Device
                                if ( !in_array( $device, $banner['devices'] ) ) 
{
                                        continue;
                                }
-                               if ( $bucket % $campaign['bucket_count'] != 
$banner['bucket'] ) {
-                                       continue;
-                               }
-                               $banner['campaign'] = $campaign['name'];
-                               $banner['campaign_throttle'] = 
$campaign['throttle'];
-                               $banner['campaign_z_index'] = 
$campaign['preferred'];
-                               $banners[] = $banner;
+
+                               // We get here if the campaign targets the 
requested country,
+                               // and has at least one banner for the 
requested logged-in status
+                               // and device.
+                               $keepCampaign = true;
+                       }
+
+                       if ( $keepCampaign ) {
+                               $filteredChoiceData[] = $campaign;
                        }
                }
+               $choiceData = $filteredChoiceData;
+       }
+
+       /**
+        * On $filteredChoiceData calculate the probability that
+        * of receiving each campaign in this.choiceData. This takes into 
account
+        * campaign priority and throttling. The equivalent client-side method
+        * is mw.cnBannerControllerLib.calculateCampaignAllocations().
+        *
+        * @param array $filteredChoiceData Data in the format provided by
+        *   filteredChoiceData().
+        */
+       public static function calculateCampaignAllocations( 
&$filteredChoiceData ) {
+
+               // Make an index of campaigns by priority level.
+               // Note that the actual values of priority levels are integers,
+               // and higher integers represent higher priority. These values 
are
+               // defined by class constants in CentralNotice.
+
+               $campaignsByPriority = array();
+               foreach ( $filteredChoiceData as &$campaign ) {
+                       $priority = $campaign['preferred'];
+                       $campaignsByPriority[$priority][] = &$campaign;
+               }
+
+               // Sort the index by priority, in descending order
+               krsort( $campaignsByPriority );
+
+               // 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.
+
+               $remainingAllocation = 1;
+
+               foreach ( $campaignsByPriority as $priority => 
&$campaignsAtThisPriority ) {
+
+                       // 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 ) {
+                               foreach ( $campaignsAtThisPriority as 
&$campaign ) {
+                                       $campaign['allocation'] = 0;
+                               }
+                               continue;
+                       }
+
+                       // If we are here, there is some allocation remaining.
+
+                       // All campaigns at a given priority level are alloted 
the same
+                       // allocation, unless they are throttled, in which case 
the
+                       // throttling value (taken as a percentage of the whole
+                       // allocation pie) is their maximum possible allocation.
+
+                       // To calculate this, we'll loop through the campaigns 
at this
+                       // level in order from the most throttled (lowest 
throttling
+                       // value) to the least throttled (highest value) and on 
each
+                       // loop, we'll re-calculate the remaining total 
allocation and
+                       // the proportional (i.e. unthrottled) allocation 
available to
+                       // each campaign.
+
+                       // First, sort the campaigns by throttling value
+
+                       usort( $campaignsAtThisPriority, function ( $a, $b ) {
+                               if ( $a['throttle'] < $b['throttle'] ) {
+                                       return -1;
+                               }
+                               if ( $a['throttle'] > $b['throttle'] ) {
+                                       return 1;
+                               }
+                               return 0;
+                       } );
+
+                       $campaignsAtThisPriorityCount = count( 
$campaignsAtThisPriority );
+                       foreach ( $campaignsAtThisPriority as $i => &$campaign 
) {
+
+                               // Calculate the proportional, unthrottled 
allocation now
+                               // available to a campaign at this level.
+                               $currentFullAllocation =
+                                       $remainingAllocation / ( 
$campaignsAtThisPriorityCount - $i );
+
+                               // A campaign may get the above amount, or 
less, if
+                               // throttling indicates that'd be too much.
+                               $actualAllocation =
+                                       min( $currentFullAllocation, 
$campaign['throttle'] / 100 );
+
+                               $campaign['allocation'] = $actualAllocation;
+
+                               // Update remaining allocation
+                               $remainingAllocation -= $actualAllocation;
+                       }
+               }
+       }
+
+       /**
+        * Filter banners for $campaign on $bucket, $status and $device, and 
return
+        * a list of possible banners for this context. The equivalent 
client-side method
+        * is mw.cnBannerControllerLib.makePossibleBanners().
+        *
+        * @param array $campaign Campaign data in the format of a single entry
+        *   in the array provided by filteredChoiceData().
+        *
+        * @param int $bucket Bucket of interest
+        *
+        * @param integer $status A status constant defined by this class (i.e.,
+        *   BannerAllocationCalculator::ANONYMOUS or
+        *   BannerAllocationCalculator::LOGGED_IN).
+        *
+        * @param string $device target device code
+        */
+       public static function makePossibleBanners( $campaign, $bucket, 
$status, $device ) {
+
+               $banners = array();
+
+               foreach ( $campaign['banners'] as $banner ) {
+
+                       // Filter for bucket
+                       if ( $bucket % $campaign['bucket_count'] != 
$banner['bucket'] ) {
+                               continue;
+                       }
+
+                       // Filter for logged-in status
+                       if ( $status === BannerAllocationCalculator::ANONYMOUS 
&&
+                               !$banner['display_anon'] ) {
+                               continue;
+                       }
+                       if ( $status === BannerAllocationCalculator::LOGGED_IN 
&&
+                               !$banner['display_account'] ) {
+                               continue;
+                       }
+
+                       // Filter for device
+                       if ( !in_array( $device, $banner['devices'] ) ) {
+                               continue;
+                       }
+
+                       $banners[] = $banner;
+               }
+
                return $banners;
        }
 
+       /**
+        * Calculate banner allocation based on relative weights. The equivalent
+        * client-side method is
+        * mw.cnBannerControllerLib.calculateBannerAllocations().
+        */
+       public static function calculateBannerAllocations( &$banners ) {
+
+               $totalWeights = 0;
+
+               // Find the sum of all banner weights
+               foreach ( $banners as $banner ) {
+                       $totalWeights += $banner['weight'];
+               }
+
+               // Set allocation property to the normalized weight
+               foreach ( $banners as &$banner ) {
+                       $banner['allocation'] = $banner['weight'] / 
$totalWeights;
+               }
+       }
+
        static public function getLoggedInStatusFromString( $s ) {
                switch ( $s ) {
                        case 'anonymous':
diff --git a/includes/BannerChoiceDataProvider.php 
b/includes/BannerChoiceDataProvider.php
index e71d451..9c1914f 100644
--- a/includes/BannerChoiceDataProvider.php
+++ b/includes/BannerChoiceDataProvider.php
@@ -248,24 +248,4 @@
 
                return $choices;
        }
-
-       /**
-        * Gets banner choices filtered by country
-        *
-        * @param string $country Country of interest
-        * @return array An array of the form returned by @see 
BannerChoiceDataProvider::getChoices
-        *   Omits all geotargeted campaigns not aimed at the given country
-        */
-       public function getChoicesForCountry( $country ) {
-               $choices = $this->getChoices();
-
-               $filteredChoices = array();
-               // Remove campaigns that are geotargeted but not to selected 
country
-               foreach( $choices as $campaign ) {
-                       if ( !$campaign['geotargeted'] || in_array( $country, 
$campaign['countries'] ) ) {
-                               $filteredChoices[] = $campaign;
-                       }
-               }
-               return $filteredChoices;
-       }
 }
diff --git a/modules/ext.centralNotice.bannerController/bannerController.js 
b/modules/ext.centralNotice.bannerController/bannerController.js
index 47d0897..9e5692e 100644
--- a/modules/ext.centralNotice.bannerController/bannerController.js
+++ b/modules/ext.centralNotice.bannerController/bannerController.js
@@ -171,63 +171,32 @@
                },
                loadRandomBanner: function () {
 
-                       var fetchBannerQueryParams = {
+                       var fetchBannerQueryParams,
+                               scriptUrl;
+
+                       mw.centralNotice.chooseRandomBanner();
+
+                       if ( mw.centralNotice.data.banner ) {
+
+                               // A banner was chosen! Let's fetch it.
+
+                               // TODO remove unneeded params
+                               fetchBannerQueryParams = {
+                                       banner: mw.centralNotice.data.banner,
+                                       campaign: 
mw.centralNotice.data.campaign,
                                        uselang: mw.config.get( 
'wgUserLanguage' ),
                                        project: mw.config.get( 
'wgNoticeProject' ),
                                        anonymous: mw.config.get( 'wgUserName' 
) === null,
                                        country: mw.centralNotice.data.country,
                                        device: mw.centralNotice.data.device,
                                        debug: 
mw.centralNotice.data.getVars.debug
-                               },
-                               scriptUrl,
-                               random;
-
-                       // Choose the banner on the client.
-
-                       // Continue only if the server sent choices
-                       if ( mw.cnBannerControllerLib.choiceData
-                               && mw.cnBannerControllerLib.choiceData.length > 0
-                       ) {
-
-                               // If the server did send one or more choices: 
let the
-                               // processing begin!
-
-                               // Filter choiceData on country and device. 
Only campaigns that
-                               // target the user's country and have at least 
one banner for
-                               // the user's logged-in status and device pass 
this filter.
-                               mw.cnBannerControllerLib.filterChoiceData();
-
-                               // Again check if there are choices available. 
This result may
-                               // have changed following the above call to 
filterChoiceData().
-                               if ( mw.cnBannerControllerLib.choiceData.length 
> 0 ) {
-
-                                       // Do all things bucket. Retrieve or 
generate buckets for all
-                                       // the campaigns remaining in 
choiceData. Then update expiry
-                                       // dates and remove expired buckets as 
necessary.
-                                       
mw.cnBannerControllerLib.processBuckets();
-
-                                       // Create a flat list of possible 
banners available for the
-                                       // user's buckets, logged-in status and 
device, and calculate
-                                       // allocations.
-                                       
mw.cnBannerControllerLib.makePossibleBanners();
-                                       
mw.cnBannerControllerLib.calculateBannerAllocations();
-
-                                       // Get a random seed or use the random= 
parameter from the URL,
-                                       // and choose the banner.
-                                       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 = new mw.Uri( mw.config.get( 
'wgCentralSelectedBannerDispatcher' ) );
                                scriptUrl.extend( fetchBannerQueryParams );
 
-                               // This will call insertBanner() after the 
banner is retrieved
+                               // The returned javascript will call 
insertBanner() after the
+                               // banner is retrieved.
                                $.ajax( {
                                        url: scriptUrl.toString(),
                                        dataType: 'script',
@@ -235,12 +204,107 @@
                                } );
 
                        } else {
-                               // Call insertBanner and set the onlySampleRI 
flag to true
-                               // to sample empty results and return them via
-                               // Special:RecordImpression.
+
+                               // No banner for this user!
+
+                               // Set the onlySampleRI flag to true to sample 
empty results
+                               // and return them via Special:RecordImpression.
                                // TODO Refactor
                                mw.centralNotice.onlySampleRI = true;
                                mw.centralNotice.insertBanner( false );
+                       }
+               },
+               /**
+                * Choose a banner or no banner randomly (or using the random1 
and
+                * random2 URL parameters). If a banner is chosen its name will 
be put
+                * in mw.centralNotice.data.banner and the campaign name will 
be put in
+                * mw.centralNotice.data.campaign.
+                */
+               chooseRandomBanner: function () {
+
+                       var random1, random2;
+
+                       // Check that we got choiceData (sanity check) and see 
if there
+                       // are choices. TODO Log if there was no choiceData.
+                       if ( !mw.cnBannerControllerLib.choiceData
+                               || mw.cnBannerControllerLib.choiceData.length 
=== 0 ) {
+                               return;
+                       }
+
+                       // The server did send one or more choices: let the 
processing begin!
+
+                       // Filter choiceData on country and device. Only 
campaigns that
+                       // target the user's country and have at least one 
banner for
+                       // the user's logged-in status and device pass this 
filter.
+                       mw.cnBannerControllerLib.filterChoiceData();
+
+                       // Again check if there are choices available. This 
result may
+                       // have changed following the above call to 
filterChoiceData().
+                       if ( mw.cnBannerControllerLib.choiceData.length === 0 ) 
{
+                               return;
+                       }
+
+                       // Optimize for the common scenario of a single 
unthrottled campaign
+                       if ( ( mw.cnBannerControllerLib.choiceData.length === 1 
) &&
+                               ( 
mw.cnBannerControllerLib.choiceData[0].throttle === 100 ) ) {
+
+                               mw.cnBannerControllerLib.setCampaign(
+                                       mw.cnBannerControllerLib.choiceData[0] 
);
+
+                       } else {
+
+                               // Otherwise, we need to calculate allocations 
and choose randomly
+
+                               // Calculate the user's probability of getting 
each campaign
+                               
mw.cnBannerControllerLib.calculateCampaignAllocations();
+
+                               // Get a random seed or use the random1= 
parameter from the URL, and
+                               // choose the campaign.
+                               random1 = mw.centralNotice.data.getVars.random1 
|| Math.random();
+                               mw.cnBannerControllerLib.chooseCampaign( 
random1 );
+                       }
+
+                       // Check if a campaign was selected (we might have 
chosen an
+                       // unallocated block).
+                       if ( !mw.cnBannerControllerLib.campaign ) {
+                               return;
+                       }
+
+                       // Do all things bucket. Retrieve or generate a bucket 
for this
+                       // campaign. Then, update expiry dates and remove 
expired buckets as
+                       // necessary.
+                       mw.cnBannerControllerLib.processBuckets();
+
+                       // Create a list of possible banners available in this 
campaign for
+                       // the user's bucket, logged-in status and device, and 
calculate
+                       // allocations.
+                       mw.cnBannerControllerLib.makePossibleBanners();
+
+                       // Because of our wonky domain model, it's possible to 
have banners
+                       // with different criteria (logged-in status, device) 
in different
+                       // buckets in the same campaign. So we might be in a 
bucket with no
+                       // banners for this user, which would mean no possible 
banners.
+                       if ( mw.cnBannerControllerLib.possibleBanners.length 
=== 0 ) {
+                               return;
+                       }
+
+                       // If there's just one banner available for the user in 
this
+                       // campaign, choose it. This is by far our most common 
scenario, so
+                       // we'll optimize for it. In this case the random2 URL 
parameter is
+                       // ignored.
+                       if ( mw.cnBannerControllerLib.possibleBanners.length 
=== 1 ) {
+
+                               mw.cnBannerControllerLib.setBanner(
+                                       
mw.cnBannerControllerLib.possibleBanners[0] );
+
+                       } else {
+                               // Otherwise, calculate allocations.
+                               
mw.cnBannerControllerLib.calculateBannerAllocations();
+
+                               // Get another random seed or use the random2= 
parameter from the
+                               // URL, and choose the banner.
+                               random2 = mw.centralNotice.data.getVars.random2 
|| Math.random();
+                               mw.cnBannerControllerLib.chooseBanner( random2 
);
                        }
                },
                // TODO: move function definitions once controller cache has 
cleared
@@ -290,27 +354,20 @@
                        } );
                },
                /**
-                * Legacy function for getting the legacy global bucket. Left 
here for
-                * compatibility.
+                * Legacy function for getting the legacy global bucket. Left 
here
+                * for paranoid JS-breakage avoidance debugging fun fun.
                 * @deprecated
                 */
                getBucket: function() {
-                       var bucket = 
mw.cnBannerControllerLib.retrieveLegacyBucket();
-                       if ( !bucket ) {
-                               bucket = 
mw.cnBannerControllerLib.getRandomBucket();
-                       }
-                       return bucket;
+                       mw.log( 'Legacy mw.bannerController.getBucket() is 
deprecated no-op.' );
                },
                /**
-                * Legacy function for storing the legacy global bucket. Left 
here for
-                * compatibility. Stores
-                * mw.centralNotice.data.bucket. See
-                * mw.cnBannerControllerLib.storeLegacyBucket.
+                * Legacy function for storing the legacy global bucket Left 
here
+                * for paranoid JS-breakage avoidance.
                 * @deprecated
                 */
                storeBucket: function() {
-                       mw.cnBannerControllerLib.storeLegacyBucket(
-                               mw.centralNotice.data.bucket );
+                       mw.log( 'Legacy mw.bannerController.storeBucket() is 
deprecated no-op.' );
                },
                initialize: function () {
                        // === Do not allow CentralNotice to be re-initialized. 
===
diff --git a/modules/ext.centralNotice.bannerController/bannerController.lib.js 
b/modules/ext.centralNotice.bannerController/bannerController.lib.js
index 04c47bd..8782899 100644
--- a/modules/ext.centralNotice.bannerController/bannerController.lib.js
+++ b/modules/ext.centralNotice.bannerController/bannerController.lib.js
@@ -1,7 +1,42 @@
 ( function ( $, mw ) {
 
-       var bucketValidityFromServer = mw.config.get( 'wgNoticeNumberOfBuckets' 
)
-               + '.' + mw.config.get( 'wgNoticeNumberOfControllerBuckets' );
+
+       /**
+        * Method used for choosing a campaign or banner from an array of
+        * allocated campaigns or banners.
+        *
+        * Given an array of objects with 'allocation' properties, the sum of 
which
+        * is greater than or equal to 0 and less than or equal to 1, return the
+        * object whose allocation block is indicated by a number greater than 
or
+        * equal to 0 and less than 1.
+        *
+        * @param array allocatedArray
+        * @param random float A random number, greater or equal to 0  and less
+        *   than 1, to use in choosing an object.
+        */
+       function chooseObjInAllocatedArray( random, allocatedArray ) {
+               var blockStart = 0,
+                       i, obj, blockEnd;
+
+               // Cycle through objects, calculating which piece of
+               // the allocation pie they should get. When random is in the 
piece,
+               // choose the object.
+
+               for ( i = 0; i < allocatedArray.length; i ++ ) {
+                       obj = allocatedArray[i];
+                       blockEnd = blockStart + obj.allocation;
+
+                       if ( ( random >= blockStart ) && ( random < blockEnd ) 
) {
+                               return obj;
+                       }
+
+                       blockStart = blockEnd;
+               }
+
+               // We get here if there is less than full allocation (including 
no
+               // allocation) and random points to the unallocated chunk.
+               return null;
+       }
 
        // FIXME Temporary location of this object on the mw hierarchy. See 
FIXME
        // in bannerController.js.
@@ -16,6 +51,13 @@
                CAMPAIGN_STALENESS_LEEWAY: 15,
 
                choiceData: null,
+
+               /**
+                * Once a campaign is chosen, this will receive a copy of the 
data for
+                * that campaign. (This is different from 
mw.centralNotice.data.campaign,
+                * which will hold just the name.)
+                */
+               campaign: null,
                bucketsByCampaign: null,
                possibleBanners: null,
 
@@ -23,94 +65,210 @@
                 * Set possible campaign and banner choices. Called by
                 * ext.centralNotice.bannerChoices.
                 */
-               setChoiceData: function ( choices ) {
+               setChoiceData: function( choices ) {
                        this.choiceData = choices;
                },
 
                /**
+                * For targeted users (users meeting the same logged-in status, 
country,
+                * and language criteria as this user) calculate the 
probability that
+                * of receiving each campaign in this.choiceData. This takes 
into account
+                * campaign priority and throttling. The equivalent server-side 
method
+                * is 
BannerAllocationCalculator::calculateCampaignAllocations().
+                */
+               calculateCampaignAllocations: function() {
+
+                       var i, campaign, campaignPriority,
+                               campaignsByPriority =[],
+                               priorities = [],
+                               priority, campaignsAtThisPriority,
+                               remainingAllocation = 1,
+                               j, campaignsAtThisPriorityCount, 
currentFullAllocation,
+                               actualAllocation;
+
+                       // Make an index of campaigns by priority level.
+                       // Note 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.choiceData.length ; i ++ ) {
+
+                               campaign = this.choiceData[i];
+                               campaignPriority = campaign.preferred;
+
+                               // Initialize index the first time we hit this 
priority
+                               if ( !campaignsByPriority[campaignPriority] ) {
+                                       campaignsByPriority[campaignPriority] = 
[];
+                               }
+
+                               campaignsByPriority[campaignPriority].push( 
campaign );
+                       }
+
+                       // Make an array of priority levels and sort in 
descending order.
+                       for ( priority in campaignsByPriority ) {
+                               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.
+
+                       for ( i = 0; i < priorities.length; i++ ) {
+
+                               campaignsAtThisPriority = 
campaignsByPriority[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 < 
campaignsAtThisPriority.length; j++ ) {
+                                               
campaignsAtThisPriority[j].allocation = 0;
+                                       }
+                                       continue;
+                               }
+
+                               // If we are here, there is some allocation 
remaining.
+
+                               // All campaigns at a given priority level are 
alloted the same
+                               // allocation, unless they are throttled, in 
which case the
+                               // throttling value (taken as a percentage of 
the whole
+                               // allocation pie) is their maximum possible 
allocation.
+
+                               // To calculate this, we'll loop through the 
campaigns at this
+                               // level in order from the most throttled 
(lowest throttling
+                               // value) to the least throttled (highest 
value) and on each
+                               // loop, we'll re-calculate the remaining total 
allocation and
+                               // the proportional (i.e. unthrottled) 
allocation available to
+                               // each campaign.
+
+                               // First, sort the campaigns by throttling value
+
+                               campaignsAtThisPriority.sort( function ( a, b ) 
{
+                                       if ( a.throttle < b.throttle ) {
+                                               return -1;
+                                       }
+                                       if ( a.throttle > b.throttle ) {
+                                               return 1;
+                                       }
+                                       return 0;
+                               } );
+
+                               campaignsAtThisPriorityCount = 
campaignsAtThisPriority.length;
+                               for ( j = 0; j < campaignsAtThisPriorityCount; 
j++ ) {
+
+                                       campaign = campaignsAtThisPriority[j];
+
+                                       // Calculate the proportional, 
unthrottled allocation now
+                                       // available to a campaign at this 
level.
+                                       currentFullAllocation =
+                                               remainingAllocation / ( 
campaignsAtThisPriorityCount - j );
+
+                                       // A campaign may get the above amount, 
or less, if
+                                       // throttling indicates that'd be too 
much.
+                                       actualAllocation =
+                                               Math.min( 
currentFullAllocation, campaign.throttle / 100 );
+
+                                       campaign.allocation = actualAllocation;
+
+                                       // Update remaining allocation
+                                       remainingAllocation -= actualAllocation;
+                               }
+                       }
+               },
+
+               /**
+                * Choose a campaign (or no campaign) as determined by random 
and the
+                * allocations in this.choiceData.
+                *
+                * @param random float A random number, greater or equal to 0  
and less
+                *   than 1, to use in choosing a campaign.
+                */
+               chooseCampaign: function( random ) {
+                       this.setCampaign(
+                               chooseObjInAllocatedArray( random, 
this.choiceData ) );
+               },
+
+               /**
+                * Set the campaign. Set this.campaign to the object received 
and set
+                * mw.centralnotice.data.campaign to the campaign name, or both 
to
+                * null if campaign is null.
+                */
+               setCampaign: function( campaign ) {
+                       this.campaign = campaign;
+                       mw.centralNotice.data.campaign = campaign ? 
campaign.name : null;
+               },
+
+               /**
                 * Do all things bucket:
-                *
-                * - Go through choiceData and retrieve or generate buckets for 
all
-                *   campaigns. If we don't already have a bucket for a 
campaign, but
-                *   we still have legacy buckets, copy those in. Otherwise 
choose a
-                *   random bucket. If we did already have a bucket for a 
campaign,
-                *   check and possibly update its expiry date.
-                *
-                * - Go through all the buckets stored, purging expired buckets.
-                *
+                * - Retrieve or generate a random bucket for the campaign in
+                *   this.campaign.
                 * - Store the updated bucket data in a cookie.
+                * - Go through all the buckets stored, purging expired buckets.
                 */
                processBuckets: function() {
 
-                       var campaign, campaignName, bucket,
-                               campaignStartDate, retrievedBucketEndDate, 
bucketEndDate,
-                               now = new Date(),
-                               bucketsModified = false,
-                               val,
+                       var campaign = this.campaign,
+                               campaignName = campaign.name,
+                               campaignStartDate,
+                               bucket, bucketEndDate, retrievedBucketEndDate, 
val,
                                extension = mw.config.get( 
'wgCentralNoticePerCampaignBucketExtension' ),
-                               i;
+                               now = new Date(),
+                               bucketsModified = false;
+
+                       campaignStartDate = new Date();
+                       campaignStartDate.setTime( campaign.start * 1000  );
+
+                       // Buckets should end the time indicated by extension 
after
+                       // the campaign's end
+                       bucketEndDate = new Date();
+                       bucketEndDate.setTime( campaign.end * 1000 );
+                       bucketEndDate.setUTCDate( bucketEndDate.getUTCDate() + 
extension );
 
                        this.retrieveBuckets();
+                       bucket = this.bucketsByCampaign[campaignName];
 
-                       for ( i = 0; i < this.choiceData.length; i++ ) {
+                       // If we have a valid bucket, just check and possibly 
update its
+                       // expiry.
 
-                               campaign = this.choiceData[i];
-                               campaignName = campaign.name;
-                               campaignStartDate = new Date();
-                               campaignStartDate.setTime( campaign.start * 
1000  );
+                       // Note that buckets that are expired but that are 
found in
+                       // the cookie (because they didn't have the chance to 
get
+                       // purged) are not considered valid. In that case, for
+                       // consistency, we choose a new random bucket, just as 
if
+                       // no bucket had been found.
 
-                               // Buckets should end the time indicated by 
extension after
-                               // the campaign's end
-                               bucketEndDate = new Date();
-                               bucketEndDate.setTime( campaign.end * 1000 );
-                               bucketEndDate.setUTCDate( 
bucketEndDate.getUTCDate() + extension );
+                       if ( bucket && bucketEndDate > now ) {
 
-                               bucket = this.bucketsByCampaign[campaignName];
+                               retrievedBucketEndDate = new Date();
+                               retrievedBucketEndDate.setTime( bucket.end * 
1000 );
 
-                               // If we have a valid bucket for this campaign, 
just check
-                               // and possibly update its expiry.
-                               // Note that buckets that are expired but that 
are found in
-                               // the cookie (because they didn't have the 
chance to get
-                               // purged) are not considered valid. In that 
case, for
-                               // consistency, we choose a new random bucket, 
just as if
-                               // no bucket had been found.
-                               if ( bucket && bucketEndDate > now ) {
+                               if ( retrievedBucketEndDate.getTime()
+                                       !== bucketEndDate.getTime() ) {
 
-                                       retrievedBucketEndDate = new Date();
-                                       retrievedBucketEndDate.setTime( 
bucket.end * 1000 );
-
-                                       if ( retrievedBucketEndDate.getTime()
-                                               !== bucketEndDate.getTime() ) {
-
-                                               bucket.end = 
bucketEndDate.getTime() / 1000;
-                                               bucketsModified = true;
-                                       }
-
-                               } else {
-
-                                       // First try to get a legacy bucket 
value. These are only
-                                       // expected to be around for one week 
after the activation
-                                       // of per-campaign buckets. Doing this 
eases the transition.
-                                       val = this.retrieveLegacyBucket();
-
-                                       if ( !val ) {
-                                               // We always use 
wgNoticeNumberOfControllerBuckets, and
-                                               // not the campaign's number of 
buckets, to determine
-                                               // how many possible buckets to 
randomly choose from. If
-                                               // the campaign actually has 
less buckets than that,
-                                               // the value is mapped down as 
necessary. This lets
-                                               // campaigns modify the number 
of buckets they use.
-                                               val = this.getRandomBucket();
-                                       }
-
-                                       this.bucketsByCampaign[campaignName] = {
-                                               val: val,
-                                               start: 
campaignStartDate.getTime() / 1000,
-                                               end: bucketEndDate.getTime() / 
1000
-                                       };
-
+                                       bucket.end = bucketEndDate.getTime() / 
1000;
                                        bucketsModified = true;
                                }
+
+                       } else {
+
+                               // We always use 
wgNoticeNumberOfControllerBuckets, and
+                               // not the campaign's number of buckets, to 
determine
+                               // how many possible buckets to randomly choose 
from. If
+                               // the campaign actually has less buckets than 
that,
+                               // the value is mapped down as necessary. This 
lets
+                               // campaigns modify the number of buckets they 
use.
+                               val = this.getRandomBucket();
+
+                               this.bucketsByCampaign[campaignName] = {
+                                       val: val,
+                                       start: campaignStartDate.getTime() / 
1000,
+                                       end: bucketEndDate.getTime() / 1000
+                               };
+
+                               bucketsModified = true;
                        }
 
                        // Purge any expired buckets
@@ -190,45 +348,18 @@
                },
 
                /**
-                * Retrieve the user's legacy global bucket from the legacy 
bucket
-                * cookie. Follow the legacy procedure for determining 
validity. If a
-                * valid bucket was available, return it, otherwise return null.
-                */
-               retrieveLegacyBucket: function() {
-                       var dataString = $.cookie( 'centralnotice_bucket' ) || 
'',
-                               bucket = dataString.split('-')[0],
-                               validity = dataString.split('-')[1];
-
-                       if ( ( bucket === null ) || ( validity !== 
bucketValidityFromServer ) ) {
-                               return null;
-                       }
-
-                       return bucket;
-               },
-
-               /**
-                * Store the legacy bucket.
-                * Puts the bucket in the legacy global bucket cookie.
-                * If such a cookie already exists, extends its expiry date as
-                * indicated by wgNoticeBucketExpiry.
-                */
-               storeLegacyBucket: function( bucket ) {
-                       $.cookie(
-                               'centralnotice_bucket',
-                               bucket + '-' + bucketValidityFromServer,
-                               { expires: mw.config.get( 
'wgNoticeBucketExpiry' ), path: '/' }
-                       );
-               },
-
-               /**
                 * Filter choiceData on the user's country, logged-in status 
and device.
                 * Campaigns that don't target the user's country or have no 
banners for
                 * their logged-in status and device will be removed.
                 *
+                * The server-side equivalent of this method is
+                * BannerAllocationCalculator::filterChoiceData().
+                *
                 * We also check for campaigns that are have already ended, 
which might
                 * happen due to incorrect caching of choiceData between us and 
the user.
                 * If that happens we just toss everything out because one 
stale campaign
-                * spoils the basket. TODO: Log when this happens.
+                * spoils the basket. (This freshness check is not performed in 
the
+                * server-side method.) TODO: Log when this happens.
                 *
                 * We operate on this.choiceData.
                 */
@@ -267,9 +398,6 @@
                                }
 
                                // Now filter by banner logged-in status and 
device.
-                               // To make buckets work consistently even for 
strangely
-                               // configured campaigns, we won't chose buckets 
yet, so we'll
-                               // filter on them a little later.
                                for ( j = 0; j < campaign.banners.length; j++ ) 
{
                                        banner = campaign.banners[j];
 
@@ -303,259 +431,91 @@
                },
 
                /**
-                * Filter the choice data on the user's logged-in status, 
device and
-                * per-campaign buckets (some banners that are not for the 
user's status
-                * or device may remain following previous filters) and create 
a flat
-                * list of possible banners to chose from. Add some extra data 
on to
-                * each banner entry. The result is placed in possibleBanners.
+                * Filter banners for this.campaign on the user's logged-in 
status,
+                * device and bucket (some banners that are not for the user's 
status
+                * or device may remain following previous filters) and create 
a list
+                * of possible banners to chose from. The result is placed in
+                * this.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 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.
+                * The equivalent server-side method
+                * BannerAllocationCalculator::makePossibleBanners().
                 */
                makePossibleBanners: function() {
 
-                       var i, campaign, campaignName, j, banner;
+                       var i, campaign, campaignName, banner;
                        this.possibleBanners = [];
 
-                       for ( i = 0; i < this.choiceData.length; i++ ) {
-                               campaign = this.choiceData[i];
-                               campaignName = campaign.name;
+                       campaign = this.campaign;
+                       campaignName = campaign.name;
 
-                               for ( j = 0; j < campaign.banners.length; j++ ) 
{
-                                       banner = campaign.banners[j];
+                       for ( i = 0; i < campaign.banners.length; i++ ) {
+                               banner = campaign.banners[i];
 
-                                       // Filter for bucket
-                                       if ( 
this.bucketsByCampaign[campaignName].val %
-                                               campaign.bucket_count !== 
banner.bucket ) {
-                                               continue;
-                                       }
-
-                                       // Filter for logged-in status
-                                       if ( mw.centralNotice.data.anonymous && 
!banner.display_anon ) {
-                                               continue;
-                                       }
-                                       if ( !mw.centralNotice.data.anonymous 
&& !banner.display_account ) {
-                                               continue;
-                                       }
-
-                                       // Filter for device
-                                       if ( $.inArray(
-                                               mw.centralNotice.data.device, 
banner.devices ) === -1 ) {
-                                               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 = campaignName;
-                                       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;
-                                       }
+                               // Filter for bucket
+                               if ( this.bucketsByCampaign[campaignName].val %
+                                       campaign.bucket_count !== banner.bucket 
) {
                                        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;
+                               // Filter for logged-in status
+                               if ( mw.centralNotice.data.anonymous && 
!banner.display_anon ) {
+                                       continue;
+                               }
+                               if ( !mw.centralNotice.data.anonymous && 
!banner.display_account ) {
+                                       continue;
                                }
 
-                               // 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;
+                               // Filter for device
+                               if ( $.inArray(
+                                       mw.centralNotice.data.device, 
banner.devices ) === -1 ) {
+                                       continue;
                                }
+
+                               this.possibleBanners.push( banner );
                        }
                },
 
                /**
-                * 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.
+                * Calculate banner allocation based on relative weights.
+                * The equivalent server-side method
+                * BannerAllocationCalculator::calculateBannerAllocations().
                 */
-               chooseBanner: function( random ) {
-                       var blockStart = 0,
-                               i, banner, blockEnd;
+               calculateBannerAllocations: function() {
+                       var i, banner,
+                               totalWeights = 0;
 
-                       // 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;
+                       // Find the sum of all banner weights
+                       for ( i = 0; i < this.possibleBanners.length; i++ ) {
+                               totalWeights += this.possibleBanners[i].weight;
                        }
 
-                       // We get here if there is less than full allocation 
(including no
-                       // allocation) and random points to the unallocated 
chunk.
-                       mw.centralNotice.data.banner = null;
+                       // Set allocation property to the normalized weight
+                       for ( i = 0; i < this.possibleBanners.length; i++ ) {
+                               banner = this.possibleBanners[i];
+                               banner.allocation = banner.weight / 
totalWeights;
+                       }
+               },
+
+               /**
+                * Choose a banner as determined by random and the allocations 
in
+                * this.possibleBanners. Set the banner's name in
+                * mw.centralNotice.data.banner.
+                *
+                * @param random float A random number, greater or equal to 0  
and less
+                *   than 1, to use in choosing a banner.
+                */
+               chooseBanner: function( random ) {
+                       // Since we never have an allocation gap for banners, 
this should
+                       // always work.
+                       this.setBanner(
+                               chooseObjInAllocatedArray( random, 
this.possibleBanners ) );
+               },
+
+               /**
+                * Set the banner (should never be null).
+                */
+               setBanner: function( banner ) {
+                       mw.centralNotice.data.banner = banner.name;
                }
        };
 
diff --git a/special/SpecialBannerAllocation.php 
b/special/SpecialBannerAllocation.php
index 4e13a64..2ab058c 100644
--- a/special/SpecialBannerAllocation.php
+++ b/special/SpecialBannerAllocation.php
@@ -206,15 +206,19 @@
                // Given our project and language combination, get banner 
choice data,
                // then filter on country
                $provider = new BannerChoiceDataProvider( $project, $language );
-               $choice_data = $provider->getChoicesForCountry( $country );
+               $choiceDataBase = $provider->getChoices();
 
                // Iterate through each possible device type and get allocation 
information
                $devices = CNDeviceTarget::getAvailableDevices();
-               foreach( $devices as $device_id => $device_data ) {
+               foreach( $devices as $deviceId => $deviceData ) {
+
+                       // Use a copy of choiceData since it'll be modified in 
this process
+                       $choiceData = $choiceDataBase;
+
                        $htmlOut .= Html::openElement(
                                'div',
                                array(
-                                        'id' => 
"cn-allocation-{$project}-{$language}-{$country}-{$device_id}",
+                                        'id' => 
"cn-allocation-{$project}-{$language}-{$country}-{$deviceId}",
                                         'class' => 'cn-allocation-group'
                                )
                        );
@@ -226,10 +230,11 @@
                                        htmlspecialchars( $language ),
                                        htmlspecialchars( $project ),
                                        htmlspecialchars( $country ),
-                                       $this->getOutput()->parseInline( 
$device_data['label'] )
+                                       $this->getOutput()->parseInline( 
$deviceData['label'] )
                                )->text()
                        );
 
+                       // FIXME Figure out the following comments and remove 
as needed
                        // FIXME bannerstats is toast
                        // Build campaign list for bannerstats.js
                        //$campaignsUsed = array_keys($anonCampaigns + 
$accountCampaigns);
@@ -258,14 +263,34 @@
                                $label .= ' -- ' . $this->msg( 
'centralnotice-bucket-letter' )->
                                        rawParams( chr( $target['bucket'] + 65 
) )->text();
 
-                               $banners = 
BannerAllocationCalculator::filterAndTransformBanners(
-                                       $choice_data,
-                                       $status,
-                                       $device_data['header'],
-                                       intval( $target['bucket'] )
-                               );
-                               $banners = 
BannerAllocationCalculator::calculateAllocations( $banners );
-                               $htmlOut .= $this->getTable( $label, $banners );
+                               BannerAllocationCalculator::filterChoiceData(
+                                       $choiceData, $country, $status, 
$deviceData['header'] );
+
+                               
BannerAllocationCalculator::calculateCampaignAllocations( $choiceData );
+
+                               $possibelBannersAllCampaigns = array();
+                               foreach ( $choiceData as $campaign ) {
+
+                                       $possibleBanners = 
BannerAllocationCalculator::makePossibleBanners(
+                                               $campaign,
+                                               intval( $target['bucket'] ),
+                                               $status,
+                                               $deviceData['header']
+                                       );
+
+                                       
BannerAllocationCalculator::calculateBannerAllocations( $possibleBanners );
+
+                                       foreach ( $possibleBanners as $banner ) 
{
+                                               $banner['campaign'] = 
$campaign['name'];
+
+                                               $banner['allocation'] =
+                                                       $banner['allocation'] * 
$campaign['allocation'];
+
+                                               $possibelBannersAllCampaigns[] 
= $banner;
+                                       }
+                               }
+
+                               $htmlOut .= $this->getTable( $label, 
$possibelBannersAllCampaigns );
                        }
 
                        $htmlOut .= Html::closeElement( 'div' );
diff --git 
a/tests/qunit/ext.centralNotice.bannerController.lib/bannerController.lib.tests.js
 
b/tests/qunit/ext.centralNotice.bannerController.lib/bannerController.lib.tests.js
index a575a25..00e1d2d 100644
--- 
a/tests/qunit/ext.centralNotice.bannerController.lib/bannerController.lib.tests.js
+++ 
b/tests/qunit/ext.centralNotice.bannerController.lib/bannerController.lib.tests.js
@@ -65,6 +65,7 @@
                        // TODO: make separate tests for each method
                        lib.setChoiceData( choices );
                        lib.filterChoiceData();
+                       lib.calculateCampaignAllocations();
                        lib.makePossibleBanners();
                        lib.calculateBannerAllocations();
 

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I42985cf4de2dfeba9244b8d82f63b3d9d8319c81
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/CentralNotice
Gerrit-Branch: master
Gerrit-Owner: AndyRussG <andrew.green...@gmail.com>

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

Reply via email to