allow developer to opt into strictly enforced region choices (bug 896613)
This commit is contained in:
Родитель
09dc3c5e6e
Коммит
8494c035d4
|
@ -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> ·
|
||||
<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> ·
|
||||
<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)
|
||||
|
|
Загрузка…
Ссылка в новой задаче