Adamw has uploaded a new change for review.
https://gerrit.wikimedia.org/r/61929
Change subject: WIP: banner DonationForm backend support
......................................................................
WIP: banner DonationForm backend support
Change-Id: I212499e904210222eeee6cfb2a9b2c618c4d3b18
---
A mixins/DonationForm/AskString.js
A mixins/DonationForm/DonationForm.php
A mixins/DonationForm/FormController.js
A mixins/DonationForm/PaymentMethods.js
A mixins/FundraisingBannerMixins.php
A mixins/Thermometer/Thermometer.php
6 files changed, 589 insertions(+), 0 deletions(-)
git pull
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DonationInterface
refs/changes/29/61929/1
diff --git a/mixins/DonationForm/AskString.js b/mixins/DonationForm/AskString.js
new file mode 100644
index 0000000..3338bf1
--- /dev/null
+++ b/mixins/DonationForm/AskString.js
@@ -0,0 +1,254 @@
+function convertAsk(amount, currency, country) {
+ // given an amount in USD, find an "equivalent" amount for the given
currency and country
+
+ var usdamount = parseInt(amount, 10);
+ if(isNaN(usdamount)){
+ return 0;
+ }
+
+ var usdbase = [3, 5, 10, 20, 30, 50, 100];
+
+ if(currency === 'USD'){
+ if(country === 'US'){
+ return usdamount;
+ }
+ }
+
+ var other = {
+ 'EUR' : {
+ 'default' : [3, 5, 10, 20, 30, 50, 100]
+ },
+ 'GBP' : [3, 5, 10, 20, 30, 50, 100],
+ 'CAD' : [3, 5, 10, 20, 30, 50, 100],
+ 'AUD' : [3, 5, 10, 20, 30, 50, 100],
+ 'NZD' : [3, 5, 10, 20, 30, 50, 100],
+
+ 'INR' : [100, 200, 300, 500, 1000, 2000, 3000],
+ 'JPY' : [1000, 1500, 2000, 3000, 5000, 10000, 15000],
+ 'RUB' : [100, 150, 200, 500, 1000, 2000, 3000],
+ 'BRL' : [10, 20, 30, 50, 100, 250, 500],
+ 'SEK' : [50, 100, 150, 250, 500, 750, 1000],
+ 'NOK' : [50, 100, 150, 250, 500, 750, 1000],
+ 'ILS' : [25, 50, 100, 200, 300, 500, 1000],
+ 'DKK' : [50, 100, 150, 200, 300, 500, 750],
+ 'SGD' : [5, 20, 50, 75, 100, 150, 300],
+ 'HKD' : [50, 200, 300, 500, 1000, 1500, 2000],
+ 'TWD' : [150, 300, 500, 1000, 1500, 2000, 5000],
+ 'CNY' : [50, 75, 100, 300, 500, 1000, 1500],
+ 'PLN' : [20, 35, 50, 100, 150, 300],
+ 'CZK' : [100, 150, 300, 1000, 1500, 3000,],
+ 'ZAR' : [25, 50, 100, 300, 500, 750, 1000],
+ 'KRW' : [5000, 10000, 20000, 30000, 50000, 75000, 100000],
+ 'TRY' : [10, 25, 50, 100, 150, 200, 500],
+ 'SAR' : [50, 100, 200, 500, 750, 1000, 1500],
+ 'THB' : [100, 250, 500, 750, 1000, 2000, 3000],
+ 'HUF' : [1000, 2000, 5000, 10000, 20000, 50000, 100000],
+ 'IDR' : [50000, 75000, 100000, 150000, 200000, 500000, 1000000],
+ 'ARS' : [20, 50, 100, 200, 500, 750, 1000],
+ 'PHP' : [200, 500, 750, 1000, 2000, 3000, 5000],
+ 'CLP' : [2500, 5000, 10000, 20000, 30000],
+ 'MXN' : [50, 100, 200, 500, 750, 1000, 2000],
+ 'UYU' : [100, 200, 400, 1000, 1500, 1950, 5000],
+ 'COP' : [5000, 10000, 20000, 50000, 100000, 150000, 200000],
+ 'NIO' : [100, 250, 500, 1000, 1500, 2500, 5500],
+ 'DOP' : [200, 500, 1000, 2000, 5000, 7500, 10000],
+ 'UAH' : [50, 75, 150, 300, 500, 750, 1000],
+ 'MYR' : [20, 30, 50, 100, 200, 300, 500],
+ 'RON' : [25, 50, 75, 100, 200, 300, 500],
+ 'BGN' : [10, 25, 50, 75, 100, 150, 200],
+ 'HRK' : [35, 50, 100, 250, 500, 1000, 1500],
+ 'QAR' : [20, 50, 75, 185, 250, 350, 1000],
+ 'KWD' : [2, 5, 10,15, 25, 30, 75],
+ 'LTL' : [15, 25, 50, 100, 200, 250, 600],
+ 'KZT' : [750, 1500, 3000, 7500, 12000, 15000, 35000],
+ 'LVL' : [5, 10, 20, 30, 40, 50, 75],
+ 'CRC' : [2500, 5000, 10000, 20000, 50000, 75000, 100000],
+ 'VEF' : [5, 10, 20, 50, 75, 100, 250],
+ 'BHD' : [4, 5, 10, 25, 50, 100],
+ 'PEN' : [15, 30, 50, 150, 200, 275, 700],
+ // the following USD ask strings are for countries in which we
fundraise
+ // in USD, but that need a difference ask string than in the US
+ 'USD' : {
+ 'default' : usdbase
+ }
+ };
+
+
+ if (other[currency] == null) {
+ return usdamount;
+ }
+ var index = $.inArray(usdamount, usdbase);
+ if (index == -1) {
+ // the amount is not in the USD ask array, find a near neighbor
+ index = 0;
+ while (usdbase[index+1] < usdamount && index < usdbase.length + 1) {
+ index++;
+ }
+ }
+
+ if ( other[currency] instanceof Array ) { // simplest case, just one array
for this currency
+ return other[currency][index];
+ } else { // arrays for multiple countries
+ if (other[currency][country] != null) {
+ return other[currency][country][index];
+ } else {
+ return other[currency]['default'][index];
+ }
+ }
+
+};
+
+function getAverage(currency, country, language) {
+ // return a string of the "average" donation amount in the given currency
& country for use in the banner text
+ var usdaverage = 30;
+
+ var average = {
+ 'EUR' : {
+ 'default' : 30
+ },
+ 'GBP' : 20,
+ 'CAD' : 30,
+ 'AUD' : 30,
+ 'NZD' : 30,
+
+ 'INR' : 1500,
+ 'JPY' : 3000,
+ 'RUB' : 1000,
+ 'BRL' : 60,
+ 'SEK' : 200,
+ 'NOK' : 150,
+ 'ILS' : 100,
+ 'DKK' : 150,
+ 'SGD' : 30,
+ 'HKD' : 200,
+ 'TWD' : 1000,
+ 'CNY' : 200,
+ 'PLN' : 100,
+ 'CZK' : 500,
+ 'ZAR' : 300,
+ 'KRW' : 30000,
+ 'TRY' : 50,
+ 'SAR' : 100,
+ 'THB' : 1000,
+ 'HUF' : 5000,
+ 'IDR' : 300000,
+ 'ARS' : 150,
+ 'ISK' : 4000,
+ 'PHP' : 1500,
+ 'CLP' : 20000,
+ 'MXN' : 300,
+ 'UYU' : 750,
+ 'COP' : 30000,
+ 'NIO' : 750,
+ 'DOP' : 1500,
+ 'UAH' : 300,
+ 'MYR' : 150,
+ 'RON' : 200,
+ 'BGN' : 75,
+ 'HRK' : 250,
+ 'QAR' : 185,
+ 'KWD' : 15,
+ 'LTL' : 100,
+ 'KZT' : 5000,
+ 'LVL' : 30,
+ 'CRC' : 20000,
+ 'VEF' : 50,
+ 'BHD' : 25,
+ 'PEN' : 100,
+ 'USD' : {
+ 'default' : usdaverage
+ }
+ };
+
+ if (average[currency] == null) {
+ return currencyLocalize('USD', usdaverage, language); // fallback to
$30 if not defined
+ }
+
+ if ( typeof(average[currency]) === 'number' ) { // simplest case, single
average for currency
+ return currencyLocalize(currency, average[currency], language);
+ } else { // different averages for different countries
+ if (average[currency][country] != null) {
+ return currencyLocalize(currency, average[currency][country],
language);
+ } else {
+ return currencyLocalize(currency, average[currency]['default'],
language);
+ }
+ }
+
+};
+
+function getMinimumString(currency, country, language) {
+ // return a string of the "minimum" donation amount in the given currency
& country for use in the banner text
+ var usdminimum = 3;
+
+ var minimum = {
+ 'EUR' : {
+ 'default' : 3
+ },
+ 'GBP' : 3,
+ 'CAD' : 3,
+ 'AUD' : 3,
+ 'NZD' : 3,
+
+ 'INR' : 100,
+ 'JPY' : 1000,
+ 'RUB' : 100,
+ 'BRL' : 10,
+ 'SEK' : 50,
+ 'NOK' : 50,
+ 'ILS' : 25,
+ 'DKK' : 50,
+ 'SGD' : 5,
+ 'HKD' : 50,
+ 'TWD' : 150,
+ 'CNY' : 50,
+ 'PLN' : 20,
+ 'CZK' : 100,
+ 'ZAR' : 25,
+ 'KRW' : 5000,
+ 'TRY' : 10,
+ 'SAR' : 50,
+ 'THB' : 100,
+ 'HUF' : 1000,
+ 'IDR' : 50000,
+ 'ARS' : 20,
+ 'PHP' : 200,
+ 'CLP' : 2500,
+ 'MXN' : 50,
+ 'UYU' : 100,
+ 'COP' : 5000,
+ 'NIO' : 100,
+ 'DIO' : 200,
+ 'UAH' : 50,
+ 'MYR' : 20,
+ 'RON' : 25,
+ 'BGN' : 10,
+ 'HRK' : 25,
+ 'QAR' : 20,
+ 'KWD' : 1,
+ 'LTL' : 15,
+ 'KZT' : 750,
+ 'LVL' : 5,
+ 'CRC' : 2500,
+ 'VEF' : 5,
+ 'BHD' : 4,
+ 'PEN' : 15,
+ 'USD' : {
+ 'default' : usdminimum
+ }
+ };
+
+ if (minimum[currency] == null) {
+ return currencyLocalize('USD', usdminimum, language); // fallback to
$30 if not defined
+ }
+
+ if ( typeof(minimum[currency]) === 'number' ) { // simplest case, single
minimum for currency
+ return currencyLocalize(currency, minimum[currency], language);
+ } else { // different minimums for different countries
+ if (minimum[currency][country] != null) {
+ return currencyLocalize(currency, minimum[currency][country],
language);
+ } else {
+ return currencyLocalize(currency, minimum[currency]['default'],
language);
+ }
+ }
+
+}
diff --git a/mixins/DonationForm/DonationForm.php
b/mixins/DonationForm/DonationForm.php
new file mode 100644
index 0000000..b2af1fa
--- /dev/null
+++ b/mixins/DonationForm/DonationForm.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * This is a WIP, do not use.
+ */
+class DonationForm implements IBannerMixin {
+ protected $controller;
+
+ function register( MixinController $controller ) {
+ $this->controller = $controller;
+
+ $controller->registerMagicWord( 'amount', array( $this,
'getLocalizedAmount' ) );
+ $controller->registerMagicWord( 'currency', array( $this,
'getCurrency' ) );
+ $controller->registerMagicWord( 'minimum-amount', array( $this,
'getMinimumAmount' ) );
+ $controller->registerMagicWord( "ask-amount", array( $this,
'getAskAmount' ) );
+ $controller->registerMagicWord( "ask-value", array( $this,
'getAskValue' ) );
+ }
+
+ function getCurrency() {
+ return $this->controller->getFrContext()->getCountry() . "$";
+ }
+
+ function getMinimumAmount() {
+ $currency = $this->getCurrency();
+ //FIXME: round
+ return 1.0 / ExchangeRates::getConversion( $currency );
+ }
+
+ function getLocalizedAmount( $amount ) {
+ return "$amount ZROW";
+ }
+
+ function getAskAmount( $level ) {
+ return $this->getLocalizedAmount( $this->getAskValue( $level )
);
+ }
+
+ function getAskValue( $level ) {
+ return $level * 10;
+ }
+}
diff --git a/mixins/DonationForm/FormController.js
b/mixins/DonationForm/FormController.js
new file mode 100644
index 0000000..501620a
--- /dev/null
+++ b/mixins/DonationForm/FormController.js
@@ -0,0 +1,169 @@
+function validateForm(form) {
+ var error = true;
+
+ // Get amount selection
+ var amount = null;
+ for (var i = 0; i < form.amount.length; i++) {
+ if (form.amount[i].checked) {
+ amount = form.amount[i].value;
+ }
+ }
+ if (form.amountGiven.value != '') {
+ var otherAmount = form.amountGiven.value;
+ otherAmount = otherAmount.replace(/[,.](\d)$/, '\:$10');
+ otherAmount = otherAmount.replace(/[,.](\d)(\d)$/, '\:$1$2');
+ otherAmount = otherAmount.replace(/[\$,.]/g, '');
+ otherAmount = otherAmount.replace(/:/, '.');
+ form.amountGiven.value = otherAmount;
+ amount = otherAmount;
+ }
+ // Check amount is a real number
+ error = ( amount == null || isNaN(amount) || amount.value <= 0 );
+ // Check amount is at least the minimum
+ var currency = form.currency_code.value;
+ if (amount < getMinimum(currency) || error) {
+
alert('{{{validation-error-minimum|{{int:fr2013-dropdown-smallamount-error}}}}}'.replace('$1',
getMinimum(currency) + ' ' + currency));
+ error = true;
+ }
+ return !error;
+}
+
+function redirectPayment(paymentMethod, paymentSubMethod) {
+ if (typeof paymentSubMethod == 'undefined'){
+ paymentSubMethod = '';
+ }
+ var form = document.paypalcontribution; // we should really change this
some day
+ var language = $("input[name='language']").val();
+
+ var paymentsURL =
'https://payments.wikimedia.org/index.php/Special:GatewayFormChooser';
+ var paypalURL =
'https://wikimediafoundation.org/wiki/Special:ContributionTracking/' + language;
+
+ var params = {
+ 'uselang' : language,
+ 'language' : language,
+ 'currency' : $("input[name='currency_code']").val(),
+ 'country' : $("input[name='country']").val(),
+ 'paymentmethod' : paymentMethod
+ };
+ if( paymentSubMethod != '' ){
+ params['submethod'] = paymentSubMethod;
+ }
+
+ var frequency = $("input[name='frequency']:checked").val();
+ if( frequency !== 'monthly' ){
+ frequency = 'onetime';
+ } else {
+ params['recurring'] = 'true';
+ // the following is only for contribution_tracking, do not submit 'r'
to payments
+ paymentMethod = 'r' + paymentMethod;
+ }
+
+ form.action = paymentsURL + '?' + $.param(params);
+ form.utm_source.value = '{{{banner}}}.no-LP' + '.' + paymentMethod;
+ form.payment_method.value = paymentMethod;
+ if( paymentSubMethod != '' ){
+ form.payment_method.value = form.payment_method.value + + '.' +
paymentSubMethod;
+ }
+
+ if (validateForm(document.paypalcontribution)) {
+ form.submit();
+ }
+}
+
+function toggleMonthly( monthly ){
+ if( monthly.type === 'checkbox' ){
+ monthly = monthly.checked;
+ }
+
+ var onetimeonly = $(".no-monthly");
+
+ if( monthly ){
+ onetimeonly.css("display", "none");
+ } else {
+ onetimeonly.css("display", "");
+ }
+}
+
+$(document).ready( function () {
+ if ( wgCanonicalSpecialPageName != "CentralNotice" &&
wgCanonicalSpecialPageName != "NoticeTemplate" ){
+ // append the banner count in utm-key
+ var cookieName = 'centralnotice_bannercount_fr12';
+ var count = $.cookie(cookieName);
+ $('[name="paypalcontribution"]').append(
+ $('<input type="hidden" name="utm_key" />').attr('value', count));
+
+ var currency = getCurrency(Geo.country);
+ var language = mw.config.get('wgUserLanguage');
+
+ // hide CC or PP buttons anywhere we need to
+ var noCC = [];
+ if ($.inArray(Geo.country, noCC) != -1) {
+ $(".paymentmethod-cc").remove();
+ }
+ var noPP = ['RU'];
+ if ($.inArray(Geo.country, noPP) != -1){
+ $(".paymentmethod-pp").remove();
+ }
+
+ // can't do monthly credit card in India
+ if (Geo.country === 'IN') {
+ $(".paymentmethod-cc").addClass("no-monthly");
+ }
+
+ // show any extra local payment methods, or remove them if not needed
+ var extrapaymentmethods = {
+ 'amazon' : ['US'],
+ 'bpay' : ['AU'],
+ 'ideal' : ['NL'],
+ 'yandex' : ['RU'],
+ 'webmoney' : ['RU'],
+ 'sofort' : ['AT', 'BE', 'CH', 'DE'],
+ 'dd' : ['AT', 'DE', 'ES', 'NL'],
+ 'boletos' : ['BR']
+ };
+
+ for (var method in extrapaymentmethods) {
+ var $methodbutton = $('.paymentmethod-' + method);
+
+ if ($.inArray(Geo.country, extrapaymentmethods[method]) != -1) {
// country is in the list
+ $methodbutton.show();
+ } else {
+ $methodbutton.remove();
+ }
+ }
+
+ // set the form fields
+ $("input[name='country']").val(Geo.country);
+ $("input[name='currency_code']").val(currency);
+ $("input[name='language']").val(mw.config.get('wgUserLanguage'));
+ $("input[name='return_to']").val("Thank_You/" +
mw.config.get('wgUserLanguage'));
+
+ // do fun things to localize currency in the banner and form
+ $("input[name='amount']").each(function(index){
+ var id = $(this).attr("id");
+ var label = $("label[for='" + id + "']");
+ if(id.indexOf("other") == -1){
+ var amount = convertAsk($(this).val(), currency, Geo.country);
+ $(this).val(amount);
+ label.text(currencyLocalize(currency, amount, language));
+ } else {
+ // simply replace the currency symbol
+ label.text(label.text().replace(/\$/,
currencyLocalize(currency, "", language)));
+ }
+ });
+
+ // handle pressing Enter on "Other" field
+ $('input[name="amountGiven"]').keydown(function(e){
+ if (e.keyCode == 13) {
+ e.preventDefault();
+ redirectPayment('cc'); // use credit card by default. Might be
nice to have different defaults for some countries, but this will do for now.
+ return false;
+ }
+ });
+
+ // if there are no recurring payment methods available, hide the
"monthly" radio button.
+ if ( !$('form[name="paypalcontribution"]
button[class^="paymentmethod-"]:not(.no-monthly)').length ) {
+ $('#frequency_monthly').prop('disabled', 'disabled');
+ }
+ }
+});
diff --git a/mixins/DonationForm/PaymentMethods.js
b/mixins/DonationForm/PaymentMethods.js
new file mode 100644
index 0000000..a8310d9
--- /dev/null
+++ b/mixins/DonationForm/PaymentMethods.js
@@ -0,0 +1 @@
+// Define payment gateway capabilities
diff --git a/mixins/FundraisingBannerMixins.php
b/mixins/FundraisingBannerMixins.php
new file mode 100644
index 0000000..bdb384d
--- /dev/null
+++ b/mixins/FundraisingBannerMixins.php
@@ -0,0 +1,19 @@
+<?php
+
+$wgResourceModules[ 'ext.donationInterface.mixins.DonationForm' ] = array(
+ 'localBasePath' => __DIR__ . '/DonationForm',
+ 'remoteExtPath' => 'DonationInterface/mixins/DonationForm',
+ 'scripts' => array(
+ 'PaymentMethods.js',
+ 'FormController.js',
+ ),
+ 'position' => 'top',
+ 'targets' => array( 'desktop' ),
+);
+
+$wgNoticeMixins['DonationForm'] = array(
+ 'localBasePath' => __DIR__ . "/DonationForm",
+
+ 'php' => "DonationForm.php",
+ 'resourceLoader' => "ext.donationInterface.mixins.DonationForm",
+);
diff --git a/mixins/Thermometer/Thermometer.php
b/mixins/Thermometer/Thermometer.php
new file mode 100644
index 0000000..6897674
--- /dev/null
+++ b/mixins/Thermometer/Thermometer.php
@@ -0,0 +1,107 @@
+<?php //namespace CentralNotice\Mixins;
+
+class Thermometer implements IBannerMixin {
+ protected $context = null;
+
+ function register( MixinController $controller ) {
+ $this->context = $controller->getContext();
+
+ $controller->registerMagicWord( 'cumulative-amount', array(
$this, "getDonationAmount" ) );
+ $controller->registerMagicWord( 'daily-amount', array( $this,
"getDailyDonationAmount" ) );
+ }
+
+ /**
+ * Pull the current amount raised during a fundraiser
+ * @throws SpecialBannerLoaderException
+ */
+ function getDonationAmount( $arg1 ) {
+ return "fake-out!: $arg1";
+
+ global $wgNoticeCounterSource, $wgMemc;
+ // Pull short-cached amount
+ $count = intval( $wgMemc->get( wfMemcKey( 'centralnotice',
'counter' ) ) );
+ if ( !$count ) {
+ // Pull from dynamic counter -- WHAT
+ $counter_value = Http::get( $wgNoticeCounterSource );
+ if( !$counter_value ) {
+ throw new RemoteServerProblemException();
+ }
+ $count = intval( $counter_value );
+ if ( !$count ) {
+ // Pull long-cached amount
+ $count = intval( $wgMemc->get(
+ wfMemcKey( 'centralnotice', 'counter',
'fallback' ) ) );
+ if ( !$count ) {
+ throw new
DonationAmountUnknownException();
+ }
+ }
+ // Expire in 60 seconds
+ $wgMemc->set( wfMemcKey( 'centralnotice', 'counter' ),
$count, 60 );
+ // No expiration
+ $wgMemc->set( wfMemcKey( 'centralnotice', 'counter',
'fallback' ), $count );
+ }
+
+ $num = $this->toMillions( $count );
+ return "{{{amount|$num}}}";
+ }
+
+ /**
+ * Pull the amount raised so far today during a fundraiser
+ * @throws SpecialBannerLoaderException
+ */
+ function getDailyDonationAmount() {
+ return "fake-out!";
+
+ global $wgNoticeDailyCounterSource, $wgMemc;
+ // Pull short-cached amount
+ $count = intval( $wgMemc->get( wfMemcKey( 'centralnotice',
'dailycounter' ) ) );
+ if ( !$count ) {
+ // Pull from dynamic counter
+ $counter_value = Http::get( $wgNoticeDailyCounterSource
);
+ if( !$counter_value ) {
+ throw new RemoteServerProblemException();
+ }
+ $count = intval( $counter_value );
+ if ( !$count ) {
+ // Pull long-cached amount
+ $count = intval( $wgMemc->get(
+ wfMemcKey( 'centralnotice',
'dailycounter', 'fallback' ) ) );
+ if ( !$count ) {
+ throw new
DonationAmountUnknownException();
+ }
+ }
+ // Expire in 60 seconds
+ $wgMemc->set( wfMemcKey( 'centralnotice',
'dailycounter' ), $count, 60 );
+ // No expiration
+ $wgMemc->set( wfMemcKey( 'centralnotice',
'dailycounter', 'fallback' ), $count );
+ }
+
+ $num = $this->toThousands( $count );
+ return "{{{daily-amount|$num}}}";
+ }
+
+ /**
+ * Convert number of dollars to millions of dollars
+ */
+ protected function toMillions( $num ) {
+ $num = sprintf( "%.1f", $num / 1e6 );
+ if ( substr( $num, - 2 ) == '.0' ) {
+ $num = substr( $num, 0, - 2 );
+ }
+ return $this->context->getLanguage()->formatNum( $num );
+ }
+
+ /**
+ * Convert number of dollars to thousands of dollars
+ */
+ protected function toThousands( $num ) {
+ $num = sprintf( "%d", $num / 1000 );
+ return $this->context->getLanguage()->formatNum( $num );
+ }
+}
+
+class RemoteServerProblemException extends BannerLoaderException {
+}
+
+class DonationAmountUnknownException extends BannerLoaderException {
+}
--
To view, visit https://gerrit.wikimedia.org/r/61929
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I212499e904210222eeee6cfb2a9b2c618c4d3b18
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/DonationInterface
Gerrit-Branch: master
Gerrit-Owner: Adamw <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits