Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read Promoted Group Information Via Models #23082

Merged
merged 18 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/topics/api/addons.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid.
:>json int position: The position in the list of previews images.
:>json int previews[].thumbnail_size[]: width, height dimensions of of the preview image thumbnail.
:>json string previews[].thumbnail_url: The URL (including a cachebusting query string) to the preview image thumbnail.
:>json object|null promoted: Object holding promotion information about the add-on. Null if the add-on is not currently promoted.
:>json string promoted.category: The name of the :ref:`promoted category <addon-detail-promoted-category>` for the add-on.
:>json array promoted.apps[]: Array of the :ref:`applications <addon-detail-application>` for which the add-on is promoted.
:>json array promoted: Array holding promotion information about the add-on.
:>json string promoted[].category: The name of the :ref:`promoted category <addon-detail-promoted-category>` for the add-on.
:>json array promoted[].apps[]: Array of the :ref:`applications <addon-detail-application>` for which the add-on is promoted.
:>json object ratings: Object holding ratings summary information about the add-on.
:>json int ratings.count: The total number of user ratings for the add-on.
:>json int ratings.text_count: The number of user ratings with review text for the add-on.
Expand Down
12 changes: 7 additions & 5 deletions src/olympia/abuse/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,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(currently_approved=False).high_profile
any(addon.promoted_groups(currently_approved=False).high_profile)
for addon in self.target.addons.all()
)
)
Expand Down Expand Up @@ -289,7 +289,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(currently_approved=False).high_profile
and any(self.target.promoted_groups(currently_approved=False).high_profile)
)

def log_action(self, activity_log_action, *extra_args, extra_details=None):
Expand Down Expand Up @@ -519,9 +519,11 @@ def should_hold_action(self):
return bool(
not self.target.deleted
and self.target.reply_to
and self.target.addon.promoted_group(
currently_approved=False
).high_profile_rating
and any(
self.target.addon.promoted_groups(
currently_approved=False
).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 @@ -351,7 +351,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)
promoted_group = self.addon.promoted_groups(currently_approved=False)
data = {
'id': self.id,
'average_daily_users': self.addon.average_daily_users,
Expand All @@ -361,7 +361,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(promoted_group.name),
}
return data

Expand Down
16 changes: 8 additions & 8 deletions src/olympia/addons/indexers.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,8 +656,9 @@ def extract_document(cls, obj):
data['has_eula'] = bool(obj.eula)
data['has_privacy_policy'] = bool(obj.privacy_policy)

data['is_recommended'] = bool(
obj.promoted and obj.promoted.group_id == PROMOTED_GROUP_CHOICES.RECOMMENDED
data['is_recommended'] = any(
PROMOTED_GROUP_CHOICES.RECOMMENDED == promotion.group_id
for promotion in obj.cached_promoted_groups
)

data['previews'] = [
Expand All @@ -670,17 +671,16 @@ def extract_document(cls, obj):
for preview in obj.current_previews
]

data['promoted'] = (
data['promoted'] = [
{
'group_id': obj.promoted.group_id,
'group_id': promotion.group_id,
# 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_for(promotion)
],
}
if obj.promoted
else None
)
for promotion in obj.cached_promoted_groups
]

data['ratings'] = {
'average': obj.average_rating,
Expand Down
95 changes: 71 additions & 24 deletions src/olympia/addons/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
from olympia.constants.categories import CATEGORIES_BY_ID
from olympia.constants.promoted import (
PROMOTED_GROUP_CHOICES,
PROMOTED_GROUPS_BY_ID,
)
from olympia.constants.reviewers import REPUTATION_CHOICES
from olympia.files.models import File
Expand Down Expand Up @@ -282,7 +281,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,7 +1554,7 @@ def _is_recommended_theme(self):
).exists()
)

def promoted_group(self, *, currently_approved=True):
def promoted_groups(self, *, currently_approved=True):
"""Is the addon currently promoted for the current applications?

Returns the group constant, or NOT_PROMOTED (which is falsey)
Expand All @@ -1567,32 +1565,75 @@ def promoted_group(self, *, currently_approved=True):
If currently_approved=False then promotions where there isn't approval
are returned too.
"""
from olympia.promoted.models import PromotedAddon
from olympia.promoted.models import PromotedGroup

try:
promoted = self.promotedaddon
except PromotedAddon.DoesNotExist:
return PROMOTED_GROUPS_BY_ID[PROMOTED_GROUP_CHOICES.NOT_PROMOTED]
is_promoted = not currently_approved or promoted.approved_applications
return (
promoted.group
if is_promoted
else PROMOTED_GROUPS_BY_ID[PROMOTED_GROUP_CHOICES.NOT_PROMOTED]
PromotedGroup.objects.approved_for(addon=self)
if currently_approved
else PromotedGroup.objects.all_for(addon=self)
)

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

if self._is_recommended_theme():
return PromotedTheme(
addon=self, group_id=PROMOTED_GROUP_CHOICES.RECOMMENDED
)
return None
from olympia.promoted.models import PromotedGroup

return [
PromotedGroup.objects.get(
group_id=PROMOTED_GROUP_CHOICES.RECOMMENDED
)
]
return []

@property
def all_applications(self):
from olympia.addons.serializers import APP_IDS
from olympia.constants.applications import APP_USAGE

apps = self.promotedaddonpromotion.values_list('application_id', flat=True)

return (
[APP_IDS.get(app_id) for app_id in apps]
if apps
else [app for app in APP_USAGE]
)

@property
def approved_applications(self):
"""All the applications that the current addon is approved for,
for the current version."""
return self.approved_applications_for()

def approved_applications_for(self, promoted_group=None):
"""The applications that the given promoted group is approved for,
for the current version."""

if self._is_recommended_theme():
return self.all_applications if self.current_version else []

return [
version
for version in self._promoted_addon_versions(
promoted_group=promoted_group
).approved_applications
if version in self.all_applications
]

def _promoted_addon_versions(self, version=None, promoted_group=None):
"""
Returns the versions associated with an approval (i.e approved promotions)
for the given version & group, or current version if none is given.
"""
from olympia.promoted.models import PromotedAddonVersion

return PromotedAddonVersion.objects.filter(
version=version if version else self.current_version,
**({'promoted_group': promoted_group} if promoted_group else {}),
)

@cached_property
def compatible_apps(self):
Expand All @@ -1615,10 +1656,16 @@ def can_set_compatibility(self):
def can_be_compatible_with_all_fenix_versions(self):
"""Whether or not the addon is allowed to be compatible with all Fenix
versions (i.e. it's a recommended/line extension for Android)."""
promotions = self.cached_promoted_groups
approved_applications = self.approved_applications

return (
self.promoted
and self.promoted.group.can_be_compatible_with_all_fenix_versions
and amo.ANDROID in self.promoted.approved_applications
promotions
and all(
promotion.can_be_compatible_with_all_fenix_versions
for promotion in promotions
)
and amo.ANDROID in approved_applications
)

def has_author(self, user):
Expand Down
68 changes: 51 additions & 17 deletions src/olympia/addons/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@
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
from olympia.constants.promoted import PROMOTED_GROUP_CHOICES
from olympia.constants.promoted import PROMOTED_GROUP_CHOICES, PROMOTED_GROUPS_BY_ID
from olympia.core.languages import AMO_LANGUAGES
from olympia.files.models import File, FileUpload
from olympia.files.utils import DuplicateAddonID, parse_addon
from olympia.promoted.models import PromotedAddon
from olympia.promoted.models import PromotedGroup
from olympia.ratings.utils import get_grouped_ratings
from olympia.search.filters import AddonAppVersionQueryParam
from olympia.tags.models import Tag
Expand Down Expand Up @@ -513,7 +513,7 @@ 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()
group = version.addon.promoted_groups()
msg = gettext(
'The latest approved version of this %s add-on cannot be deleted '
'because the previous version was not approved for %s promotion. '
Expand Down Expand Up @@ -961,22 +961,26 @@ def validate_user_id(self, value):
return value


class PromotedAddonSerializer(AMOModelSerializer):
class PromotedGroupSerializer(AMOModelSerializer):
apps = serializers.SerializerMethodField()
category = ReverseChoiceField(
choices=PROMOTED_GROUP_CHOICES.api_choices,
source='group_id',
)

class Meta:
model = PromotedAddon
model = PromotedGroup
fields = (
'apps',
'category',
)

def __init__(self, *args, **kwargs):
self.addon = kwargs.pop('addon', None)
super().__init__(*args, **kwargs)

def get_apps(self, obj):
return [app.short for app in obj.approved_applications]
return [app.short for app in self.addon.approved_applications_for(obj)]


class AddonSerializer(AMOModelSerializer):
Expand Down Expand Up @@ -1022,7 +1026,7 @@ class AddonSerializer(AMOModelSerializer):
],
)
previews = PreviewSerializer(many=True, source='current_previews', read_only=True)
promoted = PromotedAddonSerializer(read_only=True)
promoted = serializers.SerializerMethodField()
ratings = serializers.SerializerMethodField()
ratings_url = serializers.SerializerMethodField()
review_url = serializers.SerializerMethodField()
Expand Down Expand Up @@ -1138,17 +1142,30 @@ def to_representation(self, obj):
data.pop('is_source_public', None)
if request and not is_gate_active(request, 'is-featured-addon-shim'):
data.pop('is_featured', None)
if request and is_gate_active(request, 'promoted-groups-shim'):
promoted = data.pop('promoted', None)
data['promoted'] = promoted[0] if promoted else None
return data

def get_promoted(self, obj):
promoted = obj.cached_promoted_groups
return PromotedGroupSerializer(
many=True, read_only=True, instance=promoted, addon=obj
).data

def get_has_eula(self, obj):
return bool(getattr(obj, 'has_eula', obj.eula))

def get_is_featured(self, obj):
# featured is gone, but we need to keep the API backwards compatible so
# fake it with promoted status instead.
return bool(
obj.promoted and obj.promoted.group_id == PROMOTED_GROUP_CHOICES.RECOMMENDED
)
def is_recommended(obj):
return any(
PROMOTED_GROUP_CHOICES.RECOMMENDED == promotion.group_id
for promotion in obj.cached_promoted_groups
)

return bool(obj.cached_promoted_groups and is_recommended(obj))

def get_has_privacy_policy(self, obj):
return bool(getattr(obj, 'has_privacy_policy', obj.privacy_policy))
Expand Down Expand Up @@ -1562,23 +1579,28 @@ def fake_object(self, data):

promoted = data.get('promoted', None)
if promoted:
promoted = promoted[0]
# 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'],
)
group = PROMOTED_GROUPS_BY_ID[promoted['group_id']]

obj.promoted = [
{
'group_id': group.id,
'category': group.api_name,
'apps': [APP_IDS.get(app_id).short for app_id in approved_for_apps],
}
]

# we can safely regenerate these tuples because
# .appproved_applications only cares about the current group
obj._current_version.approved_for_groups = (
(obj.promoted.group, APP_IDS.get(app_id))
for app_id in approved_for_apps
)
else:
obj.promoted = None
obj.promoted = []

ratings = data.get('ratings', {})
obj.average_rating = ratings.get('average')
Expand Down Expand Up @@ -1611,6 +1633,18 @@ def to_representation(self, obj):
data.pop('_score')
return data

def get_promoted(self, obj):
return obj.promoted

def get_is_featured(self, obj):
def is_recommended(obj):
return any(
PROMOTED_GROUP_CHOICES.RECOMMENDED == promotion['group_id']
for promotion in obj.promoted
)

return bool(obj.promoted and is_recommended(obj))


class ESAddonAutoCompleteSerializer(ESAddonSerializer):
class Meta(ESAddonSerializer.Meta):
Expand Down
Loading
Loading