Skip to content

Commit

Permalink
Allow multiple PromotedAddons to a single Addon
Browse files Browse the repository at this point in the history
  • Loading branch information
chrstinalin committed Nov 25, 2024
1 parent f6513eb commit 16d4576
Show file tree
Hide file tree
Showing 38 changed files with 287 additions and 207 deletions.
7 changes: 4 additions & 3 deletions src/olympia/abuse/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _

from olympia.constants.promoted import HIGH_PROFILE, HIGH_PROFILE_RATING
import waffle

import olympia
Expand Down Expand Up @@ -250,7 +251,7 @@ def should_hold_action(self):
or self.target.groups_list # has any permissions
# owns a high profile add-on
or any(
addon.promoted_group().high_profile
addon.get(HIGH_PROFILE)
for addon in self.target.addons.all()
)
)
Expand Down Expand Up @@ -281,7 +282,7 @@ def should_hold_action(self):
return bool(
self.target.status != amo.STATUS_DISABLED
# is a high profile add-on
and self.target.promoted_group().high_profile
and self.target.get(HIGH_PROFILE)
)

def process_action(self):
Expand Down Expand Up @@ -369,7 +370,7 @@ def should_hold_action(self):
return bool(
not self.target.deleted
and self.target.reply_to
and self.target.addon.promoted_group().high_profile_rating
and self.target.addon.get(HIGH_PROFILE_RATING)
)

def process_action(self):
Expand Down
4 changes: 2 additions & 2 deletions src/olympia/abuse/cinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def get_attributes(self):
# promoted in any way, but we don't care about the promotion being
# approved for the current version, it would make more queries and it's
# not useful for moderation purposes anyway.
promoted_group = self.addon.promoted_group(currently_approved=False)
group_name = self.addon.group_name(currently_approved=False)
data = {
'id': self.id,
'average_daily_users': self.addon.average_daily_users,
Expand All @@ -341,7 +341,7 @@ def get_attributes(self):
'name': self.get_str(self.addon.name),
'slug': self.addon.slug,
'summary': self.get_str(self.addon.summary),
'promoted': self.get_str(promoted_group.name if promoted_group else ''),
'promoted': self.get_str(group_name),
}
return data

Expand Down
6 changes: 3 additions & 3 deletions src/olympia/addons/indexers.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ def extract_document(cls, obj):
data['has_privacy_policy'] = bool(obj.privacy_policy)

data['is_recommended'] = bool(
obj.promoted and obj.promoted.group == RECOMMENDED
RECOMMENDED in obj.promoted_group()
)

data['previews'] = [
Expand All @@ -677,10 +677,10 @@ def extract_document(cls, obj):

data['promoted'] = (
{
'group_id': obj.promoted.group_id,
'group_ids': obj.group_ids,
# store the app approvals because .approved_applications needs it.
'approved_for_apps': [
app.id for app in obj.promoted.approved_applications
app.id for app in obj.approved_applications
],
}
if obj.promoted
Expand Down
70 changes: 59 additions & 11 deletions src/olympia/addons/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
)
from olympia.constants.browsers import BROWSERS
from olympia.constants.categories import CATEGORIES_BY_ID
from olympia.constants.promoted import NOT_PROMOTED, RECOMMENDED
from olympia.constants.promoted import CAN_BE_COMPATIBLE_WITH_ALL_FENIX_VERSIONS, NOT_PROMOTED, RECOMMENDED, PromotedClass
from olympia.constants.reviewers import REPUTATION_CHOICES
from olympia.files.models import File
from olympia.files.utils import extract_translations, resolve_i18n_message
Expand Down Expand Up @@ -279,7 +279,6 @@ def get_base_queryset_for_queue(
select_related_fields = [
'reviewerflags',
'addonapprovalscounter',
'promotedaddon',
]
if select_related_fields_for_listed:
# Most listed queues need these to avoid extra queries because
Expand Down Expand Up @@ -1556,8 +1555,7 @@ def _is_recommended_theme(self):
def promoted_group(self, *, currently_approved=True):
"""Is the addon currently promoted for the current applications?
Returns the group constant, or NOT_PROMOTED (which is falsey)
otherwise.
Returns the list of group constants.
`currently_approved=True` means only returns True if
self.current_version is approved for the current promotion & apps.
Expand All @@ -1567,22 +1565,72 @@ def promoted_group(self, *, currently_approved=True):
from olympia.promoted.models import PromotedAddon

try:
promoted = self.promotedaddon
promoted_addons = self.promoted_addons.all()
except PromotedAddon.DoesNotExist:
return NOT_PROMOTED
is_promoted = not currently_approved or promoted.approved_applications
return promoted.group if is_promoted else NOT_PROMOTED
return []

return [promoted.group for promoted in promoted_addons if not currently_approved or promoted.approved_applications]

def group_name(self, *, currently_approved=True):
""" Returns the string name of the currently groups, comma separated.
`currently_approved=True` means only returns True if
self.current_version is approved for the current promotion & apps.
If currently_approved=False then promotions where there isn't approval
are returned too.
"""
groups = self.promoted_group(currently_approved=currently_approved)
return ', '.join(group.name for group in groups)

def get(self, permission, currently_approved=True):
""" Fetch the given permission.
Based on the type of the permission, returns --
Bool -> If any group is true
Int -> The maximum value from the groups
Dict -> return the first truthy value, or {} if none.
`currently_approved=True` means only returns True if
self.current_version is approved for the current promotion & apps.
If currently_approved=False then promotions where there isn't approval
are returned too.
"""
groups = self.promoted_group(currently_approved=currently_approved)
type = PromotedClass.type(permission)

if type == int:
return max(getattr(group, permission) for group in groups if getattr(group, permission) is not None)
if type == bool:
return any(getattr(group, permission, False) for group in groups)

for group in groups:
value = getattr(group, permission, None)
if value:
return value
return {}

@property
def group_ids(self):
groups = self.promoted_group()
return [group.id for group in groups]

@property
def approved_applications(self):
approved_apps = set()
for promoted in self.promoted_addons.all():
approved_apps.update(promoted.approved_applications)
return approved_apps

@cached_property
def promoted(self):
promoted_group = self.promoted_group()
if promoted_group:
return self.promotedaddon
return self.promoted_addons
else:
from olympia.promoted.models import PromotedTheme

if self._is_recommended_theme():
return PromotedTheme(addon=self, group_id=RECOMMENDED.id)
return [PromotedTheme(addon=self, group_id=RECOMMENDED.id)]
return None

@cached_property
Expand All @@ -1608,7 +1656,7 @@ def can_be_compatible_with_all_fenix_versions(self):
versions (i.e. it's a recommended/line extension for Android)."""
return (
self.promoted
and self.promoted.group.can_be_compatible_with_all_fenix_versions
and self.get(CAN_BE_COMPATIBLE_WITH_ALL_FENIX_VERSIONS)
and amo.ANDROID in self.promoted.approved_applications
)

Expand Down
17 changes: 9 additions & 8 deletions src/olympia/addons/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,12 +513,12 @@ def validate_is_disabled(self, disable):
):
raise exceptions.ValidationError(gettext('File is already disabled.'))
if not version.can_be_disabled_and_deleted():
group = version.addon.promoted_group()
name = version.addon.group_name()
msg = gettext(
'The latest approved version of this %s add-on cannot be deleted '
'because the previous version was not approved for %s promotion. '
'Please contact AMO Admins if you need help with this.'
) % (group.name, group.name)
) % (name, name)
raise exceptions.ValidationError(msg)
return disable

Expand Down Expand Up @@ -1562,12 +1562,13 @@ def fake_object(self, data):
# set .approved_for_groups cached_property because it's used in
# .approved_applications.
approved_for_apps = promoted.get('approved_for_apps')
obj.promoted = PromotedAddon(
addon=obj,
approved_application_ids=approved_for_apps,
created=None,
group_id=promoted['group_id'],
)
for group_id in promoted['group_ids']:
obj.promoted = PromotedAddon(
addon=obj,
approved_application_ids=approved_for_apps,
created=None,
group_id=group_id,
)
# we can safely regenerate these tuples because
# .appproved_applications only cares about the current group
obj._current_version.approved_for_groups = (
Expand Down
6 changes: 3 additions & 3 deletions src/olympia/addons/tests/test_indexers.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,15 +513,15 @@ def test_extract_promoted(self):
self.addon = addon_factory(promoted=RECOMMENDED)
extracted = self._extract()
assert extracted['promoted']
assert extracted['promoted']['group_id'] == RECOMMENDED.id
assert RECOMMENDED.id in extracted['promoted']['group_ids']
assert extracted['promoted']['approved_for_apps'] == [
amo.FIREFOX.id,
amo.ANDROID.id,
]
assert extracted['is_recommended'] is True

# Specific application.
self.addon.promotedaddon.update(application_id=amo.FIREFOX.id)
self.addon.promoted_addons.first().update(application_id=amo.FIREFOX.id)
extracted = self._extract()
assert extracted['promoted']['approved_for_apps'] == [amo.FIREFOX.id]
assert extracted['is_recommended'] is True
Expand All @@ -534,7 +534,7 @@ def test_extract_promoted(self):
featured_collection.add_addon(self.addon)
extracted = self._extract()
assert extracted['promoted']
assert extracted['promoted']['group_id'] == RECOMMENDED.id
assert RECOMMENDED.id in extracted['promoted']['group_ids']
assert extracted['promoted']['approved_for_apps'] == [
amo.FIREFOX.id,
amo.ANDROID.id,
Expand Down
15 changes: 5 additions & 10 deletions src/olympia/addons/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1727,34 +1727,31 @@ def test_promoted_group(self):
addon = addon_factory()
# default case - no group so not recommended
assert not addon.promoted_group()
# NOT_PROMOTED is falsey
assert addon.promoted_group() == NOT_PROMOTED
assert not addon.promoted_group(currently_approved=False)

# It's promoted but nothing has been approved
promoted = PromotedAddon.objects.create(addon=addon, group_id=LINE.id)
assert addon.promoted_group(currently_approved=False)
assert addon.promoted_group() == NOT_PROMOTED
assert not addon.promoted_group()

# The latest version is approved for the same group.
promoted.approve_for_version(version=addon.current_version)
assert addon.promoted_group()
assert addon.promoted_group() == LINE
assert LINE in addon.promoted_group()

# if the group has changes the approval for the current version isn't
# valid
promoted.update(group_id=SPOTLIGHT.id)
assert not addon.promoted_group()
assert addon.promoted_group(currently_approved=False)
assert addon.promoted_group(currently_approved=False) == SPOTLIGHT
assert SPOTLIGHT in addon.promoted_group(currently_approved=False)

promoted.approve_for_version(version=addon.current_version)
assert addon.promoted_group() == SPOTLIGHT
assert SPOTLIGHT in addon.promoted_group()

# Application specific group membership should work too
# if no app is specifed in the PromotedAddon everything should match
assert addon.promoted_group() == SPOTLIGHT
assert SPOTLIGHT in addon.promoted_group()
# update to mobile app
promoted.update(application_id=amo.ANDROID.id)
assert addon.promoted_group()
Expand All @@ -1765,14 +1762,13 @@ def test_promoted_group(self):
del addon.current_version.approved_for_groups
assert not addon.promoted_group()
promoted.update(application_id=amo.FIREFOX.id)
assert addon.promoted_group() == SPOTLIGHT
assert SPOTLIGHT in addon.promoted_group()

# check it doesn't error if there's no current_version
addon.current_version.file.update(status=amo.STATUS_DISABLED)
addon.update_version()
assert not addon.current_version
assert not addon.promoted_group()
assert addon.promoted_group() == NOT_PROMOTED
assert addon.promoted_group(currently_approved=False)

def test_promoted(self):
Expand Down Expand Up @@ -1821,7 +1817,6 @@ def test_promoted_theme(self):
featured_collection.remove_addon(addon)
del addon.promoted
addon = Addon.objects.get(id=addon.id)
# assert not addon.promotedaddon
# but not when it's removed.
assert addon.promoted is None

Expand Down
2 changes: 1 addition & 1 deletion src/olympia/addons/tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ def test_promoted(self):
assert result['promoted']['apps'] == [amo.FIREFOX.short]

# With a recommended theme.
self.addon.promotedaddon.delete()
self.addon.promoted_addons.all().delete()
self.addon.update(type=amo.ADDON_STATICTHEME)
featured_collection, _ = Collection.objects.get_or_create(
id=settings.COLLECTION_FEATURED_THEMES_ID
Expand Down
Loading

0 comments on commit 16d4576

Please sign in to comment.