Merge per-application categories, add command to merge and clean up obsolete ones (#21212)
Expose categories flat in API v5, add compatibility shim for previous versions.
This commit is contained in:
Родитель
b80a1fb979
Коммит
e5de4ecc7c
|
@ -14,9 +14,9 @@ Category List
|
|||
|
||||
.. _category-list:
|
||||
|
||||
Categories are defined by a name, a slug, a type and an application. Slugs are
|
||||
only guaranteed to be unique for a given ``app`` and ``type`` combination, and
|
||||
can therefore be re-used for different categories.
|
||||
Categories are defined by a name, a slug and a type. Slugs are
|
||||
only guaranteed to be unique for a given ``type``, and can therefore be re-used
|
||||
for different categories.
|
||||
|
||||
This endpoint is not paginated.
|
||||
|
||||
|
@ -25,7 +25,6 @@ This endpoint is not paginated.
|
|||
:>json int id: The category id.
|
||||
:>json string name: The category name. Returns the already translated string.
|
||||
:>json string slug: The category slug. See :ref:`csv table <category-csv-table>` for more possible values.
|
||||
:>json string application: Application, see :ref:`add-on application <addon-detail-application>` for more details.
|
||||
:>json boolean misc: Whether or not the category is miscellaneous.
|
||||
:>json string type: Category type, see :ref:`add-on type <addon-detail-type>` for more details.
|
||||
:>json int weight: Category weight used in sort ordering.
|
||||
|
@ -39,102 +38,37 @@ Current categories
|
|||
------------------
|
||||
|
||||
.. csv-table::
|
||||
:header: "Name", "Slug", "Type", "Application"
|
||||
:header: "Name", "Slug", "Type"
|
||||
|
||||
"Alerts & Updates", alerts-updates, extension, firefox
|
||||
"Appearance", appearance, extension, firefox
|
||||
"Bookmarks", bookmarks, extension, firefox
|
||||
"Download Management", download-management, extension, firefox
|
||||
"Feeds, News & Blogging", feeds-news-blogging, extension, firefox
|
||||
"Games & Entertainment", games-entertainment, extension, firefox
|
||||
"Language Support", language-support, extension, firefox
|
||||
"Photos, Music & Videos", photos-music-videos, extension, firefox
|
||||
"Privacy & Security", privacy-security, extension, firefox
|
||||
"Search Tools", search-tools, extension, firefox
|
||||
"Shopping", shopping, extension, firefox
|
||||
"Social & Communication", social-communication, extension, firefox
|
||||
"Tabs", tabs, extension, firefox
|
||||
"Web Development", web-development, extension, firefox
|
||||
"Other", other, extension, firefox
|
||||
"Animals", animals, theme, firefox
|
||||
"Compact", compact, theme, firefox
|
||||
"Large", large, theme, firefox
|
||||
"Miscellaneous", miscellaneous, theme, firefox
|
||||
"Modern", modern, theme, firefox
|
||||
"Nature", nature, theme, firefox
|
||||
"OS Integration", os-integration, theme, firefox
|
||||
"Retro", retro, theme, firefox
|
||||
"Sports", sports, theme, firefox
|
||||
"General", general, dictionary, firefox
|
||||
"Bookmarks", bookmarks, search, firefox
|
||||
"Business", business, search, firefox
|
||||
"Dictionaries & Encyclopedias", dictionaries-encyclopedias, search, firefox
|
||||
"General", general, search, firefox
|
||||
"Kids", kids, search, firefox
|
||||
"Multiple Search", multiple-search, search, firefox
|
||||
"Music", music, search, firefox
|
||||
"News & Blogs", news-blogs, search, firefox
|
||||
"Photos & Images", photos-images, search, firefox
|
||||
"Shopping & E-Commerce", shopping-e-commerce, search, firefox
|
||||
"Social & People", social-people, search, firefox
|
||||
"Sports", sports, search, firefox
|
||||
"Travel", travel, search, firefox
|
||||
"Video", video, search, firefox
|
||||
"General", general, language, firefox
|
||||
"Abstract", abstract, persona, firefox
|
||||
"Causes", causes, persona, firefox
|
||||
"Fashion", fashion, persona, firefox
|
||||
"Film and TV", film-and-tv, persona, firefox
|
||||
"Firefox", firefox, persona, firefox
|
||||
"Foxkeh", foxkeh, persona, firefox
|
||||
"Holiday", holiday, persona, firefox
|
||||
"Music", music, persona, firefox
|
||||
"Nature", nature, persona, firefox
|
||||
"Other", other, persona, firefox
|
||||
"Scenery", scenery, persona, firefox
|
||||
"Seasonal", seasonal, persona, firefox
|
||||
"Solid", solid, persona, firefox
|
||||
"Sports", sports, persona, firefox
|
||||
"Websites", websites, persona, firefox
|
||||
"Appearance and Customization", appearance, extension, thunderbird
|
||||
"Calendar and Date/Time", calendar, extension, thunderbird
|
||||
"Chat and IM", chat, extension, thunderbird
|
||||
"Contacts", contacts, extension, thunderbird
|
||||
"Folders and Filters", folders-and-filters, extension, thunderbird
|
||||
"Import/Export", importexport, extension, thunderbird
|
||||
"Language Support", language-support, extension, thunderbird
|
||||
"Message Composition", composition, extension, thunderbird
|
||||
"Message and News Reading", message-and-news-reading, extension, thunderbird
|
||||
"Miscellaneous", miscellaneous, extension, thunderbird
|
||||
"Privacy and Security", privacy-and-security, extension, thunderbird
|
||||
"Tags", tags, extension, thunderbird
|
||||
"Compact", compact, theme, thunderbird
|
||||
"Miscellaneous", miscellaneous, theme, thunderbird
|
||||
"Modern", modern, theme, thunderbird
|
||||
"Nature", nature, theme, thunderbird
|
||||
"General", general, dictionary, thunderbird
|
||||
"General", general, language, thunderbird
|
||||
"Bookmarks", bookmarks, extension, seamonkey
|
||||
"Downloading and File Management", downloading-and-file-management, extension, seamonkey
|
||||
"Interface Customizations", interface-customizations, extension, seamonkey
|
||||
"Language Support and Translation", language-support-and-translation, extension, seamonkey
|
||||
"Miscellaneous", miscellaneous, extension, seamonkey
|
||||
"Photos and Media", photos-and-media, extension, seamonkey
|
||||
"Privacy and Security", privacy-and-security, extension, seamonkey
|
||||
"RSS, News and Blogging", rss-news-and-blogging, extension, seamonkey
|
||||
"Search Tools", search-tools, extension, seamonkey
|
||||
"Site-specific", site-specific, extension, seamonkey
|
||||
"Web and Developer Tools", web-and-developer-tools, extension, seamonkey
|
||||
"Miscellaneous", miscellaneous, theme, seamonkey
|
||||
"General", general, dictionary, seamonkey
|
||||
"General", general, language, seamonkey
|
||||
"Device Features & Location", device-features-location, extension, android
|
||||
"Experimental", experimental, extension, android
|
||||
"Feeds, News, & Blogging", feeds-news-blogging, extension, android
|
||||
"Performance", performance, extension, android
|
||||
"Photos & Media", photos-media, extension, android
|
||||
"Security & Privacy", security-privacy, extension, android
|
||||
"Shopping", shopping, extension, android
|
||||
"Social Networking", social-networking, extension, android
|
||||
"Sports & Games", sports-games, extension, android
|
||||
"User Interface", user-interface, extension, android
|
||||
"Alerts & Updates", alerts-updates, extension
|
||||
"Appearance", appearance, extension
|
||||
"Bookmarks", bookmarks, extension
|
||||
"Download Management", download-management, extension
|
||||
"Feeds, News & Blogging", feeds-news-blogging, extension
|
||||
"Games & Entertainment", games-entertainment, extension
|
||||
"Language Support", language-support, extension
|
||||
"Photos, Music & Videos", photos-music-videos, extension
|
||||
"Privacy & Security", privacy-security, extension
|
||||
"Search Tools", search-tools, extension
|
||||
"Shopping", shopping, extension
|
||||
"Social & Communication", social-communication, extension
|
||||
"Tabs", tabs, extension
|
||||
"Web Development", web-development, extension
|
||||
"Other", other, extension
|
||||
"General", general, dictionary
|
||||
"General", general, language
|
||||
"Abstract", abstract, statictheme
|
||||
"Causes", causes, statictheme
|
||||
"Fashion", fashion, statictheme
|
||||
"Film and TV", film-and-tv, statictheme
|
||||
"Firefox", firefox, statictheme
|
||||
"Foxkeh", foxkeh, statictheme
|
||||
"Holiday", holiday, statictheme
|
||||
"Music", music, statictheme
|
||||
"Nature", nature, statictheme
|
||||
"Other", other, statictheme
|
||||
"Scenery", scenery, statictheme
|
||||
"Seasonal", seasonal, statictheme
|
||||
"Solid", solid, statictheme
|
||||
"Sports", sports, statictheme
|
||||
"Websites", websites, statictheme
|
||||
|
|
|
@ -466,6 +466,7 @@ These are `v5` specific changes - `v4` changes apply also.
|
|||
* 2023-07-06: added ``is_all_versions`` to blocklist block endpoint. https://github.com/mozilla/addons-server/issues/20857
|
||||
* 2023-10-12: added ``reporter_name`` and ``reporter_email`` as two optional alternatives to an authenticated reporter in the abuse api. https://github.com/mozilla/addons-server/issues/21268
|
||||
* 2023-10-26: added ``location`` to abuse api. https://github.com/mozilla/addons-server/issues/21330
|
||||
* 2023-11-02: removed ``application`` from categories endpoint, flattened ``categories`` in addon detail/search endpoint. https://github.com/mozilla/addons-server/issues/5989
|
||||
|
||||
.. _`#11380`: https://github.com/mozilla/addons-server/issues/11380/
|
||||
.. _`#11379`: https://github.com/mozilla/addons-server/issues/11379/
|
||||
|
|
|
@ -15,15 +15,15 @@ from rest_framework import exceptions, serializers
|
|||
|
||||
from olympia import amo
|
||||
from olympia.amo.templatetags.jinja_helpers import absolutify
|
||||
from olympia.amo.utils import ImageCheck, sorted_groupby
|
||||
from olympia.amo.utils import ImageCheck
|
||||
from olympia.api.fields import (
|
||||
ESTranslationSerializerField,
|
||||
GetTextTranslationSerializerField,
|
||||
OutgoingURLField,
|
||||
TranslationSerializerField,
|
||||
)
|
||||
from olympia.api.utils import is_gate_active
|
||||
from olympia.applications.models import AppVersion
|
||||
from olympia.constants.applications import APPS
|
||||
from olympia.constants.categories import CATEGORIES
|
||||
from olympia.constants.licenses import LICENSES_BY_SLUG
|
||||
from olympia.files.utils import SafeTar, SafeZip
|
||||
|
@ -35,51 +35,57 @@ from olympia.versions.models import (
|
|||
|
||||
|
||||
class CategoriesSerializerField(serializers.Field):
|
||||
@property
|
||||
def flat(self):
|
||||
request = self.context.get('request', None)
|
||||
return not is_gate_active(request, 'categories-application')
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
categories = []
|
||||
for app_name, category_names in data.items():
|
||||
if len(category_names) > amo.MAX_CATEGORIES:
|
||||
raise exceptions.ValidationError(
|
||||
gettext(
|
||||
'Maximum number of categories per application '
|
||||
'({MAX_CATEGORIES}) exceeded'
|
||||
).format(MAX_CATEGORIES=amo.MAX_CATEGORIES)
|
||||
)
|
||||
if len(category_names) > 1 and 'other' in category_names:
|
||||
raise exceptions.ValidationError(
|
||||
gettext(
|
||||
'The "other" category cannot be combined with another '
|
||||
'category'
|
||||
)
|
||||
)
|
||||
app_cats = CATEGORIES[APPS[app_name].id]
|
||||
# We don't know the addon_type at this point, so try them all and we'll
|
||||
# drop anything that's wrong later in AddonSerializer.validate
|
||||
all_cat_slugs = set()
|
||||
for type_cats in app_cats.values():
|
||||
categories.extend(
|
||||
type_cats[name] for name in category_names if name in type_cats
|
||||
)
|
||||
all_cat_slugs.update(type_cats.keys())
|
||||
# Now double-check all the category names were found
|
||||
if not all_cat_slugs.issuperset(category_names):
|
||||
raise exceptions.ValidationError(gettext('Invalid category name.'))
|
||||
if not categories and self.required:
|
||||
self.fail('required')
|
||||
return categories
|
||||
except KeyError:
|
||||
raise exceptions.ValidationError(gettext('Invalid app name.'))
|
||||
# Basic backwards-compatibility: accept categories as a dict, but only
|
||||
# look at the Firefox ones since we removed the Android ones.
|
||||
if isinstance(data, dict):
|
||||
category_names = data.get('firefox', [])
|
||||
else:
|
||||
category_names = data
|
||||
if not isinstance(category_names, list):
|
||||
raise exceptions.ValidationError(gettext('Invalid value'))
|
||||
|
||||
if len(category_names) > amo.MAX_CATEGORIES:
|
||||
raise exceptions.ValidationError(
|
||||
gettext(
|
||||
'Maximum number of categories per application '
|
||||
'({MAX_CATEGORIES}) exceeded'
|
||||
).format(MAX_CATEGORIES=amo.MAX_CATEGORIES)
|
||||
)
|
||||
if len(category_names) > 1 and 'other' in category_names:
|
||||
raise exceptions.ValidationError(
|
||||
gettext(
|
||||
'The "other" category cannot be combined with another ' 'category'
|
||||
)
|
||||
)
|
||||
|
||||
categories = []
|
||||
# We don't know the addon_type at this point, so try them all and we'll
|
||||
# drop anything that's wrong later in AddonSerializer.validate
|
||||
all_cat_slugs = set()
|
||||
for type_cats in CATEGORIES.values():
|
||||
categories.extend(
|
||||
type_cats[name] for name in category_names if name in type_cats
|
||||
)
|
||||
all_cat_slugs.update(type_cats.keys())
|
||||
# Now double-check all the category names were found
|
||||
if not all_cat_slugs.issuperset(category_names):
|
||||
raise exceptions.ValidationError(gettext('Invalid category name.'))
|
||||
|
||||
if not categories and self.required:
|
||||
self.fail('required')
|
||||
return categories
|
||||
|
||||
def to_representation(self, value):
|
||||
grouped = sorted_groupby(
|
||||
sorted(value),
|
||||
key=lambda x: getattr(amo.APP_IDS.get(x.application), 'short', ''),
|
||||
)
|
||||
return {
|
||||
app_name: [cat.slug for cat in categories]
|
||||
for app_name, categories in grouped
|
||||
}
|
||||
if self.flat:
|
||||
return [cat.slug for cat in value]
|
||||
else:
|
||||
return {amo.FIREFOX.short: [cat.slug for cat in value]}
|
||||
|
||||
|
||||
class ContributionSerializerField(OutgoingURLField):
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import time
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
import olympia.core.logger
|
||||
from olympia.addons.models import AddonCategory
|
||||
from olympia.constants.categories import CATEGORIES_BY_ID
|
||||
|
||||
|
||||
# Mapping between old Android categories and new global ones.
|
||||
MAPPING_BY_ID = {
|
||||
145: 73, # 'device-features-location' —> 'other'
|
||||
151: 73, # 'experimental' —> 'other'
|
||||
147: 1, # 'feeds-news-blogging' —> 'feeds-news-blogging'
|
||||
144: 73, # 'performance' —> 'other'
|
||||
143: 38, # 'photos-media' —> 'photos-music-videos'
|
||||
149: 12, # 'security-privacy' —> 'privacy-security'
|
||||
150: 141, # 'shopping' —> 'shopping'
|
||||
148: 71, # 'social-networking' —> 'social-communication'
|
||||
146: 142, # 'sports-games' —> 'games-entertainment'
|
||||
152: 14, # 'user-interface' —> 'appearance'
|
||||
153: 73, # 'other' —> 'other
|
||||
}
|
||||
|
||||
log = olympia.core.logger.getLogger('z.addons.update_and_clean_categories')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
BATCH_SIZE = 1000
|
||||
|
||||
def add_new_categories_for_old_android_categories(self):
|
||||
for old_category_id, new_category_id in MAPPING_BY_ID.items():
|
||||
addon_ids = AddonCategory.objects.filter(
|
||||
category_id=old_category_id
|
||||
).values_list('addon_id', flat=True)
|
||||
addon_categories = [
|
||||
AddonCategory(category_id=new_category_id, addon_id=addon_id)
|
||||
for addon_id in addon_ids
|
||||
]
|
||||
# We can't run a single UPDATE query, because we might run into
|
||||
# constraint violations, as we could potentially force an add-on to
|
||||
# have the same category twice. To work around that we create new
|
||||
# categories instead in bulk, ignoring conflicts.
|
||||
# The old extra categories will be deleted later.
|
||||
objs = AddonCategory.objects.bulk_create(
|
||||
addon_categories,
|
||||
batch_size=self.BATCH_SIZE,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
log.info('Created (or ignored) %d AddonCategory rows', len(objs))
|
||||
log.info('Done updating old android categories')
|
||||
|
||||
def delete_old_categories(self):
|
||||
qs = AddonCategory.objects.exclude(
|
||||
category_id__in=list(CATEGORIES_BY_ID.keys())
|
||||
)
|
||||
threshold = qs.order_by('-pk').values_list('pk', flat=True).first()
|
||||
ceiling = qs.order_by('pk').values_list('pk', flat=True).first()
|
||||
count = 0
|
||||
while threshold and ceiling and threshold >= ceiling:
|
||||
print(f'In loop {threshold}')
|
||||
try:
|
||||
# Delete by batch. Django doesn't support deleting with a limit
|
||||
# and offset, but that's inefficient anyway, so we do it by pk,
|
||||
# deleting _at most_ BATCH_SIZE per iteration.
|
||||
threshold -= self.BATCH_SIZE
|
||||
loop_count = qs.filter(
|
||||
pk__gte=threshold, pk__lte=threshold + self.BATCH_SIZE
|
||||
).delete()[0]
|
||||
except IndexError:
|
||||
break
|
||||
log.info('Deleted %d AddonCategory rows', loop_count)
|
||||
if loop_count:
|
||||
time.sleep(1)
|
||||
count += loop_count
|
||||
log.info('Done deleting %d obsolete categories', count)
|
||||
|
||||
def handle(self):
|
||||
self.add_new_categories_for_old_android_categories()
|
||||
self.delete_old_categories()
|
|
@ -1640,17 +1640,6 @@ class Addon(OnChangeMixin, ModelBase):
|
|||
def has_per_version_previews(self):
|
||||
return self.type == amo.ADDON_STATICTHEME
|
||||
|
||||
@property
|
||||
def app_categories(self):
|
||||
app_cats = {}
|
||||
categories = sorted_groupby(
|
||||
sorted(self.all_categories),
|
||||
key=lambda x: getattr(amo.APP_IDS.get(x.application), 'short', ''),
|
||||
)
|
||||
for app, cats in categories:
|
||||
app_cats[app] = list(cats)
|
||||
return app_cats
|
||||
|
||||
def remove_locale(self, locale):
|
||||
"""NULLify strings in this locale for the add-on and versions."""
|
||||
for o in itertools.chain([self], self.versions.all()):
|
||||
|
|
|
@ -1613,8 +1613,16 @@ class StaticCategorySerializer(serializers.Serializer):
|
|||
weight = serializers.IntegerField()
|
||||
description = serializers.CharField()
|
||||
|
||||
def to_representation(self, obj):
|
||||
data = super().to_representation(obj)
|
||||
request = self.context['request']
|
||||
if request and not is_gate_active(request, 'categories-application'):
|
||||
data.pop('application', None)
|
||||
return data
|
||||
|
||||
def get_application(self, obj):
|
||||
return APPS_ALL[obj.application].short
|
||||
# Fake application for API < v5
|
||||
return amo.FIREFOX.short
|
||||
|
||||
def get_type(self, obj):
|
||||
return ADDON_TYPE_CHOICES_API[obj.type]
|
||||
|
|
|
@ -15,8 +15,9 @@ from olympia.abuse.models import AbuseReport
|
|||
from olympia.addons.management.commands import (
|
||||
fix_langpacks_with_max_version_star,
|
||||
process_addons,
|
||||
update_and_clean_categories,
|
||||
)
|
||||
from olympia.addons.models import Addon, DeniedGuid
|
||||
from olympia.addons.models import Addon, AddonCategory, DeniedGuid
|
||||
from olympia.amo.tests import (
|
||||
TestCase,
|
||||
addon_factory,
|
||||
|
@ -24,6 +25,7 @@ from olympia.amo.tests import (
|
|||
version_factory,
|
||||
)
|
||||
from olympia.applications.models import AppVersion
|
||||
from olympia.constants.categories import CATEGORIES
|
||||
from olympia.files.models import FileValidation
|
||||
from olympia.ratings.models import Rating, RatingAggregate
|
||||
from olympia.reviewers.models import AutoApprovalSummary
|
||||
|
@ -576,3 +578,105 @@ def test_delete_list_theme_previews():
|
|||
assert VersionPreview.objects.filter(id=other_firefox_preview.id).exists()
|
||||
assert VersionPreview.objects.filter(id=other_amo_preview.id).exists()
|
||||
assert not VersionPreview.objects.filter(id=other_old_list_preview.id).exists()
|
||||
|
||||
|
||||
class TestUpdateAndCleanCategoriesCommand(TestCase):
|
||||
def test_add_new_categories_for_old_android_categories(self):
|
||||
regular_addon = addon_factory(
|
||||
category=CATEGORIES[amo.ADDON_EXTENSION]['bookmarks']
|
||||
)
|
||||
addon_with_obsolete_category_and_other = addon_factory(
|
||||
category=CATEGORIES[amo.ADDON_EXTENSION]['other']
|
||||
)
|
||||
addon_with_obsolete_category_and_regular_one = addon_factory(
|
||||
category=CATEGORIES[amo.ADDON_EXTENSION]['shopping']
|
||||
)
|
||||
AddonCategory.objects.create(
|
||||
# Obsolete category that would map to `other` which the add-on
|
||||
# already has.
|
||||
addon=addon_with_obsolete_category_and_other,
|
||||
category_id=153,
|
||||
)
|
||||
AddonCategory.objects.create(
|
||||
# Obsolete extra category that maps to `feeds-news-blogging`.
|
||||
addon=addon_with_obsolete_category_and_regular_one,
|
||||
category_id=147,
|
||||
)
|
||||
assert AddonCategory.objects.count() == 5
|
||||
|
||||
command = update_and_clean_categories.Command()
|
||||
command.add_new_categories_for_old_android_categories()
|
||||
|
||||
assert AddonCategory.objects.count() == 6
|
||||
# Existing categories unchanged (even the obsolete ones)
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=regular_addon,
|
||||
category_id=CATEGORIES[amo.ADDON_EXTENSION]['bookmarks'].id,
|
||||
).exists()
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=addon_with_obsolete_category_and_other,
|
||||
category_id=CATEGORIES[amo.ADDON_EXTENSION]['other'].id,
|
||||
).exists()
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=addon_with_obsolete_category_and_regular_one,
|
||||
category_id=CATEGORIES[amo.ADDON_EXTENSION]['shopping'].id,
|
||||
).exists()
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=addon_with_obsolete_category_and_other, category_id=153
|
||||
).exists()
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=addon_with_obsolete_category_and_regular_one, category_id=147
|
||||
).exists()
|
||||
# New category added to replace 147. No category was added for 153
|
||||
# since it maps to `other`, which would be a duplicate.
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=addon_with_obsolete_category_and_regular_one,
|
||||
category_id=CATEGORIES[amo.ADDON_EXTENSION]['feeds-news-blogging'].id,
|
||||
).exists()
|
||||
|
||||
def test_delete_old_categories(self):
|
||||
regular_addon = addon_factory(
|
||||
category=CATEGORIES[amo.ADDON_EXTENSION]['bookmarks']
|
||||
)
|
||||
addon_with_obsolete_category_and_other = addon_factory(
|
||||
category=CATEGORIES[amo.ADDON_EXTENSION]['other']
|
||||
)
|
||||
addon_with_obsolete_category_and_regular_one = addon_factory(
|
||||
category=CATEGORIES[amo.ADDON_EXTENSION]['shopping']
|
||||
)
|
||||
addon_with_bad_category = addon_factory()
|
||||
AddonCategory.objects.create(
|
||||
# Obsolete category that would map to `other` which the add-on
|
||||
# already has.
|
||||
addon=addon_with_obsolete_category_and_other,
|
||||
category_id=153,
|
||||
)
|
||||
AddonCategory.objects.create(
|
||||
# Obsolete extra category that maps to `feeds-news-blogging`.
|
||||
addon=addon_with_obsolete_category_and_regular_one,
|
||||
category_id=147,
|
||||
)
|
||||
AddonCategory.objects.filter(addon=addon_with_bad_category).update(
|
||||
# Obsolete extra category that doesn't map to anything.
|
||||
category_id=666,
|
||||
)
|
||||
assert AddonCategory.objects.count() == 6
|
||||
|
||||
command = update_and_clean_categories.Command()
|
||||
command.BATCH_SIZE = 1
|
||||
command.delete_old_categories()
|
||||
|
||||
assert AddonCategory.objects.count() == 3
|
||||
# Non-obsolete categories were kept.
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=regular_addon,
|
||||
category_id=CATEGORIES[amo.ADDON_EXTENSION]['bookmarks'].id,
|
||||
).exists()
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=addon_with_obsolete_category_and_other,
|
||||
category_id=CATEGORIES[amo.ADDON_EXTENSION]['other'].id,
|
||||
).exists()
|
||||
assert AddonCategory.objects.filter(
|
||||
addon=addon_with_obsolete_category_and_regular_one,
|
||||
category_id=CATEGORIES[amo.ADDON_EXTENSION]['shopping'].id,
|
||||
).exists()
|
||||
|
|
|
@ -1166,77 +1166,6 @@ class TestAddonModels(TestCase):
|
|||
|
||||
assert self.newlines_helper(before) == after
|
||||
|
||||
def test_app_categories(self):
|
||||
def get_addon():
|
||||
return Addon.objects.get(pk=3615)
|
||||
|
||||
# This add-on is already associated with three Firefox categories
|
||||
# using fixtures: Bookmarks, Feeds, Social.
|
||||
FIREFOX_EXT_CATS = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]
|
||||
expected_firefox_cats = [
|
||||
FIREFOX_EXT_CATS['bookmarks'],
|
||||
FIREFOX_EXT_CATS['feeds-news-blogging'],
|
||||
FIREFOX_EXT_CATS['social-communication'],
|
||||
]
|
||||
|
||||
addon = get_addon()
|
||||
assert sorted(addon.all_categories) == expected_firefox_cats
|
||||
assert addon.app_categories == {'firefox': expected_firefox_cats}
|
||||
|
||||
# Let's add a ANDROID category.
|
||||
android_category = CATEGORIES[amo.ANDROID.id][amo.ADDON_EXTENSION][
|
||||
'sports-games'
|
||||
]
|
||||
AddonCategory.objects.create(addon=addon, category_id=android_category.id)
|
||||
|
||||
# Reload the addon to get a fresh, uncached categories list.
|
||||
addon = get_addon()
|
||||
|
||||
# Test that the ANDROID category was added correctly.
|
||||
assert sorted(addon.all_categories) == sorted(
|
||||
expected_firefox_cats + [android_category]
|
||||
)
|
||||
assert sorted(addon.app_categories.keys()) == ['android', 'firefox']
|
||||
assert addon.app_categories['firefox'] == expected_firefox_cats
|
||||
assert addon.app_categories['android'] == [android_category]
|
||||
|
||||
def test_app_categories_ignore_unknown_cats(self):
|
||||
def get_addon():
|
||||
return Addon.objects.get(pk=3615)
|
||||
|
||||
# This add-on is already associated with three Firefox categories
|
||||
# using fixtures: Bookmarks, Feeds, Social.
|
||||
FIREFOX_EXT_CATS = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]
|
||||
expected_firefox_cats = [
|
||||
FIREFOX_EXT_CATS['bookmarks'],
|
||||
FIREFOX_EXT_CATS['feeds-news-blogging'],
|
||||
FIREFOX_EXT_CATS['social-communication'],
|
||||
]
|
||||
|
||||
addon = get_addon()
|
||||
assert sorted(addon.all_categories) == sorted(expected_firefox_cats)
|
||||
assert addon.app_categories == {'firefox': expected_firefox_cats}
|
||||
|
||||
# Associate this add-on with a couple more categories, including
|
||||
# one that does not exist in the constants.
|
||||
AddonCategory.objects.create(addon=addon, category_id=12345)
|
||||
android_static_cat = CATEGORIES[amo.ANDROID.id][amo.ADDON_EXTENSION][
|
||||
'sports-games'
|
||||
]
|
||||
AddonCategory.objects.create(addon=addon, category=android_static_cat)
|
||||
|
||||
# Reload the addon to get a fresh, uncached categories list.
|
||||
addon = get_addon()
|
||||
|
||||
# The sunbird category should not be present since it does not match
|
||||
# an existing static category, android one should have been added.
|
||||
assert sorted(addon.all_categories) == sorted(
|
||||
expected_firefox_cats + [android_static_cat]
|
||||
)
|
||||
assert sorted(addon.app_categories.keys()) == ['android', 'firefox']
|
||||
assert addon.app_categories['firefox'] == expected_firefox_cats
|
||||
assert addon.app_categories['android'] == [android_static_cat]
|
||||
|
||||
def test_review_replies(self):
|
||||
"""
|
||||
Make sure that developer replies are not returned as if they were
|
||||
|
@ -1408,11 +1337,7 @@ class TestAddonModels(TestCase):
|
|||
|
||||
def test_category_transform(self):
|
||||
addon = Addon.objects.get(id=3615)
|
||||
cats = CATEGORIES[amo.FIREFOX.id][addon.type].values()
|
||||
names = [c.name for c in cats]
|
||||
|
||||
appname = getattr(amo.APP_IDS.get(amo.FIREFOX.id), 'short', '')
|
||||
assert addon.app_categories.get(appname)[0].name in names
|
||||
assert addon.all_categories[0] in CATEGORIES[addon.type].values()
|
||||
|
||||
def test_listed_has_complete_metadata_no_categories(self):
|
||||
addon = Addon.objects.get(id=3615)
|
||||
|
|
|
@ -145,7 +145,7 @@ class AddonSerializerOutputTestMixin:
|
|||
assert data['version'] == version.version
|
||||
|
||||
def test_basic(self):
|
||||
cat1 = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['bookmarks']
|
||||
cat1 = CATEGORIES[amo.ADDON_EXTENSION]['bookmarks']
|
||||
license = License.objects.create(
|
||||
name={
|
||||
'en-US': 'My License',
|
||||
|
@ -214,20 +214,15 @@ class AddonSerializerOutputTestMixin:
|
|||
min=av_min,
|
||||
max=av_max,
|
||||
)
|
||||
cat2 = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['alerts-updates']
|
||||
cat2 = CATEGORIES[amo.ADDON_EXTENSION]['alerts-updates']
|
||||
AddonCategory.objects.create(addon=self.addon, category=cat2)
|
||||
cat3 = CATEGORIES[amo.ANDROID.id][amo.ADDON_EXTENSION]['sports-games']
|
||||
AddonCategory.objects.create(addon=self.addon, category=cat3)
|
||||
|
||||
result = self.serialize()
|
||||
|
||||
assert result['id'] == self.addon.pk
|
||||
|
||||
assert result['average_daily_users'] == self.addon.average_daily_users
|
||||
assert result['categories'] == {
|
||||
'firefox': ['alerts-updates', 'bookmarks'],
|
||||
'android': ['sports-games'],
|
||||
}
|
||||
assert result['categories'] == ['bookmarks', 'alerts-updates']
|
||||
|
||||
# In this serializer latest_unlisted_version is omitted.
|
||||
assert 'latest_unlisted_version' not in result
|
||||
|
@ -793,6 +788,18 @@ class AddonSerializerOutputTestMixin:
|
|||
'url': 'http://www.gnu.org/licenses/gpl-3.0.html',
|
||||
}
|
||||
|
||||
def test_categories_as_object(self):
|
||||
self.addon = addon_factory(
|
||||
category=CATEGORIES[amo.ADDON_EXTENSION]['bookmarks']
|
||||
)
|
||||
result = self.serialize()
|
||||
assert result['categories'] == ['bookmarks']
|
||||
|
||||
gates = {self.request.version: ('categories-application',)}
|
||||
with override_settings(DRF_API_GATES=gates):
|
||||
result = self.serialize()
|
||||
assert result['categories'] == {'firefox': ['bookmarks']}
|
||||
|
||||
|
||||
class TestAddonSerializerOutput(AddonSerializerOutputTestMixin, TestCase):
|
||||
serializer_class = AddonSerializer
|
||||
|
@ -1125,7 +1132,7 @@ class TestESAddonSerializerOutput(AddonSerializerOutputTestMixin, ESTestCase):
|
|||
addons[0].all_categories = [category, old_category]
|
||||
|
||||
category_name = 'music'
|
||||
category = CATEGORIES[amo.FIREFOX.id][amo.ADDON_STATICTHEME][category_name]
|
||||
category = CATEGORIES[amo.ADDON_STATICTHEME][category_name]
|
||||
old_category = copy.copy(category)
|
||||
object.__setattr__(old_category, 'id', 666666)
|
||||
object.__setattr__(old_category, 'application', amo.ANDROID.id)
|
||||
|
@ -1133,7 +1140,7 @@ class TestESAddonSerializerOutput(AddonSerializerOutputTestMixin, ESTestCase):
|
|||
|
||||
self.addon = addon_factory(type=amo.ADDON_STATICTHEME, category=category)
|
||||
result = self.serialize()
|
||||
assert result['categories'] == {amo.FIREFOX.short: [category_name]}
|
||||
assert result['categories'] == [category_name]
|
||||
|
||||
|
||||
class TestVersionSerializerOutput(TestCase):
|
||||
|
|
|
@ -959,7 +959,7 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
self.upload.update(channel=amo.CHANNEL_LISTED)
|
||||
response = self.request(
|
||||
data={
|
||||
'categories': {'firefox': ['bookmarks']},
|
||||
'categories': ['bookmarks'],
|
||||
'version': {
|
||||
'upload': self.upload.uuid,
|
||||
'license': self.license.slug,
|
||||
|
@ -1044,7 +1044,7 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
data={
|
||||
'summary': {'en-US': None},
|
||||
'name': {'en-US': None},
|
||||
'categories': {'firefox': ['bookmarks']},
|
||||
'categories': ['bookmarks'],
|
||||
'version': {
|
||||
'upload': self.upload.uuid,
|
||||
'license': self.license.slug,
|
||||
|
@ -1091,7 +1091,7 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
|
||||
def test_missing_version(self):
|
||||
self.minimal_data = {}
|
||||
response = self.request(data={'categories': {'firefox': ['bookmarks']}})
|
||||
response = self.request(data={'categories': ['bookmarks']})
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {'version': ['This field is required.']}
|
||||
assert not Addon.objects.all()
|
||||
|
@ -1099,23 +1099,21 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
def test_invalid_categories(self):
|
||||
response = self.request(
|
||||
# performance is an android category
|
||||
data={'categories': {'firefox': ['performance']}},
|
||||
data={'categories': ['performance']},
|
||||
)
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {'categories': ['Invalid category name.']}
|
||||
|
||||
response = self.request(
|
||||
# general is an firefox category but for dicts and lang packs
|
||||
data={'categories': {'firefox': ['general']}}
|
||||
data={'categories': ['general']}
|
||||
)
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {'categories': ['Invalid category name.']}
|
||||
assert not Addon.objects.all()
|
||||
|
||||
def test_other_category_cannot_be_combined(self):
|
||||
response = self.request(
|
||||
data={'categories': {'firefox': ['bookmarks', 'other']}}
|
||||
)
|
||||
response = self.request(data={'categories': ['bookmarks', 'other']})
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {
|
||||
'categories': [
|
||||
|
@ -1124,33 +1122,15 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
}
|
||||
assert not Addon.objects.all()
|
||||
|
||||
# but it's only enforced per app though.
|
||||
response = self.request(
|
||||
data={'categories': {'firefox': ['bookmarks'], 'android': ['other']}}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
def test_too_many_categories(self):
|
||||
response = self.request(
|
||||
data={
|
||||
'categories': {'android': ['performance', 'shopping', 'experimental']}
|
||||
},
|
||||
data={'categories': ['appearance', 'download-management', 'shopping']},
|
||||
)
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {
|
||||
'categories': ['Maximum number of categories per application (2) exceeded']
|
||||
}
|
||||
|
||||
# check the limit is only applied per app - more than 2 in total is okay.
|
||||
response = self.request(
|
||||
data={
|
||||
'categories': {
|
||||
'android': ['performance', 'experimental'],
|
||||
'firefox': ['bookmarks'],
|
||||
},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201, response.content
|
||||
assert not Addon.objects.all()
|
||||
|
||||
def test_set_slug(self):
|
||||
# Check for slugs with invalid characters in it
|
||||
|
@ -1192,7 +1172,7 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
def test_set_extra_data(self):
|
||||
self.upload.update(channel=amo.CHANNEL_LISTED)
|
||||
data = {
|
||||
'categories': {'firefox': ['bookmarks']},
|
||||
'categories': ['bookmarks'],
|
||||
'description': {'en-US': 'new description'},
|
||||
'developer_comments': {'en-US': 'comments'},
|
||||
'homepage': {'en-US': 'https://my.home.page/'},
|
||||
|
@ -1214,10 +1194,9 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
assert response.status_code == 201, response.content
|
||||
addon = Addon.objects.get()
|
||||
data = response.data
|
||||
assert data['categories'] == {'firefox': ['bookmarks']}
|
||||
assert addon.all_categories == [
|
||||
CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['bookmarks']
|
||||
]
|
||||
assert data['categories'] == ['bookmarks'] # v5 representation
|
||||
assert addon.all_categories == [CATEGORIES[amo.ADDON_EXTENSION]['bookmarks']]
|
||||
response = {'lol': 'blah'}
|
||||
assert data['description'] == {'en-US': 'new description'}
|
||||
assert addon.description == 'new description'
|
||||
assert data['developer_comments'] == {'en-US': 'comments'}
|
||||
|
@ -1546,7 +1525,7 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
response = self.request(
|
||||
data={
|
||||
'version': {'upload': upload.uuid, 'license': self.license.slug},
|
||||
'categories': {'firefox': ['other']},
|
||||
'categories': ['other'],
|
||||
'support_email': { # this field has the required locales
|
||||
'it': 'rusiczki.ioana@gmail.com',
|
||||
'ro': 'rusiczki.ioana@gmail.com',
|
||||
|
@ -1573,7 +1552,7 @@ class TestAddonViewSetCreate(UploadMixin, AddonViewSetCreateUpdateMixin, TestCas
|
|||
response = self.request(
|
||||
data={
|
||||
'version': {'upload': upload.uuid, 'license': self.license.slug},
|
||||
'categories': {'firefox': ['other']},
|
||||
'categories': ['other'],
|
||||
'support_email': {
|
||||
'it': 'rusiczki.ioana@gmail.com',
|
||||
'ro': 'rusiczki.ioana@gmail.com',
|
||||
|
@ -2038,47 +2017,47 @@ class TestAddonViewSetUpdate(AddonViewSetCreateUpdateMixin, TestCase):
|
|||
assert self.addon.current_version.reload().release_notes != 'new notes'
|
||||
|
||||
def test_update_categories(self):
|
||||
bookmarks_cat = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['bookmarks']
|
||||
tabs_cat = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['tabs']
|
||||
other_cat = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['other']
|
||||
bookmarks_cat = CATEGORIES[amo.ADDON_EXTENSION]['bookmarks']
|
||||
tabs_cat = CATEGORIES[amo.ADDON_EXTENSION]['tabs']
|
||||
other_cat = CATEGORIES[amo.ADDON_EXTENSION]['other']
|
||||
AddonCategory.objects.filter(addon=self.addon).update(category_id=tabs_cat.id)
|
||||
assert self.addon.app_categories == {'firefox': [tabs_cat]}
|
||||
assert self.addon.all_categories == [tabs_cat]
|
||||
|
||||
response = self.request(data={'categories': {'firefox': ['bookmarks']}})
|
||||
response = self.request(data={'categories': ['bookmarks']})
|
||||
assert response.status_code == 200, response.content
|
||||
assert response.data['categories'] == {'firefox': ['bookmarks']}
|
||||
assert response.data['categories'] == ['bookmarks']
|
||||
self.addon = Addon.objects.get()
|
||||
assert self.addon.reload().app_categories == {'firefox': [bookmarks_cat]}
|
||||
assert self.addon.reload().all_categories == [bookmarks_cat]
|
||||
self.addon.versions.first().update(version='0.123.1')
|
||||
|
||||
# repeat, but with the `other` category
|
||||
response = self.request(data={'categories': {'firefox': ['other']}})
|
||||
response = self.request(data={'categories': ['other']})
|
||||
assert response.status_code == 200, response.content
|
||||
assert response.data['categories'] == {'firefox': ['other']}
|
||||
assert response.data['categories'] == ['other']
|
||||
self.addon = Addon.objects.get()
|
||||
assert self.addon.reload().app_categories == {'firefox': [other_cat]}
|
||||
assert self.addon.reload().all_categories == [other_cat]
|
||||
|
||||
def test_invalid_categories(self):
|
||||
tabs_cat = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['tabs']
|
||||
tabs_cat = CATEGORIES[amo.ADDON_EXTENSION]['tabs']
|
||||
AddonCategory.objects.filter(addon=self.addon).update(category_id=tabs_cat.id)
|
||||
assert self.addon.app_categories == {'firefox': [tabs_cat]}
|
||||
assert self.addon.all_categories == [tabs_cat]
|
||||
del self.addon.all_categories
|
||||
|
||||
response = self.request(
|
||||
# performance is an android category
|
||||
data={'categories': {'firefox': ['performance']}}
|
||||
data={'categories': ['performance']}
|
||||
)
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {'categories': ['Invalid category name.']}
|
||||
assert self.addon.reload().app_categories == {'firefox': [tabs_cat]}
|
||||
assert self.addon.reload().all_categories == [tabs_cat]
|
||||
|
||||
response = self.request(
|
||||
# general is a firefox category, but for langpacks and dicts only
|
||||
data={'categories': {'firefox': ['general']}},
|
||||
data={'categories': ['general']},
|
||||
)
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {'categories': ['Invalid category name.']}
|
||||
assert self.addon.reload().app_categories == {'firefox': [tabs_cat]}
|
||||
assert self.addon.reload().all_categories == [tabs_cat]
|
||||
|
||||
def test_set_slug_invalid(self):
|
||||
response = self.request(
|
||||
|
@ -5517,13 +5496,13 @@ class TestAddonSearchView(ESTestCase):
|
|||
assert data['results'][0]['id'] == addon.pk
|
||||
|
||||
def test_filter_by_category(self):
|
||||
category = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['alerts-updates']
|
||||
category = CATEGORIES[amo.ADDON_EXTENSION]['alerts-updates']
|
||||
addon = addon_factory(slug='my-addon', name='My Addôn', category=category)
|
||||
|
||||
self.refresh()
|
||||
|
||||
# Create an add-on in a different category.
|
||||
other_category = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['tabs']
|
||||
other_category = CATEGORIES[amo.ADDON_EXTENSION]['tabs']
|
||||
addon_factory(slug='different-addon', category=other_category)
|
||||
|
||||
self.refresh()
|
||||
|
@ -5538,7 +5517,7 @@ class TestAddonSearchView(ESTestCase):
|
|||
|
||||
def test_filter_by_category_multiple_types(self):
|
||||
def get_category(type_, name):
|
||||
return CATEGORIES[amo.FIREFOX.id][type_][name]
|
||||
return CATEGORIES[type_][name]
|
||||
|
||||
addon_ext = addon_factory(
|
||||
slug='my-addon-ext',
|
||||
|
@ -6268,7 +6247,7 @@ class TestStaticCategoryView(TestCase):
|
|||
assert response.status_code == 200
|
||||
data = json.loads(force_str(response.content))
|
||||
|
||||
assert len(data) == 43
|
||||
assert len(data) == 32
|
||||
|
||||
# some basic checks to verify integrity
|
||||
entry = data[0]
|
||||
|
@ -6278,7 +6257,6 @@ class TestStaticCategoryView(TestCase):
|
|||
'weight': 0,
|
||||
'misc': False,
|
||||
'id': 1,
|
||||
'application': 'firefox',
|
||||
'description': (
|
||||
'Download Firefox extensions that remove clutter so you '
|
||||
'can stay up-to-date on social media, catch up on blogs, '
|
||||
|
@ -6297,7 +6275,7 @@ class TestStaticCategoryView(TestCase):
|
|||
assert response.status_code == 200
|
||||
data = json.loads(force_str(response.content))
|
||||
|
||||
assert len(data) == 43
|
||||
assert len(data) == 32
|
||||
|
||||
# some basic checks to verify integrity
|
||||
entry = data[0]
|
||||
|
@ -6307,7 +6285,6 @@ class TestStaticCategoryView(TestCase):
|
|||
'weight': 0,
|
||||
'misc': False,
|
||||
'id': 1,
|
||||
'application': 'firefox',
|
||||
'description': 'does stuff',
|
||||
'type': 'extension',
|
||||
'slug': 'feeds-news-blogging',
|
||||
|
@ -6328,6 +6305,16 @@ class TestStaticCategoryView(TestCase):
|
|||
assert response.status_code == 200
|
||||
assert response['cache-control'] == 'max-age=21600'
|
||||
|
||||
@override_settings(DRF_API_GATES={'v5': ('categories-application',)})
|
||||
def test_with_application(self):
|
||||
with self.assertNumQueries(0):
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.content)
|
||||
assert len(data) == 32
|
||||
for entry in data:
|
||||
assert entry['application'] == 'firefox'
|
||||
|
||||
|
||||
class TestLanguageToolsView(TestCase):
|
||||
client_class = APITestClientSessionID
|
||||
|
|
|
@ -229,9 +229,9 @@ class CategoriesSitemap(Sitemap):
|
|||
page_size = settings.REST_FRAMEWORK['PAGE_SIZE']
|
||||
page_count_max = settings.ES_MAX_RESULT_WINDOW // page_size
|
||||
|
||||
def additems(type):
|
||||
def additems(type_):
|
||||
items = []
|
||||
for category in CATEGORIES[current_app.id][type].values():
|
||||
for category in CATEGORIES[type_].values():
|
||||
items.append((category, 1))
|
||||
pages_needed = min(
|
||||
math.ceil(addon_counts.get(category.id, 1) / page_size),
|
||||
|
|
|
@ -758,9 +758,9 @@ def addon_factory(status=amo.STATUS_APPROVED, version_kw=None, file_kw=None, **k
|
|||
for user in users:
|
||||
addon.addonuser_set.create(user=user)
|
||||
|
||||
application = version_kw.get('application', amo.FIREFOX.id)
|
||||
if not category and addon.type in CATEGORIES[application]:
|
||||
category = random.choice(list(CATEGORIES[application][addon.type].values()))
|
||||
version_kw.get('application', amo.FIREFOX.id)
|
||||
if not category and addon.type in CATEGORIES:
|
||||
category = random.choice(list(CATEGORIES[addon.type].values()))
|
||||
if category:
|
||||
AddonCategory.objects.create(addon=addon, category=category)
|
||||
|
||||
|
|
|
@ -179,18 +179,12 @@ def test_categories_sitemap():
|
|||
# without any addons we should still generate a url for each category
|
||||
empty_cats = list(CategoriesSitemap().items())
|
||||
assert empty_cats == [
|
||||
*(
|
||||
(category, 1)
|
||||
for category in CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION].values()
|
||||
),
|
||||
*(
|
||||
(category, 1)
|
||||
for category in CATEGORIES[amo.FIREFOX.id][amo.ADDON_STATICTHEME].values()
|
||||
),
|
||||
*((category, 1) for category in CATEGORIES[amo.ADDON_EXTENSION].values()),
|
||||
*((category, 1) for category in CATEGORIES[amo.ADDON_STATICTHEME].values()),
|
||||
]
|
||||
# add some addons and check we generate extra pages when frontend would paginate
|
||||
bookmarks_category = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['bookmarks']
|
||||
shopping_category = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['shopping']
|
||||
bookmarks_category = CATEGORIES[amo.ADDON_EXTENSION]['bookmarks']
|
||||
shopping_category = CATEGORIES[amo.ADDON_EXTENSION]['shopping']
|
||||
AddonCategory.objects.create(
|
||||
addon=addon_factory(category=bookmarks_category), category=shopping_category
|
||||
)
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import copy
|
||||
from functools import total_ordering
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from olympia.constants.applications import ANDROID, FIREFOX
|
||||
from olympia.constants.base import (
|
||||
_ADDON_PERSONA,
|
||||
_ADDON_SEARCH,
|
||||
_ADDON_THEME,
|
||||
ADDON_DICT,
|
||||
ADDON_EXTENSION,
|
||||
ADDON_LPAPP,
|
||||
|
@ -27,9 +22,10 @@ class StaticCategory:
|
|||
to hard to debug sporadic test-failures.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, description=None, weight=0):
|
||||
def __init__(self, *, id, name=None, description=None, weight=0):
|
||||
# Avoid triggering our own __setattr__ implementation
|
||||
# to keep immutability intact but set initial values.
|
||||
object.__setattr__(self, 'id', id)
|
||||
object.__setattr__(self, 'name', name)
|
||||
object.__setattr__(self, 'weight', weight)
|
||||
object.__setattr__(self, 'description', description)
|
||||
|
@ -38,10 +34,9 @@ class StaticCategory:
|
|||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<{}: {} ({})>'.format(
|
||||
return '<{}: {}>'.format(
|
||||
self.__class__.__name__,
|
||||
force_bytes(self),
|
||||
self.application,
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
|
@ -64,9 +59,12 @@ class StaticCategory:
|
|||
return self.id
|
||||
|
||||
|
||||
CATEGORIES_NO_APP = {
|
||||
# The category ids are used in AddonCategory. To add a category you can pick
|
||||
# any unused id.
|
||||
CATEGORIES = {
|
||||
ADDON_EXTENSION: {
|
||||
'alerts-updates': StaticCategory(
|
||||
id=72,
|
||||
name=_('Alerts & Updates'),
|
||||
description=_(
|
||||
'Download Firefox extensions that help you stay '
|
||||
|
@ -76,6 +74,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'appearance': StaticCategory(
|
||||
id=14,
|
||||
name=_('Appearance'),
|
||||
description=_(
|
||||
'Download extensions that modify the appearance of '
|
||||
|
@ -85,6 +84,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'bookmarks': StaticCategory(
|
||||
id=22,
|
||||
name=_('Bookmarks'),
|
||||
description=_(
|
||||
'Download extensions that enhance bookmarks by '
|
||||
|
@ -93,6 +93,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'download-management': StaticCategory(
|
||||
id=5,
|
||||
name=_('Download Management'),
|
||||
description=_(
|
||||
'Download Firefox extensions that can help download web, '
|
||||
|
@ -101,6 +102,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'feeds-news-blogging': StaticCategory(
|
||||
id=1,
|
||||
name=_('Feeds, News & Blogging'),
|
||||
description=_(
|
||||
'Download Firefox extensions that remove clutter so you '
|
||||
|
@ -109,6 +111,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'games-entertainment': StaticCategory(
|
||||
id=142,
|
||||
name=_('Games & Entertainment'),
|
||||
description=_(
|
||||
'Download Firefox extensions to boost your entertainment '
|
||||
|
@ -117,6 +120,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'language-support': StaticCategory(
|
||||
id=37,
|
||||
name=_('Language Support'),
|
||||
description=_(
|
||||
'Download Firefox extensions that offer language support '
|
||||
|
@ -125,6 +129,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'photos-music-videos': StaticCategory(
|
||||
id=38,
|
||||
name=_('Photos, Music & Videos'),
|
||||
description=_(
|
||||
'Download Firefox extensions that enhance photo, music '
|
||||
|
@ -133,6 +138,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'privacy-security': StaticCategory(
|
||||
id=12,
|
||||
name=_('Privacy & Security'),
|
||||
description=_(
|
||||
'Download Firefox extensions to browse privately and '
|
||||
|
@ -142,6 +148,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'search-tools': StaticCategory(
|
||||
id=13,
|
||||
name=_('Search Tools'),
|
||||
description=_(
|
||||
'Download Firefox extensions for search and look-up. '
|
||||
|
@ -150,6 +157,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'shopping': StaticCategory(
|
||||
id=141,
|
||||
name=_('Shopping'),
|
||||
description=_(
|
||||
'Download Firefox extensions that can enhance your '
|
||||
|
@ -158,6 +166,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'social-communication': StaticCategory(
|
||||
id=71,
|
||||
name=_('Social & Communication'),
|
||||
description=_(
|
||||
'Download Firefox extensions to enhance social media and '
|
||||
|
@ -166,6 +175,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'tabs': StaticCategory(
|
||||
id=93,
|
||||
name=_('Tabs'),
|
||||
description=_(
|
||||
'Download Firefox extension to customize tabs and the '
|
||||
|
@ -174,6 +184,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'web-development': StaticCategory(
|
||||
id=4,
|
||||
name=_('Web Development'),
|
||||
description=_(
|
||||
'Download Firefox extensions that feature web '
|
||||
|
@ -183,6 +194,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'other': StaticCategory(
|
||||
id=73,
|
||||
name=_('Other'),
|
||||
weight=333,
|
||||
description=_(
|
||||
|
@ -190,85 +202,10 @@ CATEGORIES_NO_APP = {
|
|||
'and creative, yet useful for those odd tasks.'
|
||||
),
|
||||
),
|
||||
# Android only categories:
|
||||
'device-features-location': StaticCategory(
|
||||
name=_('Device Features & Location'),
|
||||
description=_(
|
||||
'Download extensions to enhance Firefox for Android. '
|
||||
'Perform quick searches, free up system resources, take '
|
||||
'notes, and more.'
|
||||
),
|
||||
),
|
||||
'experimental': StaticCategory(
|
||||
name=_('Experimental'),
|
||||
description=_(
|
||||
'Download Firefox extensions that are regularly updated '
|
||||
'and ready for public testing. Your feedback informs '
|
||||
'developers on changes to make in upcoming versions.'
|
||||
),
|
||||
),
|
||||
'performance': StaticCategory(
|
||||
name=_('Performance'),
|
||||
description=_(
|
||||
'Download extensions that give Firefox a performance '
|
||||
'boost. Find extensions that help you be more productive '
|
||||
'and efficient by blocking annoying ads and more.'
|
||||
),
|
||||
),
|
||||
'photos-media': StaticCategory(
|
||||
name=_('Photos & Media'),
|
||||
description=_(
|
||||
'Download Firefox extensions to enhance photos and '
|
||||
'media. This category includes extensions to reverse '
|
||||
'search images, capture full page screenshots, and more.'
|
||||
),
|
||||
),
|
||||
'security-privacy': StaticCategory(
|
||||
name=_('Security & Privacy'),
|
||||
description=_(
|
||||
'Download Firefox extensions to surf safely and '
|
||||
'privately. Discover extensions that can stop sneaky ad '
|
||||
'trackers in their tracks, easily clear browsing '
|
||||
'history, and more.'
|
||||
),
|
||||
),
|
||||
'social-networking': StaticCategory(
|
||||
name=_('Social Networking'),
|
||||
description=_(
|
||||
'Download Firefox extensions to enhance your experience '
|
||||
'on popular social networking websites such as YouTube, '
|
||||
'GitHub, Reddit, and more.'
|
||||
),
|
||||
),
|
||||
'sports-games': StaticCategory(
|
||||
name=_('Sports & Games'),
|
||||
description=_(
|
||||
'Download Firefox extensions to give your entertainment '
|
||||
'experience a boost with live stream enhancers, sports '
|
||||
'updates, and more.'
|
||||
),
|
||||
),
|
||||
'user-interface': StaticCategory(
|
||||
name=_('User Interface'),
|
||||
description=_(
|
||||
'Download user interface Firefox extensions to alter web '
|
||||
'pages for easier reading, searching, browsing, and more.'
|
||||
),
|
||||
),
|
||||
},
|
||||
_ADDON_THEME: {
|
||||
'animals': StaticCategory(name=_('Animals')),
|
||||
'compact': StaticCategory(name=_('Compact')),
|
||||
'large': StaticCategory(name=_('Large')),
|
||||
'miscellaneous': StaticCategory(name=_('Miscellaneous')),
|
||||
'modern': StaticCategory(name=_('Modern')),
|
||||
'nature': StaticCategory(name=_('Nature')),
|
||||
'os-integration': StaticCategory(name=_('OS Integration')),
|
||||
'retro': StaticCategory(name=_('Retro')),
|
||||
'sports': StaticCategory(name=_('Sports')),
|
||||
},
|
||||
ADDON_STATICTHEME: {
|
||||
'abstract': StaticCategory(
|
||||
id=300,
|
||||
name=_('Abstract'),
|
||||
description=_(
|
||||
'Download Firefox artistic and conceptual themes. This '
|
||||
|
@ -277,6 +214,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'causes': StaticCategory(
|
||||
id=320,
|
||||
name=_('Causes'),
|
||||
description=_(
|
||||
'Download Firefox themes for niche interests and topics. '
|
||||
|
@ -285,6 +223,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'fashion': StaticCategory(
|
||||
id=324,
|
||||
name=_('Fashion'),
|
||||
description=_(
|
||||
'Download Firefox themes that celebrate style of all '
|
||||
|
@ -292,6 +231,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'film-and-tv': StaticCategory(
|
||||
id=326,
|
||||
name=_('Film and TV'),
|
||||
description=_(
|
||||
'Download Firefox themes with movies and television. '
|
||||
|
@ -300,6 +240,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'firefox': StaticCategory(
|
||||
id=308,
|
||||
name=_('Firefox'),
|
||||
description=_(
|
||||
'Download Firefox themes with the Firefox browser theme. '
|
||||
|
@ -308,6 +249,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'foxkeh': StaticCategory(
|
||||
id=310,
|
||||
name=_('Foxkeh'),
|
||||
description=_(
|
||||
'Download Firefox themes with the Japanese Firefox. This '
|
||||
|
@ -316,6 +258,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'holiday': StaticCategory(
|
||||
id=328,
|
||||
name=_('Holiday'),
|
||||
description=_(
|
||||
'Download Firefox themes with holidays. This category '
|
||||
|
@ -324,6 +267,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'music': StaticCategory(
|
||||
id=322,
|
||||
name=_('Music'),
|
||||
description=_(
|
||||
'Download Firefox themes for musical interests and '
|
||||
|
@ -333,6 +277,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'nature': StaticCategory(
|
||||
id=302,
|
||||
name=_('Nature'),
|
||||
description=_(
|
||||
'Download Firefox themes with animals and natural '
|
||||
|
@ -341,6 +286,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'other': StaticCategory(
|
||||
id=314,
|
||||
name=_('Other'),
|
||||
weight=333,
|
||||
description=_(
|
||||
|
@ -348,6 +294,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'scenery': StaticCategory(
|
||||
id=306,
|
||||
name=_('Scenery'),
|
||||
description=_(
|
||||
'Download Firefox themes that feature the environment '
|
||||
|
@ -356,6 +303,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'seasonal': StaticCategory(
|
||||
id=312,
|
||||
name=_('Seasonal'),
|
||||
description=_(
|
||||
'Download Firefox themes for all four seasons—fall, '
|
||||
|
@ -364,6 +312,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'solid': StaticCategory(
|
||||
id=318,
|
||||
name=_('Solid'),
|
||||
description=_(
|
||||
'Download Firefox themes with solid and gradient colors '
|
||||
|
@ -372,6 +321,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'sports': StaticCategory(
|
||||
id=304,
|
||||
name=_('Sports'),
|
||||
description=_(
|
||||
'Download Firefox themes that feature a variety of '
|
||||
|
@ -380,6 +330,7 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
'websites': StaticCategory(
|
||||
id=316,
|
||||
name=_('Websites'),
|
||||
description=_(
|
||||
'Download Firefox themes that capture the essence of the '
|
||||
|
@ -387,34 +338,12 @@ CATEGORIES_NO_APP = {
|
|||
),
|
||||
),
|
||||
},
|
||||
ADDON_DICT: {'general': StaticCategory(name=_('General'))},
|
||||
_ADDON_SEARCH: {
|
||||
'bookmarks': StaticCategory(name=_('Bookmarks')),
|
||||
'business': StaticCategory(name=_('Business')),
|
||||
'dictionaries-encyclopedias': StaticCategory(
|
||||
name=_('Dictionaries & Encyclopedias')
|
||||
),
|
||||
'general': StaticCategory(name=_('General')),
|
||||
'kids': StaticCategory(name=_('Kids')),
|
||||
'multiple-search': StaticCategory(name=_('Multiple Search')),
|
||||
'music': StaticCategory(name=_('Music')),
|
||||
'news-blogs': StaticCategory(name=_('News & Blogs')),
|
||||
'photos-images': StaticCategory(name=_('Photos & Images')),
|
||||
'shopping-e-commerce': StaticCategory(name=_('Shopping & E-Commerce')),
|
||||
'social-people': StaticCategory(name=_('Social & People')),
|
||||
'sports': StaticCategory(name=_('Sports')),
|
||||
'travel': StaticCategory(name=_('Travel')),
|
||||
'video': StaticCategory(name=_('Video')),
|
||||
},
|
||||
ADDON_LPAPP: {'general': StaticCategory(name=_('General'))},
|
||||
ADDON_DICT: {'general': StaticCategory(id=95, name=_('General'))},
|
||||
ADDON_LPAPP: {'general': StaticCategory(id=98, name=_('General'))},
|
||||
}
|
||||
|
||||
CATEGORIES_NO_APP[_ADDON_PERSONA] = {
|
||||
slug: copy.copy(cat) for slug, cat in CATEGORIES_NO_APP[ADDON_STATICTHEME].items()
|
||||
}
|
||||
|
||||
for type_ in CATEGORIES_NO_APP:
|
||||
for slug, cat in CATEGORIES_NO_APP[type_].items():
|
||||
for type_ in CATEGORIES:
|
||||
for slug, cat in CATEGORIES[type_].items():
|
||||
# Flatten some values and set them, avoiding immutability
|
||||
# of `StaticCategory` by calling `object.__setattr__` directly.
|
||||
object.__setattr__(cat, 'slug', slug)
|
||||
|
@ -422,78 +351,8 @@ for type_ in CATEGORIES_NO_APP:
|
|||
object.__setattr__(cat, 'misc', slug in ('miscellaneous', 'other'))
|
||||
|
||||
|
||||
# These category ids are used in AddonCategory. To add a category to an app you can use
|
||||
# any unused id.
|
||||
CATEGORIES = {
|
||||
FIREFOX.id: {
|
||||
ADDON_EXTENSION: {
|
||||
'alerts-updates': 72,
|
||||
'appearance': 14,
|
||||
'bookmarks': 22,
|
||||
'download-management': 5,
|
||||
'feeds-news-blogging': 1,
|
||||
'games-entertainment': 142,
|
||||
'language-support': 37,
|
||||
'photos-music-videos': 38,
|
||||
'privacy-security': 12,
|
||||
'search-tools': 13,
|
||||
'shopping': 141,
|
||||
'social-communication': 71,
|
||||
'tabs': 93,
|
||||
'web-development': 4,
|
||||
'other': 73,
|
||||
},
|
||||
ADDON_STATICTHEME: {
|
||||
'abstract': 300,
|
||||
'causes': 320,
|
||||
'fashion': 324,
|
||||
'film-and-tv': 326,
|
||||
'firefox': 308,
|
||||
'foxkeh': 310,
|
||||
'holiday': 328,
|
||||
'music': 322,
|
||||
'nature': 302,
|
||||
'other': 314,
|
||||
'scenery': 306,
|
||||
'seasonal': 312,
|
||||
'solid': 318,
|
||||
'sports': 304,
|
||||
'websites': 316,
|
||||
},
|
||||
ADDON_LPAPP: {
|
||||
'general': 98,
|
||||
},
|
||||
ADDON_DICT: {
|
||||
'general': 95,
|
||||
},
|
||||
},
|
||||
ANDROID.id: {
|
||||
ADDON_EXTENSION: {
|
||||
'device-features-location': 145,
|
||||
'experimental': 151,
|
||||
'feeds-news-blogging': 147,
|
||||
'performance': 144,
|
||||
'photos-media': 143,
|
||||
'security-privacy': 149,
|
||||
'shopping': 150,
|
||||
'social-networking': 148,
|
||||
'sports-games': 146,
|
||||
'user-interface': 152,
|
||||
'other': 153,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
CATEGORIES_BY_ID = {}
|
||||
|
||||
for app in CATEGORIES:
|
||||
for type_ in CATEGORIES[app]:
|
||||
for slug, id_ in CATEGORIES[app][type_].items():
|
||||
cat = copy.copy(CATEGORIES_NO_APP[type_][slug])
|
||||
# Flatten some values and set them, avoiding immutability
|
||||
# of `StaticCategory` by calling `object.__setattr__` directly.
|
||||
object.__setattr__(cat, 'id', id_)
|
||||
object.__setattr__(cat, 'application', app)
|
||||
CATEGORIES_BY_ID[id_] = cat
|
||||
CATEGORIES[app][type_][slug] = cat
|
||||
for type_ in CATEGORIES:
|
||||
for cat in CATEGORIES[type_].values():
|
||||
CATEGORIES_BY_ID[cat.id] = cat
|
||||
|
|
|
@ -7,7 +7,6 @@ from django import forms
|
|||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db.models import Q
|
||||
from django.forms.formsets import BaseFormSet, formset_factory
|
||||
from django.forms.models import BaseModelFormSet, modelformset_factory
|
||||
from django.forms.widgets import RadioSelect
|
||||
from django.urls import reverse
|
||||
|
@ -45,7 +44,7 @@ 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.constants.categories import CATEGORIES, CATEGORIES_BY_ID, CATEGORIES_NO_APP
|
||||
from olympia.constants.categories import CATEGORIES, CATEGORIES_BY_ID
|
||||
from olympia.devhub.widgets import CategoriesSelectMultiple, IconTypeSelect
|
||||
from olympia.files.models import FileUpload
|
||||
from olympia.files.utils import SafeTar, SafeZip, parse_addon
|
||||
|
@ -139,45 +138,56 @@ class AddonFormBase(TranslationFormMixin, AMOModelForm):
|
|||
|
||||
|
||||
class CategoryForm(forms.Form):
|
||||
application = forms.TypedChoiceField(
|
||||
choices=amo.APPS_CHOICES, coerce=int, widget=forms.HiddenInput, required=True
|
||||
)
|
||||
categories = forms.MultipleChoiceField(choices=(), widget=CategoriesSelectMultiple)
|
||||
|
||||
def save(self, addon):
|
||||
application = self.cleaned_data.get('application')
|
||||
def __init__(self, *args, **kw):
|
||||
self.addon = kw.pop('addon')
|
||||
self.request = kw.pop('request', None)
|
||||
super().__init__(*args, **kw)
|
||||
cats = sorted(
|
||||
CATEGORIES.get(self.addon.type, {}).values(),
|
||||
key=lambda x: x.name,
|
||||
)
|
||||
if self.addon.type == amo.ADDON_STATICTHEME:
|
||||
self.max_categories = 1
|
||||
self.fields['categories'] = forms.ChoiceField(widget=forms.RadioSelect)
|
||||
else:
|
||||
self.max_categories = amo.MAX_CATEGORIES
|
||||
self.fields['categories'].choices = [(c.id, c.name) for c in cats]
|
||||
self.fields['categories'].initial = [c.id for c in self.addon.all_categories]
|
||||
|
||||
def save(self):
|
||||
categories_new = [int(c) for c in self.cleaned_data['categories']]
|
||||
categories_old = [
|
||||
c.id for c in addon.app_categories.get(amo.APP_IDS[application].short, [])
|
||||
]
|
||||
categories_old = [c.id for c in self.addon.all_categories]
|
||||
|
||||
# Add new categories.
|
||||
for c_id in set(categories_new) - set(categories_old):
|
||||
AddonCategory(addon=addon, category_id=c_id).save()
|
||||
AddonCategory(addon=self.addon, category_id=c_id).save()
|
||||
|
||||
# Remove old categories.
|
||||
for c_id in set(categories_old) - set(categories_new):
|
||||
AddonCategory.objects.filter(addon=addon, category_id=c_id).delete()
|
||||
AddonCategory.objects.filter(addon=self.addon, category_id=c_id).delete()
|
||||
|
||||
# Remove old, outdated categories cache on the model.
|
||||
del addon.all_categories
|
||||
del self.addon.all_categories
|
||||
|
||||
# Make sure the add-on is properly re-indexed
|
||||
addons_tasks.index_addons.delay([addon.id])
|
||||
addons_tasks.index_addons.delay([self.addon.id])
|
||||
|
||||
def clean_categories(self):
|
||||
categories = self.cleaned_data['categories']
|
||||
if isinstance(categories, str):
|
||||
categories = [categories]
|
||||
total = len(categories)
|
||||
max_cat = amo.MAX_CATEGORIES
|
||||
|
||||
if total > max_cat:
|
||||
if total > self.max_categories:
|
||||
# L10n: {0} is the number of categories.
|
||||
raise forms.ValidationError(
|
||||
ngettext(
|
||||
'You can have only {0} category.',
|
||||
'You can have only {0} categories.',
|
||||
max_cat,
|
||||
).format(max_cat)
|
||||
self.max_categories,
|
||||
).format(self.max_categories)
|
||||
)
|
||||
|
||||
has_misc = list(filter(lambda x: CATEGORIES_BY_ID.get(int(x)).misc, categories))
|
||||
|
@ -192,47 +202,6 @@ class CategoryForm(forms.Form):
|
|||
return categories
|
||||
|
||||
|
||||
class BaseCategoryFormSet(BaseFormSet):
|
||||
def __init__(self, *args, **kw):
|
||||
self.addon = kw.pop('addon')
|
||||
self.request = kw.pop('request', None)
|
||||
super().__init__(*args, **kw)
|
||||
self.initial = []
|
||||
apps = sorted(self.addon.compatible_apps.keys(), key=lambda x: x.id)
|
||||
|
||||
# Drop any apps that don't have appropriate categories.
|
||||
for app in list(apps):
|
||||
if app and not CATEGORIES.get(app.id, {}).get(self.addon.type):
|
||||
apps.remove(app)
|
||||
|
||||
if not CATEGORIES_NO_APP.get(self.addon.type):
|
||||
apps = []
|
||||
|
||||
for app in apps:
|
||||
cats = self.addon.app_categories.get(app.short, [])
|
||||
self.initial.append({'categories': [c.id for c in cats]})
|
||||
|
||||
for app, form in zip(apps, self.forms, strict=True):
|
||||
key = app.id if app else None
|
||||
form.request = self.request
|
||||
form.initial['application'] = key
|
||||
form.app = app
|
||||
cats = sorted(
|
||||
CATEGORIES.get(key, {}).get(self.addon.type, {}).values(),
|
||||
key=lambda x: x.name,
|
||||
)
|
||||
form.fields['categories'].choices = [(c.id, c.name) for c in cats]
|
||||
|
||||
def save(self):
|
||||
for f in self.forms:
|
||||
f.save(self.addon)
|
||||
|
||||
|
||||
CategoryFormSet = formset_factory(
|
||||
form=CategoryForm, formset=BaseCategoryFormSet, extra=0
|
||||
)
|
||||
|
||||
|
||||
ICON_TYPES = [('', 'default'), ('image/jpeg', 'jpeg'), ('image/png', 'png')]
|
||||
|
||||
|
||||
|
@ -1448,31 +1417,3 @@ class AgreementForm(forms.Form):
|
|||
if not checker.is_submission_allowed(check_dev_agreement=False):
|
||||
raise forms.ValidationError(checker.get_error_message())
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class SingleCategoryForm(forms.Form):
|
||||
category = forms.ChoiceField(widget=forms.RadioSelect)
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
self.addon = kw.pop('addon')
|
||||
self.request = kw.pop('request', None)
|
||||
if len(self.addon.all_categories) > 0:
|
||||
kw['initial'] = {'category': self.addon.all_categories[0].slug}
|
||||
super().__init__(*args, **kw)
|
||||
|
||||
sorted_cats = sorted(
|
||||
CATEGORIES_NO_APP[self.addon.type].items(), key=lambda slug_cat: slug_cat[0]
|
||||
)
|
||||
self.fields['category'].choices = [(slug, c.name) for slug, c in sorted_cats]
|
||||
|
||||
def save(self):
|
||||
category_slug = self.cleaned_data['category']
|
||||
# Clear any old categor[y|ies]
|
||||
AddonCategory.objects.filter(addon=self.addon).delete()
|
||||
# Add new categor[y|ies]
|
||||
for app in CATEGORIES.keys():
|
||||
category = CATEGORIES[app].get(self.addon.type, {}).get(category_slug, None)
|
||||
if category:
|
||||
AddonCategory(addon=self.addon, category_id=category.id).save()
|
||||
# Remove old, outdated categories cache on the model.
|
||||
del self.addon.all_categories
|
||||
|
|
|
@ -190,26 +190,16 @@
|
|||
"exposure.")) }}
|
||||
</th>
|
||||
<td id="addon-categories-edit"
|
||||
data-max-categories="{{ amo.MAX_CATEGORIES }}">
|
||||
{% if editable and addon.type != amo.ADDON_STATICTHEME %}
|
||||
{{ cat_form.non_form_errors() }}
|
||||
{{ cat_form.management_form }}
|
||||
{% for form in cat_form.initial_forms %}
|
||||
{{ select_cats(amo.MAX_CATEGORIES, form) }}
|
||||
{% endfor %}
|
||||
{% elif editable and addon.type == amo.ADDON_STATICTHEME %}
|
||||
{{ cat_form.category.errors }}
|
||||
{{ cat_form.category }}
|
||||
{% if cat_form %}data-max-categories="{{ cat_form.max_categories }}"{% endif %}>
|
||||
{% if editable %}
|
||||
{{ select_cats(cat_form) }}
|
||||
{% else %}
|
||||
{% set categories = addon.app_categories %}
|
||||
{% set categories = addon.all_categories %}
|
||||
{% call empty_unless(categories) %}
|
||||
<ul class="addon-app-cats-inline">
|
||||
{% for app_short_name, cats in categories.items() %}
|
||||
<li>
|
||||
<b>{{ amo.APPS[app_short_name].pretty }}:</b>
|
||||
{{ cats|join(' · ')|safe }}
|
||||
{{ categories|join(' · ')|safe }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
|
|
@ -141,21 +141,11 @@
|
|||
"in order to work.")
|
||||
}}">?</span>
|
||||
</div>
|
||||
<div id="addon-categories-edit" class="addon-submission-field"
|
||||
data-max-categories="{{ amo.MAX_CATEGORIES }}">
|
||||
{{ cat_form.non_form_errors() }}
|
||||
{{ cat_form.management_form }}
|
||||
{% for form in cat_form.initial_forms %}
|
||||
{{ select_cats(amo.MAX_CATEGORIES, form) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="addon-categories-edit" class="addon-submission-field">
|
||||
<label>{{ _('Select a category for this add-on:') }}<span class="req" title="{{ _('required') }}">*</span></label>
|
||||
{{ cat_form.category.errors }}
|
||||
{{ cat_form.category }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="addon-categories-edit" class="addon-submission-field"
|
||||
data-max-categories="{{ cat_form.max_categories }}">
|
||||
{{ select_cats(cat_form) }}
|
||||
</div>
|
||||
<div class="addon-submission-field">
|
||||
<label for="{{ describe_form.support_email.auto_id }}">
|
||||
{{ _('Support email:') }}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% from "devhub/includes/macros.html" import some_html_tip, select_cats %}
|
||||
{% from "devhub/includes/macros.html" import some_html_tip %}
|
||||
{% extends "devhub/addons/submit/base.html" %}
|
||||
|
||||
{% block title %}{{ dev_page_title(_('Describe Add-on'), addon) }}{% endblock %}
|
||||
|
|
|
@ -62,16 +62,14 @@
|
|||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro select_cats(max, form) %}
|
||||
{% macro select_cats(form) %}
|
||||
<div class="addon-app-cats">
|
||||
<label>
|
||||
{# L10n: {0} is the maximum number of add-on categories allowed.
|
||||
{1} is the application name. #}
|
||||
{{ ngettext('Select <b>up to {0}</b> {1} category for this add-on:',
|
||||
'Select <b>up to {0}</b> {1} categories for this add-on:',
|
||||
max)|format_html(max, form.app.pretty if form.app else '') }}
|
||||
{# L10n: {0} is the maximum number of add-on categories allowed #}
|
||||
{{ ngettext('Select a category for this add-on:',
|
||||
'Select <b>up to {0}</b> categories for this add-on:',
|
||||
form.max_categories)|format_html(form.max_categories) }}
|
||||
</label>
|
||||
{{ form.application }}
|
||||
{{ form.categories }}
|
||||
{{ form.categories.errors }}
|
||||
</div>
|
||||
|
|
|
@ -1232,12 +1232,11 @@ class TestIconForm(TestCase):
|
|||
|
||||
|
||||
class TestCategoryForm(TestCase):
|
||||
def test_no_possible_categories(self):
|
||||
def test_only_one_possible_category_for_dicts(self):
|
||||
addon = addon_factory(type=amo.ADDON_DICT)
|
||||
request = req_factory_factory('/')
|
||||
form = forms.CategoryFormSet(addon=addon, request=request)
|
||||
apps = [f.app for f in form.forms]
|
||||
assert apps == [amo.FIREFOX]
|
||||
form = forms.CategoryForm(addon=addon, request=request)
|
||||
assert form.fields['categories'].choices == [(95, 'General')]
|
||||
|
||||
|
||||
class TestVersionForm(TestCase):
|
||||
|
|
|
@ -76,7 +76,7 @@ class BaseTestEditDescribe(BaseTestEdit):
|
|||
self.describe_edit_url = self.get_url('describe', edit=True)
|
||||
if self.listed:
|
||||
ctx = self.client.get(self.describe_edit_url).context
|
||||
self.cat_initial = initial(ctx['cat_form'].initial_forms[0])
|
||||
self.cat_initial = initial(ctx['cat_form'])
|
||||
|
||||
def get_dict(self, **kw):
|
||||
result = {
|
||||
|
@ -86,9 +86,8 @@ class BaseTestEditDescribe(BaseTestEdit):
|
|||
'description': 'new description',
|
||||
}
|
||||
if self.listed:
|
||||
fs = formset(self.cat_initial, initial_count=1)
|
||||
result.update({'is_experimental': True, 'requires_payment': True})
|
||||
result.update(fs)
|
||||
result.update(self.cat_initial)
|
||||
|
||||
result.update(**kw)
|
||||
return result
|
||||
|
@ -546,29 +545,23 @@ class TestEditDescribeListed(BaseTestEditDescribe, L10nTestsMixin):
|
|||
|
||||
def test_edit_categories_required(self):
|
||||
del self.cat_initial['categories']
|
||||
response = self.client.post(
|
||||
self.describe_edit_url, formset(self.cat_initial, initial_count=1)
|
||||
)
|
||||
assert response.context['cat_form'].errors[0]['categories'] == (
|
||||
response = self.client.post(self.describe_edit_url, self.cat_initial)
|
||||
assert response.context['cat_form'].errors['categories'] == (
|
||||
['This field is required.']
|
||||
)
|
||||
|
||||
def test_edit_categories_max(self):
|
||||
assert amo.MAX_CATEGORIES == 2
|
||||
self.cat_initial['categories'] = [22, 1, 71]
|
||||
response = self.client.post(
|
||||
self.describe_edit_url, formset(self.cat_initial, initial_count=1)
|
||||
)
|
||||
assert response.context['cat_form'].errors[0]['categories'] == (
|
||||
response = self.client.post(self.describe_edit_url, self.cat_initial)
|
||||
assert response.context['cat_form'].errors['categories'] == (
|
||||
['You can have only 2 categories.']
|
||||
)
|
||||
|
||||
def test_edit_categories_other_failure(self):
|
||||
self.cat_initial['categories'] = [73, 1]
|
||||
response = self.client.post(
|
||||
self.describe_edit_url, formset(self.cat_initial, initial_count=1)
|
||||
)
|
||||
assert response.context['cat_form'].errors[0]['categories'] == (
|
||||
response = self.client.post(self.describe_edit_url, self.cat_initial)
|
||||
assert response.context['cat_form'].errors['categories'] == (
|
||||
[
|
||||
'The miscellaneous category cannot be combined with additional '
|
||||
'categories.'
|
||||
|
@ -577,10 +570,8 @@ class TestEditDescribeListed(BaseTestEditDescribe, L10nTestsMixin):
|
|||
|
||||
def test_edit_categories_nonexistent(self):
|
||||
self.cat_initial['categories'] = [100]
|
||||
response = self.client.post(
|
||||
self.describe_edit_url, formset(self.cat_initial, initial_count=1)
|
||||
)
|
||||
assert response.context['cat_form'].errors[0]['categories'] == (
|
||||
response = self.client.post(self.describe_edit_url, self.cat_initial)
|
||||
assert response.context['cat_form'].errors['categories'] == (
|
||||
['Select a valid choice. 100 is not one of the available choices.']
|
||||
)
|
||||
|
||||
|
@ -1567,7 +1558,7 @@ class TestEditDescribeStaticThemeListed(
|
|||
def test_edit_categories_set(self):
|
||||
assert [cat.id for cat in self.get_addon().all_categories] == []
|
||||
response = self.client.post(
|
||||
self.describe_edit_url, self.get_dict(category='firefox')
|
||||
self.describe_edit_url, self.get_dict(categories=[308])
|
||||
)
|
||||
assert response.context['addon'].all_categories == (
|
||||
self.get_addon().all_categories
|
||||
|
@ -1580,17 +1571,17 @@ class TestEditDescribeStaticThemeListed(
|
|||
AddonCategory(addon=self.addon, category_id=300).save()
|
||||
assert sorted(cat.id for cat in self.get_addon().all_categories) == [300]
|
||||
|
||||
self.client.post(self.describe_edit_url, self.get_dict(category='firefox'))
|
||||
self.client.post(self.describe_edit_url, self.get_dict(categories=[308]))
|
||||
category_ids_new = [cat.id for cat in self.get_addon().all_categories]
|
||||
# Only ever one category for Static Themes (per application)
|
||||
assert category_ids_new == [308]
|
||||
|
||||
def test_edit_categories_required(self):
|
||||
data = self.get_dict(category='')
|
||||
data = self.get_dict(categories='')
|
||||
response = self.client.post(self.describe_edit_url, data)
|
||||
assert response.status_code == 200
|
||||
self.assertFormError(
|
||||
response, 'cat_form', 'category', 'This field is required.'
|
||||
response, 'cat_form', 'categories', 'This field is required.'
|
||||
)
|
||||
|
||||
def test_theme_preview_shown(self):
|
||||
|
|
|
@ -27,7 +27,6 @@ from olympia.amo.tests import (
|
|||
TestCase,
|
||||
addon_factory,
|
||||
create_default_webext_appversion,
|
||||
formset,
|
||||
initial,
|
||||
version_factory,
|
||||
)
|
||||
|
@ -1393,8 +1392,8 @@ class TestAddonSubmitDetails(DetailsPageMixin, TestSubmitBase):
|
|||
AddonCategory.objects.filter(addon=addon, category_id=1).delete()
|
||||
AddonCategory.objects.filter(addon=addon, category_id=71).delete()
|
||||
|
||||
ctx = self.client.get(self.url).context['cat_form']
|
||||
self.cat_initial = initial(ctx.initial_forms[0])
|
||||
cat_form = self.client.get(self.url).context['cat_form']
|
||||
self.cat_initial = initial(cat_form)
|
||||
self.next_step = reverse('devhub.submit.finish', args=['a3615'])
|
||||
License.objects.create(builtin=3)
|
||||
|
||||
|
@ -1419,8 +1418,7 @@ class TestAddonSubmitDetails(DetailsPageMixin, TestSubmitBase):
|
|||
'support_email': 'black@hole.org',
|
||||
}
|
||||
)
|
||||
cat_initial = kw.pop('cat_initial', self.cat_initial)
|
||||
cat_form = formset(cat_initial, initial_count=1)
|
||||
cat_form = kw.pop('cat_initial', self.cat_initial)
|
||||
license_form = {'license-builtin': 3}
|
||||
policy_form = (
|
||||
{}
|
||||
|
@ -1539,7 +1537,7 @@ class TestAddonSubmitDetails(DetailsPageMixin, TestSubmitBase):
|
|||
response = self.client.post(
|
||||
self.url, self.get_dict(cat_initial=self.cat_initial)
|
||||
)
|
||||
assert response.context['cat_form'].errors[0]['categories'] == (
|
||||
assert response.context['cat_form'].errors['categories'] == (
|
||||
['This field is required.']
|
||||
)
|
||||
|
||||
|
@ -1549,7 +1547,7 @@ class TestAddonSubmitDetails(DetailsPageMixin, TestSubmitBase):
|
|||
response = self.client.post(
|
||||
self.url, self.get_dict(cat_initial=self.cat_initial)
|
||||
)
|
||||
assert response.context['cat_form'].errors[0]['categories'] == (
|
||||
assert response.context['cat_form'].errors['categories'] == (
|
||||
['You can have only 2 categories.']
|
||||
)
|
||||
|
||||
|
@ -1680,7 +1678,7 @@ class TestStaticThemeSubmitDetails(DetailsPageMixin, TestSubmitBase):
|
|||
'support_email': 'black@hole.org',
|
||||
}
|
||||
)
|
||||
cat_form = {'category': 'abstract'}
|
||||
cat_form = {'categories': [300]}
|
||||
license_form = {'license-builtin': 11}
|
||||
result.update(describe_form)
|
||||
result.update(cat_form)
|
||||
|
@ -1730,7 +1728,7 @@ class TestStaticThemeSubmitDetails(DetailsPageMixin, TestSubmitBase):
|
|||
|
||||
def test_submit_categories_set(self):
|
||||
assert [cat.id for cat in self.get_addon().all_categories] == []
|
||||
self.is_success(self.get_dict(category='firefox'))
|
||||
self.is_success(self.get_dict(categories=[308]))
|
||||
|
||||
addon_cats = [c.id for c in self.get_addon().all_categories]
|
||||
assert sorted(addon_cats) == [308]
|
||||
|
@ -1739,7 +1737,7 @@ class TestStaticThemeSubmitDetails(DetailsPageMixin, TestSubmitBase):
|
|||
AddonCategory(addon=self.addon, category_id=300).save()
|
||||
assert sorted(cat.id for cat in self.get_addon().all_categories) == [300]
|
||||
|
||||
self.client.post(self.url, self.get_dict(category='firefox'))
|
||||
self.client.post(self.url, self.get_dict(categories=[308]))
|
||||
category_ids_new = [cat.id for cat in self.get_addon().all_categories]
|
||||
# Only ever one category for Static Themes
|
||||
assert category_ids_new == [308]
|
||||
|
@ -2701,10 +2699,7 @@ class TestVersionSubmitDetails(TestSubmitBase):
|
|||
'name': str(self.addon.name),
|
||||
'slug': self.addon.slug,
|
||||
'summary': str(self.addon.summary),
|
||||
'form-0-categories': [22, 1],
|
||||
'form-0-application': 1,
|
||||
'form-INITIAL_FORMS': 1,
|
||||
'form-TOTAL_FORMS': 1,
|
||||
'categories': [22, 1],
|
||||
'license-builtin': 3,
|
||||
}
|
||||
response = self.client.post(self.url, data)
|
||||
|
|
|
@ -864,11 +864,10 @@ def addons_section(request, addon_id, addon, section, editable=False):
|
|||
whiteboard = None
|
||||
|
||||
if section == 'describe' and show_listed:
|
||||
category_form_class = (
|
||||
forms.SingleCategoryForm if static_theme else forms.CategoryFormSet
|
||||
)
|
||||
cat_form = category_form_class(
|
||||
request.POST or None, addon=addon, request=request
|
||||
cat_form = forms.CategoryForm(
|
||||
request.POST if request.method == 'POST' else None,
|
||||
addon=addon,
|
||||
request=request,
|
||||
)
|
||||
|
||||
elif section == 'additional_details':
|
||||
|
@ -1752,10 +1751,7 @@ def _submit_details(request, addon, version):
|
|||
describe_form = forms.DescribeForm(
|
||||
post_data, instance=addon, request=request, version=version
|
||||
)
|
||||
cat_form_class = (
|
||||
forms.CategoryFormSet if not static_theme else forms.SingleCategoryForm
|
||||
)
|
||||
cat_form = cat_form_class(post_data, addon=addon, request=request)
|
||||
cat_form = forms.CategoryForm(post_data, addon=addon, request=request)
|
||||
policy_form = forms.PolicyForm(post_data, addon=addon)
|
||||
license_form = forms.LicenseForm(
|
||||
post_data, version=latest_version, prefix='license'
|
||||
|
|
|
@ -20,12 +20,11 @@ from .user import generate_addon_user_and_category, generate_user
|
|||
from .version import generate_version
|
||||
|
||||
|
||||
def _yield_name_and_cat(num, app=None, type=None):
|
||||
def _yield_name_and_cat(num, type_=None):
|
||||
"""
|
||||
Yield `num` tuples of (addon_name, category) for the given `app`
|
||||
and `type`.
|
||||
Yield `num` tuples of (addon_name, category) for the given `type`.
|
||||
"""
|
||||
categories = list(CATEGORIES[app.id][type].values())
|
||||
categories = list(CATEGORIES[type_].values())
|
||||
if num > len(generate_names()):
|
||||
base_names = islice(cycle(generate_names()), num)
|
||||
addons = [f'{name} {i}' for i, name in enumerate(base_names)]
|
||||
|
@ -71,7 +70,7 @@ def generate_addons(num, owner, app_name, addon_type=ADDON_EXTENSION):
|
|||
featured_categories = collections.defaultdict(int)
|
||||
user = generate_user(owner)
|
||||
app = APPS[app_name]
|
||||
for name, category in _yield_name_and_cat(num, app=app, type=addon_type):
|
||||
for name, category in _yield_name_and_cat(num, type_=addon_type):
|
||||
addon = create_addon(name=name, application=app, type=addon_type)
|
||||
generate_addon_user_and_category(addon, user, category)
|
||||
generate_addon_preview(addon)
|
||||
|
|
|
@ -117,11 +117,11 @@ class Command(BaseCommand):
|
|||
else:
|
||||
category = None
|
||||
|
||||
if category not in CATEGORIES[amo.FIREFOX.id][addon_type]:
|
||||
if category not in CATEGORIES[addon_type]:
|
||||
category = None
|
||||
print('Category %s' % category, 'not found')
|
||||
else:
|
||||
category = CATEGORIES[amo.FIREFOX.id][addon_type][category]
|
||||
category = CATEGORIES[addon_type][category]
|
||||
|
||||
print('Creating add-on %s' % addon_data['slug'])
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from olympia.versions.models import Version
|
|||
class _BaseAddonGeneratorMixin:
|
||||
def test_tinyset(self):
|
||||
size = 4
|
||||
data = list(_yield_name_and_cat(size, self.app, self.type))
|
||||
data = list(_yield_name_and_cat(size, self.type))
|
||||
assert len(data) == size
|
||||
# Names are unique.
|
||||
assert len({addonname for addonname, cat in data}) == size
|
||||
|
@ -21,21 +21,21 @@ class _BaseAddonGeneratorMixin:
|
|||
assert not any(addonname[-1].isdigit() for addonname, cat in data)
|
||||
|
||||
def test_smallset(self):
|
||||
size = len(CATEGORIES[self.app.id][self.type]) * 6
|
||||
data = list(_yield_name_and_cat(size, self.app, self.type))
|
||||
size = len(CATEGORIES[self.type]) * 6
|
||||
data = list(_yield_name_and_cat(size, self.type))
|
||||
assert len(data) == size
|
||||
# Addons are split up equally within each categories.
|
||||
categories = collections.defaultdict(int)
|
||||
for _addonname, category in data:
|
||||
categories[category.slug] += 1
|
||||
length = len(CATEGORIES[self.app.id][self.type])
|
||||
length = len(CATEGORIES[self.type])
|
||||
assert set(categories.values()) == {size / length}
|
||||
assert len({addonname for addonname, cat in data}) == size
|
||||
assert not any(addonname[-1].isdigit() for addonname, cat in data)
|
||||
|
||||
def test_bigset(self):
|
||||
size = 300
|
||||
data = list(_yield_name_and_cat(size, self.app, self.type))
|
||||
data = list(_yield_name_and_cat(size, self.type))
|
||||
assert len(data) == size
|
||||
categories = collections.defaultdict(int)
|
||||
for _addonname, cat in data:
|
||||
|
|
|
@ -14,7 +14,7 @@ class RatingsTests(TestCase):
|
|||
|
||||
def test_generate_addon_user_and_category(self):
|
||||
user = UserProfile.objects.create(email=self.email)
|
||||
category = CATEGORIES[amo.FIREFOX.id][amo.ADDON_STATICTHEME]['abstract']
|
||||
category = CATEGORIES[amo.ADDON_STATICTHEME]['abstract']
|
||||
generate_addon_user_and_category(self.addon, user, category)
|
||||
assert AddonCategory.objects.all().count() == 1
|
||||
assert AddonUser.objects.all().count() == 1
|
||||
|
|
|
@ -1348,6 +1348,7 @@ DRF_API_GATES = {
|
|||
'version-files',
|
||||
'del-version-license-slug',
|
||||
'del-preview-position',
|
||||
'categories-application',
|
||||
),
|
||||
'v4': (
|
||||
'l10n_flat_input_output',
|
||||
|
@ -1362,6 +1363,7 @@ DRF_API_GATES = {
|
|||
'version-files',
|
||||
'del-version-license-slug',
|
||||
'del-preview-position',
|
||||
'categories-application',
|
||||
),
|
||||
'v5': (
|
||||
'addons-search-_score-field',
|
||||
|
|
|
@ -303,20 +303,18 @@ class AddonCategoryQueryParam(AddonQueryParam):
|
|||
|
||||
def __init__(self, request):
|
||||
super().__init__(request)
|
||||
# Category slugs are only unique for a given type+app combination.
|
||||
# Category slugs are only unique for a given type.
|
||||
# Once we have that, it's just a matter of selecting the corresponding
|
||||
# dict in the categories constants and use that as the reverse dict,
|
||||
# and make sure to use get_value_from_object_from_reverse_dict().
|
||||
try:
|
||||
app = AddonAppQueryParam(self.query_data).get_value()
|
||||
types = AddonTypeQueryParam(self.query_data).get_values()
|
||||
self.reverse_dict = [CATEGORIES[app][type_] for type_ in types]
|
||||
self.reverse_dict = [CATEGORIES[type_] for type_ in types]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
gettext(
|
||||
'Invalid combination of "%s", "%s" and "%s" parameters.'
|
||||
'Invalid combination of %s" and "%s" parameters.'
|
||||
% (
|
||||
AddonAppQueryParam.query_param,
|
||||
AddonTypeQueryParam.query_param,
|
||||
self.query_param,
|
||||
)
|
||||
|
|
|
@ -731,18 +731,8 @@ class TestSearchParameterFilter(FilterTestsBase):
|
|||
'range': {'current_version.compatible_apps.1.max': {'gte': 46000000000100}}
|
||||
} in filter_
|
||||
|
||||
def test_search_by_category_slug_no_app_or_type(self):
|
||||
with self.assertRaises(serializers.ValidationError) as context:
|
||||
self._filter(data={'category': 'other'})
|
||||
assert context.exception.detail == ['Invalid "app" parameter.']
|
||||
|
||||
def test_search_by_category_id_no_app_or_type(self):
|
||||
with self.assertRaises(serializers.ValidationError) as context:
|
||||
self._filter(data={'category': 1})
|
||||
assert context.exception.detail == ['Invalid "app" parameter.']
|
||||
|
||||
def test_search_by_category_slug(self):
|
||||
category = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['other']
|
||||
category = CATEGORIES[amo.ADDON_EXTENSION]['other']
|
||||
qs = self._filter(
|
||||
data={'category': 'other', 'app': 'firefox', 'type': 'extension'}
|
||||
)
|
||||
|
@ -752,8 +742,8 @@ class TestSearchParameterFilter(FilterTestsBase):
|
|||
assert {'terms': {'category': [category.id]}} in filter_
|
||||
|
||||
def test_search_by_category_slug_multiple_types(self):
|
||||
category_a = CATEGORIES[amo.FIREFOX.id][amo.ADDON_EXTENSION]['other']
|
||||
category_b = CATEGORIES[amo.FIREFOX.id][amo.ADDON_STATICTHEME]['other']
|
||||
category_a = CATEGORIES[amo.ADDON_EXTENSION]['other']
|
||||
category_b = CATEGORIES[amo.ADDON_STATICTHEME]['other']
|
||||
qs = self._filter(
|
||||
data={
|
||||
'category': 'other',
|
||||
|
|
|
@ -234,18 +234,6 @@ form .char-count b {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
#edit-addon-describe .addon-app-cats {
|
||||
border-top: 1px dotted #add0dc;
|
||||
margin: 8px 0 0;
|
||||
padding-top: 8px;
|
||||
}
|
||||
#edit-addon-describe input + .addon-app-cats,
|
||||
#edit-addon-describe p.addon-app-cats:first-child {
|
||||
border-top-width: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.addon-categories {
|
||||
-moz-column-count: 3;
|
||||
-webkit-column-count: 3;
|
||||
|
|
Загрузка…
Ссылка в новой задаче