Skip to content

Commit

Permalink
TMP: more
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinMind committed Jul 16, 2024
1 parent 88c9fe5 commit a29722e
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 166 deletions.
60 changes: 45 additions & 15 deletions src/olympia/abuse/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,11 +491,15 @@ class AbuseReport(ModelBase):
('OTHER', 127, 'Other'),
)
REPORT_ENTRY_POINTS = APIChoicesWithNone(
('UNINSTALL', 1, 'Uninstall'),
('MENU', 2, 'Menu'),
('TOOLBAR_CONTEXT_MENU', 3, 'Toolbar context menu'),
('AMO', 4, 'AMO'),
('UNIFIED_CONTEXT_MENU', 5, 'Unified extensions context menu'),
('UNINSTALL', 1, 'Report button shown at uninstall time'),
('MENU', 2, 'Report menu in Add-ons Manager'),
('TOOLBAR_CONTEXT_MENU', 3, 'Report context menu on add-on toolbar'),
(
'AMO',
4,
'Report button on an AMO page (using `navigator.mozAddonManager.reportAbuse`)',
),
('UNIFIED_CONTEXT_MENU', 5, 'Report unified extensions (context) menu'),
)
LOCATION = APIChoicesWithNone(
('AMO', 1, 'Add-on page on AMO'),
Expand Down Expand Up @@ -571,10 +575,18 @@ class AbuseReport(ModelBase):
help_text='The add-on summary in the locale used by the client.',
)
addon_version = models.CharField(
default=None, max_length=255, blank=True, null=True, help_text='The add-on version string.'
default=None,
max_length=255,
blank=True,
null=True,
help_text='The add-on version string.',
)
addon_signature = models.PositiveSmallIntegerField(
default=None, choices=ADDON_SIGNATURES.choices, blank=True, null=True, help_text=' The add-on signature state.'
default=None,
choices=ADDON_SIGNATURES.choices,
blank=True,
null=True,
help_text=' The add-on signature state.',
)
application = models.PositiveSmallIntegerField(
default=amo.FIREFOX.id, choices=amo.APPS_CHOICES, blank=True, null=True
Expand All @@ -586,14 +598,28 @@ class AbuseReport(ModelBase):
default=None, max_length=255, blank=True, null=True
)
operating_system = models.CharField(
default=None, max_length=255, blank=True, null=True, help_text="The client's operating system."
default=None,
max_length=255,
blank=True,
null=True,
help_text="The client's operating system.",
)
operating_system_version = models.CharField(
default=None, max_length=255, blank=True, null=True, help_text="The client's operating system version."
default=None,
max_length=255,
blank=True,
null=True,
help_text="The client's operating system version.",
)
install_date = models.DateTimeField(
default=None, blank=True, null=True, help_text='The add-on install date.'
)
install_date = models.DateTimeField(default=None, blank=True, null=True, help_text='The add-on install date.')
reason = models.PositiveSmallIntegerField(
default=None, choices=REASONS.choices, blank=True, null=True, help_text='The reason for the report.'
default=None,
choices=REASONS.choices,
blank=True,
null=True,
help_text='The reason for the report.',
)
addon_install_origin = models.CharField(
# Supposed to be an URL, but the scheme could be moz-foo: or something
Expand All @@ -604,7 +630,7 @@ class AbuseReport(ModelBase):
max_length=255,
blank=True,
null=True,
help_text='The add-on install origin.'
help_text='The add-on install origin.',
)
addon_install_method = models.PositiveSmallIntegerField(
default=None,
Expand Down Expand Up @@ -634,10 +660,14 @@ class AbuseReport(ModelBase):
max_length=255,
blank=True,
null=True,
help_text='The add-on install source URL.'
help_text='The add-on install source URL.',
)
report_entry_point = models.PositiveSmallIntegerField(
default=None, choices=REPORT_ENTRY_POINTS.choices, blank=True, null=True
default=None,
choices=REPORT_ENTRY_POINTS.choices,
blank=True,
null=True,
help_text='Where and in what context was the report sent from.',
)
location = models.PositiveSmallIntegerField(
default=None,
Expand All @@ -661,7 +691,7 @@ class AbuseReport(ModelBase):
choices=ILLEGAL_CATEGORIES.choices,
blank=True,
null=True,
help_text='The type of illegal content - only required when the reason is set to illegal.'
help_text='The type of illegal content - only required when the reason is set to illegal.',
)
illegal_subcategory = models.PositiveSmallIntegerField(
default=None,
Expand Down
14 changes: 8 additions & 6 deletions src/olympia/abuse/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,35 +171,37 @@ class AddonAbuseReportSerializer(BaseAbuseReportSerializer):
choices=list((v.id, k) for k, v in amo.APPS.items()),
required=False,
source='application',
help_text='The application used by the client.'
help_text='The application used by the client.',
)
appversion = serializers.CharField(
required=False, source='application_version', max_length=255, help_text='The application version used by the client.'
required=False,
source='application_version',
max_length=255,
help_text='The application version used by the client.',
)

report_entry_point = ReverseChoiceField(
choices=list(AbuseReport.REPORT_ENTRY_POINTS.api_choices),
required=False,
allow_null=True,
help_text='The report entry point.'
)
addon_install_method = ReverseChoiceField(
choices=list(AbuseReport.ADDON_INSTALL_METHODS.api_choices),
required=False,
allow_null=True,
help_text='The add-on install method.'
help_text='The add-on install method.',
)
addon_install_source = ReverseChoiceField(
choices=list(AbuseReport.ADDON_INSTALL_SOURCES.api_choices),
required=False,
allow_null=True,
help_text='The add-on install source.'
help_text='The add-on install source.',
)
addon_signature = ReverseChoiceField(
choices=list(AbuseReport.ADDON_SIGNATURES.api_choices),
required=False,
allow_null=True,
help_text='The add-on signature state.'
help_text='The add-on signature state.',
)
reason = ReverseChoiceField(
# For add-ons we use the full list of reasons as choices.
Expand Down
3 changes: 2 additions & 1 deletion src/olympia/abuse/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.template.response import TemplateResponse
from django.utils.encoding import force_bytes

from drf_spectacular.utils import extend_schema_view, extend_schema
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status
from rest_framework.decorators import (
api_view,
Expand Down Expand Up @@ -87,6 +87,7 @@ def get_target_object(self):
self.target_object = self.get_target_viewset().get_object()
return self.target_object


@extend_schema_view(
create=extend_schema(
description=(
Expand Down
182 changes: 45 additions & 137 deletions src/olympia/addons/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from django.utils.cache import patch_cache_control
from django.utils.translation import gettext

from drf_spectacular.utils import extend_schema_view, extend_schema
import django_filters
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, extend_schema_view
from elasticsearch_dsl import Q, Search, query
from rest_framework import exceptions, serializers, status
from rest_framework.decorators import action
Expand Down Expand Up @@ -1020,159 +1022,66 @@ def finalize_response(self, request, response, *args, **kwargs):
patch_cache_control(response, max_age=60 * 60 * 6)
return response

class AddonFilter(django_filters.FilterSet):
application = django_filters.NumberFilter(method='filter_application')
types = django_filters.CharFilter(method='filter_types')
appversions = django_filters.CharFilter(method='filter_appversions')
authors = django_filters.CharFilter(method='filter_authors')

class Meta:
model = Addon
fields = []

def filter_application(self, queryset, name, value):
try:
application = AddonAppQueryParam(value).get_value()
return queryset.filter(versions__apps__application=application)
except ValueError:
raise exceptions.ParseError('Invalid application parameter')

def filter_types(self, queryset, name, value):
try:
addon_types = tuple(AddonTypeQueryParam(value).get_values())
return queryset.filter(type__in=addon_types)
except ValueError:
raise exceptions.ParseError('Invalid type parameter')

def filter_appversions(self, queryset, name, value):
try:
appversions = AddonAppVersionQueryParam(value).get_values()
appversions_dict = {'min': appversions[1], 'max': appversions[2]}
return queryset.filter(
versions__apps__min__version_int__lte=appversions_dict['min'],
versions__apps__max__version_int__gte=appversions_dict['max'],
)
except ValueError:
raise exceptions.ParseError('Invalid appversion parameter')

def filter_authors(self, queryset, name, value):
authors = AddonAuthorQueryParam(value).get_values()
return queryset.filter(addonuser__user__username__in=authors, addonuser__listed=True).distinct()


class LanguageToolsView(ListAPIView):
authentication_classes = []
pagination_class = None
permission_classes = []
serializer_class = LanguageToolsSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = AddonFilter

@classmethod
def as_view(cls, **initkwargs):
"""The API is read-only so we can turn off atomic requests."""
return non_atomic_requests(super().as_view(**initkwargs))

def get_query_params(self):
"""
Parse query parameters that this API supports:
- app (ignored unless appversion, then mandatory)
- type (optional)
- appversion (optional, makes type mandatory)
- author (optional)
Can raise ParseError() in case a mandatory parameter is missing or a
parameter is invalid.
Returns a dict containing application (int), types (tuple or None),
appversions (dict or None) and author (string or None).
"""
# appversion parameter is optional.
if AddonAppVersionQueryParam.query_param in self.request.GET:
# app parameter is mandatory with appversion
try:
application = AddonAppQueryParam(self.request.GET).get_value()
except ValueError:
raise exceptions.ParseError(
'Invalid or missing app parameter while appversion parameter is '
'set.'
)
try:
value = AddonAppVersionQueryParam(self.request.GET).get_values()
appversions = {'min': value[1], 'max': value[2]}
except ValueError:
raise exceptions.ParseError('Invalid appversion parameter.')
else:
appversions = None
application = None

# type is optional, unless appversion is set. That's because the way
# dicts and language packs have their compatibility info set in the
# database differs, so to make things simpler for us we force clients
# to filter by type if they want appversion filtering.
if AddonTypeQueryParam.query_param in self.request.GET or appversions:
try:
addon_types = tuple(AddonTypeQueryParam(self.request.GET).get_values())
except ValueError:
raise exceptions.ParseError(
'Invalid or missing type parameter while appversion '
'parameter is set.'
)
else:
addon_types = (amo.ADDON_LPAPP, amo.ADDON_DICT)

# author is optional. It's a string representing the username(s) we're
# filtering on.
if AddonAuthorQueryParam.query_param in self.request.GET:
authors = AddonAuthorQueryParam(self.request.GET).get_values()
else:
authors = None

return {
'application': application,
'types': addon_types,
'appversions': appversions,
'authors': authors,
}

def get_queryset(self):
"""
Return queryset to use for this view, depending on query parameters.
"""
# application, addon_types, appversions
params = self.get_query_params()
if params['types'] == (amo.ADDON_LPAPP,) and params['appversions']:
qs = self.get_language_packs_queryset_with_appversions(
params['application'], params['appversions']
)
else:
qs = self.get_queryset_base(params['types'])

if params['authors']:
qs = qs.filter(
addonuser__user__username__in=params['authors'], addonuser__listed=True
).distinct()
qs = Addon.objects.public().exclude(target_locale='').only_translations().order_by()
return qs

def get_queryset_base(self, addon_types):
"""
Return base queryset to be used as the starting point in both
get_queryset() and get_language_packs_queryset_with_appversions().
"""
return (
Addon.objects.public()
.filter(
type__in=addon_types,
target_locale__isnull=False,
)
.exclude(target_locale='')
# Deactivate default transforms which fetch a ton of stuff we
# don't need here like authors, previews or current version.
# It would be nice to avoid translations entirely, because the
# translations transformer is going to fetch a lot of translations
# we don't need, but some language packs or dictionaries have
# custom names, so we can't use a generic one for them...
.only_translations()
# FIXME: we want to prefetch file.webext_permission instances in here
# Since we're fetching everything with no pagination, might as well
# not order it.
.order_by()
)

def get_language_packs_queryset_with_appversions(self, application, appversions):
"""
Return queryset to use specifically when requesting language packs
compatible with a given app + versions.
application is an application id, and appversions is a dict with min
and max keys pointing to application versions expressed as ints.
"""
# Base queryset.
qs = self.get_queryset_base((amo.ADDON_LPAPP,))
# Version queryset we'll prefetch once for all results. We need to
# find the ones compatible with the app+appversion requested, and we
# can avoid loading translations by removing transforms and then
# re-applying the default one that takes care of the compat info.
versions_qs = (
Version.objects.latest_public_compatible_with(application, appversions)
.no_transforms()
.transform(Version.transformer)
)
return (
qs.prefetch_related(
Prefetch(
'versions', to_attr='compatible_versions', queryset=versions_qs
)
)
.filter(
versions__apps__application=application,
versions__apps__min__version_int__lte=appversions['min'],
versions__apps__max__version_int__gte=appversions['max'],
versions__channel=amo.CHANNEL_LISTED,
versions__file__status=amo.STATUS_APPROVED,
)
.distinct()
)

def list(self, request, *args, **kwargs):
# Ignore pagination (return everything) but do wrap the data in a
# 'results' property to mimic what the default implementation of list()
Expand All @@ -1184,7 +1093,6 @@ def list(self, request, *args, **kwargs):
patch_cache_control(response, max_age=60 * 60 * 24)
return response


class ReplacementAddonView(ListAPIView):
authentication_classes = []
queryset = ReplacementAddon.objects.all()
Expand Down
Loading

0 comments on commit a29722e

Please sign in to comment.