support webhook addon decisions (#21408)

* Record different reasons for a DISABLED override of an original status

* restore file statuses on force_enable

* add to test_ban_and_disable_related_content_bulk

* Apply suggestions from code review

Co-authored-by: Mathieu Pillard <diox@users.noreply.github.com>

* restore bulk File disabling

* move activity log inside conditions in is_user_disabled setter

---------

Co-authored-by: Mathieu Pillard <diox@users.noreply.github.com>
This commit is contained in:
Andrew Williamson 2023-11-08 15:23:51 +00:00 коммит произвёл GitHub
Родитель 52c0ec13a8
Коммит 33da485b0a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 226 добавлений и 47 удалений

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

@ -78,7 +78,6 @@ class CinderActionApprove(CinderAction):
target = self.abuse_report.target
if isinstance(target, Addon) and target.status == amo.STATUS_DISABLED:
target.force_enable()
# TODO: renable versions
self.notify_targets(target.authors.all())
elif isinstance(target, UserProfile) and target.banned:

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

@ -652,7 +652,7 @@ class Addon(OnChangeMixin, ModelBase):
).update(is_active=False)
self.update_all_due_dates()
# https://github.com/mozilla/addons-server/issues/13194
self.disable_all_files()
Addon.disable_all_files([self], File.STATUS_DISABLED_REASONS.ADDON_DISABLE)
def force_enable(self, skip_activity_log=False):
if not skip_activity_log:
@ -660,6 +660,16 @@ class Addon(OnChangeMixin, ModelBase):
log.info(
'Addon "%s" status force-changed to: %s', self.slug, amo.STATUS_APPROVED
)
qs = File.objects.filter(
version__addon=self,
status=amo.STATUS_DISABLED,
status_disabled_reason=File.STATUS_DISABLED_REASONS.ADDON_DISABLE,
).exclude(original_status=amo.STATUS_NULL)
qs.update(status=F('original_status'))
qs.update(
status_disabled_reason=File.STATUS_DISABLED_REASONS.NONE,
original_status=amo.STATUS_NULL,
)
self.update(status=amo.STATUS_APPROVED)
# Call update_status() to fix the status if the add-on is not actually
# in a state that allows it to be public.
@ -683,8 +693,16 @@ class Addon(OnChangeMixin, ModelBase):
log.info('Allow resubmission for addon "%s"', self.slug)
DeniedGuid.objects.filter(guid=self.guid).delete()
def disable_all_files(self):
File.objects.filter(version__addon=self).update(status=amo.STATUS_DISABLED)
@classmethod
def disable_all_files(cls, addons, reason):
qs = File.objects.filter(version__addon__in=addons).exclude(
status=amo.STATUS_DISABLED
)
qs.update(original_status=F('status'))
qs.update(
status=amo.STATUS_DISABLED,
status_disabled_reason=reason,
)
def set_needs_human_review_on_latest_versions(
self, *, reason, due_date=None, ignore_reviewed=True, unique_reason=False
@ -843,7 +861,7 @@ class Addon(OnChangeMixin, ModelBase):
rating.delete(skip_activity_log=True)
# We avoid triggering signals for Version & File on purpose to
# avoid extra work.
self.disable_all_files()
Addon.disable_all_files([self], File.STATUS_DISABLED_REASONS.ADDON_DELETE)
self.versions.all().update(deleted=True)
VersionReviewerFlags.objects.filter(version__addon=self).update(
@ -1912,7 +1930,11 @@ def watch_status(old_attr=None, new_attr=None, instance=None, sender=None, **kwa
# review should be disabled right away, we don't want reviewers to look
# at it. That might in turn change the add-on status from NOMINATED
# back to NULL, through update_status().
latest_version.file.update(status=amo.STATUS_DISABLED)
latest_version.file.update(
status=amo.STATUS_DISABLED,
original_status=latest_version.file.status,
status_disabled_reason=File.STATUS_DISABLED_REASONS.DEVELOPER,
)
instance.update_status()
elif old_status == amo.STATUS_NOMINATED:
# Update latest version due date if necessary for nominated add-ons.

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

@ -422,7 +422,7 @@ def disable_addons(addon_ids, **kw):
for addon in addons:
activity.log_create(amo.LOG.FORCE_DISABLE, addon, user=get_task_user())
addons.update(status=amo.STATUS_DISABLED, _current_version=None)
File.objects.filter(version__addon__in=addons).update(status=amo.STATUS_DISABLED)
Addon.disable_all_files(addons, File.STATUS_DISABLED_REASONS.ADDON_DISABLE)
index_addons.delay(addon_ids)

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

@ -753,6 +753,10 @@ class TestAddonModels(TestCase):
assert files
for file_ in files:
assert file_.status == amo.STATUS_DISABLED
assert (
file_.status_disabled_reason
== File.STATUS_DISABLED_REASONS.ADDON_DELETE
)
for version in versions:
assert version.deleted
@ -771,6 +775,13 @@ class TestAddonModels(TestCase):
assert files
for file_ in files:
assert file_.status != amo.STATUS_DISABLED
already_disabled_version = version_factory(
addon=addon,
file_kw={
'status': amo.STATUS_DISABLED,
'status_disabled_reason': File.STATUS_DISABLED_REASONS.DEVELOPER,
},
)
assert version1.due_date
assert version2.due_date
@ -784,6 +795,20 @@ class TestAddonModels(TestCase):
assert files
for file_ in files:
assert file_.status == amo.STATUS_DISABLED
if file_.version == already_disabled_version:
assert (
file_.status_disabled_reason
== File.STATUS_DISABLED_REASONS.DEVELOPER
)
else:
assert (
file_.status_disabled_reason
== File.STATUS_DISABLED_REASONS.ADDON_DISABLE
)
assert file_.original_status in (
amo.STATUS_APPROVED,
amo.STATUS_AWAITING_REVIEW,
)
assert not file_.version.due_date
assert not file_.version.needshumanreview_set.filter(
is_active=True
@ -801,9 +826,34 @@ class TestAddonModels(TestCase):
def test_force_enable(self):
core.set_user(UserProfile.objects.get(email='admin@mozilla.com'))
addon = Addon.unfiltered.get(pk=3615)
v1 = addon.current_version
v2 = version_factory(addon=addon)
v3 = version_factory(addon=addon)
addon.update(status=amo.STATUS_DISABLED)
v1.file.update(
status=amo.STATUS_DISABLED,
original_status=amo.STATUS_APPROVED,
# We don't want to re-enable a version the developer disabled
status_disabled_reason=File.STATUS_DISABLED_REASONS.DEVELOPER,
)
v2.file.update(
status=amo.STATUS_DISABLED,
original_status=amo.STATUS_APPROVED,
# we also don't want to re-enable a version we rejected
status_disabled_reason=File.STATUS_DISABLED_REASONS.NONE,
)
v3.file.update(
status=amo.STATUS_DISABLED,
original_status=amo.STATUS_APPROVED,
# but we do want to re-enable a version we only disabled with the addon
status_disabled_reason=File.STATUS_DISABLED_REASONS.ADDON_DISABLE,
)
addon.force_enable()
assert addon.status == amo.STATUS_APPROVED
assert addon.reload().status == amo.STATUS_APPROVED
assert v1.file.reload().status == amo.STATUS_DISABLED
assert v2.file.reload().status == amo.STATUS_DISABLED
assert v3.file.reload().status == amo.STATUS_APPROVED
log = ActivityLog.objects.latest('pk')
assert log.action == amo.LOG.FORCE_ENABLE.id
@ -1237,6 +1287,11 @@ class TestAddonModels(TestCase):
file_ = version.file
file_.reload()
assert version.file.status == amo.STATUS_DISABLED
assert version.file.original_status == amo.STATUS_AWAITING_REVIEW
assert (
version.file.status_disabled_reason
== File.STATUS_DISABLED_REASONS.DEVELOPER
)
assert addon.status == amo.STATUS_NULL
assert addon.is_disabled

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

@ -522,6 +522,10 @@ def test_disable_addons(index_addons_mock):
assert addon.status == amo.STATUS_DISABLED
assert addon.current_version is None
assert addon.versions.all()[0].file.status == amo.STATUS_DISABLED
assert (
addon.versions.all()[0].file.status_disabled_reason
== File.STATUS_DISABLED_REASONS.ADDON_DISABLE
)
assert ActivityLog.objects.filter(
action=amo.LOG.FORCE_DISABLE.id, addonlog__addon=addon

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

@ -26,6 +26,7 @@ from olympia.amo.tests import (
)
from olympia.applications.models import AppVersion
from olympia.constants.promoted import RECOMMENDED
from olympia.files.models import File
from olympia.reviewers.models import AutoApprovalSummary
from olympia.users.models import Group, UserProfile
from olympia.versions.models import ApplicationsVersions, Version, VersionReviewerFlags
@ -258,7 +259,9 @@ class TestVersion(TestCase):
def test_reenable_version(self):
Version.objects.get(pk=81551).file.update(
status=amo.STATUS_DISABLED, original_status=amo.STATUS_APPROVED
status=amo.STATUS_DISABLED,
original_status=amo.STATUS_APPROVED,
status_disabled_reason=File.STATUS_DISABLED_REASONS.DEVELOPER,
)
self.reenable_url = reverse('devhub.versions.reenable', args=['a3615'])
response = self.client.post(self.reenable_url, self.delete_data, follow=True)

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

@ -45,7 +45,11 @@ class FileAdmin(AMOModelAdmin):
(
'Details',
{
'fields': ('cert_serial_num', 'original_status'),
'fields': (
'cert_serial_num',
'original_status',
'status_disabled_reason',
),
},
),
(

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

@ -0,0 +1,18 @@
# Generated by Django 4.2.7 on 2023-11-07 13:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0030_remove-add-guid-to-manifest-waffle-switch'),
]
operations = [
migrations.AddField(
model_name='file',
name='status_disabled_reason',
field=models.PositiveSmallIntegerField(choices=[(0, 'None'), (1, 'Developer disabled'), (2, 'Add-on disabled'), (3, 'Add-on deleted'), (4, 'Version deleted')], default=0),
),
]

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

@ -0,0 +1,26 @@
# Generated by Django 4.2.7 on 2023-11-07 15:46
from django.db import migrations
from olympia import amo
from olympia.files.models import File as FileModel
def set_status_disabled_reason(apps, schema_editor):
File = apps.get_model('files', 'File')
(
File.objects.filter(status=amo.STATUS_DISABLED)
.exclude(original_status=amo.STATUS_NULL)
.update(status_disabled_reason=FileModel.STATUS_DISABLED_REASONS.DEVELOPER)
)
class Migration(migrations.Migration):
dependencies = [
('files', '0031_file_status_disabled_reason'),
]
operations = [
migrations.RunPython(set_status_disabled_reason)
]

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

@ -18,6 +18,7 @@ from django.utils.text import slugify
from django.utils.translation import gettext
from django_statsd.clients import statsd
from extended_choices import Choices
import olympia.core.logger
from olympia import amo, core
@ -76,6 +77,13 @@ def files_storage():
class File(OnChangeMixin, ModelBase):
id = PositiveAutoField(primary_key=True)
STATUS_CHOICES = amo.STATUS_CHOICES_FILE
STATUS_DISABLED_REASONS = Choices(
('NONE', 0, 'None'),
('DEVELOPER', 1, 'Developer disabled'),
('ADDON_DISABLE', 2, 'Add-on disabled'),
('ADDON_DELETE', 3, 'Add-on deleted'),
('VERSION_DELETE', 4, 'Version deleted'),
)
SUPPORTED_MANIFEST_VERSIONS = ((2, 'Manifest V2'), (3, 'Manifest V3'))
version = models.OneToOneField('versions.Version', on_delete=models.CASCADE)
@ -106,9 +114,12 @@ class File(OnChangeMixin, ModelBase):
# Is the file a special "Mozilla Signed Extension"
# see https://wiki.mozilla.org/Add-ons/InternalSigning
is_mozilla_signed_extension = models.BooleanField(default=False)
# The user has disabled this file and this was its status.
# STATUS_NULL means the user didn't disable the File - i.e. Mozilla did.
# The file status has been changed to DISABLED - this was its status before.
# STATUS_NULL means the status hasn't been changed
original_status = models.PositiveSmallIntegerField(default=amo.STATUS_NULL)
status_disabled_reason = models.PositiveSmallIntegerField(
choices=STATUS_DISABLED_REASONS.choices, default=STATUS_DISABLED_REASONS.NONE
)
# The manifest_version defined in manifest.json
manifest_version = models.SmallIntegerField(choices=SUPPORTED_MANIFEST_VERSIONS)

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

@ -38,6 +38,7 @@ class TestFileAdmin(TestCase):
'original_hash': 'xxx',
'status': file_.status,
'original_status': file_.original_status,
'status_disabled_reason': file_.status_disabled_reason,
'manifest_version': 3,
}
response = self.client.post(detail_url, post_data, follow=True)

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

@ -17,7 +17,7 @@ from olympia.addons.models import Addon, AddonApprovalsCounter
from olympia.amo.models import ModelBase
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.utils import send_mail
from olympia.files.models import FileValidation
from olympia.files.models import File, FileValidation
from olympia.ratings.models import Rating
from olympia.users.models import UserProfile
from olympia.users.utils import get_task_user
@ -313,14 +313,13 @@ class AutoApprovalSummary(ModelBase):
# Average daily users: value divided by 10000 is added to the
# weight, up to a maximum of 100.
'average_daily_users': min(addon.average_daily_users // 10000, 100),
# Pas rejection history: each "recent" rejected version (disabled
# with an original status of null, so not disabled by a developer)
# Past rejection history: each "recent" rejected version
# adds 10 to the weight, up to a maximum of 100.
'past_rejection_history': min(
Version.objects.filter(
addon=addon,
human_review_date__gte=one_year_ago,
file__original_status=amo.STATUS_NULL,
file__status_disabled_reason=File.STATUS_DISABLED_REASONS.NONE,
file__status=amo.STATUS_DISABLED,
).count()
* 10,

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

@ -14,6 +14,7 @@ from olympia.amo.tests import (
version_factory,
)
from olympia.constants.reviewers import REVIEWER_DELAYED_REJECTION_PERIOD_DAYS_DEFAULT
from olympia.files.models import File
from olympia.reviewers.forms import ReviewForm
from olympia.reviewers.models import (
AutoApprovalSummary,
@ -627,6 +628,7 @@ class TestReviewForm(TestCase):
file_kw={
'status': amo.STATUS_DISABLED,
'original_status': amo.STATUS_APPROVED,
'status_disabled_reason': File.STATUS_DISABLED_REASONS.DEVELOPER,
'is_signed': True,
},
)
@ -635,6 +637,7 @@ class TestReviewForm(TestCase):
file_kw={
'status': amo.STATUS_DISABLED,
'original_status': amo.STATUS_AWAITING_REVIEW,
'status_disabled_reason': File.STATUS_DISABLED_REASONS.DEVELOPER,
'is_signed': False,
},
)

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

@ -25,7 +25,7 @@ from olympia.constants.promoted import (
STRATEGIC,
)
from olympia.constants.scanners import CUSTOMS, MAD
from olympia.files.models import FileValidation, WebextPermission
from olympia.files.models import File, FileValidation, WebextPermission
from olympia.promoted.models import PromotedAddon
from olympia.ratings.models import Rating
from olympia.reviewers.models import (
@ -485,14 +485,14 @@ class TestAutoApprovalSummary(TestCase):
},
)
# Version disabled by the developer, not Mozilla (original_status
# is set to something different than STATUS_NULL), does not count.
# Version disabled by the developer, not Mozilla
# (status_disabled_reason is DEVELOPER), does not count.
version_factory(
addon=self.addon,
human_review_date=self.days_ago(15),
file_kw={
'status': amo.STATUS_DISABLED,
'original_status': amo.STATUS_APPROVED,
'status_disabled_reason': File.STATUS_DISABLED_REASONS.DEVELOPER,
},
)

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

@ -35,6 +35,7 @@ from olympia.amo.fields import CIDRField, PositiveAutoField
from olympia.amo.models import LongNameIndex, ManagerBase, ModelBase, OnChangeMixin
from olympia.amo.utils import id_to_path
from olympia.amo.validators import OneOrMorePrintableCharacterValidator
from olympia.files.models import File
from olympia.translations.query import order_by_translation
from olympia.users.notifications import NOTIFICATIONS_BY_ID
@ -476,7 +477,6 @@ class UserProfile(OnChangeMixin, ModelBase, AbstractBaseUser):
from olympia.addons.models import Addon, AddonUser
from olympia.addons.tasks import index_addons
from olympia.bandwagon.models import Collection
from olympia.files.models import File
from olympia.ratings.models import Rating
# collect affected addons
@ -498,10 +498,8 @@ class UserProfile(OnChangeMixin, ModelBase, AbstractBaseUser):
addons_sole = Addon.unfiltered.filter(id__in=addon_ids - addon_joint_ids)
# set the status to disabled - using the manager update() method
addons_sole.update(status=amo.STATUS_DISABLED)
# collect Files that need to be disabled now the addons are disabled
File.objects.filter(version__addon__in=addons_sole).update(
status=amo.STATUS_DISABLED
)
# disable Files in bulk that need to be disabled now the addons are disabled
Addon.disable_all_files(addons_sole, File.STATUS_DISABLED_REASONS.ADDON_DISABLE)
# Finally run Addon.force_disable to add the logging; update versions.
addons_sole_ids = []

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

@ -207,6 +207,7 @@ class TestUserProfile(TestCase):
email='sole@foo.baa', fxa_id='13579', last_login_ip='127.0.0.1'
)
addon_sole = addon_factory(users=[user_sole])
addon_sole_file = addon_sole.current_version.file
self.setup_user_to_be_have_content_disabled(user_sole)
user_multi = user_factory(
email='multi@foo.baa', fxa_id='24680', last_login_ip='127.0.0.2'
@ -215,6 +216,7 @@ class TestUserProfile(TestCase):
addon_multi = addon_factory(
users=UserProfile.objects.filter(id__in=[user_multi.id, innocent_user.id])
)
addon_multi_file = addon_multi.current_version.file
self.setup_user_to_be_have_content_disabled(user_multi)
# Now that everything is set up, disable/delete related content.
@ -230,17 +232,15 @@ class TestUserProfile(TestCase):
assert list(addon_multi.authors.all()) == [innocent_user]
# the File objects have been disabled
addon_sole_file.reload()
assert addon_sole_file.status == amo.STATUS_DISABLED
assert addon_sole_file.original_status == amo.STATUS_APPROVED
assert (
not File.objects.filter(version__addon=addon_sole)
.exclude(status=amo.STATUS_DISABLED)
.exists()
addon_sole_file.status_disabled_reason
== File.STATUS_DISABLED_REASONS.ADDON_DISABLE
)
# But not for the Add-on that wasn't disabled
assert (
File.objects.filter(version__addon=addon_multi)
.exclude(status=amo.STATUS_DISABLED)
.exists()
)
assert addon_multi_file.reload().status == amo.STATUS_APPROVED
assert not user_sole._ratings_all.exists() # Even replies.
assert not user_sole.collections.exists()

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

@ -638,9 +638,13 @@ class Version(OnChangeMixin, ModelBase):
else:
# By default we soft delete so we can keep the files for comparison
# and a record of the version number.
if hasattr(self, 'file'):
if hasattr(self, 'file') and self.file.status != amo.STATUS_DISABLED:
# .file should always exist but we don't want to break delete regardless
self.file.update(status=amo.STATUS_DISABLED)
self.file.update(
status=amo.STATUS_DISABLED,
original_status=self.file.status,
status_disabled_reason=File.STATUS_DISABLED_REASONS.VERSION_DELETE,
)
self.deleted = True
self.save()
@ -666,22 +670,33 @@ class Version(OnChangeMixin, ModelBase):
def is_user_disabled(self):
return (
self.file.status == amo.STATUS_DISABLED
and self.file.original_status != amo.STATUS_NULL
and self.file.status_disabled_reason
== File.STATUS_DISABLED_REASONS.DEVELOPER
)
@is_user_disabled.setter
def is_user_disabled(self, disable):
# User wants to disable (and the File isn't already).
if disable:
activity.log_create(amo.LOG.DISABLE_VERSION, self.addon, self)
if (file_ := self.file) and file_.status != amo.STATUS_DISABLED:
file_.update(original_status=file_.status, status=amo.STATUS_DISABLED)
activity.log_create(amo.LOG.DISABLE_VERSION, self.addon, self)
file_.update(
original_status=file_.status,
status=amo.STATUS_DISABLED,
status_disabled_reason=File.STATUS_DISABLED_REASONS.DEVELOPER,
)
# User wants to re-enable (and user did the disable, not Mozilla).
else:
activity.log_create(amo.LOG.ENABLE_VERSION, self.addon, self)
if (file_ := self.file) and file_.original_status != amo.STATUS_NULL:
if (
(file_ := self.file)
and file_.status_disabled_reason
== File.STATUS_DISABLED_REASONS.DEVELOPER
):
activity.log_create(amo.LOG.ENABLE_VERSION, self.addon, self)
file_.update(
status=file_.original_status, original_status=amo.STATUS_NULL
status=file_.original_status,
original_status=amo.STATUS_NULL,
status_disabled_reason=File.STATUS_DISABLED_REASONS.NONE,
)
@cached_property

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

@ -482,12 +482,19 @@ class TestVersion(AMOPaths, TestCase):
version = Version.objects.get(pk=81551)
version_preview = VersionPreview.objects.create(version=version)
assert version.file
version_file = version.file
version.delete()
addon = Addon.objects.get(pk=3615)
assert not Version.objects.filter(addon=addon).exists()
assert Version.unfiltered.filter(addon=addon).exists()
assert File.objects.filter(version=version).exists()
version_file.reload()
assert version_file.original_status == amo.STATUS_APPROVED
assert (
version_file.status_disabled_reason
== File.STATUS_DISABLED_REASONS.VERSION_DELETE
)
delete_preview_files_mock.assert_called_with(
sender=None, instance=version_preview
)
@ -547,13 +554,18 @@ class TestVersion(AMOPaths, TestCase):
version.file.reload()
assert version.file.status == amo.STATUS_DISABLED
assert version.file.original_status == amo.STATUS_APPROVED
assert (
version.file.status_disabled_reason
== File.STATUS_DISABLED_REASONS.DEVELOPER
)
version.is_user_disabled = False
version.file.reload()
assert version.file.status == amo.STATUS_APPROVED
assert version.file.original_status == amo.STATUS_NULL
assert version.file.status_disabled_reason == File.STATUS_DISABLED_REASONS.NONE
def test_version_disable_after_mozila_disabled(self):
def test_version_disable_after_mozilla_disabled(self):
# Check that a user disable doesn't override mozilla disable
version = Version.objects.get(pk=81551)
version.file.update(status=amo.STATUS_DISABLED)
@ -562,11 +574,18 @@ class TestVersion(AMOPaths, TestCase):
version.file.reload()
assert version.file.status == amo.STATUS_DISABLED
assert version.file.original_status == amo.STATUS_NULL
assert version.file.status_disabled_reason == File.STATUS_DISABLED_REASONS.NONE
version.is_user_disabled = False
version.file.reload()
assert version.file.status == amo.STATUS_DISABLED
assert version.file.original_status == amo.STATUS_NULL
version.file.update(original_status=amo.STATUS_APPROVED)
for reason in File.STATUS_DISABLED_REASONS.values:
if reason == File.STATUS_DISABLED_REASONS.DEVELOPER:
# DEVELOPER is the only reason we expect to succeed
continue
version.file.update(status_disabled_reason=reason)
version.is_user_disabled = False
version.file.reload()
assert version.file.status == amo.STATUS_DISABLED
assert version.file.original_status == amo.STATUS_APPROVED
def _reset_version(self, version):
version.file.status = amo.STATUS_APPROVED
@ -1472,7 +1491,9 @@ class TestVersion(AMOPaths, TestCase):
assert self.version.get_review_status_display() == 'Unreviewed'
self.version.update(human_review_date=datetime.now())
assert self.version.get_review_status_display() == 'Rejected'
self.version.file.update(original_status=amo.STATUS_APPROVED)
self.version.file.update(
status_disabled_reason=File.STATUS_DISABLED_REASONS.DEVELOPER
)
assert self.version.get_review_status_display() == 'Disabled by Developer'
self.version.update(deleted=True)
assert self.version.get_review_status_display() == 'Deleted'