* track Blocked versions per version

* update api docs

* test fixes

* scanners test fixes

* drop now obsolete create_blockversions command

* address review comments; fix failing tests

* restore warning messages about a version being in a submission or block

* add extra tests + optimizations

* Fix failing blocklist/test_serializers.py test
This commit is contained in:
Andrew Williamson 2023-06-26 11:00:10 +01:00 коммит произвёл GitHub
Родитель a07f8d2eab
Коммит 706798d37e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
42 изменённых файлов: 1264 добавлений и 1864 удалений

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

@ -118,12 +118,12 @@ class FileInline(admin.TabularInline):
version__deleted.boolean = True
def version__is_blocked(self, obj):
block = self.instance.block
if not (block and block.is_version_blocked(obj.version.version)):
blockversion = getattr(obj.version, 'blockversion', None)
if not blockversion:
return ''
url = block.get_admin_url_path()
template = '<a href="{}">Blocked ({} - {})</a>'
return format_html(template, url, block.min_version, block.max_version)
url = blockversion.block.get_admin_url_path()
template = '<a href="{}">Blocked</a>'
return format_html(template, url)
version__is_blocked.short_description = 'Block status'
@ -167,7 +167,9 @@ class FileInline(admin.TabularInline):
sub_qs = NeedsHumanReview.objects.filter(
is_active=True, version=OuterRef('version')
)
return qs.select_related('version').annotate(needs_human_review=Exists(sub_qs))
return qs.select_related('version', 'version__blockversion').annotate(
needs_human_review=Exists(sub_qs)
)
class AddonAdmin(AMOModelAdmin):

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

@ -1808,12 +1808,11 @@ class Addon(OnChangeMixin, ModelBase):
# Block.guid is unique so it's either on the list or not.
return Block.objects.filter(guid=self.addonguid_guid).last()
@cached_property
def blocklistsubmission(self):
@property
def blocklistsubmissions(self):
from olympia.blocklist.models import BlocklistSubmission
# GUIDs should only exist in one (active) submission at once.
return BlocklistSubmission.get_submissions_from_guid(self.addonguid_guid).last()
return BlocklistSubmission.get_submissions_from_guid(self.addonguid_guid)
@property
def git_extraction_is_in_progress(self):

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

@ -32,7 +32,6 @@ from olympia.api.serializers import AMOModelSerializer, BaseESSerializer
from olympia.api.utils import is_gate_active
from olympia.applications.models import AppVersion
from olympia.bandwagon.models import Collection
from olympia.blocklist.models import Block
from olympia.constants.applications import APP_IDS, APPS_ALL
from olympia.constants.base import ADDON_TYPE_CHOICES_API
from olympia.constants.categories import CATEGORIES_BY_ID
@ -525,22 +524,6 @@ class DeveloperVersionSerializer(VersionSerializer):
raise exceptions.ValidationError(msg)
return disable
def _check_blocklist(self, guid, version_string):
# check the guid/version isn't in the addon blocklist
block_qs = Block.objects.filter(guid=guid) if guid else ()
if block_qs and block_qs.first().is_version_blocked(version_string):
msg = gettext(
'Version {version} matches {block_link} for this add-on. '
'You can contact {amo_admins} for additional information.'
)
raise exceptions.ValidationError(
msg.format(
version=version_string,
block_link=absolutify(reverse('blocklist.block', args=[guid])),
amo_admins='amo-admins@mozilla.com',
),
)
def _check_for_existing_versions(self, version_string):
# Make sure we don't already have this version.
existing_versions = Version.unfiltered.filter(
@ -557,9 +540,7 @@ class DeveloperVersionSerializer(VersionSerializer):
def validate(self, data):
if not self.instance:
guid = self.addon.guid if self.addon else self.parsed_data.get('guid')
version_string = self.parsed_data.get('version')
self._check_blocklist(guid, version_string)
if self.addon:
self._check_for_existing_versions(version_string)

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

@ -23,11 +23,11 @@ from olympia.amo.reverse import django_reverse
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
collection_factory,
user_factory,
version_factory,
)
from olympia.blocklist.models import Block
from olympia.constants.browsers import CHROME
from olympia.git.models import GitExtractionEntry
@ -731,15 +731,11 @@ class TestAddonAdmin(TestCase):
assert response.status_code == 200
assert 'Blocked' not in response.content.decode('utf-8')
block = Block.objects.create(
addon=addon, min_version=addon.current_version.version, updated_by=user
)
block = block_factory(addon=addon, updated_by=user)
response = self.client.get(self.detail_url, follow=True)
assert response.status_code == 200
assert f'Blocked ({addon.current_version.version} - *)' in (
response.content.decode('utf-8')
)
assert 'Blocked' in response.content.decode('utf-8')
link = pq(response.content)('.field-version__is_blocked a')[0]
assert link.attrib['href'] == block.get_admin_url_path()
@ -750,7 +746,7 @@ class TestAddonAdmin(TestCase):
self.grant_permission(user, 'Addons:Edit')
self.grant_permission(user, 'Admin:Advanced')
self.client.force_login(user)
with self.assertNumQueries(22):
with self.assertNumQueries(20):
# It's very high because most of AddonAdmin is unoptimized but we
# don't want it unexpectedly increasing.
# FIXME: explain each query
@ -760,7 +756,7 @@ class TestAddonAdmin(TestCase):
version_factory(addon=addon)
version_factory(addon=addon)
with self.assertNumQueries(22):
with self.assertNumQueries(20):
# Confirm it scales correctly by doing the same number of queries
# when number of versions increases.
# FIXME: explain each query

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

@ -1853,25 +1853,22 @@ class TestAddonModels(TestCase):
AddonGUID.objects.create(addon=addon, guid='not-a-guid')
assert addon.block == block
def test_blocklistsubmission_property(self):
def test_blocklistsubmissions_property(self):
addon = Addon.objects.get(id=3615)
assert addon.blocklistsubmission is None
assert not addon.blocklistsubmissions.exists()
del addon.blocklistsubmission
submission = BlocklistSubmission.objects.create(
input_guids=addon.guid, updated_by=user_factory()
)
assert addon.blocklistsubmission == submission
assert list(addon.blocklistsubmissions) == [submission]
del addon.blocklistsubmission
submission.update(input_guids='not-a-guid')
submission.update(to_block=[{'guid': 'not-a-guid'}])
assert addon.blocklistsubmission is None
assert not addon.blocklistsubmissions.exists()
del addon.blocklistsubmission
addon.delete()
AddonGUID.objects.create(addon=addon, guid='not-a-guid')
assert addon.blocklistsubmission == submission
assert list(addon.blocklistsubmissions) == [submission]
class TestAddonUser(TestCase):

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

@ -45,7 +45,6 @@ from olympia.amo.tests import (
from olympia.amo.tests.test_helpers import get_image_path
from olympia.amo.urlresolvers import get_outgoing_url
from olympia.bandwagon.models import CollectionAddon
from olympia.blocklist.models import Block
from olympia.constants.browsers import CHROME
from olympia.constants.categories import CATEGORIES, CATEGORIES_BY_ID
from olympia.constants.licenses import LICENSE_GPL3
@ -3433,16 +3432,6 @@ class TestVersionViewSetCreate(UploadMixin, VersionViewSetCreateUpdateMixin, Tes
assert version.approval_notes == 'This!'
self.statsd_incr_mock.assert_any_call('addons.submission.version.unlisted')
def test_check_blocklist(self):
Block.objects.create(guid=self.addon.guid, updated_by=self.user)
response = self.client.post(
self.url,
data=self.minimal_data,
)
assert response.status_code == 400
assert 'Version 0.0.1 matches ' in str(response.data['non_field_errors'])
assert self.addon.reload().versions.count() == 1
def test_cant_update_disabled_addon(self):
self.addon.update(status=amo.STATUS_DISABLED)
response = self.client.post(

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

@ -53,6 +53,7 @@ from olympia.amo.utils import SafeStorage, use_fake_fxa
from olympia.api.tests import JWTAuthKeyTester
from olympia.applications.models import AppVersion
from olympia.bandwagon.models import Collection
from olympia.blocklist.models import Block, BlockVersion
from olympia.constants.categories import CATEGORIES
from olympia.files.models import File
from olympia.promoted.models import (
@ -960,6 +961,18 @@ def version_factory(file_kw=None, **kw):
return ver
def block_factory(*, version_ids=None, **kwargs):
block = Block.objects.create(**kwargs)
if version_ids is None and block.addon:
version_ids = list(block.addon.versions.values_list('id', flat=True))
if version_ids is not None:
BlockVersion.objects.bulk_create(
BlockVersion(block=block, version_id=version_id)
for version_id in version_ids
)
return block
@pytest.mark.es_tests
class ESTestCaseMixin:
@classmethod

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

@ -1,7 +1,8 @@
from types import SimpleNamespace
from django import http
from django.contrib import admin, auth, contenttypes, messages
from django.core.exceptions import PermissionDenied
from django.forms.fields import ChoiceField
from django.forms.widgets import HiddenInput
from django.shortcuts import get_object_or_404, redirect
from django.template.loader import render_to_string
@ -13,35 +14,17 @@ from olympia.activity.models import ActivityLog
from olympia.addons.models import Addon
from olympia.amo.admin import AMOModelAdmin
from olympia.amo.utils import HttpResponseTemporaryRedirect
from olympia.versions.models import Version
from .forms import BlocklistSubmissionForm, MultiAddForm, MultiDeleteForm
from .models import Block, BlocklistSubmission
from .forms import (
BlocklistSubmissionForm,
MultiAddForm,
MultiDeleteForm,
)
from .models import Block, BlocklistSubmission, BlockVersion
from .tasks import process_blocklistsubmission
from .utils import splitlines
# The limit for how many GUIDs should be fully loaded with all metadata
GUID_FULL_LOAD_LIMIT = 100
def _get_version_choices(block, field_name):
# field_name will be `min_version` or `max_version`
default = block._meta.get_field(field_name).default
choices = [(default, default)] + list(
(version.version, version.version) for version in block.addon_versions
)
block_version = getattr(block, field_name)
if block_version and (block_version, block_version) not in choices:
# if the current version isn't in choices it's not a valid version of
# the addon. This is either because:
# - the Block was created as a multiple submission so was a free input
# - it's a new Block and the min|max_version was passed as a GET param
# - the version was hard-deleted from the addon afterwards (unlikely)
choices = [(block_version, '(invalid)')] + choices
return choices
class BlocklistSubmissionStateFilter(admin.SimpleListFilter):
title = 'Signoff State'
parameter_name = 'signoff_state'
@ -106,17 +89,9 @@ class BlockAdminAddMixin:
if request.method == 'POST':
form = MultiForm(request.POST)
if form.is_valid():
guids = splitlines(form.data.get('guids'))
if len(guids) == 1 and form.existing_block:
# If the guid already has a Block go to the change view
return redirect(
'admin:blocklist_block_change', form.existing_block.id
)
elif len(guids) > 0:
# Otherwise go to multi view.
return HttpResponseTemporaryRedirect(
reverse('admin:blocklist_blocklistsubmission_add')
)
return HttpResponseTemporaryRedirect(
reverse('admin:blocklist_blocklistsubmission_add')
)
else:
form = MultiForm()
@ -138,55 +113,36 @@ class BlockAdminAddMixin:
def add_from_addon_pk_view(self, request, pk, **kwargs):
addon = get_object_or_404(Addon.unfiltered, pk=pk or kwargs.get('pk'))
get_params = request.GET.copy()
if changed_version_ids := get_params.pop('v', None):
version_strings = Version.unfiltered.filter(
id__in=(int(id_) for id_ in changed_version_ids)
).values_list('version', flat=True)
get_params['min_version'] = min(version_strings)
get_params['max_version'] = max(version_strings)
warning_message = (
'The version id:{version_id} could not be selected because {reason}'
)
if 'min_version' in get_params or 'max_version' in get_params:
warning_message = (
f"The versions {get_params.get('min_version', '0')} to "
f"{get_params.get('max_version', '*')} could not be "
'pre-selected because {reason}'
)
else:
warning_message = None
if addon.blocklistsubmission:
if 'min_version' in get_params or 'max_version' in get_params:
if v_ids := [int(v) for v in request.GET.getlist('v')]:
submissions = BlocklistSubmission.get_all_submission_versions()
clashes = set(v_ids) & set(submissions)
for version_id in clashes:
messages.add_message(
request,
messages.WARNING,
warning_message.format(
reason='this addon is part of a pending submission'
version_id=version_id,
reason='this version is part of a pending submission',
),
)
return redirect(
reverse(
'admin:blocklist_blocklistsubmission_change',
args=(addon.blocklistsubmission.pk,),
)
)
elif addon.block:
if 'min_version' in get_params or 'max_version' in get_params:
for block in BlockVersion.objects.filter(version_id__in=v_ids):
messages.add_message(
request,
messages.WARNING,
warning_message.format(
reason='some versions have been blocked already'
version_id=block.version_id,
reason='this version is already blocked',
),
)
return redirect(
reverse('admin:blocklist_block_change', args=(addon.block.pk,))
)
else:
return redirect(
reverse('admin:blocklist_blocklistsubmission_add')
+ f'?guids={addon.addonguid_guid}&{get_params.urlencode()}'
)
return redirect(
reverse('admin:blocklist_blocklistsubmission_add')
+ f'?guids={addon.addonguid_guid}&{request.GET.urlencode()}'
)
@admin.register(BlocklistSubmission)
@ -308,8 +264,8 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
{
'fields': (
'blocks',
'min_version',
'max_version',
'disable_addon',
'changed_version_ids',
'url',
'reason',
'updated_by',
@ -338,6 +294,7 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
{
'fields': (
'blocks',
'changed_version_ids',
'updated_by',
'signoff_by',
'submission_logs',
@ -368,10 +325,7 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
ro_fields += [
'input_guids',
'action',
'min_version',
'max_version',
'existing_min_version',
'existing_max_version',
'changed_version_ids',
'delay_days',
]
if not self.has_change_permission(request, obj, strict=True):
@ -388,25 +342,9 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
)
)
def formfield_for_dbfield(self, db_field, request, **kwargs):
single_guid = len(self._get_input_guids(request)) == 1
if single_guid and db_field.name in ('min_version', 'max_version'):
return ChoiceField(**kwargs)
return super().formfield_for_dbfield(db_field, request, **kwargs)
def get_form(self, request, obj=None, change=False, **kwargs):
form = super().get_form(request, obj, change, **kwargs)
if not change:
guids = self._get_input_guids(request)
if len(guids) == 1:
block_obj = Block(guid=guids[0])
if 'min_version' in form.base_fields:
form.base_fields['min_version'].choices = _get_version_choices(
block_obj, 'min_version'
)
form.base_fields['max_version'].choices = _get_version_choices(
block_obj, 'max_version'
)
form.base_fields['input_guids'].widget = HiddenInput()
form.base_fields['action'].widget = HiddenInput()
if 'delayed_until' in form.base_fields:
@ -419,18 +357,18 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
MultiBlockForm = self.get_form(request, change=False, **kwargs)
is_delete = not self.is_add_change_submission(request, None)
guids_data = self.get_value('guids', request)
if guids_data and 'input_guids' not in request.POST:
# If we get a guids param it's a redirect from input_guids_view.
initial = {key: values for key, values in request.GET.items()}
initial.update(
**{
'input_guids': guids_data,
'existing_min_version': initial.get('min_version', Block.MIN),
'existing_max_version': initial.get('max_version', Block.MAX),
}
)
initial = {
key: value
for key, value in request.GET.items()
if key not in ('v', 'guids')
}
if version_ids := request.GET.getlist('v'):
# `v` can contain multiple version ids
initial['changed_version_ids'] = version_ids
initial.update(**{'input_guids': guids_data})
if 'action' in request.POST:
initial['action'] = request.POST['action']
form = MultiBlockForm(initial=initial)
@ -444,15 +382,8 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
self.save_model(request, obj, form, change=False)
self.log_addition(request, obj, [{'added': {}}])
return self.response_add(request, obj)
elif not is_delete:
else:
guids_data = request.POST.get('input_guids')
form_data = form.data.copy()
# each time we render the form we pass along the existing
# versions so we can detect if they've been changed and we'd '
# need a recalculation how existing blocks are affected.
form_data['existing_min_version'] = form_data['min_version']
form_data['existing_max_version'] = form_data['max_version']
form.data = form_data
else:
# if its not a POST and no ?guids there's nothing to do so go back
return redirect('admin:blocklist_block_add')
@ -468,27 +399,15 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
'title': 'Delete Blocks' if is_delete else 'Block Add-ons',
'save_as': False,
'block_history': self.block_history(self.model(input_guids=guids_data)),
'submission_complete': False,
'submission_published': False,
'site_title': None,
'is_popup': False,
'form_url': '',
}
context.update(**self._get_enhanced_guid_context(request, guids_data))
return TemplateResponse(
request, 'admin/blocklist/blocklistsubmission_add_form.html', context
)
def _get_enhanced_guid_context(self, request, guids_data, obj=None):
load_full_objects = len(splitlines(guids_data)) <= GUID_FULL_LOAD_LIMIT
objects = self.model.process_input_guids(
guids_data,
v_min=self.get_value('min_version', request, obj, Block.MIN),
v_max=self.get_value('max_version', request, obj, Block.MAX),
load_full_objects=load_full_objects,
filter_existing=self.is_add_change_submission(request, obj),
)
if load_full_objects:
Block.preload_addon_versions(objects['blocks'])
objects['total_adu'] = sum(block.current_adu for block in objects['blocks'])
return objects
def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = extra_context or {}
obj = self.get_object(request, object_id)
@ -506,19 +425,6 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
extra_context['can_reject'] = self.is_changeable(
obj
) and self.has_signoff_reject_permission(request, obj)
if obj.signoff_state != BlocklistSubmission.SIGNOFF_PUBLISHED:
extra_context.update(
**self._get_enhanced_guid_context(request, obj.input_guids, obj)
)
else:
extra_context['blocks'] = obj.get_blocks_submitted(
load_full_objects_threshold=GUID_FULL_LOAD_LIMIT
)
if len(extra_context['blocks']) <= GUID_FULL_LOAD_LIMIT:
# if it's less than the limit we loaded full Block instances
# so preload the addon_versions so the review links are
# generated efficiently.
Block.preload_addon_versions(extra_context['blocks'])
return super().change_view(
request, object_id, form_url=form_url, extra_context=extra_context
)
@ -528,7 +434,7 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
):
if change:
# add this to the instance so blocks() below can reference it.
obj._blocks = context['blocks']
obj._blocks = context['adminform'].form.blocks
return super().render_change_form(
request, context, add=add, change=change, form_url=form_url, obj=obj
)
@ -594,12 +500,13 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
def blocks(self, obj):
# Annoyingly, we don't have the full context, but we stashed blocks
# earlier in render_change_form().
complete = obj.signoff_state == BlocklistSubmission.SIGNOFF_PUBLISHED
is_published = obj.signoff_state == BlocklistSubmission.SIGNOFF_PUBLISHED
return render_to_string(
'admin/blocklist/includes/enhanced_blocks.html',
{
'blocks': obj._blocks,
'submission_complete': complete,
'form': SimpleNamespace(blocks=obj._blocks),
'submission_published': is_published,
},
)
@ -620,7 +527,7 @@ class BlocklistSubmissionAdmin(AMOModelAdmin):
@admin.register(Block)
class BlockAdmin(BlockAdminAddMixin, AMOModelAdmin):
list_display = ('guid', 'min_version', 'max_version', 'updated_by', 'modified')
list_display = ('guid', 'updated_by', 'modified')
readonly_fields = (
'addon_guid',
'addon_name',
@ -630,6 +537,7 @@ class BlockAdmin(BlockAdminAddMixin, AMOModelAdmin):
'review_unlisted_link',
'block_history',
'url_link',
'blocked_versions',
)
ordering = ['-modified']
view_on_site = False
@ -655,6 +563,11 @@ class BlockAdmin(BlockAdminAddMixin, AMOModelAdmin):
def users(self, obj):
return obj.average_daily_users_snapshot
def blocked_versions(self, obj):
return ', '.join(
sorted(obj.blockversion_set.values_list('version__version', flat=True))
)
def block_history(self, obj):
logs = (
ActivityLog.objects.for_guidblock(obj.guid)
@ -694,8 +607,7 @@ class BlockAdmin(BlockAdminAddMixin, AMOModelAdmin):
'Edit Block',
{
'fields': (
'min_version',
'max_version',
'blocked_versions',
('url', 'url_link'),
'reason',
),
@ -705,10 +617,7 @@ class BlockAdmin(BlockAdminAddMixin, AMOModelAdmin):
return (details, history, edit)
def has_change_permission(self, request, obj=None):
if obj and obj.is_readonly:
return False
else:
return super().has_change_permission(request, obj=obj)
return False
def has_delete_permission(self, request, obj=None):
if obj and obj.is_readonly:
@ -726,22 +635,6 @@ class BlockAdmin(BlockAdminAddMixin, AMOModelAdmin):
# wrong.
raise PermissionDenied
def get_form(self, request, obj=None, change=False, **kwargs):
form = super().get_form(request, obj=obj, change=change, **kwargs)
if 'min_version' in form.base_fields:
form.base_fields['min_version'].choices = _get_version_choices(
obj, 'min_version'
)
form.base_fields['max_version'].choices = _get_version_choices(
obj, 'max_version'
)
return form
def formfield_for_dbfield(self, db_field, request, **kwargs):
if db_field.name in ('min_version', 'max_version'):
return ChoiceField(**kwargs)
return super().formfield_for_dbfield(db_field, request, **kwargs)
def changeform_view(self, request, obj_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
if obj_id:

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

@ -11,17 +11,8 @@ from .models import Block, BlocklistSubmission
from .utils import splitlines
def _get_matching_guids_and_errors(guids):
error_list = []
matching = list(Block.objects.filter(guid__in=guids).values_list('guid', flat=True))
for guid in guids:
if BlocklistSubmission.get_submissions_from_guid(guid):
error_list.append(
ValidationError(
_('GUID %(guid)s is in a pending Submission'), params={'guid': guid}
)
)
return matching, error_list
# The limit for how many GUIDs should be fully loaded with all metadata
GUID_FULL_LOAD_LIMIT = 100
class MultiGUIDInputForm(forms.Form):
@ -37,18 +28,15 @@ class MultiGUIDInputForm(forms.Form):
class MultiDeleteForm(MultiGUIDInputForm):
def clean(self):
guids = splitlines(self.cleaned_data.get('guids'))
matching, errors = _get_matching_guids_and_errors(guids)
matching = Block.objects.filter(guid__in=guids).values_list('guid', flat=True)
missing_guids = [guid for guid in guids if guid not in matching]
if missing_guids:
errors.append(
[
ValidationError(
_('Block with GUID %(guid)s not found'), params={'guid': guid}
)
for guid in missing_guids
]
missing_guids = (guid for guid in guids if guid not in matching)
errors = [
ValidationError(
_('Block with GUID %(guid)s not found'), params={'guid': guid}
)
for guid in missing_guids
]
if errors:
raise ValidationError(errors)
@ -57,8 +45,8 @@ class MultiDeleteForm(MultiGUIDInputForm):
class MultiAddForm(MultiGUIDInputForm):
def clean(self):
guids = splitlines(self.cleaned_data.get('guids'))
matching, errors = _get_matching_guids_and_errors(guids)
errors = []
if len(guids) == 1:
guid = guids[0]
blk = self.existing_block = Block.objects.filter(guid=guid).first()
@ -74,13 +62,21 @@ class MultiAddForm(MultiGUIDInputForm):
raise ValidationError(errors)
def _get_version_choices(blocks, ver_filter=lambda v: True):
return [
(
block.guid,
[
(version.id, version.version)
for version in block.addon_versions
if ver_filter(version)
],
)
for block in blocks
]
class BlocklistSubmissionForm(AMOModelForm):
existing_min_version = forms.fields.CharField(
widget=forms.widgets.HiddenInput, required=False
)
existing_max_version = forms.fields.CharField(
widget=forms.widgets.HiddenInput, required=False
)
delay_days = forms.fields.IntegerField(
widget=forms.widgets.NumberInput,
initial=0,
@ -91,62 +87,76 @@ class BlocklistSubmissionForm(AMOModelForm):
delayed_until = forms.fields.DateTimeField(
widget=HTML5DateTimeInput, required=False
)
# Note we don't render the widget - we manually create the checkboxes in
# enhanced_blocks.html
changed_version_ids = forms.fields.TypedMultipleChoiceField(choices=(), coerce=int)
def _check_if_existing_blocks_changed(
self, all_guids, v_min, v_max, existing_v_min, existing_v_max
):
# shortcut if the min/max versions havn't changed
if v_min == existing_v_min and v_max == existing_v_max:
return False
def __init__(self, data=None, *args, **kw):
instance = kw.get('instance')
block_data = list(
Block.objects.filter(guid__in=all_guids).values_list(
'guid', 'min_version', 'max_version'
def get_value(field_name, default):
return (
getattr(instance, field_name, default)
if instance
else (
(data or {}).get(field_name)
or (kw.get('initial') or {}).get(field_name, default)
)
)
)
to_update_based_on_existing_v = [
guid
for (guid, min_version, max_version) in block_data
if not (min_version == existing_v_min and max_version == existing_v_max)
]
to_update_based_on_new_v = [
guid
for (guid, min_version, max_version) in block_data
if not (min_version == v_min and max_version == v_max)
]
is_add_change = get_value(
'action', str(BlocklistSubmission.ACTION_ADDCHANGE)
) == str(BlocklistSubmission.ACTION_ADDCHANGE)
input_guids = get_value('input_guids', '')
super().__init__(data, *args, **kw)
return to_update_based_on_existing_v != to_update_based_on_new_v
load_full_objects = len(splitlines(input_guids)) <= GUID_FULL_LOAD_LIMIT
if (
not instance
or instance.signoff_state != BlocklistSubmission.SIGNOFF_PUBLISHED
):
objects = BlocklistSubmission.process_input_guids(
input_guids,
load_full_objects=load_full_objects,
filter_existing=is_add_change,
)
objects['total_adu'] = sum(block.current_adu for block in objects['blocks'])
if changed_version_ids_field := self.fields.get('changed_version_ids'):
changed_version_ids_field.choices = _get_version_choices(
objects['blocks'],
# ^ is XOR
# - for add action it allows the version when it is NOT blocked
# - for delete action it allows the version when it IS blocked
lambda v: (v.is_blocked ^ is_add_change)
and not v.blocklist_submission_id,
)
self.changed_version_ids_choices = [
v_id
for _guid, opts in changed_version_ids_field.choices
for (v_id, _text) in opts
]
if not data and 'changed_version_ids' not in (self.initial or {}):
# preselect all the options
self.initial = {
**(self.initial or {}),
'changed_version_ids': self.changed_version_ids_choices,
}
for key, value in objects.items():
setattr(self, key, value)
elif instance:
self.blocks = instance.get_blocks_submitted(
load_full_objects_threshold=GUID_FULL_LOAD_LIMIT
)
if load_full_objects:
# if it's less than the limit we loaded full Block instances
# so preload the addon_versions so the review links are
# generated efficiently.
Block.preload_addon_versions(self.blocks)
def clean(self):
super().clean()
data = self.cleaned_data
guids = splitlines(data.get('input_guids'))
# Ignore for a single guid because we always update it irrespective of
# whether it needs to be updated.
is_addchange_submission = (
data.get('action', BlocklistSubmission.ACTION_ADDCHANGE)
== BlocklistSubmission.ACTION_ADDCHANGE
)
blocks, errors = _get_matching_guids_and_errors(guids)
if len(guids) > 1 and is_addchange_submission:
blocks_have_changed = self._check_if_existing_blocks_changed(
guids,
data.get('min_version'),
data.get('max_version'),
data.get('existing_min_version'),
data.get('existing_max_version'),
)
if blocks_have_changed:
errors.append(
ValidationError(
_(
'Blocks to be updated are different because Min or '
'Max version has changed.'
)
)
)
if delay_days := data.get('delay_days', 0):
data['delayed_until'] = datetime.now() + timedelta(days=delay_days)
if errors:
raise ValidationError(errors)

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

@ -1,32 +0,0 @@
from django.core.management.base import BaseCommand
import olympia.core.logger
from olympia.blocklist.models import Block, BlockVersion
from olympia.files.models import File
log = olympia.core.logger.getLogger('z.amo.blocklist')
class Command(BaseCommand):
help = 'Migration to create BlockVersion instances for every Block'
def handle(self, *args, **options):
count = 0
for block in Block.objects.all().iterator(1000):
versions_qs = (
File.objects.filter(version__addon__addonguid__guid=block.guid)
.exclude(version__blockversion__id__isnull=False)
.values_list('version__version', 'version_id')
)
block_versions = [
BlockVersion(version_id=version_id, block=block)
for version_str, version_id in versions_qs
if block.min_version <= version_str and block.max_version >= version_str
]
BlockVersion.objects.bulk_create(block_versions)
count += 1
if (count % 1000) == 0:
log.info('Progress: %s Blocks processed so far' % count)
log.info('Finished: %s Blocks processed' % count)

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

@ -1,10 +1,16 @@
# Generated by Django 2.2.16 on 2020-11-03 11:07
from django.core.exceptions import ValidationError
from django.db import migrations
import olympia.blocklist.models
import olympia.versions.fields
def no_asterisk(value):
if '*' in value:
raise ValidationError('%(value)s contains *', params={'value': value})
class Migration(migrations.Migration):
dependencies = [
@ -15,11 +21,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='block',
name='min_version',
field=olympia.versions.fields.VersionStringField(default='0', max_length=255, validators=[olympia.blocklist.models.no_asterisk]),
field=olympia.versions.fields.VersionStringField(default='0', max_length=255, validators=[no_asterisk]),
),
migrations.AlterField(
model_name='blocklistsubmission',
name='min_version',
field=olympia.versions.fields.VersionStringField(default='0', max_length=255, validators=[olympia.blocklist.models.no_asterisk]),
field=olympia.versions.fields.VersionStringField(default='0', max_length=255, validators=[no_asterisk]),
),
]

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

@ -0,0 +1,45 @@
# Generated by Django 4.2.1 on 2023-06-05 11:12
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import olympia.amo.models
class Migration(migrations.Migration):
dependencies = [
('blocklist', '0030_alter_blocklistsubmission_signoff_state_blockversion'),
]
operations = [
migrations.AddField(
model_name='blocklistsubmission',
name='disable_addon',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='blocklistsubmission',
name='changed_version_ids',
field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='block',
name='max_version',
field=models.CharField(max_length=255, null=True),
),
migrations.AlterField(
model_name='block',
name='min_version',
field=models.CharField(max_length=255, null=True),
),
migrations.AlterField(
model_name='blocklistsubmission',
name='max_version',
field=models.CharField(max_length=255, null=True),
),
migrations.AlterField(
model_name='blocklistsubmission',
name='min_version',
field=models.CharField(max_length=255, null=True),
),
]

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

@ -1,7 +1,6 @@
import json
import os
import secrets
from collections import defaultdict
from django.conf import settings
from django.utils.functional import cached_property
@ -48,39 +47,15 @@ def generate_mlbf(stats, blocked, not_blocked):
def fetch_blocked_from_db():
from olympia.blocklist.models import Block
from olympia.files.models import File
from olympia.blocklist.models import BlockVersion
blocks = Block.objects.all()
blocks_guids = [block.guid for block in blocks]
file_qs = (
File.objects.filter(
version__addon__addonguid__guid__in=blocks_guids,
is_signed=True,
all_versions = {
block_version.version_id: (
block_version.block.guid,
block_version.version.version,
)
.order_by('version_id')
.values('version__addon__addonguid__guid', 'version__version', 'version_id')
)
addons_versions = defaultdict(list)
for file_ in file_qs:
addon_key = file_['version__addon__addonguid__guid']
addons_versions[addon_key].append(
(file_['version__version'], file_['version_id'])
)
all_versions = {}
# collect all the blocked versions
for block in blocks:
is_all_versions = (
block.min_version == Block.MIN and block.max_version == Block.MAX
)
versions = {
version_id: (block.guid, version)
for version, version_id in addons_versions[block.guid]
if is_all_versions or block.is_version_blocked(version)
}
all_versions.update(versions)
for block_version in BlockVersion.objects.filter(version__file__is_signed=True)
}
return all_versions

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

@ -2,7 +2,6 @@ from collections import defaultdict, namedtuple
from datetime import datetime
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
@ -17,31 +16,17 @@ from olympia.amo.models import BaseQuerySet, ManagerBase, ModelBase
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.utils import chunked
from olympia.users.models import UserProfile
from olympia.versions.compare import VersionString
from olympia.versions.fields import VersionStringField
from olympia.versions.models import Version
from .utils import (
block_activity_log_delete,
save_guids_to_blocks,
delete_versions_from_blocks,
save_versions_to_blocks,
splitlines,
)
def no_asterisk(value):
if '*' in value:
raise ValidationError(_('%(value)s contains *'), params={'value': value})
class Block(ModelBase):
MIN = VersionString('0')
MAX = VersionString('*')
guid = models.CharField(max_length=255, unique=True, null=False)
min_version = VersionStringField(
max_length=255, blank=False, default=MIN, validators=(no_asterisk,)
)
max_version = VersionStringField(max_length=255, blank=False, default=MAX)
url = models.CharField(max_length=255, blank=True)
reason = models.TextField(blank=True)
updated_by = models.ForeignKey(UserProfile, null=True, on_delete=models.SET_NULL)
@ -102,28 +87,15 @@ class Block(ModelBase):
.annotate(**{GUID: models.F(GUID)})
.select_related('blockversion')
)
all_submission_versions = BlocklistSubmission.get_all_submission_versions()
all_addon_versions = defaultdict(list)
for version in qs:
version.blocklist_submission_id = all_submission_versions.get(version.id, 0)
all_addon_versions[getattr(version, GUID)].append(version)
for block in blocks:
block.addon_versions = all_addon_versions[block.guid]
def clean(self):
if self.id:
# We're only concerned with edits - self.guid isn't set at this
# point for new instances anyway.
choices = list(version.version for version in self.addon_versions)
if self.min_version not in choices + [self.MIN]:
raise ValidationError({'min_version': _('Invalid version')})
if self.max_version not in choices + [self.MAX]:
raise ValidationError({'max_version': _('Invalid version')})
if self.min_version > self.max_version:
raise ValidationError(_('Min version can not be greater than Max version'))
def is_version_blocked(self, version):
return self.min_version <= version and self.max_version >= version
def review_listed_link(self):
has_listed = any(
True
@ -168,22 +140,24 @@ class Block(ModelBase):
addons = list(cls.get_addons_for_guids_qs(guids).using(using_db))
# And then any existing block instances
existing_blocks = {
blocks = {
block.guid: block
for block in cls.objects.using(using_db).filter(guid__in=guids)
}
for addon in addons:
# get the existing block object or create a new instance
block = existing_blocks.get(addon.guid, None)
block = blocks.get(addon.guid, None)
if block:
# if it exists hook up the addon instance
block.addon = addon
else:
# otherwise create a new Block
block = Block(addon=addon)
existing_blocks[block.guid] = block
return list(existing_blocks.values())
blocks[block.guid] = block
blocks = list(blocks.values()) # flatten to just the Block instances
Block.preload_addon_versions(blocks)
return blocks
class BlockVersion(ModelBase):
@ -233,25 +207,30 @@ class BlocklistSubmission(ModelBase):
ACTION_ADDCHANGE: 'Add/Change',
ACTION_DELETE: 'Delete',
}
FakeBlockAddonVersion = namedtuple(
'FakeBlockAddonVersion',
(
'id',
'version',
'is_blocked',
'blocklist_submission_id',
),
)
FakeBlock = namedtuple(
'FakeBlock',
(
'id',
'guid',
'min_version',
'max_version',
'current_adu',
'addon_versions',
),
)
action = models.SmallIntegerField(choices=ACTIONS.items(), default=ACTION_ADDCHANGE)
input_guids = models.TextField()
changed_version_ids = models.JSONField(default=list)
to_block = models.JSONField(default=list)
min_version = VersionStringField(
max_length=255, blank=False, default=Block.MIN, validators=(no_asterisk,)
)
max_version = VersionStringField(max_length=255, blank=False, default=Block.MAX)
url = models.CharField(
max_length=255,
blank=True,
@ -274,6 +253,7 @@ class BlocklistSubmission(ModelBase):
blank=True,
help_text='The submission will not be published into blocks before this time.',
)
disable_addon = models.BooleanField(default=True)
objects = BlocklistSubmissionManager()
@ -297,8 +277,7 @@ class BlocklistSubmission(ModelBase):
# as a dict of property_name: (old_value, new_value).
changes = {}
properties = (
'min_version',
'max_version',
'versions',
'url',
'reason',
)
@ -307,26 +286,34 @@ class BlocklistSubmission(ModelBase):
changes[prop] = (getattr(block, prop), getattr(self, prop))
return changes
def clean(self):
if self.min_version > self.max_version:
raise ValidationError(_('Min version can not be greater than Max version'))
def get_blocks_submitted(self, load_full_objects_threshold=1_000_000_000):
blocks = self.block_set.all().order_by('id')
if blocks.count() > load_full_objects_threshold:
blocks_qs = self.block_set.all().order_by('id')
load_fakes = blocks_qs.count() > load_full_objects_threshold
if load_fakes:
blocks = list(blocks_qs.values_list('id', 'guid', named=True))
blocked_versions = list(
BlockVersion.objects.filter(
block__in=(b.id for b in blocks)
).values_list('block_id', 'version__version')
)
if load_fakes:
# If we'd be returning too many Block objects, fake them with the
# minimum needed to display the link to the Block change page.
blocks = [
return [
self.FakeBlock(
id=block.id,
guid=block.guid,
min_version=None,
max_version=None,
current_adu=None,
addon_versions=[
self.FakeBlockAddonVersion(None, bv.version, True, 0)
for bv in blocked_versions
if bv.block_id == block.id
],
)
for block in blocks
]
return blocks
else:
return blocks_qs.prefetch_related('blockversion_set')
def can_user_signoff(self, signoff_user):
require_different_users = not settings.DEBUG
@ -344,14 +331,7 @@ class BlocklistSubmission(ModelBase):
)
def has_version_changes(self):
block_ids = [block['id'] for block in self.to_block]
has_new_blocks = any(not id_ for id_ in block_ids)
blocks_with_version_changes_qs = Block.objects.filter(id__in=block_ids).exclude(
min_version=self.min_version, max_version=self.max_version
)
return has_new_blocks or blocks_with_version_changes_qs.exists()
return bool(self.changed_version_ids)
def update_signoff_for_auto_approval(self):
is_pending = self.signoff_state == self.SIGNOFF_PENDING
@ -385,8 +365,6 @@ class BlocklistSubmission(ModelBase):
processed = self.process_input_guids(
self.input_guids,
self.min_version,
self.max_version,
load_full_objects=False,
filter_existing=(self.action == self.ACTION_ADDCHANGE),
)
@ -409,15 +387,38 @@ class BlocklistSubmission(ModelBase):
# And then any existing block instances
block_qs = Block.objects.filter(guid__in=guids).values_list(
'id', 'guid', 'min_version', 'max_version', named=True
'id', 'guid', named=True
)
version_qs = (
Version.unfiltered.filter(addon__addonguid__guid__in=guids)
.order_by('id')
.values_list(
'id',
'version',
'blockversion__block_id',
'addon__addonguid__guid',
named=True,
)
)
all_submission_versions = BlocklistSubmission.get_all_submission_versions()
all_addon_versions = defaultdict(list)
for version in version_qs:
all_addon_versions[version.addon__addonguid__guid].append(
cls.FakeBlockAddonVersion(
version.id,
version.version,
version.blockversion__block_id is not None,
all_submission_versions.get(version.id, 0),
)
)
blocks = {
block.guid: cls.FakeBlock(
id=block.id,
guid=block.guid,
min_version=block.min_version,
max_version=block.max_version,
current_adu=adu_lookup.get(block.guid, -1),
addon_versions=tuple(all_addon_versions.get(block.guid, [])),
)
for block in block_qs
}
@ -431,29 +432,29 @@ class BlocklistSubmission(ModelBase):
block = cls.FakeBlock(
id=None,
guid=addon.guid,
min_version=Block.MIN,
max_version=Block.MAX,
current_adu=adu_lookup.get(addon.guid, -1),
addon_versions=tuple(all_addon_versions.get(addon.guid, [])),
)
blocks[addon.guid] = block
return list(blocks.values())
blocks_list = blocks.values()
return list(blocks_list)
@classmethod
def process_input_guids(
cls, input_guids, v_min, v_max, *, load_full_objects=True, filter_existing=True
cls, input_guids, *, load_full_objects=True, filter_existing=True
):
"""Process a line-return separated list of guids into a list of invalid
guids, a list of guids that are blocked already for v_min - vmax, and a
guids, a list of guids that are completely blocked already, and a
list of Block instances - including new Blocks (unsaved) and existing
partial Blocks. If `filter_existing` is False, all existing blocks are
included.
If `load_full_objects=False` is passed the Block instances are fake
(namedtuples) with only minimal data available in the "Block" objects:
Block.id
Block.guid,
Block.current_adu,
Block.min_version,
Block.max_version,
Block.addon_versions,
"""
all_guids = set(splitlines(input_guids))
@ -468,13 +469,13 @@ class BlocklistSubmission(ModelBase):
blocks = unfiltered_blocks
existing_guids = []
else:
# unfiltered_blocks contains blocks that don't need to be updated.
# Get a list of a blocks from unfiltered_blocks that are either new or
# not completely blocked.
blocks = [
block
for block in unfiltered_blocks
if not block.id
or block.min_version != v_min
or block.max_version != v_max
or any(not ver.is_blocked for ver in block.addon_versions)
]
existing_guids = [
block.guid for block in unfiltered_blocks if block not in blocks
@ -484,7 +485,6 @@ class BlocklistSubmission(ModelBase):
invalid_guids = list(
all_guids - set(existing_guids) - {block.guid for block in blocks}
)
return {
'invalid_guids': invalid_guids,
'existing_guids': existing_guids,
@ -496,8 +496,6 @@ class BlocklistSubmission(ModelBase):
assert self.action == self.ACTION_ADDCHANGE
fields_to_set = [
'min_version',
'max_version',
'url',
'reason',
'updated_by',
@ -505,7 +503,7 @@ class BlocklistSubmission(ModelBase):
all_guids_to_block = [block['guid'] for block in self.to_block]
for guids_chunk in chunked(all_guids_to_block, 100):
save_guids_to_blocks(guids_chunk, self, fields_to_set=fields_to_set)
save_versions_to_blocks(guids_chunk, self, fields_to_set=fields_to_set)
self.save()
self.update(signoff_state=self.SIGNOFF_PUBLISHED)
@ -513,14 +511,18 @@ class BlocklistSubmission(ModelBase):
def delete_block_objects(self):
assert self.is_submission_ready
assert self.action == self.ACTION_DELETE
block_ids_to_delete = [block['id'] for block in self.to_block]
for ids_chunk in chunked(block_ids_to_delete, 100):
blocks = list(Block.objects.filter(id__in=ids_chunk))
Block.preload_addon_versions(blocks)
for block in blocks:
block_activity_log_delete(block, submission_obj=self)
fields_to_set = [
'url',
'reason',
'updated_by',
]
all_guids_to_block = [block['guid'] for block in self.to_block]
for guids_chunk in chunked(all_guids_to_block, 100):
# This function will remove BlockVersions and delete the Block if empty
delete_versions_from_blocks(guids_chunk, self, fields_to_set=fields_to_set)
self.save()
Block.objects.filter(id__in=ids_chunk).delete()
self.update(signoff_state=self.SIGNOFF_PUBLISHED)
@ -529,3 +531,18 @@ class BlocklistSubmission(ModelBase):
return cls.objects.exclude(signoff_state__in=excludes).filter(
to_block__contains={'guid': guid}
)
@classmethod
def get_submissions_from_version_id(cls, version_id):
return cls.objects.exclude(
signoff_state__in=cls.SIGNOFF_STATES_FINISHED
).filter(changed_version_ids__contains=version_id)
@classmethod
def get_all_submission_versions(cls):
submission_qs = BlocklistSubmission.objects.exclude(
signoff_state__in=BlocklistSubmission.SIGNOFF_STATES_FINISHED
).values_list('id', 'changed_version_ids')
return {
ver_id: sub_id for sub_id, id_list in submission_qs for ver_id in id_list
}

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

@ -11,6 +11,8 @@ from .models import Block
class BlockSerializer(AMOModelSerializer):
addon_name = TranslationSerializerField(source='addon.name')
url = OutgoingURLField()
min_version = fields.SerializerMethodField()
max_version = fields.SerializerMethodField()
versions = fields.SerializerMethodField()
is_all_versions = fields.SerializerMethodField()
@ -31,11 +33,19 @@ class BlockSerializer(AMOModelSerializer):
)
def get_versions(self, obj):
return list(
obj.blockversion_set.order_by('version__version').values_list(
'version__version', flat=True
if not hasattr(obj, '_blockversion_set_qs_values_list'):
obj._blockversion_set_qs_values_list = sorted(
obj.blockversion_set.order_by('version__version').values_list(
'version__version', flat=True
)
)
)
return obj._blockversion_set_qs_values_list
def get_min_version(self, obj):
return versions[0] if (versions := self.get_versions(obj)) else ''
def get_max_version(self, obj):
return versions[-1] if (versions := self.get_versions(obj)) else ''
def get_is_all_versions(self, obj):
cannot_upload_new_versions = not obj.addon or obj.addon.status in (

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

@ -29,21 +29,21 @@
{% csrf_token %}
{{ form.action }}
{{ form.input_guids }}
{% if not existing_guids|length == 0 or not invalid_guids|length == 0 %}
{% if not form.existing_guids|length == 0 or not form.invalid_guids|length == 0 %}
<div>
<div class="form-row horizontal-grid">
<div>
<h3>{{ existing_guids|length }} Add-on GUIDs are already blocked for {{ form.min_version.value }} to {{ form.max_version.value }}:</h3>
<h3>{{ form.existing_guids|length }} Add-on GUIDs already completely blocked:</h3>
<ul class="guid_list field-existing-guids">
{% for guid in existing_guids %}
{% for guid in form.existing_guids %}
<li>{{ guid }}</li>
{% endfor %}
</ul>
</div>
<div>
<h3>{{ invalid_guids|length }} Add-on GUIDs were not found:</h3>
<h3>{{ form.invalid_guids|length }} Add-on GUIDs were not found:</h3>
<ul class="guid_list">
{% for guid in invalid_guids %}
{% for guid in form.invalid_guids %}
<li>{{ guid }}</li>
{% endfor %}
</ul>
@ -57,10 +57,11 @@
<div>
{% include 'admin/blocklist/includes/enhanced_blocks.html' %}
</div>
{{ form.changed_version_ids.errors }}
</div>
{% if block_history %}
<div class="form-row field-block_history">
<label for="id_min_version">Block History:</label>
<label>Block History:</label>
<div class="readonly">{{ block_history }}</div>
</div>
{% endif %}
@ -70,18 +71,10 @@
</div>
{% endif %}
<div class="form-row">
<div>
{{ form.min_version.errors }}
{{ form.min_version.label_tag }}
{{ form.min_version }}
{{ form.existing_min_version }}
</div>
</div>
<div class="form-row">
{{ form.max_version.errors }}
{{ form.max_version.label_tag }}
{{ form.max_version }}
{{ form.existing_max_version }}
{{ form.disable_addon.errors }}
{{ form.disable_addon.label_tag }}
{{ form.disable_addon }}
<p class="help">{{ form.disable_addon.help_text }}</p>
</div>
<div class="form-row">
{{ form.url.errors }}

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

@ -1,15 +1,15 @@
{% load humanize %}
<h3>{{ blocks|length|intcomma }} Add-on GUIDs with {{ total_adu|intcomma }} users:</h3>
<h3>{{ form.blocks|length|intcomma }} Add-on GUIDs with {{ form.total_adu|intcomma }} users:</h3>
<ul class="guid_list">
{% for block_obj in blocks %}
{% for block_obj in form.blocks %}
<li>
{{ block_obj.guid }}.
{% if block_obj.addon %}
<span class="addon-name">{{ block_obj.addon.name }}</span>
{% endif %}
{% if submission_complete|default_if_none:False %}
{% if submission_published|default_if_none:False %}
{% if block_obj.average_daily_users_snapshot is not None %}
({{ block_obj.average_daily_users_snapshot }} users).
{% endif %}
@ -20,8 +20,28 @@
{{ block_obj.review_listed_link }}
{{ block_obj.review_unlisted_link }}
{% if block_obj.id %}
<span class="existing_block">[<a href="{% url 'admin:blocklist_block_change' block_obj.id %}">Edit Block</a>: {{ block_obj.min_version }} - {{ block_obj.max_version }}]</span>
<span class="existing_block">[<a href="{% url 'admin:blocklist_block_change' block_obj.id %}">Edit Block</a>]</span>
{% endif %}
<ul>
{% for version in block_obj.addon_versions %}
<li data-version-id="{{ version.id }}">
<label><input
type="checkbox"
name="changed_version_ids"
value="{{ version.id }}"
{% if version.id in form.changed_version_ids_choices %}
{% if version.id in form.changed_version_ids.value %}checked{% endif %}
{% else %}
disabled
{% if version.is_blocked %}checked{% endif %}
{% endif %}
>{{ version.version }}</label>
{% if version.blocklist_submission_id %}
[<a href="{% url 'admin:blocklist_blocklistsubmission_change' version.blocklist_submission_id %}">Edit Submission</a>]
{% endif %}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>

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

@ -9,6 +9,8 @@
{{ log.log.short }} by {{ log.user.name }}:
{% if log.details %}{{ log.details.guid }}{% else %}{{ log.arguments.1 }}{% endif %}{% if 'min_version' in log.details %}
, versions {{ log.details.min_version }} - {{ log.details.max_version }}.
{% elif 'versions' in log.details %}
, versions [{{ log.details.versions|join:',' }}].
{% else %}.
{% endif %}
<ul>

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -4,9 +4,13 @@ from django.conf import settings
from django.core.management import call_command
from olympia import amo
from olympia.amo.tests import TestCase, addon_factory, user_factory, version_factory
from ..models import Block, BlockVersion
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
)
class TestExportBlocklist(TestCase):
@ -14,19 +18,19 @@ class TestExportBlocklist(TestCase):
user = user_factory()
for _idx in range(0, 5):
addon_factory()
# one version, 0 - *
Block.objects.create(
# all versions
block_factory(
addon=addon_factory(file_kw={'is_signed': True}),
updated_by=user,
)
# one version, 0 - 9999
Block.objects.create(
# one version
one = block_factory(
addon=addon_factory(file_kw={'is_signed': True}),
updated_by=user,
max_version='9999',
)
# one version, 0 - *, unlisted
Block.objects.create(
version_factory(addon=one.addon)
# all versions, unlisted
block_factory(
addon=addon_factory(
version_kw={'channel': amo.CHANNEL_UNLISTED},
file_kw={'is_signed': True},
@ -37,45 +41,3 @@ class TestExportBlocklist(TestCase):
call_command('export_blocklist', '1')
out_path = os.path.join(settings.MLBF_STORAGE_PATH, '1', 'filter')
assert os.path.exists(out_path)
class TestCreateBlockversions(TestCase):
def test_command(self):
user = user_factory()
Block.objects.create(guid='missing@', min_version='123', updated_by=user)
addon = addon_factory(version_kw={'version': '0.1'})
v1 = version_factory(addon=addon, version='1')
v2 = version_factory(addon=addon, version='2.0.0')
v2.delete()
minmax_block = Block.objects.create(
addon=addon, min_version='0.1.1', max_version='2', updated_by=user
)
full_block = Block.objects.create(addon=addon_factory(), updated_by=user)
call_command('create_blockversions')
assert BlockVersion.objects.count() == 3
v1.refresh_from_db()
v2.refresh_from_db()
full_block.refresh_from_db()
assert v1.blockversion.block == minmax_block
assert v2.blockversion.block == minmax_block
assert full_block.addon.current_version.blockversion.block == full_block
# we can run it again without a problem
call_command('create_blockversions')
assert BlockVersion.objects.count() == 3
# and extra versions / blocks are created
new_version = version_factory(
addon=addon, channel=amo.CHANNEL_UNLISTED, version='0.2'
)
new_block = Block.objects.create(addon=addon_factory(), updated_by=user)
call_command('create_blockversions')
assert BlockVersion.objects.count() == 5
new_block.refresh_from_db()
new_version.refresh_from_db()
assert new_version.blockversion.block == minmax_block
assert new_block.addon.current_version.blockversion.block == new_block

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

@ -10,7 +10,13 @@ import pytest
from freezegun import freeze_time
from waffle.testutils import override_switch
from olympia.amo.tests import TestCase, addon_factory, user_factory, version_factory
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
)
from olympia.blocklist.cron import (
get_blocklist_last_modified_time,
process_blocklistsubmissions,
@ -33,7 +39,7 @@ class TestUploadToRemoteSettings(TestCase):
addon = addon_factory()
version_factory(addon=addon)
version_factory(addon=addon)
self.block = Block.objects.create(
self.block = block_factory(
addon=addon_factory(
version_kw={'version': '1.2b3'},
file_kw={'is_signed': True},
@ -316,7 +322,7 @@ class TestUploadToRemoteSettings(TestCase):
self.cleanup_files_mock.assert_not_called()
# But if we add a new Block a new filter is needed
Block.objects.create(
block_factory(
addon=addon_factory(file_kw={'is_signed': True}),
updated_by=user_factory(),
)

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

@ -2,7 +2,13 @@ from django.contrib import admin as admin_site
from django.core.exceptions import ValidationError
from django.test import RequestFactory
from olympia.amo.tests import TestCase, addon_factory, user_factory
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
)
from olympia.blocklist.admin import BlocklistSubmissionAdmin
from olympia.blocklist.forms import MultiAddForm, MultiDeleteForm
from olympia.blocklist.models import Block, BlocklistSubmission
@ -16,116 +22,146 @@ class TestBlocklistSubmissionForm(TestCase):
self.another_new_addon = addon_factory(
guid='another@new',
average_daily_users=100000,
version_kw={'version': '34.545'},
)
self.existing_one_to_ten = Block.objects.create(
addon=addon_factory(guid='partial@existing'),
min_version='1',
max_version='10',
self.full_existing_addon = addon_factory(guid='full@existing')
self.full_existing_addon_v1 = self.full_existing_addon.current_version
self.full_existing_addon_v2 = version_factory(addon=self.full_existing_addon)
self.existing_block_full = block_factory(
addon=self.full_existing_addon,
updated_by=user_factory(),
)
self.existing_zero_to_max = Block.objects.create(
addon=addon_factory(
guid='full@existing',
average_daily_users=99,
version_kw={'version': '10'},
self.partial_existing_addon = addon_factory(
guid='partial@existing',
average_daily_users=99,
)
self.partial_existing_addon_v_blocked = (
self.partial_existing_addon.current_version
)
self.existing_block_partial = block_factory(
addon=self.partial_existing_addon,
updated_by=user_factory(),
)
self.partial_existing_addon_v_notblocked = version_factory(
addon=self.partial_existing_addon
)
def test_changed_version_ids_choices_add_action(self):
block_admin = BlocklistSubmissionAdmin(
model=BlocklistSubmission, admin_site=admin_site
)
request = RequestFactory().get('/')
Form = block_admin.get_form(request=request)
data = {
'action': str(BlocklistSubmission.ACTION_ADDCHANGE),
'input_guids': f'{self.new_addon.guid}\n'
f'{self.existing_block_full.guid}\n'
f'{self.existing_block_partial.guid}\n'
'invalid@guid',
}
form = Form(data=data)
assert form.fields['changed_version_ids'].choices == [
(
self.new_addon.guid,
[
(
self.new_addon.current_version.id,
self.new_addon.current_version.version,
)
],
),
min_version='0',
max_version='*',
updated_by=user_factory(),
)
(
self.existing_block_partial.guid,
[
(
self.partial_existing_addon_v_notblocked.id,
self.partial_existing_addon_v_notblocked.version,
)
],
),
]
assert form.invalid_guids == ['invalid@guid']
def test_existing_blocks_no_existing(self):
data = {
'input_guids': 'any@new\nanother@new',
'min_version': '0',
'max_version': '*',
'existing_min_version': '1',
'existing_max_version': '10',
form = Form(
data={**data, 'changed_version_ids': [self.new_addon.current_version.id]}
)
assert form.is_valid()
assert not form.errors
form = Form(
data={
**data,
'changed_version_ids': [self.partial_existing_addon_v_blocked.id],
}
)
assert not form.is_valid()
assert form.errors == {
'changed_version_ids': [
f'Select a valid choice. {self.partial_existing_addon_v_blocked.id} is '
'not one of the available choices.'
]
}
def test_test_changed_version_ids_choices_delete_action(self):
block_admin = BlocklistSubmissionAdmin(
model=BlocklistSubmission, admin_site=admin_site
)
request = RequestFactory().get('/')
# All new guids should always be fine
form = block_admin.get_form(request=request)(data=data)
form.is_valid()
form.clean() # would raise if there needed to be a recalculation
def test_existing_blocks_some_existing(self):
Form = block_admin.get_form(request=request)
data = {
'input_guids': 'full@existing',
'min_version': '0',
'max_version': '*',
'existing_min_version': '1',
'existing_max_version': '10',
'action': str(BlocklistSubmission.ACTION_DELETE),
'input_guids': f'{self.new_addon.guid}\n'
f'{self.existing_block_full.guid}\n'
f'{self.existing_block_partial.guid}\n'
'invalid@guid',
}
block_admin = BlocklistSubmissionAdmin(
model=BlocklistSubmission, admin_site=admin_site
form = Form(data=data)
assert form.fields['changed_version_ids'].choices == [
(
self.existing_block_full.guid,
[
(
self.full_existing_addon_v1.id,
self.full_existing_addon_v1.version,
),
(
self.full_existing_addon_v2.id,
self.full_existing_addon_v2.version,
),
],
),
(self.new_addon.guid, []),
(
self.existing_block_partial.guid,
[
(
self.partial_existing_addon_v_blocked.id,
self.partial_existing_addon_v_blocked.version,
)
],
),
]
assert form.invalid_guids == ['invalid@guid']
form = Form(
data={**data, 'changed_version_ids': [self.full_existing_addon_v1.id]}
)
request = RequestFactory().get('/')
assert form.is_valid()
assert not form.errors
# A single guid is always updated so checks are bypassed
form = block_admin.get_form(request=request)(data=data)
form.is_valid()
form.clean() # would raise
# Two or more guids trigger the checks
data.update(input_guids='partial@existing\nfull@existing')
form = block_admin.get_form(request=request)(data=data)
form.is_valid()
with self.assertRaises(ValidationError):
form.clean()
# Not if the existing min/max versions match, i.e. they've not been
# changed
data.update(
existing_min_version=data['min_version'],
existing_max_version=data['max_version'],
form = Form(
data={**data, 'changed_version_ids': [self.new_addon.current_version.id]}
)
form = block_admin.get_form(request=request)(data=data)
form.is_valid()
form.clean() # would raise
# It should also be okay if the min/max *have* changed but the blocks
# affected are the same
data = {
'input_guids': 'partial@existing\nfull@existing',
'min_version': '56',
'max_version': '156',
'existing_min_version': '23',
'existing_max_version': '123',
assert not form.is_valid()
assert form.errors == {
'changed_version_ids': [
f'Select a valid choice. {self.new_addon.current_version.id} is not '
'one of the available choices.'
]
}
form = block_admin.get_form(request=request)(data=data)
form.is_valid()
form.clean() # would raise
def test_all_existing_blocks_but_delete_action(self):
data = {
'input_guids': 'any@thing\nsecond@thing',
'action': BlocklistSubmission.ACTION_DELETE,
}
block_admin = BlocklistSubmissionAdmin(
model=BlocklistSubmission, admin_site=admin_site
)
request = RequestFactory().get('/')
# checks are bypassed if action != BlocklistSubmission.ACTION_ADDCHANGE
form = block_admin.get_form(request=request)(data=data)
form.is_valid()
form.clean() # would raise
# Even if min_version or max_version are provided
data.update(
min_version='0',
max_version='*',
existing_min_version='1234',
existing_max_version='4567',
)
form = block_admin.get_form(request=request)(data=data)
form.is_valid()
form.clean() # would raise
class TestMultiDeleteForm(TestCase):
@ -145,15 +181,6 @@ class TestMultiDeleteForm(TestCase):
form.is_valid()
form.clean() # would raise
# except if one of the Blocks is already being changed/deleted
bls = BlocklistSubmission.objects.create(
input_guids=data['guids'], action=BlocklistSubmission.ACTION_DELETE
)
bls.save()
form.is_valid()
with self.assertRaises(ValidationError):
form.clean()
class TestMultiAddForm(TestCase):
def test_guid_must_exist_in_database(self):
@ -179,12 +206,3 @@ class TestMultiAddForm(TestCase):
addon_factory(guid='second@thing')
form.is_valid()
form.clean() # would raise
# except if one of the Blocks is already being changed/deleted
bls = BlocklistSubmission.objects.create(
input_guids=data['guids'], action=BlocklistSubmission.ACTION_ADDCHANGE
)
bls.save()
form.is_valid()
with self.assertRaises(ValidationError):
form.clean()

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

@ -7,7 +7,13 @@ from filtercascade import FilterCascade
from olympia import amo
from olympia.addons.models import GUID_REUSE_FORMAT
from olympia.amo.tests import TestCase, addon_factory, user_factory, version_factory
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
)
from olympia.files.models import File
from ..mlbf import (
@ -16,30 +22,24 @@ from ..mlbf import (
fetch_blocked_from_db,
generate_mlbf,
)
from ..models import Block
class TestMLBF(TestCase):
def setup_data(self):
user = user_factory()
for idx in range(0, 5):
for _idx in range(0, 5):
addon_factory()
# one version, 0 - *
Block.objects.create(
# one version, listed (twice)
block_factory(
addon=addon_factory(file_kw={'is_signed': True}),
updated_by=user,
)
# one version, 0 - 9999
Block.objects.create(
addon=addon_factory(
version_kw={'version': '11.7'},
file_kw={'is_signed': True},
),
block_factory(
addon=addon_factory(file_kw={'is_signed': True}),
updated_by=user,
max_version='9999',
)
# one version, 0 - *, unlisted
Block.objects.create(
# one version, unlisted
block_factory(
addon=addon_factory(
version_kw={'channel': amo.CHANNEL_UNLISTED},
file_kw={'is_signed': True},
@ -47,53 +47,60 @@ class TestMLBF(TestCase):
updated_by=user,
)
# five versions, but only two within block (123.40, 123.5)
self.five_ver_block = Block.objects.create(
addon=addon_factory(
version_kw={'version': '123.40'},
file_kw={'is_signed': True},
),
updated_by=user,
max_version='123.45',
five_ver_block_addon = addon_factory(
version_kw={'version': '123.40'},
file_kw={'is_signed': True},
)
self.five_ver_123_40 = self.five_ver_block.addon.current_version
self.five_ver_123_40 = five_ver_block_addon.current_version
self.five_ver_123_5 = version_factory(
addon=self.five_ver_block.addon,
addon=five_ver_block_addon,
version='123.5',
deleted=True,
file_kw={'is_signed': True},
)
self.five_ver_123_45_1 = version_factory(
addon=self.five_ver_block.addon,
addon=five_ver_block_addon,
version='123.45.1',
file_kw={'is_signed': True},
)
# these two would be included if they were signed
self.not_signed_version = version_factory(
addon=self.five_ver_block.addon,
addon=five_ver_block_addon,
version='123.5.1',
file_kw={'is_signed': False},
)
self.not_signed_version2 = version_factory(
addon=self.five_ver_block.addon,
addon=five_ver_block_addon,
version='123.5.2',
file_kw={'is_signed': False},
)
self.five_ver_block = block_factory(
addon=five_ver_block_addon,
updated_by=user,
version_ids=[
self.five_ver_123_40.id,
self.five_ver_123_5.id,
self.not_signed_version.id,
self.not_signed_version2.id,
],
)
# no matching versions (edge cases)
self.over = Block.objects.create(
self.no_versions = block_factory(
addon=addon_factory(
version_kw={'version': '0.1'},
file_kw={'is_signed': True},
),
updated_by=user,
max_version='0',
version_ids=[],
)
self.under = Block.objects.create(
self.no_versions2 = block_factory(
addon=addon_factory(
version_kw={'version': '9998.0'},
version_kw={'version': '0.1'},
file_kw={'is_signed': True},
),
updated_by=user,
min_version='9999',
version_ids=[],
)
# A blocked addon has been uploaded and deleted before
@ -122,7 +129,7 @@ class TestMLBF(TestCase):
# not signed, but shouldn't override the signed 2.1 version
file_kw={'is_signed': False},
)
version_factory(
self.addon_deleted_before_3_0_ver = version_factory(
addon=current_addon,
version='3.0',
file_kw={'is_signed': True},
@ -131,8 +138,15 @@ class TestMLBF(TestCase):
reused_2_5_addon.update(guid=GUID_REUSE_FORMAT.format(reused_2_1_addon.id))
reused_2_1_addon.addonguid.update(guid=current_addon.guid)
reused_2_5_addon.addonguid.update(guid=current_addon.guid)
self.addon_deleted_before_block = Block.objects.create(
guid=current_addon.guid, min_version='2.0.1', updated_by=user
self.addon_deleted_before_block = block_factory(
guid=current_addon.guid,
updated_by=user,
version_ids=[
self.addon_deleted_before_2_1_ver.id,
self.addon_deleted_before_2_5_ver.id,
self.addon_deleted_before_unsigned_ver.id,
self.addon_deleted_before_3_0_ver.id,
],
)
def test_fetch_all_versions_from_db(self):
@ -144,10 +158,16 @@ class TestMLBF(TestCase):
assert (self.five_ver_block.guid, '123.45.1') in all_versions
assert (self.five_ver_block.guid, '123.5.1') in all_versions
assert (self.five_ver_block.guid, '123.5.2') in all_versions
over_tuple = (self.over.guid, self.over.addon.current_version.version)
under_tuple = (self.under.guid, self.under.addon.current_version.version)
assert over_tuple in all_versions
assert under_tuple in all_versions
no_versions_tuple = (
self.no_versions.guid,
self.no_versions.addon.current_version.version,
)
assert no_versions_tuple in all_versions
no_versions2_tuple = (
self.no_versions2.guid,
self.no_versions2.addon.current_version.version,
)
assert no_versions2_tuple in all_versions
assert (self.addon_deleted_before_block.guid, '2') in all_versions
# this is fine; test_hash_filter_inputs removes duplicates.
@ -164,10 +184,16 @@ class TestMLBF(TestCase):
assert (self.five_ver_block.guid, '123.45.1') in all_versions
assert (self.five_ver_block.guid, '123.5.1') in all_versions
assert (self.five_ver_block.guid, '123.5.2') in all_versions
over_tuple = (self.over.guid, self.over.addon.current_version.version)
under_tuple = (self.under.guid, self.under.addon.current_version.version)
assert over_tuple in all_versions
assert under_tuple in all_versions
no_versions_tuple = (
self.no_versions.guid,
self.no_versions.addon.current_version.version,
)
assert no_versions_tuple in all_versions
no_versions2_tuple = (
self.no_versions2.guid,
self.no_versions2.addon.current_version.version,
)
assert no_versions2_tuple in all_versions
def test_fetch_blocked_from_db(self):
self.setup_data()
@ -181,10 +207,16 @@ class TestMLBF(TestCase):
assert (self.five_ver_block.guid, '123.45.1') not in blocked_guids
assert (self.five_ver_block.guid, '123.5.1') not in blocked_guids
assert (self.five_ver_block.guid, '123.5.2') not in blocked_guids
over_tuple = (self.over.guid, self.over.addon.current_version.version)
under_tuple = (self.under.guid, self.under.addon.current_version.version)
assert over_tuple not in blocked_guids
assert under_tuple not in blocked_guids
no_versions_tuple = (
self.no_versions.guid,
self.no_versions.addon.current_version.version,
)
assert no_versions_tuple not in blocked_guids
no_versions2_tuple = (
self.no_versions2.guid,
self.no_versions2.addon.current_version.version,
)
assert no_versions2_tuple not in blocked_guids
assert (self.addon_deleted_before_block.guid, '2.1') in blocked_guids
assert (self.addon_deleted_before_block.guid, '2.5') in blocked_guids
assert (self.addon_deleted_before_block.guid, '3.0') in blocked_guids
@ -196,8 +228,8 @@ class TestMLBF(TestCase):
assert self.five_ver_123_45_1.id not in blocked_versions
assert self.not_signed_version.id not in blocked_versions
assert self.not_signed_version2.id not in blocked_versions
assert self.over.addon.current_version.id not in blocked_versions
assert self.under.addon.current_version.id not in blocked_versions
assert self.no_versions.addon.current_version.id not in blocked_versions
assert self.no_versions2.addon.current_version.id not in blocked_versions
assert self.addon_deleted_before_unblocked_ver.id not in (blocked_versions)
assert self.addon_deleted_before_unsigned_ver.id not in (blocked_versions)
@ -349,7 +381,7 @@ class TestMLBF(TestCase):
assert json.load(stash_file) == empty_stash
assert new_mlbf.stash_json == empty_stash
# add a new Block and delete one
Block.objects.create(
block_factory(
addon=addon_factory(
guid='fooo@baaaa',
version_kw={'version': '999'},
@ -385,7 +417,7 @@ class TestMLBF(TestCase):
assert not no_change_mlbf.blocks_changed_since_previous(base_mlbf)
# make some changes
Block.objects.create(
block_factory(
addon=addon_factory(
guid='fooo@baaaa',
version_kw={'version': '999'},

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

@ -1,61 +1,27 @@
from datetime import datetime, timedelta
from django.core.exceptions import ValidationError
from django.test.utils import override_settings
from olympia.amo.tests import TestCase, addon_factory, user_factory
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
)
from ..models import Block, BlocklistSubmission
from ..models import BlocklistSubmission
class TestBlock(TestCase):
def test_is_version_blocked(self):
block = Block.objects.create(guid='anyguid@', updated_by=user_factory())
# default is 0 to *
assert block.is_version_blocked('0')
# 999999999 is the maximum version part permitted by the linter
over_linter_max = str(999999999 + 1234)
assert block.is_version_blocked(over_linter_max)
# Now with some restricted version range
block.update(min_version='2.0')
assert not block.is_version_blocked('1')
assert not block.is_version_blocked('2.0b1')
assert block.is_version_blocked('2')
assert block.is_version_blocked('3')
assert block.is_version_blocked(over_linter_max)
block.update(max_version='10.*')
assert not block.is_version_blocked('11')
assert not block.is_version_blocked(over_linter_max)
assert block.is_version_blocked('10')
assert block.is_version_blocked('9')
assert block.is_version_blocked('10.1')
assert block.is_version_blocked('10.%s' % (over_linter_max))
def test_is_readonly(self):
block = Block.objects.create(guid='foo@baa', updated_by=user_factory())
block = block_factory(guid='foo@baa', updated_by=user_factory())
# not read only by default
assert not block.is_readonly
# but should be if there's an active BlocklistSubmission
block.active_submissions = [object()] # just needs to be non-empty
assert block.is_readonly
def test_no_asterisk_in_min_version(self):
non_user_writeable_fields = (
'average_daily_users_snapshot',
'guid',
)
block = Block(min_version='123.4', max_version='*', updated_by=user_factory())
block.full_clean(exclude=non_user_writeable_fields)
block.min_version = '*'
with self.assertRaises(ValidationError):
block.full_clean(exclude=non_user_writeable_fields)
block.min_version = '0'
block.full_clean(exclude=non_user_writeable_fields)
block.min_version = '123.*'
with self.assertRaises(ValidationError):
block.full_clean(exclude=non_user_writeable_fields)
class TestBlocklistSubmissionManager(TestCase):
def test_delayed(self):
@ -149,20 +115,11 @@ class TestBlocklistSubmission(TestCase):
assert list(BlocklistSubmission.get_submissions_from_guid('ggguid@')) == []
def test_all_adu_safe(self):
Block.objects.create(
addon=addon_factory(guid='zero@adu', average_daily_users=0),
updated_by=user_factory(),
)
Block.objects.create(
addon=addon_factory(guid='normal@adu', average_daily_users=500),
updated_by=user_factory(),
)
Block.objects.create(
addon=addon_factory(guid='high@adu', average_daily_users=999_999),
updated_by=user_factory(),
)
addon_factory(guid='zero@adu', average_daily_users=0)
addon_factory(guid='normal@adu', average_daily_users=500)
addon_factory(guid='high@adu', average_daily_users=999_999)
submission = BlocklistSubmission.objects.create(
input_guids='zero@adu\nnormal@adu', min_version=99
input_guids='zero@adu\nnormal@adu'
)
submission.to_block = submission._serialize_blocks()
@ -180,38 +137,20 @@ class TestBlocklistSubmission(TestCase):
assert not submission.all_adu_safe()
def test_has_version_changes(self):
block = Block.objects.create(
addon=addon_factory(guid='guid@'), updated_by=user_factory()
addon = addon_factory(guid='guid@')
block_factory(addon=addon, updated_by=user_factory(), reason='things')
new_version = version_factory(addon=addon)
submission = BlocklistSubmission.objects.create(
input_guids=addon.guid, changed_version_ids=[]
)
submission = BlocklistSubmission.objects.create(input_guids='guid@')
submission.to_block = submission._serialize_blocks()
# no changes to anything
# reason is chaning, but no versions are being changed
assert not submission.has_version_changes()
block.update(min_version='999', reason='things')
# min_version has changed (and reason)
submission.update(changed_version_ids=[new_version.id])
assert submission.has_version_changes()
submission.update(min_version='999')
# if min_version is the same then it's only the metadata (reason)
assert not submission.has_version_changes()
def test_no_asterisk_in_min_version(self):
non_user_writeable_fields = ('updated_by', 'signoff_by', 'to_block')
submission = BlocklistSubmission(
min_version='123.4', max_version='*', input_guids='df@'
)
submission.full_clean(exclude=non_user_writeable_fields)
submission.min_version = '*'
with self.assertRaises(ValidationError):
submission.full_clean(exclude=non_user_writeable_fields)
submission.min_version = '0'
submission.full_clean(exclude=non_user_writeable_fields)
submission.min_version = '123.*'
with self.assertRaises(ValidationError):
submission.full_clean(exclude=non_user_writeable_fields)
def test_is_delayed(self):
now = datetime.now()
submission = BlocklistSubmission.objects.create(

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

@ -10,7 +10,6 @@ class TestBlockSerializer(TestCase):
def setUp(self):
self.block = Block.objects.create(
guid='foo@baa',
min_version='45',
reason='something happened',
url='https://goo.gol',
updated_by=user_factory(),
@ -22,8 +21,8 @@ class TestBlockSerializer(TestCase):
'id': self.block.id,
'addon_name': None,
'guid': 'foo@baa',
'min_version': '45',
'max_version': '*',
'min_version': '',
'max_version': '',
'reason': 'something happened',
'url': {
'url': 'https://goo.gol',
@ -36,13 +35,36 @@ class TestBlockSerializer(TestCase):
}
def test_with_addon(self):
addon_factory(guid=self.block.guid, name='Addón náme')
BlockVersion.objects.create(
block=self.block, version=self.block.addon.current_version
addon = addon_factory(
guid=self.block.guid, name='Addón náme', version_kw={'version': '1.0'}
)
_version_1 = addon.current_version
_version_5 = version_factory(addon=addon, version='5555')
version_2 = version_factory(
addon=addon, channel=amo.CHANNEL_UNLISTED, version='2.0.2'
)
version_4 = version_factory(addon=addon, version='4')
_version_3 = version_factory(addon=addon, version='3b1')
BlockVersion.objects.create(block=self.block, version=version_2)
BlockVersion.objects.create(block=self.block, version=version_4)
serializer = BlockSerializer(instance=self.block)
assert serializer.data['addon_name'] == {'en-US': 'Addón náme'}
assert serializer.data['versions'] == [self.block.addon.current_version.version]
assert serializer.data == {
'id': self.block.id,
'addon_name': {'en-US': 'Addón náme'},
'guid': 'foo@baa',
'min_version': version_2.version,
'max_version': version_4.version,
'reason': 'something happened',
'url': {
'url': 'https://goo.gol',
'outgoing': get_outgoing_url('https://goo.gol'),
},
'versions': [version_2.version, version_4.version],
'is_all_versions': False,
'created': self.block.created.isoformat()[:-7] + 'Z',
'modified': self.block.modified.isoformat()[:-7] + 'Z',
}
def test_is_all_versions(self):
# no add-on so True

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

@ -1,16 +1,23 @@
from django.test.utils import override_settings
from olympia.amo.tests import TestCase, addon_factory, reverse_ns, user_factory
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
reverse_ns,
user_factory,
)
from olympia.amo.urlresolvers import get_outgoing_url
from olympia.blocklist.models import Block
from olympia.blocklist.serializers import BlockSerializer
class TestBlockViewSet(TestCase):
def setUp(self):
self.block = Block.objects.create(
guid='foo@baa.com',
min_version='45',
self.addon = addon_factory(
guid='foo@baa.com', name='English name', default_locale='en-CA'
)
self.block = block_factory(
addon=self.addon,
reason='something happened',
url='https://goo.gol',
updated_by=user_factory(),
@ -43,11 +50,8 @@ class TestBlockViewSet(TestCase):
}
def test_addon_name(self):
addon = addon_factory(
guid=self.block.guid, name='English name', default_locale='en-CA'
)
addon.name = {'fr': 'Lé name Francois'}
addon.save()
self.addon.name = {'fr': 'Lé name Francois'}
self.addon.save()
response = self.client.get(self.url)
assert response.status_code == 200
assert response.json()['addon_name'] == {

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

@ -9,28 +9,32 @@ from olympia.users.utils import get_task_user
log = olympia.core.logger.getLogger('z.amo.blocklist')
def add_version_log_for_blocked_versions(obj, old_obj, al):
def add_version_log_for_blocked_versions(obj, al, submission_obj=None):
from olympia.activity.models import VersionLog
VersionLog.objects.bulk_create(
[
VersionLog(activity_log=al, version_id=version.id)
VersionLog(activity_log=al, version=version)
for version in obj.addon_versions
if obj.is_version_blocked(version.version)
or old_obj.is_version_blocked(version.version)
if version.is_blocked
or (submission_obj and version.id in submission_obj.changed_version_ids)
]
)
def block_activity_log_save(obj, change, submission_obj=None, old_obj=None):
def block_activity_log_save(
obj,
change,
submission_obj=None,
):
action = amo.LOG.BLOCKLIST_BLOCK_EDITED if change else amo.LOG.BLOCKLIST_BLOCK_ADDED
version_ids = sorted(ver.id for ver in obj.addon_versions if ver.is_blocked)
details = {
'guid': obj.guid,
'min_version': obj.min_version,
'max_version': obj.max_version,
'versions': version_ids,
'url': obj.url,
'reason': obj.reason,
'comments': f'Versions {obj.min_version} - {obj.max_version} blocked.',
'comments': f'{len(version_ids)} versions blocked.',
}
if submission_obj:
details['signoff_state'] = submission_obj.SIGNOFF_STATES.get(
@ -50,19 +54,25 @@ def block_activity_log_save(obj, change, submission_obj=None, old_obj=None):
user=submission_obj.signoff_by,
)
add_version_log_for_blocked_versions(obj, old_obj or obj, al)
add_version_log_for_blocked_versions(obj, al)
def block_activity_log_delete(obj, *, submission_obj=None, delete_user=None):
def block_activity_log_delete(obj, deleted, *, submission_obj=None, delete_user=None):
assert submission_obj or delete_user
version_ids = [ver.id for ver in obj.addon_versions if ver.is_blocked]
details = {
'guid': obj.guid,
'min_version': obj.min_version,
'max_version': obj.max_version,
'versions': version_ids,
'url': obj.url,
'reason': obj.reason,
'comments': f'Versions {obj.min_version} - {obj.max_version} unblocked.',
'comments': f'{len(version_ids)} versions unblocked.',
}
action = (
amo.LOG.BLOCKLIST_BLOCK_EDITED
if not deleted
else amo.LOG.BLOCKLIST_BLOCK_DELETED
)
if submission_obj:
details['signoff_state'] = submission_obj.SIGNOFF_STATES.get(
submission_obj.signoff_state
@ -70,22 +80,21 @@ def block_activity_log_delete(obj, *, submission_obj=None, delete_user=None):
if submission_obj.signoff_by:
details['signoff_by'] = submission_obj.signoff_by.id
addon = obj.addon
args = (
[amo.LOG.BLOCKLIST_BLOCK_DELETED] + ([addon] if addon else []) + [obj.guid, obj]
)
al = log_create(
*args,
*[action, *([addon] if addon else []), obj.guid, obj],
details=details,
user=submission_obj.updated_by if submission_obj else delete_user,
)
if addon:
add_version_log_for_blocked_versions(obj, obj, al)
add_version_log_for_blocked_versions(obj, al, submission_obj)
if submission_obj and submission_obj.signoff_by:
args = (
[amo.LOG.BLOCKLIST_SIGNOFF]
+ ([addon] if addon else [])
+ [obj.guid, amo.LOG.BLOCKLIST_BLOCK_DELETED.action_class, obj]
)
args = [
amo.LOG.BLOCKLIST_SIGNOFF,
*([addon] if addon else []),
obj.guid,
action.action_class,
obj,
]
log_create(*args, user=submission_obj.signoff_by)
@ -99,14 +108,10 @@ def datetime_to_ts(dt=None):
return int((dt or datetime.now()).timestamp() * 1000)
def disable_addon_for_block(block):
"""Disable appropriate addon versions that are affected by the Block, and
the addon too if 0 - *."""
from olympia.addons.models import GuidAlreadyDeniedError
def disable_versions_for_block(block, submission):
"""Disable appropriate addon versions that are affected by the Block."""
from olympia.reviewers.utils import ReviewBase
from .models import Block
review = ReviewBase(
addon=block.addon,
version=None,
@ -120,7 +125,7 @@ def disable_addon_for_block(block):
# We don't need to reject versions from older deleted instances
# and already disabled files
if ver.addon == block.addon
and block.is_version_blocked(ver.version)
and ver.id in submission.changed_version_ids
and ver.file.status != amo.STATUS_DISABLED
]
review.set_data({'versions': versions_to_reject})
@ -132,51 +137,34 @@ def disable_addon_for_block(block):
# versions we are rejecting, which is only a subset).
review.clear_specific_needs_human_review_flags(version)
if block.min_version == Block.MIN and block.max_version == Block.MAX:
if block.addon.status == amo.STATUS_DELETED:
try:
block.addon.deny_resubmission()
except GuidAlreadyDeniedError:
pass
else:
block.addon.update(status=amo.STATUS_DISABLED)
def save_versions_to_blocks(guids, submission, *, fields_to_set):
from olympia.addons.models import GuidAlreadyDeniedError
def save_guids_to_blocks(guids, submission, *, fields_to_set):
from .models import Block, BlockVersion
common_args = {field: getattr(submission, field) for field in fields_to_set}
modified_datetime = datetime.now()
blocks = Block.get_blocks_from_guids(guids)
Block.preload_addon_versions(blocks)
for block in blocks:
change = bool(block.id)
if change:
block_obj_before_change = Block(
min_version=block.min_version, max_version=block.max_version
)
setattr(block, 'modified', modified_datetime)
else:
block_obj_before_change = None
for field, val in common_args.items():
setattr(block, field, val)
block.average_daily_users_snapshot = block.current_adu
block.save()
if change:
# if not a new Block then delete any BlockVersions that are outside min-max
BlockVersion.objects.filter(block=block).exclude(
version__version__gte=block.min_version,
version__version__lte=block.max_version,
).delete()
# create BlockVersions for versions in min-max range, that don't already exist
BlockVersion.objects.bulk_create(
BlockVersion(version=version, block=block)
for version in block.addon_versions
if not hasattr(version, 'blockversion')
and block.is_version_blocked(version.version)
)
# And now update the BlockVersion instances - instances to add first
block_versions_to_create = []
for version in block.addon_versions:
if version.id in submission.changed_version_ids and (
not change or not version.is_blocked
):
block_version = BlockVersion(block=block, version=version)
block_versions_to_create.append(block_version)
version.blockversion = block_version
BlockVersion.objects.bulk_create(block_versions_to_create)
if submission.id:
block.submission.add(submission)
@ -184,8 +172,56 @@ def save_guids_to_blocks(guids, submission, *, fields_to_set):
block,
change=change,
submission_obj=submission if submission.id else None,
old_obj=block_obj_before_change,
)
disable_addon_for_block(block)
disable_versions_for_block(block, submission)
if submission.disable_addon:
if block.addon.status == amo.STATUS_DELETED:
try:
block.addon.deny_resubmission()
except GuidAlreadyDeniedError:
pass
else:
block.addon.update(status=amo.STATUS_DISABLED)
return blocks
def delete_versions_from_blocks(guids, submission, *, fields_to_set):
from .models import Block, BlockVersion
common_args = {field: getattr(submission, field) for field in fields_to_set}
modified_datetime = datetime.now()
blocks = Block.get_blocks_from_guids(guids)
for block in blocks:
if not block.id:
continue
setattr(block, 'modified', modified_datetime)
BlockVersion.objects.filter(
block=block, version_id__in=submission.changed_version_ids
).delete()
if BlockVersion.objects.filter(block=block).exists():
# if there are still other versions blocked update the metadata
for field, val in common_args.items():
setattr(block, field, val)
block.average_daily_users_snapshot = block.current_adu
block.save()
should_delete = False
if submission.id:
block.submission.add(submission)
else:
# otherwise we can delete the Block instance
should_delete = True
block_activity_log_delete(
block,
deleted=should_delete,
submission_obj=submission if submission.id else None,
)
if should_delete:
block.delete()
return blocks

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

@ -12,7 +12,7 @@ from django.forms.models import BaseModelFormSet, modelformset_factory
from django.forms.widgets import RadioSelect
from django.urls import reverse
from django.utils.functional import keep_lazy_text
from django.utils.html import escape, format_html
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext, gettext_lazy as _, ngettext
@ -45,7 +45,6 @@ from olympia.amo.messages import DoubleSafe
from olympia.amo.utils import remove_icons, slug_validator
from olympia.amo.validators import OneOrMoreLetterOrNumberCharacterValidator
from olympia.applications.models import AppVersion
from olympia.blocklist.models import Block
from olympia.constants.categories import CATEGORIES, CATEGORIES_BY_ID, CATEGORIES_NO_APP
from olympia.devhub.widgets import CategoriesSelectMultiple, IconTypeSelect
from olympia.files.models import FileUpload
@ -1051,31 +1050,6 @@ class NewUploadForm(CheckThrottlesMixin, forms.Form):
gettext('There was an error with your upload. Please try again.')
)
def check_blocklist(self, guid, version_string):
# check the guid/version isn't in the addon blocklist
block = Block.objects.filter(guid=guid).first()
if block and block.is_version_blocked(version_string):
msg = escape(
gettext(
'Version {version} matches {block_link} for this add-on. '
'You can contact {amo_admins} for additional information.'
)
)
formatted_msg = DoubleSafe(
msg.format(
version=version_string,
block_link=format_html(
'<a href="{}">{}</a>',
reverse('blocklist.block', args=[guid]),
gettext('a blocklist entry'),
),
amo_admins=(
'<a href="mailto:amo-admins@mozilla.com">AMO Admins</a>'
),
)
)
raise forms.ValidationError(formatted_msg)
def check_for_existing_versions(self, version_string):
# Make sure we don't already have this version.
existing_versions = Version.unfiltered.filter(
@ -1111,10 +1085,6 @@ class NewUploadForm(CheckThrottlesMixin, forms.Form):
self.cleaned_data['upload'], self.addon, user=self.request.user
)
self.check_blocklist(
self.addon.guid if self.addon else parsed_data.get('guid'),
parsed_data.get('version'),
)
if self.addon:
self.check_for_existing_versions(parsed_data.get('version'))
if self.cleaned_data['upload'].channel == amo.CHANNEL_LISTED:

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

@ -28,10 +28,8 @@ from olympia.amo.tests import (
create_default_webext_appversion,
formset,
initial,
user_factory,
version_factory,
)
from olympia.blocklist.models import Block
from olympia.constants.licenses import LICENSES_BY_BUILTIN
from olympia.constants.promoted import NOTABLE
from olympia.devhub import views
@ -539,24 +537,6 @@ class TestAddonSubmitUpload(UploadMixin, TestCase):
assert 'new_addon_form' not in response.context
assert get_addon_count('Beastify') == 2
def test_new_addon_is_already_blocked(self):
self.upload = self.get_upload('webextension.xpi', user=self.user)
guid = '@webextension-guid'
block = Block.objects.create(guid=guid, updated_by=user_factory())
response = self.post(expect_errors=True)
assert pq(response.content)('ul.errorlist').text() == (
'Version 0.0.1 matches a blocklist entry for this add-on. '
'You can contact AMO Admins for additional information.'
)
assert pq(response.content)('ul.errorlist a').attr('href') == (
reverse('blocklist.block', args=[guid])
)
# Though we allow if the version is outside of the specified range
block.update(min_version='0.0.2')
response = self.post(expect_errors=False)
def test_success_listed(self):
assert Addon.objects.count() == 0
response = self.post()
@ -2024,21 +2004,6 @@ class VersionSubmitUploadMixin:
)
)
def test_addon_version_is_blocked(self):
block = Block.objects.create(guid=self.addon.guid, updated_by=user_factory())
response = self.post(expected_status=200)
assert pq(response.content)('ul.errorlist').text() == (
'Version 0.0.1 matches a blocklist entry for this add-on. '
'You can contact AMO Admins for additional information.'
)
assert pq(response.content)('ul.errorlist a').attr('href') == (
reverse('blocklist.block', args=[self.addon.guid])
)
# Though we allow if the version is outside of the specified range
block.update(min_version='0.2')
response = self.post(expected_status=302), response.content
def test_distribution_link(self):
response = self.client.get(self.url)
channel_text = 'listed' if self.channel == amo.CHANNEL_LISTED else 'unlisted'

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

@ -395,10 +395,11 @@
{% endif %}
<li>
{% if addon.blocklistsubmission %}
<a href="{{ url('admin:blocklist_blocklistsubmission_change', addon.blocklistsubmission.id) }}" class="button"
id="edit_addon_blocklistsubmission" type="button">View Blocklist Submission</a>
{% elif not addon.block %}
{% if addon.blocklistsubmissions %}
<a href="{{ url('admin:blocklist_blocklistsubmission_change', addon.blocklistsubmissions[0].id) }}" class="button"
id="edit_addon_blocklistsubmission" type="button">View Active Blocklist Submission</a>
{% endif %}
{% if not addon.block %}
<a href="{{ url('admin:blocklist_block_addaddon', addon.pk) }}" class="button"
id="block_addon" type="button">Block add-on</a>
{% else %}

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

@ -6,8 +6,13 @@ from pyquery import PyQuery as pq
from olympia import amo
from olympia.addons.models import Addon
from olympia.amo.tests import TestCase, addon_factory, user_factory, version_factory
from olympia.blocklist.models import Block
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
)
from olympia.constants.reviewers import REVIEWER_DELAYED_REJECTION_PERIOD_DAYS_DEFAULT
from olympia.reviewers.forms import ReviewForm
from olympia.reviewers.models import (
@ -368,10 +373,9 @@ class TestReviewForm(TestCase):
channel=amo.CHANNEL_LISTED,
file_kw={'status': amo.STATUS_DISABLED},
)
Block.objects.create(
addon=self.addon,
min_version=blocked_version.version,
max_version=blocked_version.version,
block_factory(
addon=blocked_version.addon,
version_ids=[blocked_version.id],
updated_by=user_factory(),
)
# auto-approve everything (including self.addon.current_version)
@ -480,10 +484,9 @@ class TestReviewForm(TestCase):
channel=amo.CHANNEL_UNLISTED,
file_kw={'status': amo.STATUS_DISABLED},
)
Block.objects.create(
addon=self.addon,
min_version=blocked_version.version,
max_version=blocked_version.version,
block_factory(
addon=blocked_version.addon,
version_ids=[blocked_version.id],
updated_by=user_factory(),
)
self.version.update(channel=amo.CHANNEL_UNLISTED)

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

@ -13,10 +13,11 @@ from olympia.addons.models import AddonApprovalsCounter, AddonReviewerFlags, Add
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
)
from olympia.blocklist.models import Block
from olympia.blocklist.models import BlockVersion
from olympia.constants.promoted import (
LINE,
NOTABLE,
@ -1285,18 +1286,18 @@ class TestAutoApprovalSummary(TestCase):
def test_check_is_blocked(self):
assert AutoApprovalSummary.check_is_blocked(self.version) is False
block = Block.objects.create(addon=self.addon, updated_by=user_factory())
del self.version.addon.block
block_factory(
addon=self.addon, updated_by=user_factory(), version_ids=[self.version.id]
)
self.version.refresh_from_db()
assert AutoApprovalSummary.check_is_blocked(self.version) is True
block.update(min_version='9999999')
del self.version.addon.block
BlockVersion.objects.get().update(
version=version_factory(addon=self.version.addon)
)
self.version.refresh_from_db()
assert AutoApprovalSummary.check_is_blocked(self.version) is False
block.update(min_version='0')
del self.version.addon.block
assert AutoApprovalSummary.check_is_blocked(self.version) is True
def test_check_is_locked(self):
assert AutoApprovalSummary.check_is_locked(self.version) is False

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

@ -18,6 +18,7 @@ from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
version_review_flags_factory,
@ -676,8 +677,9 @@ class TestReviewHelper(TestReviewHelperBase):
)
# But when the add-on is blocked 'public' shouldn't be available
block = Block.objects.create(addon=self.addon, updated_by=self.user)
del self.addon.block
block_factory(addon=self.addon, updated_by=self.user)
self.review_version.refresh_from_db()
assert self.review_version.is_blocked
expected = [
'reject',
'reject_multiple_versions',
@ -695,8 +697,10 @@ class TestReviewHelper(TestReviewHelperBase):
== expected
)
# it's okay if the version is outside the blocked range though
block.update(min_version=self.review_version.version + '.1')
# it's okay if a different version of the add-on is blocked though
self.review_version = version_factory(addon=self.review_version.addon)
self.file = self.review_version.file
assert not self.review_version.is_blocked
expected = [
'public',
'reject',
@ -705,7 +709,15 @@ class TestReviewHelper(TestReviewHelperBase):
'reply',
'comment',
]
del self.addon.block
assert (
list(
self.get_review_actions(
addon_status=amo.STATUS_APPROVED,
file_status=amo.STATUS_AWAITING_REVIEW,
).keys()
)
== expected
)
def test_actions_pending_rejection(self):
# An addon having its latest version pending rejection won't be
@ -2837,7 +2849,7 @@ class TestReviewHelper(TestReviewHelperBase):
self._test_block_multiple_unlisted_versions(redirect_url)
def test_existing_block_multiple_unlisted_versions(self):
Block.objects.create(guid=self.addon.guid, updated_by=user_factory())
block_factory(guid=self.addon.guid, updated_by=user_factory())
redirect_url = (
reverse('admin:blocklist_block_addaddon', args=(self.addon.id,))
+ '?v=%s&v=%s'
@ -2845,9 +2857,9 @@ class TestReviewHelper(TestReviewHelperBase):
self._test_block_multiple_unlisted_versions(redirect_url)
def test_approve_latest_version_fails_for_blocked_version(self):
Block.objects.create(addon=self.addon, updated_by=user_factory())
block_factory(addon=self.addon, updated_by=user_factory())
self.review_version.refresh_from_db()
self.setup_data(amo.STATUS_NOMINATED)
del self.addon.block
with self.assertRaises(AssertionError):
self.helper.handler.approve_latest_version()

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

@ -46,6 +46,7 @@ from olympia.amo.tests import (
APITestClientSessionID,
TestCase,
addon_factory,
block_factory,
check_links,
formset,
initial,
@ -54,7 +55,7 @@ from olympia.amo.tests import (
version_factory,
version_review_flags_factory,
)
from olympia.blocklist.models import Block, BlocklistSubmission
from olympia.blocklist.models import Block, BlocklistSubmission, BlockVersion
from olympia.blocklist.utils import block_activity_log_save
from olympia.constants.promoted import LINE, NOTABLE, RECOMMENDED, SPOTLIGHT, STRATEGIC
from olympia.constants.reviewers import REVIEWER_DELAYED_REJECTION_PERIOD_DAYS_DEFAULT
@ -3241,13 +3242,13 @@ class TestReview(ReviewBase):
reverse('admin:blocklist_block_addaddon', args=(self.addon.id,))
)
# If the guid is in a pending submission we show a link to that instead
# If the guid is in a pending submission we show a link to that too
subm = BlocklistSubmission.objects.create(input_guids=self.addon.guid)
response = self.client.get(self.url)
assert response.status_code == 200
doc = pq(response.content)
assert not doc('#block_addon')
assert not doc('#edit_addon_block')
assert doc('#edit_addon_block')
blocklistsubmission_block = doc('#edit_addon_blocklistsubmission')
assert blocklistsubmission_block
assert blocklistsubmission_block[0].attrib.get('href') == (
@ -4460,10 +4461,8 @@ class TestReview(ReviewBase):
new_block_url = reverse(
'admin:blocklist_blocklistsubmission_add'
) + '?guids={}&min_version={}&max_version={}'.format(
self.addon.guid,
old_version.version,
self.version.version,
) + '?guids={}&v={}&v={}'.format(
self.addon.guid, old_version.pk, self.version.pk
)
self.assertRedirects(response, new_block_url)
@ -5332,21 +5331,24 @@ class TestReview(ReviewBase):
assert response.status_code == 200
assert b'Blocked' not in response.content
block = Block.objects.create(guid=self.addon.guid, updated_by=user_factory())
block = block_factory(guid=self.addon.guid, updated_by=user_factory())
response = self.client.get(self.url)
assert b'Blocked' in response.content
span = pq(response.content)('#versions-history .blocked-version')
assert span.text() == 'Blocked'
assert span.length == 1 # addon only has 1 version
version_factory(addon=self.addon, version='99')
blockversion = BlockVersion.objects.create(
block=block, version=version_factory(addon=self.addon, version='99')
)
response = self.client.get(self.url)
span = pq(response.content)('#versions-history .blocked-version')
assert span.text() == 'Blocked Blocked'
assert span.length == 2 # a new version is blocked too
block_reason = 'Very bad addon!'
block.update(max_version='98', reason=block_reason)
blockversion.delete()
block.update(reason=block_reason)
block_activity_log_save(obj=block, change=False)
response = self.client.get(self.url)
span = pq(response.content)('#versions-history .blocked-version')

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

@ -495,6 +495,7 @@ def review(request, addon, channel=None):
.select_related('autoapprovalsummary')
.select_related('reviewerflags')
.select_related('file___webext_permissions')
.select_related('blockversion')
# Prefetch needshumanreview existence into a property that the
# VersionsChoiceWidget will use.
.annotate(needs_human_review=Exists(needs_human_review_qs))

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

@ -10,10 +10,10 @@ from olympia import amo
from olympia.amo.tests import (
TestCase,
addon_factory,
block_factory,
user_factory,
version_factory,
)
from olympia.blocklist.models import Block
from olympia.constants.scanners import (
ABORTED,
ABORTING,
@ -749,7 +749,7 @@ class TestRunYaraQueryRule(TestCase):
def test_run_on_chunk_was_blocked(self):
self.rule.update(state=RUNNING) # Pretend we started running the rule.
Block.objects.create(guid=self.version.addon.guid, updated_by=user_factory())
block_factory(addon=self.version.addon, updated_by=user_factory())
run_yara_query_rule_on_versions_chunk([self.version.pk], self.rule.pk)
yara_results = ScannerQueryResult.objects.all()
@ -761,13 +761,16 @@ class TestRunYaraQueryRule(TestCase):
def test_run_on_chunk_not_blocked(self):
self.rule.update(state=RUNNING) # Pretend we started running the rule.
self.version.update(version='2.0')
Block.objects.create(
guid=self.version.addon.guid,
updated_by=user_factory(),
max_version='1.0',
another_version = version_factory(
addon=self.version.addon, channel=amo.CHANNEL_UNLISTED
)
Block.objects.create(
guid='@differentguid',
block_factory(
addon=self.version.addon,
updated_by=user_factory(),
version_ids=[another_version.id],
)
block_factory(
addon=addon_factory(guid='@differentguid'),
updated_by=user_factory(),
)
run_yara_query_rule_on_versions_chunk([self.version.pk], self.rule.pk)

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

@ -9,12 +9,12 @@ from olympia.amo.tests import (
APITestClientSessionID,
TestCase,
addon_factory,
block_factory,
reverse_ns,
user_factory,
version_factory,
)
from olympia.api.tests.utils import APIKeyAuthTestMixin
from olympia.blocklist.models import Block
from olympia.blocklist.utils import block_activity_log_save
from olympia.constants.scanners import (
CUSTOMS,
@ -231,21 +231,21 @@ class TestScannerResultViewInternal(TestCase):
blocked_addon_1 = addon_factory()
blocked_version_1 = version_factory(addon=blocked_addon_1)
ScannerResult.objects.create(scanner=YARA, version=blocked_version_1)
block_1 = Block.objects.create(guid=blocked_addon_1.guid, updated_by=self.user)
block_1 = block_factory(guid=blocked_addon_1.guid, updated_by=self.user)
block_activity_log_save(block_1, change=False)
# result labelled as "bad" because the add-on is blocked and the block
# has been edited.
blocked_addon_2 = addon_factory()
blocked_version_2 = version_factory(addon=blocked_addon_2)
ScannerResult.objects.create(scanner=YARA, version=blocked_version_2)
block_2 = Block.objects.create(guid=blocked_addon_2.guid, updated_by=self.user)
block_2 = block_factory(guid=blocked_addon_2.guid, updated_by=self.user)
block_activity_log_save(block_2, change=True)
# result labelled as "bad" because the add-on is blocked and the block
# has been added *and* edited. It should only return one result.
blocked_addon_3 = addon_factory()
blocked_version_3 = version_factory(addon=blocked_addon_3)
ScannerResult.objects.create(scanner=YARA, version=blocked_version_3)
block_3 = Block.objects.create(guid=blocked_addon_3.guid, updated_by=self.user)
block_3 = block_factory(guid=blocked_addon_3.guid, updated_by=self.user)
block_activity_log_save(block_3, change=False)
block_activity_log_save(block_3, change=True)
# result labelled as "bad" because its state is TRUE_POSITIVE and the
@ -255,7 +255,7 @@ class TestScannerResultViewInternal(TestCase):
ScannerResult.objects.create(
scanner=YARA, version=blocked_version_4, state=TRUE_POSITIVE
)
block_4 = Block.objects.create(guid=blocked_addon_4.guid, updated_by=self.user)
block_4 = block_factory(guid=blocked_addon_4.guid, updated_by=self.user)
block_activity_log_save(block_4, change=False)
response = self.client.get(self.url)
@ -267,7 +267,7 @@ class TestScannerResultViewInternal(TestCase):
result_1 = ScannerResult.objects.create(scanner=CUSTOMS, version=version_1)
ActivityLog.create(amo.LOG.APPROVE_VERSION, version_1, user=self.user)
# Oh noes! The version has been blocked.
block_1 = Block.objects.create(guid=version_1.addon.guid, updated_by=self.user)
block_1 = block_factory(guid=version_1.addon.guid, updated_by=self.user)
block_activity_log_save(block_1, change=False)
response = self.client.get(self.url)

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

@ -7,7 +7,6 @@ from django.conf import settings
from django.forms import ValidationError
from django.test.testcases import TransactionTestCase
from django.test.utils import override_settings
from django.urls import reverse
from django.utils import translation
import responses
@ -26,10 +25,8 @@ from olympia.amo.tests import (
developer_factory,
get_random_ip,
reverse_ns,
user_factory,
)
from olympia.api.tests.utils import APIKeyAuthTestMixin
from olympia.blocklist.models import Block
from olympia.files.models import File, FileUpload
from olympia.files.utils import get_sha256
from olympia.users.models import (
@ -894,78 +891,6 @@ class TestUploadVersion(BaseUploadVersionTestMixin, TestCase):
)
assert response.status_code == 202
def test_version_blocked(self):
block = Block.objects.create(
guid=self.guid, max_version='3.0', updated_by=user_factory()
)
response = self.request('PUT', self.url(self.guid, '3.0'))
assert response.status_code == 400
block_url = absolutify(reverse('blocklist.block', args=(self.guid,)))
assert response.data['error'] == (
f'Version 3.0 matches {block_url} for this add-on. '
'You can contact amo-admins@mozilla.com for additional '
'information.'
)
# it's okay if it's outside of the blocked range though
block.update(max_version='2.9')
response = self.request('PUT', self.url(self.guid, '3.0'))
assert response.status_code == 202
def test_addon_blocked(self):
guid = '@create-webextension'
block = Block.objects.create(
guid=guid, max_version='3.0', updated_by=user_factory()
)
qs = Addon.unfiltered.filter(guid=guid)
assert not qs.exists()
# Testing when a new addon guid is specified in the url
response = self.request('PUT', guid=guid, version='1.0')
assert response.status_code == 400
block_url = absolutify(reverse('blocklist.block', args=(guid,)))
error_msg = (
f'Version 1.0 matches {block_url} for this add-on. '
'You can contact amo-admins@mozilla.com for additional '
'information.'
)
assert response.data['error'] == error_msg
assert not qs.exists()
# it's okay if it's outside of the blocked range though
block.update(min_version='2.0')
response = self.request('PUT', guid=guid, version='1.0')
assert response.status_code == 201
def test_addon_blocked_guid_in_xpi(self):
guid = '@webextension-with-guid'
block = Block.objects.create(
guid=guid, max_version='3.0', updated_by=user_factory()
)
qs = Addon.unfiltered.filter(guid=guid)
assert not qs.exists()
filename = self.xpi_filepath('@create-webextension-with-guid', '1.0')
url = reverse_ns('signing.version', api_version='v4')
response = self.request(
'POST', guid=guid, version='1.0', filename=filename, url=url
)
assert response.status_code == 400
block_url = absolutify(reverse('blocklist.block', args=(guid,)))
error_msg = (
f'Version 1.0 matches {block_url} for this add-on. '
'You can contact amo-admins@mozilla.com for additional '
'information.'
)
assert response.data['error'] == error_msg
assert not qs.exists()
# it's okay if it's outside of the blocked range though
block.update(min_version='2.0')
response = self.request(
'POST', guid=guid, version='1.0', filename=filename, url=url
)
assert response.status_code == 201
def test_deleted_webextension(self):
guid = '@webextension-with-guid'
addon = addon_factory(guid=guid, users=[self.user])

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

@ -1,7 +1,6 @@
import functools
from django import forms
from django.urls import reverse
from django.utils.translation import gettext
from rest_framework import status
@ -18,10 +17,8 @@ from olympia.addons.utils import (
webext_version_stats,
)
from olympia.amo.decorators import use_primary_db
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.api.authentication import JWTKeyAuthentication
from olympia.api.throttling import addon_submission_throttles
from olympia.blocklist.models import Block
from olympia.devhub.permissions import IsSubmissionAllowedFor
from olympia.devhub.views import handle_upload as devhub_handle_upload
from olympia.files.models import FileUpload
@ -188,22 +185,6 @@ class VersionView(APIView):
status.HTTP_400_BAD_REQUEST,
)
block_qs = Block.objects.filter(guid=addon.guid if addon else guid)
if block_qs and block_qs.first().is_version_blocked(version_string):
msg = gettext(
'Version {version} matches {block_link} for this add-on. '
'You can contact {amo_admins} for additional information.'
)
raise forms.ValidationError(
msg.format(
version=version_string,
block_link=absolutify(reverse('blocklist.block', args=[guid])),
amo_admins='amo-admins@mozilla.com',
),
status.HTTP_400_BAD_REQUEST,
)
# channel will be ignored for new addons.
if addon is None:
channel = amo.CHANNEL_UNLISTED # New is always unlisted.

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

@ -1034,8 +1034,22 @@ class Version(OnChangeMixin, ModelBase):
@property
def is_blocked(self):
block = self.addon.block
return bool(block and block.is_version_blocked(self.version))
return hasattr(self, 'blockversion')
@cached_property
def blocklist_submission_id(self):
from olympia.blocklist.models import BlocklistSubmission
return (
submission.id
if self.id
and (
submission := BlocklistSubmission.get_submissions_from_version_id(
self.id
).last()
)
else 0
)
@property
def pending_rejection(self):

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

@ -25,7 +25,7 @@ from olympia.amo.tests import (
from olympia.amo.tests.test_models import BasePreviewMixin
from olympia.amo.utils import utc_millesecs_from_epoch
from olympia.applications.models import AppVersion
from olympia.blocklist.models import Block
from olympia.blocklist.models import Block, BlockVersion
from olympia.constants.promoted import (
LINE,
NOT_PROMOTED,
@ -1241,17 +1241,18 @@ class TestVersion(AMOPaths, TestCase):
assert not addon.current_version.can_be_disabled_and_deleted()
def test_is_blocked(self):
addon = Addon.objects.get(id=3615)
assert addon.current_version.is_blocked is False
version = Addon.objects.get(id=3615).current_version
assert version.is_blocked is False
block = Block.objects.create(addon=addon, updated_by=user_factory())
assert Addon.objects.get(id=3615).current_version.is_blocked is True
block = Block.objects.create(addon=version.addon, updated_by=user_factory())
assert version.reload().is_blocked is False
block.update(min_version='999999999')
assert Addon.objects.get(id=3615).current_version.is_blocked is False
blockversion = BlockVersion.objects.create(block=block, version=version)
assert version.reload().is_blocked is True
block.update(min_version='0')
assert Addon.objects.get(id=3615).current_version.is_blocked is True
blockversion.update(version=version_factory(addon=version.addon))
version.refresh_from_db()
assert version.is_blocked is False
def test_pending_rejection_property(self):
addon = Addon.objects.get(id=3615)

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

@ -13,6 +13,10 @@ form ul.guid_list {
padding: 5px 20px;
}
form .guid_list li ul {
margin: 0;
}
form ul.guid_list li {
list-style-type: disc;
}