allow developer to opt into strictly enforced region choices (bug 896613)

This commit is contained in:
Chris Van 2013-10-17 16:41:59 -07:00
Родитель 09dc3c5e6e
Коммит 8494c035d4
21 изменённых файлов: 476 добавлений и 147 удалений

Просмотреть файл

@ -335,7 +335,6 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
# This gets overwritten in the transformer.
share_counts = collections.defaultdict(int)
# Note: this will be initially only utilised by paid webapps.
enable_new_regions = models.BooleanField(default=False, db_index=True)
objects = AddonManager()

Просмотреть файл

@ -40,7 +40,7 @@ import amo
import amo.search
import stats.search
from access.models import Group, GroupUser
from addons.models import (Addon, AddonCategory, Category, Persona,
from addons.models import (Addon, Category, Persona,
update_search_index as addon_update_search_index)
from addons.tasks import unindex_addons
from amo.urlresolvers import get_url_prefix, Prefixer, reverse, set_url_prefix
@ -371,7 +371,7 @@ class TestCase(MockEsMixin, RedisTest, test_utils.TestCase):
scheme, netloc, path, query, fragment = urlsplit(url)
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(
expected_url)
expected_url)
if (scheme and not e_scheme) and (netloc and not e_netloc):
expected_url = urlunsplit(('http', 'testserver', e_path, e_query,
e_fragment))
@ -507,8 +507,10 @@ class TestCase(MockEsMixin, RedisTest, test_utils.TestCase):
return days_ago(days)
def login(self, profile):
assert self.client.login(username=getattr(profile, 'email', profile),
password='password')
email = getattr(profile, 'email', profile)
if '@' not in email:
email += '@mozilla.com'
assert self.client.login(username=email, password='password')
def trans_eq(self, trans, locale, localized_string):
eq_(Translation.objects.get(id=trans.id,
@ -852,15 +854,20 @@ class WebappTestCase(TestCase):
def get_app(self):
return Addon.objects.get(id=337141)
def make_game(self, rated=False):
cat, created = Category.objects.get_or_create(slug='games',
type=amo.ADDON_WEBAPP)
AddonCategory.objects.get_or_create(addon=self.app, category=cat)
if rated:
ContentRating.objects.get_or_create(
addon=self.app, ratings_body=mkt.ratingsbodies.CLASSIND.id,
rating=mkt.ratingsbodies.CLASSIND_18.id)
ContentRating.objects.get_or_create(
addon=self.app, ratings_body=mkt.ratingsbodies.CLASSIND.id,
rating=mkt.ratingsbodies.CLASSIND_L.id)
self.app = self.get_app()
def make_game(self, app=None, rated=False):
app = make_game(self.app or app, rated)
def make_game(app, rated):
cat, created = Category.objects.get_or_create(slug='games',
type=amo.ADDON_WEBAPP)
app.addoncategory_set.create(category=cat)
if rated:
ContentRating.objects.get_or_create(
addon=app, ratings_body=mkt.ratingsbodies.CLASSIND.id,
rating=mkt.ratingsbodies.CLASSIND_18.id)
ContentRating.objects.get_or_create(
addon=app, ratings_body=mkt.ratingsbodies.CLASSIND.id,
rating=mkt.ratingsbodies.CLASSIND_L.id)
app = app.reload()
return app

Просмотреть файл

@ -34,12 +34,16 @@ class DevMailerForm(happyforms.Form):
'Developers who have set up EULAs for active add-ons'),
('sdk', 'Developers of active SDK add-ons'),
('apps', 'Developers of active apps (not add-ons)'),
('free_apps_region_enabled',
'Developers of free apps and new region enabled'),
('free_apps_region_disabled',
'Developers of free apps with new regions disabled'),
('payments',
'Developers of non-deleted apps (not add-ons) with payments'),
('payments_region_enabled',
'Developers of apps with payments and new region enabled'),
'Developers of apps with payments and new regions enabled'),
('payments_region_disabled',
'Developers of apps with payments and new region disabled'),
'Developers of apps with payments and new regions disabled'),
('desktop_apps',
'Developers of non-deleted apps supported on desktop'),
('all_extensions', 'All extension developers')]

Просмотреть файл

@ -318,7 +318,7 @@ class TestBulkValidation(BulkValidationTest):
self.assertNoFormErrors(r)
self.assertRedirects(r, reverse('zadmin.validation'))
assert bulk_validate_file.delay.called, (
'Addon with status %s should be validated' % status)
'Addon with status %s should be validated' % self.addon.status)
def test_grid(self):
job = self.create_job()
@ -1847,12 +1847,8 @@ class TestEmailDevs(amo.tests.TestCase):
fixtures = ['base/addon_3615', 'base/users']
def setUp(self):
assert self.client.login(username='admin@mozilla.com',
password='password')
ad = Addon.objects.get(pk=3615)
ad.eula = 'Accept this license'
ad.save()
self.addon = ad
self.login('admin')
self.addon = Addon.objects.get(pk=3615)
def post(self, recipients='eula', subject='subject', message='msg',
preview_only=False):
@ -1918,6 +1914,26 @@ class TestEmailDevs(amo.tests.TestCase):
self.assertNoFormErrors(res)
eq_(len(mail.outbox), 0)
def test_only_free_apps_with_new_regions(self):
self.addon.update(type=amo.ADDON_WEBAPP)
res = self.post(recipients='free_apps_region_enabled')
self.assertNoFormErrors(res)
eq_(len(mail.outbox), 0)
mail.outbox = []
res = self.post(recipients='free_apps_region_disabled')
self.assertNoFormErrors(res)
eq_(len(mail.outbox), 1)
mail.outbox = []
self.addon.update(enable_new_regions=True)
res = self.post(recipients='free_apps_region_enabled')
self.assertNoFormErrors(res)
eq_(len(mail.outbox), 1)
mail.outbox = []
res = self.post(recipients='free_apps_region_disabled')
self.assertNoFormErrors(res)
eq_(len(mail.outbox), 0)
def test_only_apps_with_payments_and_new_regions(self):
self.addon.update(type=amo.ADDON_WEBAPP,
premium_type=amo.ADDON_PREMIUM)

Просмотреть файл

@ -115,9 +115,9 @@ def langpacks(request):
return redirect('zadmin.langpacks')
addons = (Addon.objects.no_cache()
.filter(addonuser__user__email=settings.LANGPACK_OWNER_EMAIL,
type=amo.ADDON_LPAPP)
.order_by('name'))
.filter(addonuser__user__email=settings.LANGPACK_OWNER_EMAIL,
type=amo.ADDON_LPAPP)
.order_by('name'))
data = {'addons': addons, 'base_url': settings.LANGPACK_DOWNLOAD_BASE,
'default_path': settings.LANGPACK_PATH_DEFAULT % (
@ -630,8 +630,13 @@ def email_devs(request):
qs = qs.filter(addon__enable_new_regions=True)
elif data['recipients'] == 'payments_region_disabled':
qs = qs.filter(addon__enable_new_regions=False)
elif data['recipients'] == 'apps':
elif data['recipients'] in ('apps', 'free_apps_region_enabled',
'free_apps_region_disabled'):
qs = qs.filter(addon__type=amo.ADDON_WEBAPP)
if data['recipients'] == 'free_apps_region_enabled':
qs = qs.filter(addon__enable_new_regions=True)
elif data['recipients'] == 'free_apps_region_disabled':
qs = qs.filter(addon__enable_new_regions=False)
elif data['recipients'] == 'desktop_apps':
qs = (qs.filter(addon__type=amo.ADDON_WEBAPP,
addon__addondevicetype__device_type=amo.DEVICE_DESKTOP.id))

Просмотреть файл

@ -98,7 +98,8 @@ label, .label {
}
.choice label,
label.choice,
li label {
li label,
td label {
cursor: pointer;
}
.choice label,

Просмотреть файл

@ -120,9 +120,6 @@
margin-bottom: .25em;
}
}
#regions button {
margin-top: 14px;
}
}
.devhub-form .learn-more {
@ -429,9 +426,9 @@ label.disabled {
color: #ccc;
}
.devhub-form table td.region-container {
border: 0;
padding-top: 0;
#page .devhub-form table td.region-container h3 {
color: $dark-gray;
font-weight: 600;
}
.devhub-form table th.region-toggle {
@ -444,17 +441,32 @@ label.disabled {
hidetext();
}
.local-currency-heading,
.my-currency {
display: block;
}
.my-currency,
td .local-retail {
display: block;
}
.local-currency-heading {
color: $note-gray;
padding-top: 2px;
}
th {
color: $dark-gray;
width: 40%;
&:last-child {
width: 20%;
}
}
table td {
border-color: $faint-gray;
line-height: 1.5em;
min-height: 1.5em;
padding: 1em 0;
}
table {
border-bottom: 1px dotted $faint-gray;
margin: 20px 0;
}
}
.paid-regions {
@ -469,6 +481,69 @@ body > label {
margin-bottom: 1em;
}
#regions {
// Less room to breathe for radios.
input[type=radio] {
margin: 1px 3px 1px 1px;
}
li + li input[type=radio] {
margin-top: 6px;
}
// More room to breathe for region checkboxes.
[data-region] input[type=checkbox] {
margin-right: 6px;
}
}
#regions-island {
h2 {
margin-top: 1.5em;
}
#regions {
// Some nicer formatting around the radio buttons.
> ul {
margin: 2px 2px 0;
}
header,
.region-choices,
.disabled-regions,
.other-regions {
margin: 10px 22px 5px;
}
header {
margin-top: 0;
}
.toggles {
padding-top: 0;
}
.other-regions {
margin-top: 15px;
padding-top: 15px;
}
}
}
.region-cb.disabled:after {
content: " *";
}
.disabled-regions:before {
color: $note-gray;
content: "* ";
font-size: 14px;
font-weight: 600;
vertical-align: top;
}
.paid-regions {
margin-top: 15px;
}
.region-choices li {
min-height: 19px;
}
#regions li label {
font-weight: normal;
}

Просмотреть файл

@ -100,7 +100,9 @@ define('payments', [], function() {
'zIndex': zIndex});
element.hide();
$temp.animate({'top': parseInt(newOffset.top, 10), 'left': parseInt(newOffset.left, 10) }, 'slow', function(){
$temp.animate({top: parseInt(newOffset.top, 10),
left: parseInt(newOffset.left, 10)},
'slow', function() {
$temp.remove();
$element.show();
if ($elmToRemove) {
@ -254,6 +256,36 @@ define('payments', [], function() {
$free_island.toggle(tab.id == 'free-tab-header');
});
if ($('#paid-regions-island').length) {
// Paid apps for the time being must be restricted.
var $restricted = $('input[name=restricted]');
// Remove the first radio button for
// "Make my app available everywhere."
$restricted.filter('[value="0"]').closest('li').remove();
// Keep the label but hide the radio button for
// "Choose where my app is available."
$restricted.filter('[value="1"]').prop('checked', true).hide();
// Show the restricted fields (the checkboxes) which would be
// otherwise hidden (for free apps).
$('.restricted').removeClass('hidden');
} else {
// Free apps can toggle between restricted and not.
z.doc.on('change', '#regions input[name=restricted]:checked', function() {
// Coerce string ('0' or '1') to boolean ('true' or 'false').
var restricted = !!+$(this).val();
$('.restricted').toggle(restricted);
if (!restricted) {
$('input.restricted:not([disabled])').prop('checked', true);
$('input[name="enable_new_regions"]').prop('checked', true);
}
window.location.hash = 'regions-island';
});
$('#regions input[name=restricted]:checked').trigger('change');
}
// Only update if we can edit. If the user can't edit all fields will be disabled.
if (!z.body.hasClass('no-edit')) {
$priceSelect.on('change', updatePrices);

Просмотреть файл

@ -0,0 +1,11 @@
CREATE TABLE `webapps_geodata` (
`id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
`addon_id` int(11) unsigned NOT NULL UNIQUE,
`restricted` bool NOT NULL,
`popular_region` varchar(10)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
ALTER TABLE `webapps_geodata` ADD CONSTRAINT `webapps_geodata_addon_id_fk`
FOREIGN KEY (`addon_id`) REFERENCES `addons` (`id`) ON DELETE CASCADE;

Просмотреть файл

@ -3,7 +3,6 @@ import cronjobs
from amo.utils import chunked
import mkt
from mkt.developers.tasks import region_email, region_exclude
from mkt.webapps.models import AddonExcludedRegion, Webapp
@ -35,8 +34,7 @@ def _region_exclude(ids, regions):
@cronjobs.register
def exclude_new_region(regions):
"""
Update regional blacklist for app developers who opted out of being
automatically added to new regions.
Update regional blacklist based on a list of regions to exclude.
"""
excluded = (AddonExcludedRegion.objects
.filter(region__in=[r.id for r in regions])

Просмотреть файл

@ -650,22 +650,24 @@ class RegionForm(forms.Form):
_lazy(u'You must select at least one region.')})
enable_new_regions = forms.BooleanField(required=False,
label=_lazy(u'Enable new regions'))
restricted = forms.ChoiceField(required=False,
choices=[(0, _lazy('Make my app available everywhere')),
(1, _lazy('Choose where my app is made available'))],
widget=forms.RadioSelect(attrs={'class': 'choices'}),
initial=0)
def __init__(self, *args, **kw):
self.product = kw.pop('product', None)
self.request = kw.pop('request', None)
self.region_ids = self.product.get_region_ids(worldwide=True)
super(RegionForm, self).__init__(*args, **kw)
# If we have excluded regions, uncheck those.
# Otherwise, default to everything checked.
self.regions_before = self.product.get_region_ids(worldwide=True)
# If we have future excluded regions, uncheck box.
self.future_exclusions = self.product.enable_new_regions
self.initial = {
'regions': self.regions_before,
'restricted': int(self.product.geodata.restricted),
'enable_new_regions': self.product.enable_new_regions,
}
@ -696,7 +698,7 @@ class RegionForm(forms.Form):
def clean(self):
data = self.cleaned_data
if (not data.get('regions') and not self.is_toggling()):
if not data.get('regions') and not self.is_toggling():
raise forms.ValidationError(
_('You must select at least one region.'))
return data
@ -706,24 +708,38 @@ class RegionForm(forms.Form):
if self.is_toggling():
return
before = set(self.regions_before)
after = set(map(int, self.cleaned_data['regions']))
restricted = int(self.cleaned_data['restricted'])
# Add new region exclusions.
to_add = before - after
for region in to_add:
aer, created = AddonExcludedRegion.objects.get_or_create(
addon=self.product, region=region)
if created:
log.info(u'[Webapp:%s] Excluded from new region (%s).'
if restricted:
before = set(self.regions_before)
after = set(map(int, self.cleaned_data['regions']))
log.info(u'[Webapp:%s] App mark as restricted.' % self.product)
# Add new region exclusions.
to_add = before - after
for region in to_add:
aer, created = AddonExcludedRegion.objects.get_or_create(
addon=self.product, region=region)
if created:
log.info(u'[Webapp:%s] Excluded from new region (%s).'
% (self.product, region))
# Remove old region exclusions.
to_remove = after - before
for region in to_remove:
self.product.addonexcludedregion.filter(
region=region).delete()
log.info(u'[Webapp:%s] No longer exluded from region (%s).'
% (self.product, region))
else:
self.product.addonexcludedregion.all().delete()
log.info(u'[Webapp:%s] App mark as unrestricted.' % self.product)
# Remove old region exclusions.
to_remove = after - before
for region in to_remove:
self.product.addonexcludedregion.filter(region=region).delete()
log.info(u'[Webapp:%s] No longer exluded from region (%s).'
% (self.product, region))
self.product.geodata.update(restricted=restricted)
# TODO: Stop saving AddonExcludedRegion objects when IARC work lands.
ban_unrated_game(self.product)
if self.cleaned_data['enable_new_regions']:
self.product.update(enable_new_regions=True)
@ -734,8 +750,6 @@ class RegionForm(forms.Form):
log.info(u'[Webapp:%s] will not be added to future regions.'
% self.product)
ban_unrated_game(self.product)
class CategoryForm(happyforms.Form):
categories = forms.ModelMultipleChoiceField(

Просмотреть файл

@ -0,0 +1,57 @@
import logging
from django.core.management.base import BaseCommand
import amo
from mkt.constants.regions import (ALL_REGIONS_WITH_CONTENT_RATINGS,
REGIONS_CHOICES_ID_DICT)
from mkt.webapps.models import Webapp
log = logging.getLogger('z.task')
class Command(BaseCommand):
"""
Backfill Webapp Geodata by inferring regional popularity from
AddonExcludedRegion objects (or lack thereof).
Remove AddonExcludedRegion objects for free apps (except unrated games).
"""
def handle(self, *args, **options):
paid_types = amo.ADDON_PREMIUMS + (amo.ADDON_FREE_INAPP,)
games_cat = Webapp.category('games')
content_region_ids = [x.id for x in ALL_REGIONS_WITH_CONTENT_RATINGS]
apps = Webapp.objects.all()
for app in apps:
geodata = {}
# If this app was excluded in every region except one,
# let's consider it regionally popular in that particular region.
region_ids = app.get_region_ids()
if len(region_ids) == 1:
geodata['popular_region'] = (
REGIONS_CHOICES_ID_DICT[region_ids[0]].slug
)
if app.premium_type in paid_types:
geodata['restricted'] = True
else:
for exclusion in app.addonexcludedregion.all():
# Do not delete region exclusions meant for unrated games.
region = REGIONS_CHOICES_ID_DICT[exclusion.region]
if (games_cat and region.id in content_region_ids and
app.listed_in(category='games') and
not app.content_ratings_in(region)):
continue
log.info('[App %s - %s] Removed exclusion: %s'
% (app.pk, app.slug, exclusion))
# Remove all other existing exclusions, since all apps
# are public in every region by default. If developers
# want to hard-restrict their apps they can now do that.
exclusion.delete()
app.geodata.update(**geodata)

Просмотреть файл

@ -1,12 +1,12 @@
{{ region_form.regions.errors }}
<div class="checkbox-choices region-choices"{% if not is_paid %} id="region-list" data-disabled-regions="{{ region_form.disabled_regions|json }}"{% endif %}>
<div class="hidden restricted checkbox-choices region-choices"{% if not is_paid %} id="region-list" data-disabled-regions="{{ region_form.disabled_regions|json }}"{% endif %}>
<ul>
{% for value, text in region_form.regions.field.choices %}
<li data-region="{{ value }}">
<label class="region-cb{% if value in region_form.disabled_regions %} disabled{% endif %}">
<input type="checkbox"
{% if value in region_form.disabled_regions %}disabled{% endif %}
{% if value in region_form.initial.regions %}checked{% endif %}
<input type="checkbox" class="restricted"
{% if value in region_form.disabled_regions %}disabled
{% elif value in region_form.initial.regions %}checked{% endif %}
name="regions" value="{{ value }}">{{ text }}</label>
</li>
{% endfor %}
@ -15,6 +15,6 @@
{% if region_form.disabled_regions %}
{% include 'developers/payments/includes/regions_disabled.html' %}
{% endif %}
<div class="other-regions">
<div class="other-regions{{ ' restricted' if not is_paid }}">
{% include 'developers/payments/includes/regions_other.html' %}
</div>

Просмотреть файл

@ -1,4 +1,4 @@
<p class="note disabled-regions">
<a href="https://developer.mozilla.org/en-US/docs/Apps/Marketplace_Review" target="_blank">
<p class="note restricted disabled-regions">
<a href="https://developer.mozilla.org/Apps/Publishing/Marketplace_review_criteria#Content" target="_blank">
{{ _('Learn why some regions are restricted.') }}</a>
</p>

Просмотреть файл

@ -1,6 +1,9 @@
<label data-for="region">{{ region_form.regions.label }}</label>
{{ region_form.non_field_errors() }}
<p class="toggles">
<a href="#" class="all">{{ _('Select All') }}</a> &middot;
<a href="#" class="none">{{ _('None') }}</a>
</p>
{{ region_form.restricted }}
{{ region_form.restricted.errors }}
<header class="hidden restricted">
{{ region_form.non_field_errors() }}
<p class="toggles">
<a href="#" class="all">{{ _('Select All') }}</a> &middot;
<a href="#" class="none">{{ _('None') }}</a>
</p>
</header>

Просмотреть файл

@ -172,24 +172,23 @@
data-payment-methods="{{ payment_methods|json }}"
data-pricelist-api-url="{{ api_pricelist_url }}"
data-tier-zero-id="{{ tier_zero_id }}">
<h3>{{ _('Regions') }}</h3>
{{ region_form.regions.errors }}
{% include 'developers/payments/includes/regions_toggle.html' %}
<div class="paid-regions">
<h3>{{ _('Available Regions') }}</h3>
<div class="hint note">
<p>
{% trans status_url='/developers/docs/payments/status' %}
Check out the <a href="{{ status_url }}">status page</a> to see what
countries are currently enabled for sale.
{% endtrans %}
</p>
</div>
<h4>{{ _('Available Regions') }}</h4>
<p class="hint note">
{% trans status_url='/developers/docs/payments/status' %}
Check out the <a href="{{ status_url }}">status page</a> to see what
countries are currently enabled for sale.
{% endtrans %}
</p>
<div class="hint note">
<p>{{ _('Your app will be available in the following selected regions:') }}</p>
</div>
<p class="hint note">
{{ _('Your app will be available in the following selected regions:') }}
</p>
<table id="paid-regions">
<thead>
<tr>
@ -207,16 +206,16 @@
</table>
</div>
<h3>{{ _("Other regions") }}</h3>
<div class="hint note">
<p>
{% trans %}
Your app will be available in the following selected regions in the future when payments are available based on your chosen price point.
To exclude your app from appearing in a region please un-check that region before saving.
{% endtrans %}
</p>
</div>
<h4>{{ _("Other regions") }}</h4>
<p class="hint note">
{% trans %}
Your app will become available in the following
selected regions once payments become available
based on your chosen price point. To exclude
your app from appearing in a region, you may
uncheck that region before saving.
{% endtrans %}
</p>
{% include 'developers/payments/includes/region_form.html' %}
</div>
@ -232,11 +231,13 @@
{% else %}
{# Non-paid app region lists #}
<div id="regions-island">
<h2>{{ _('Regions and listings') }}</h2>
<section id="regions" class="island">
<h2>{{ _('Regions and listings') }}</h2>
{% include 'developers/payments/includes/regions_toggle.html' %}
{% include 'developers/payments/includes/region_form.html' %}
<button>{{ _('Save Changes') }}</button>
<div class="listing-footer">
<button class="button">{{ _('Save Changes') }}</button>
</div>
</section>
</div>
{% endif %}

Просмотреть файл

@ -3,30 +3,31 @@ from nose.tools import eq_
import amo
import amo.tests
from addons.models import Addon, AddonPremium
from mkt.constants.regions import WORLDWIDE
from addons.models import AddonPremium
import mkt
from mkt.developers.management.commands import (
cleanup_addon_premium,
migrate_free_apps_without_worldwide_aer
migrate_free_apps_without_worldwide_aer,
migrate_geodata
)
from mkt.site.fixtures import fixture
from mkt.webapps.models import AddonExcludedRegion as AER, Webapp
from mkt.webapps.models import Webapp
class TestCommandViews(amo.tests.TestCase):
fixtures = fixture('webapp_337141')
def setUp(self):
self.webapp = self.get_webapp()
def get_webapp(self):
return Addon.objects.get(pk=337141)
self.webapp = Webapp.objects.get(pk=337141)
def test_cleanup_addonpremium(self):
self.make_premium(self.webapp)
eq_(AddonPremium.objects.all().count(), 1)
cleanup_addon_premium.Command().handle()
eq_(AddonPremium.objects.all().count(), 1)
self.webapp.update(premium_type=amo.ADDON_FREE)
cleanup_addon_premium.Command().handle()
eq_(AddonPremium.objects.all().count(), 0)
@ -41,7 +42,7 @@ class TestMigrateFreeAppsWithoutWorldAER(amo.tests.TestCase):
def test_migration_of_free_apps_without_world_aer(self):
eq_(self.webapp.enable_new_regions, False)
eq_(self.webapp.addonexcludedregion.filter(
region=WORLDWIDE.id).count(), 0)
region=mkt.regions.WORLDWIDE.id).count(), 0)
migrate_free_apps_without_worldwide_aer.Command().handle()
eq_(Webapp.objects.no_cache().get(pk=337141).enable_new_regions, True)
@ -53,18 +54,85 @@ class TestMigrateFreeAppsWithoutWorldAER(amo.tests.TestCase):
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
eq_(self.webapp.enable_new_regions, False)
eq_(self.webapp.addonexcludedregion.filter(
region=WORLDWIDE.id).count(), 0)
region=mkt.regions.WORLDWIDE.id).count(), 0)
migrate_free_apps_without_worldwide_aer.Command().handle()
eq_(Webapp.objects.no_cache().get(pk=337141).enable_new_regions, False)
def test_no_migration_of_free_apps_with_world_aer(self):
eq_(self.webapp.enable_new_regions, False)
AER.objects.create(addon=self.webapp, region=WORLDWIDE.id)
self.webapp.addonexcludedregion.create(region=mkt.regions.WORLDWIDE.id)
migrate_free_apps_without_worldwide_aer.Command().handle()
eq_(Webapp.objects.no_cache().get(pk=337141).enable_new_regions, False)
def test_no_migration_of_free_apps_with_world_aer_already_enabled(self):
self.webapp.update(enable_new_regions=True)
AER.objects.create(addon=self.webapp, region=WORLDWIDE.id)
self.webapp.addonexcludedregion.create(region=mkt.regions.WORLDWIDE.id)
migrate_free_apps_without_worldwide_aer.Command().handle()
eq_(Webapp.objects.no_cache().get(pk=337141).enable_new_regions, True)
class TestMigrateGeodata(amo.tests.TestCase):
fixtures = fixture('webapp_337141')
def setUp(self):
self.webapp = Webapp.objects.get(pk=337141)
def test_restricted_no_migration_of_paid_apps_exclusions(self):
self.make_premium(self.webapp)
self.webapp.addonexcludedregion.create(region=mkt.regions.US.id)
eq_(self.webapp.geodata.reload().restricted, False)
migrate_geodata.Command().handle()
eq_(self.webapp.reload().addonexcludedregion.count(), 1)
eq_(self.webapp.geodata.reload().restricted, True)
def test_unrestricted_migration_of_free_apps_exclusions(self):
self.webapp.addonexcludedregion.create(region=mkt.regions.US.id)
eq_(self.webapp.geodata.reload().restricted, False)
migrate_geodata.Command().handle()
eq_(self.webapp.reload().addonexcludedregion.count(), 0)
eq_(self.webapp.geodata.reload().restricted, False)
def test_migration_of_regional_content(self):
# Exclude in every where except Brazil.
regions = list(mkt.regions.REGIONS_CHOICES_ID_DICT)
regions.remove(mkt.regions.BR.id)
for region in regions:
self.webapp.addonexcludedregion.create(region=region)
eq_(self.webapp.geodata.reload().popular_region, None)
migrate_geodata.Command().handle()
eq_(self.webapp.reload().addonexcludedregion.count(), 0)
eq_(self.webapp.geodata.reload().popular_region, mkt.regions.BR.slug)
def test_migration_of_rated_games(self):
# This adds a ContentRating for only Brazil, not Germany.
amo.tests.make_game(self.webapp, rated=True)
regions = (mkt.regions.BR.id, mkt.regions.DE.id)
for region in regions:
self.webapp.addonexcludedregion.create(region=region)
migrate_geodata.Command().handle()
self.assertSetEqual(self.webapp.reload().addonexcludedregion
.values_list('region', flat=True),
[mkt.regions.DE.id])
def test_no_migration_of_unrated_games(self):
amo.tests.make_game(self.webapp, rated=False)
regions = (mkt.regions.BR.id, mkt.regions.DE.id)
for region in regions:
self.webapp.addonexcludedregion.create(region=region)
migrate_geodata.Command().handle()
self.assertSetEqual(self.webapp.reload().addonexcludedregion
.values_list('region', flat=True),
regions)

Просмотреть файл

@ -5,7 +5,6 @@ import amo.tests
import mkt
from mkt.developers.cron import exclude_new_region, send_new_region_emails
from mkt.webapps.models import AddonExcludedRegion
class TestSendNewRegionEmails(amo.tests.WebappTestCase):
@ -14,19 +13,18 @@ class TestSendNewRegionEmails(amo.tests.WebappTestCase):
def test_called(self, _region_email_mock):
self.app.update(enable_new_regions=True)
send_new_region_emails([mkt.regions.UK])
eq_(list(_region_email_mock.call_args_list[0][0][0]),
[self.app.id])
eq_(list(_region_email_mock.call_args_list[0][0][0]), [self.app.id])
@mock.patch('mkt.developers.cron._region_email')
def test_not_called_with_exclusions(self, _region_email_mock):
AddonExcludedRegion.objects.create(addon=self.app,
region=mkt.regions.UK.id)
self.app.addonexcludedregion.create(region=mkt.regions.UK.id)
send_new_region_emails([mkt.regions.UK])
eq_(list(_region_email_mock.call_args_list[0][0][0]), [])
@mock.patch('mkt.developers.cron._region_email')
def test_not_called_with_enable_new_regions_false(self, _region_email_mock):
# Check enable_new_regions is False by default.
def test_not_called_with_enable_new_regions_false(self,
_region_email_mock):
"""Check enable_new_regions is False by default."""
eq_(self.app.enable_new_regions, False)
send_new_region_emails([mkt.regions.UK])
eq_(list(_region_email_mock.call_args_list[0][0][0]), [])
@ -38,13 +36,11 @@ class TestExcludeNewRegion(amo.tests.WebappTestCase):
def test_not_called_enable_new_regions_true(self, _region_exclude_mock):
self.app.update(enable_new_regions=True)
exclude_new_region([mkt.regions.UK])
eq_(list(_region_exclude_mock.call_args_list[0][0][0]),
[])
eq_(list(_region_exclude_mock.call_args_list[0][0][0]), [])
@mock.patch('mkt.developers.cron._region_exclude')
def test_not_called_with_ordinary_exclusions(self, _region_exclude_mock):
AddonExcludedRegion.objects.create(addon=self.app,
region=mkt.regions.UK.id)
self.app.addonexcludedregion.create(region=mkt.regions.UK.id)
exclude_new_region([mkt.regions.UK])
eq_(list(_region_exclude_mock.call_args_list[0][0][0]), [])

Просмотреть файл

@ -153,20 +153,12 @@ class TestRegionForm(amo.tests.WebappTestCase):
eq_(form.initial['enable_new_regions'], False)
def test_worldwide_only(self):
form = forms.RegionForm(data={'regions': [mkt.regions.WORLDWIDE.id]},
form = forms.RegionForm({'regions': [mkt.regions.WORLDWIDE.id]},
**self.kwargs)
assert form.is_valid(), form.errors
def test_enable_new_regions(self):
form = forms.RegionForm(data={'enable_new_regions': 'on',
'regions': mkt.regions.ALL_REGION_IDS},
**self.kwargs)
assert form.is_valid(), form.errors
form.save()
eq_(self.app.enable_new_regions, True)
def test_no_regions(self):
form = forms.RegionForm(data={}, **self.kwargs)
form = forms.RegionForm({'enable_new_regions': True}, **self.kwargs)
assert not form.is_valid()
eq_(form.errors,
{'__all__': ['You must select at least one region.']})
@ -178,20 +170,25 @@ class TestRegionForm(amo.tests.WebappTestCase):
to_exclude = list(mkt.regions.ALL_REGION_IDS)
to_exclude.remove(region_id)
form = forms.RegionForm(
data={'regions': to_exclude,
'enable_new_regions': 'on'}, **self.kwargs)
form = forms.RegionForm({'regions': to_exclude,
'restricted': '1',
'enable_new_regions': True},
**self.kwargs)
assert form.is_valid(), form.errors
form.save()
eq_(self.app.get_region_ids(True), to_exclude)
r_id = mkt.regions.REGIONS_CHOICES_ID_DICT[region_id]
eq_(self.app.reload().get_region_ids(True), to_exclude,
'Failed for %s' % r_id)
def test_unrated_games_excluded(self):
games = Category.objects.create(type=amo.ADDON_WEBAPP, slug='games')
AddonCategory.objects.create(addon=self.app, category=games)
form = forms.RegionForm({'regions': mkt.regions.REGION_IDS,
'enable_new_regions': True}, **self.kwargs)
'restricted': '1',
'enable_new_regions': True},
**self.kwargs)
# Developers should still be able to save form OK, even
# if they pass a bad region. Think of the grandfathered developers.
@ -201,7 +198,7 @@ class TestRegionForm(amo.tests.WebappTestCase):
# No matter what the developer tells us, still exclude Brazilian
# and German games.
form = forms.RegionForm(data=None, **self.kwargs)
eq_(set(form.initial['regions']),
self.assertSetEqual(form.initial['regions'],
set(mkt.regions.REGION_IDS) -
set([mkt.regions.BR.id, mkt.regions.DE.id,
mkt.regions.WORLDWIDE.id]))
@ -216,13 +213,15 @@ class TestRegionForm(amo.tests.WebappTestCase):
AddonCategory.objects.create(addon=self.app, category=games)
form = forms.RegionForm({'regions': mkt.regions.REGION_IDS,
'enable_new_regions': True}, **self.kwargs)
'restricted': '1',
'enable_new_regions': True},
**self.kwargs)
assert form.is_valid()
form.save()
form = forms.RegionForm(data=None, **self.kwargs)
eq_(set(form.initial['regions']),
self.assertSetEqual(form.initial['regions'],
set(mkt.regions.REGION_IDS) -
set(regions + [mkt.regions.WORLDWIDE.id]))
eq_(form.initial['enable_new_regions'], True)
@ -238,7 +237,8 @@ class TestRegionForm(amo.tests.WebappTestCase):
AddonCategory.objects.create(addon=self.app, category=games)
form = forms.RegionForm({'regions': mkt.regions.ALL_REGION_IDS,
'enable_new_regions': 'on'}, **self.kwargs)
'enable_new_regions': True},
**self.kwargs)
assert form.is_valid(), form.errors
form.save()
@ -246,6 +246,7 @@ class TestRegionForm(amo.tests.WebappTestCase):
def test_exclude_worldwide(self):
form = forms.RegionForm({'regions': mkt.regions.REGION_IDS,
'restricted': '1',
'enable_new_regions': False}, **self.kwargs)
assert form.is_valid(), form.errors
form.save()

Просмотреть файл

@ -155,6 +155,10 @@ class Webapp(Addon):
# Set the slug once we have an id to keep things in order.
self.update(slug='app-%s' % self.id)
# Create Geodata object (a 1-to-1 relationship).
if not hasattr(self, '_geodata'):
Geodata.objects.create(addon=self)
@staticmethod
def transformer(apps):
# I think we can do less than the Addon transformer, so at some point
@ -204,6 +208,12 @@ class Webapp(Addon):
qs = apps.transform(t)
return qs
@property
def geodata(self):
if hasattr(self, '_geodata'):
return self._geodata
return Geodata.objects.create(addon=self)
def get_api_url(self, action=None, api=None, resource=None, pk=False):
"""Reverse a URL for the API."""
kwargs = {'api_name': api or 'apps',
@ -1618,3 +1628,18 @@ class AppManifest(amo.models.ModelBase):
class Meta:
db_table = 'app_manifest'
class Geodata(amo.models.ModelBase):
"""TODO: Forgo AER and use bool columns for every region and carrier."""
addon = models.OneToOneField('addons.Addon', related_name='_geodata')
restricted = models.BooleanField(default=False)
popular_region = models.CharField(max_length=10, null=True)
class Meta:
db_table = 'webapps_geodata'
def __unicode__(self):
return u'%s (%s): <Webapp %s>' % (self.id,
'restricted' if self.restricted else 'unrestricted',
self.addon.id)

Просмотреть файл

@ -42,8 +42,9 @@ from mkt.constants.ratingsdescriptors import RATING_DESCS
from mkt.site.fixtures import fixture
from mkt.submit.tests.test_views import BasePackagedAppTest, BaseWebAppTest
from mkt.webapps.models import (AddonExcludedRegion, AppFeatures, AppManifest,
ContentRating, get_excluded_in, Installed,
RatingDescriptors, Webapp, WebappIndexer)
ContentRating, Geodata, get_excluded_in,
Installed, RatingDescriptors, Webapp,
WebappIndexer)
class TestWebapp(amo.tests.TestCase):
@ -117,6 +118,11 @@ class TestWebapp(amo.tests.TestCase):
w.save()
eq_(w.app_slug, 'slug~')
def test_geodata_upon_app_creation(self):
app = Webapp.objects.create(type=amo.ADDON_WEBAPP)
assert app.geodata, (
'Geodata was not created with Webapp.')
def test_get_url_path(self):
webapp = Webapp(app_slug='woo')
eq_(webapp.get_url_path(), '/app/woo/')
@ -1507,3 +1513,13 @@ class TestManifestUpload(BaseUploadTest, amo.tests.TestCase):
version = app.current_version.reload()
eq_(version.version, '4.1')
eq_(version.developer_name, truncated_developer_name)
class TestGeodata(amo.tests.WebappTestCase):
def setUp(self):
super(TestGeodata, self).setUp()
self.geo = self.app.geodata
def test_app_geodata(self):
assert isinstance(Webapp(id=337141).geodata, Geodata)