Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 204
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialBannerAllocation
0.00% covered (danger)
0.00%
0 / 204
0.00% covered (danger)
0.00%
0 / 5
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 1
156
 showList
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
42
 getTable
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 createRows
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Wikimedia Foundation
4 *
5 * LICENSE
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 */
18
19use MediaWiki\Html\Html;
20use MediaWiki\MediaWikiServices;
21
22/**
23 * SpecialBannerAllocation
24 *
25 * Special page for handling banner allocation.
26 */
27class SpecialBannerAllocation extends CentralNotice {
28    /**
29     * The project being used for banner allocation.
30     *
31     * @see $wgNoticeProjects
32     *
33     * @var string
34     */
35    public $project = 'wikipedia';
36
37    /**
38     * The language being used for banner allocation
39     *
40     * This should always be a lowercase language code.
41     *
42     * @var string
43     */
44    public $language = 'en';
45
46    /**
47     * The country being used for banner allocation.
48     *
49     * This should always be an uppercase country code or the empty string.
50     *
51     * @var string
52     */
53    public $locationCountry = 'US';
54
55    /**
56     * The region being used for banner allocation.
57     *
58     * This should always be an uppercase region code or the empty string.
59     *
60     * @var string
61     */
62    public $locationRegion = '';
63
64    public function __construct() {
65        // Register special page
66        SpecialPage::__construct( 'BannerAllocation' );
67    }
68
69    /**
70     * Handle different types of page requests
71     * @param string|null $sub
72     */
73    public function execute( $sub ) {
74        global $wgNoticeProjects, $wgLanguageCode, $wgNoticeProject;
75        $out = $this->getOutput();
76        $request = $this->getRequest();
77
78        $this->project = $request->getText( 'project', $wgNoticeProject );
79        $this->language = $request->getText( 'language', $wgLanguageCode );
80
81        // If the form has been submitted, the country code or region code should be passed along.
82        $locationCountrySubmitted = $request->getVal( 'country' );
83        $locationRegionSubmitted = $request->getVal( 'region' );
84        $this->locationCountry = $locationCountrySubmitted ?: $this->locationCountry;
85        $this->locationRegion = $locationRegionSubmitted ?: $this->locationRegion;
86
87        // Convert submitted location to boolean value. If it true, showList() will be called.
88        $locationSubmitted = ( $locationCountrySubmitted || $locationRegionSubmitted );
89
90        // Begin output
91        $this->setHeaders();
92
93        // Output ResourceLoader module for styling and javascript functions
94        $out->addModules( [
95            'ext.centralNotice.adminUi',
96        ] );
97
98        // Initialize error variable
99        $this->centralNoticeError = false;
100
101        // Allow users to add a custom nav bar (T138284)
102        $navBar = $this->msg( 'centralnotice-navbar' )->inContentLanguage();
103        if ( !$navBar->isDisabled() ) {
104            $out->addHTML( $navBar->parseAsBlock() );
105        }
106        // Show summary
107        $out->addWikiMsg( 'centralnotice-summary' );
108
109        // Begin Banners tab content
110        $out->addHTML( Html::openElement( 'div', [ 'id' => 'preferences' ] ) );
111
112        $htmlOut = '';
113
114        // Begin Allocation selection fieldset
115        $htmlOut .= Html::openElement( 'fieldset', [ 'class' => 'prefsection' ] );
116
117        $htmlOut .= Html::openElement( 'form', [ 'method' => 'get' ] );
118        $htmlOut .= Html::element( 'h2', [], $this->msg( 'centralnotice-view-allocation' )->text() );
119        $htmlOut .= $this->msg( 'centralnotice-allocation-instructions' )->parseAsBlock();
120
121        $htmlOut .= Html::openElement( 'table', [ 'id' => 'envpicker', 'cellpadding' => 7 ] );
122        $htmlOut .= Html::openElement( 'tr' );
123        $htmlOut .= Html::rawElement( 'td',
124            [ 'style' => 'width: 20%;' ],
125            $this->msg( 'centralnotice-project-name' )->parse() );
126        $htmlOut .= Html::openElement( 'td' );
127        $htmlOut .= Html::openElement( 'select', [ 'name' => 'project' ] );
128
129        foreach ( $wgNoticeProjects as $value ) {
130            $htmlOut .= Xml::option( $value, $value, $value === $this->project );
131        }
132
133        $htmlOut .= Html::closeElement( 'select' );
134        $htmlOut .= Html::closeElement( 'td' );
135        $htmlOut .= Html::closeElement( 'tr' );
136        $htmlOut .= Html::openElement( 'tr' );
137        $htmlOut .= Html::element( 'td',
138            [ 'valign' => 'top' ],
139            $this->msg( 'centralnotice-project-lang' )->text() );
140        $htmlOut .= Html::openElement( 'td' );
141
142        // Retrieve the list of languages in user's language
143        $languages = MediaWikiServices::getInstance()->getLanguageNameUtils()
144            ->getLanguageNames( $this->getLanguage()->getCode() );
145        // Make sure the site language is in the list; a custom language code
146        // might not have a defined name...
147        if ( !array_key_exists( $wgLanguageCode, $languages ) ) {
148            $languages[$wgLanguageCode] = $wgLanguageCode;
149        }
150
151        ksort( $languages );
152
153        $htmlOut .= Html::openElement( 'select', [ 'name' => 'language' ] );
154
155        foreach ( $languages as $code => $name ) {
156            $htmlOut .= Xml::option(
157                $this->msg( 'centralnotice-language-listing', $code, $name )->text(),
158                $code, $code === $this->language );
159        }
160
161        $htmlOut .= Html::closeElement( 'select' );
162        $htmlOut .= Html::closeElement( 'td' );
163        $htmlOut .= Html::closeElement( 'tr' );
164
165        // Country dropdown
166        $htmlOut .= Html::openElement( 'tr' );
167        $htmlOut .= Html::element( 'td', [], $this->msg( 'centralnotice-country' )->text() );
168        $htmlOut .= Html::openElement( 'td' );
169
170        $userLanguageCode = $this->getLanguage()->getCode();
171        $countries = GeoTarget::getCountriesList( $userLanguageCode );
172
173        $htmlOut .= Html::openElement(
174            'select', [ 'name' => 'country', 'id' => 'centralnotice-country' ]
175        );
176
177        foreach ( $countries as $code => $country ) {
178            $htmlOut .= Xml::option(
179                $country->getName(), $code, $code === $this->locationCountry
180            );
181        }
182
183        $htmlOut .= Html::closeElement( 'select' );
184        $htmlOut .= Html::closeElement( 'td' );
185        $htmlOut .= Html::closeElement( 'tr' );
186        // End Country dropdown
187
188        // Region dropdown
189        $htmlOut .= Html::openElement( 'tr' );
190        $htmlOut .= Html::element( 'td', [], $this->msg( 'centralnotice-region' )->text() );
191        $htmlOut .= Html::openElement( 'td' );
192        $htmlOut .= Html::openElement(
193            'select', [ 'name' => 'region', 'id' => 'centralnotice-region' ]
194        );
195
196        // set a client-side config variable with an associative array so we can
197        // dynamically populate this dropdown based on selected country.
198        $regionOptions = [];
199        foreach ( $countries as $countryCode => $country ) {
200            $regionOptions[$countryCode] = [];
201            foreach ( $country->getRegions() as $regionCode => $regionName ) {
202                $regionOptions[$countryCode][$regionCode] = $regionName;
203            }
204        }
205        $out->addJsConfigVars( [ 'CentralNoticeRegionOptions' => $regionOptions ] );
206
207        $htmlOut .= Html::closeElement( 'select' );
208        $htmlOut .= Html::closeElement( 'td' );
209        $htmlOut .= Html::closeElement( 'tr' );
210        // End Region dropdown
211
212        $htmlOut .= Html::closeElement( 'table' );
213
214        $htmlOut .= Html::rawElement( 'div',
215            [ 'class' => 'cn-buttons' ],
216            Xml::submitButton( $this->msg( 'centralnotice-view' )->text() )
217        );
218        $htmlOut .= Html::closeElement( 'form' );
219
220        // End Allocation selection fieldset
221        $htmlOut .= Html::closeElement( 'fieldset' );
222
223        $out->addHTML( $htmlOut );
224
225        // Handle form submissions
226        if ( $locationSubmitted ) {
227            $this->showList();
228        }
229
230        // End Banners tab content
231        $out->addHTML( Html::closeElement( 'div' ) );
232    }
233
234    /**
235     * Show a list of banners with allocation. Newer banners are shown first.
236     */
237    public function showList() {
238        global $wgNoticeNumberOfBuckets;
239
240        // Obtain all banners & campaigns
241        $request = $this->getRequest();
242        $project = $request->getText( 'project' );
243        $country = $request->getText( 'country' );
244        $region = $request->getText( 'region' );
245        $language = $request->getText( 'language' );
246
247        // Begin building HTML
248        $htmlOut = '';
249
250        // Begin Allocation list fieldset
251        $htmlOut .= Html::openElement( 'fieldset', [ 'class' => 'prefsection' ] );
252
253        // Given our project and language combination, get banner choice data,
254        // then filter on country
255        $choiceData = ChoiceDataProvider::getChoices( $project, $language );
256
257        // Iterate through each possible device type and get allocation information
258        $devices = CNDeviceTarget::getAvailableDevices();
259        foreach ( $devices as $deviceId => $deviceData ) {
260            $htmlOut .= Html::openElement(
261                'div',
262                [
263                    'id' => "cn-allocation-{$project}-{$language}-{$country}-{$deviceId}",
264                    'class' => 'cn-allocation-group'
265                ]
266            );
267
268            $htmlOut .= Html::rawElement(
269                'h3', [],
270                $this->msg(
271                    'centralnotice-allocation-description',
272                    wfEscapeWikiText( $language ),
273                    wfEscapeWikiText( $project ),
274                    wfEscapeWikiText( $country ),
275                    $deviceData['label']
276                )->parse()
277            );
278
279            // FIXME matrix is chosen dynamically based on more UI inputs
280            $matrix = [];
281            for ( $i = 0; $i < $wgNoticeNumberOfBuckets; $i++ ) {
282                $matrix[] = [ 'anonymous' => 'true', 'bucket' => $i ];
283            }
284            for ( $i = 0; $i < $wgNoticeNumberOfBuckets; $i++ ) {
285                $matrix[] = [ 'anonymous' => 'false', 'bucket' => $i ];
286            }
287
288            foreach ( $matrix as $target ) {
289                if ( $target['anonymous'] === 'true' ) {
290                    $label = $this->msg( 'centralnotice-banner-anonymous' )->text();
291                    $status = AllocationCalculator::ANONYMOUS;
292                } else {
293                    $label = $this->msg( 'centralnotice-banner-logged-in' )->text();
294                    $status = AllocationCalculator::LOGGED_IN;
295                }
296                $label .= ' -- ' . $this->msg( 'centralnotice-bucket-letter' )->
297                    rawParams( chr( $target['bucket'] + 65 ) )->text();
298
299                $possibleBannersAllCampaigns =
300                    AllocationCalculator::filterAndAllocate( $country,
301                    $region, $status, $deviceData['header'], $target['bucket'],
302                    $choiceData );
303
304                $htmlOut .= $this->getTable( $label, $possibleBannersAllCampaigns );
305            }
306
307            $htmlOut .= Html::closeElement( 'div' );
308        }
309
310        // End Allocation list fieldset
311        $htmlOut .= Html::closeElement( 'fieldset' );
312
313        $this->getOutput()->addHTML( $htmlOut );
314        $this->getOutput()->addModuleStyles( 'jquery.tablesorter.styles' );
315        $this->getOutput()->addModules( 'jquery.tablesorter' );
316    }
317
318    /**
319     * Generate the HTML for an allocation table
320     * @param string $type The title for the table
321     * @param array $banners The banners as allocated by AllocationCalculator
322     * @return string HTML for the table
323     */
324    public function getTable( $type, $banners ) {
325        $htmlOut = Html::openElement( 'table',
326            [ 'cellpadding' => 9, 'class' => 'wikitable sortable', 'style' => 'margin: 1em 0;' ]
327        );
328        $htmlOut .= Html::element( 'h4', [], $type );
329
330        if ( count( $banners ) > 0 ) {
331            $htmlOut .= Html::rawElement( 'tr', [],
332                Html::element( 'th', [ 'width' => '5%' ],
333                    $this->msg( 'centralnotice-percentage' )->text() ) .
334                Html::element( 'th', [ 'width' => '30%' ],
335                    $this->msg( 'centralnotice-banner' )->text() ) .
336                Html::element( 'th', [ 'width' => '30%' ],
337                    $this->msg( 'centralnotice-notice' )->text() )
338            );
339        }
340        $htmlOut .= $this->createRows( $banners );
341
342        $htmlOut .= Html::closeElement( 'table' );
343
344        return $htmlOut;
345    }
346
347    /**
348     * @param array[] $banners
349     * @return string HTML
350     */
351    public function createRows( $banners ) {
352        $viewCampaign = $this->getTitleFor( 'CentralNotice' );
353        $htmlOut = '';
354        if ( count( $banners ) > 0 ) {
355            $linkRenderer = $this->getLinkRenderer();
356            foreach ( $banners as $banner ) {
357                $percentage = sprintf( "%0.2f", round( $banner['allocation'] * 100, 2 ) );
358
359                // Row begin
360                $htmlOut .= Html::openElement( 'tr', [ 'class' => 'mw-sp-centralnotice-allocationrow' ] );
361
362                // Percentage
363                $htmlOut .= Html::element( 'td', [ 'align' => 'right' ],
364                    $this->msg( 'percent', $percentage )->text()
365                );
366
367                // Banner name
368                $viewBanner = $this->getTitleFor( 'CentralNoticeBanners', "edit/{$banner['name']}" );
369
370                $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top' ],
371                    Html::rawElement( 'span',
372                        [ 'class' => 'cn-' . $banner['campaign'] . '-' . $banner['name'] ],
373                        $linkRenderer->makeLink(
374                            $viewBanner,
375                            $banner['name'],
376                            [],
377                            [ 'template' => $banner['name'] ]
378                        )
379                    )
380                );
381
382                // Campaign name
383                $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top' ],
384                    $linkRenderer->makeLink(
385                        $viewCampaign,
386                        $banner['campaign'],
387                        [],
388                        [
389                            'subaction' => 'noticeDetail',
390                            'notice' => $banner['campaign']
391                        ]
392                    )
393                );
394
395                // Row end
396                $htmlOut .= Html::closeElement( 'tr' );
397            }
398
399        } else {
400            $htmlOut .= Html::rawElement( 'tr', [],
401                Html::rawElement( 'td', [],
402                    $this->msg( 'centralnotice-no-allocation' )->parseAsBlock()
403                )
404            );
405        }
406        return $htmlOut;
407    }
408}