Skip to content

Commit

Permalink
Add PromotedGroup model to replace legacy PromotedClass
Browse files Browse the repository at this point in the history
- Create new PromotedGroup model with comprehensive fields for promotion rules
- Add migration to populate PromotedGroup from existing constants
- Implement test coverage to ensure data integrity and migration correctness
  • Loading branch information
KevinMind committed Feb 5, 2025
1 parent 3c7abc4 commit 2358f05
Show file tree
Hide file tree
Showing 4 changed files with 580 additions and 17 deletions.
30 changes: 15 additions & 15 deletions src/olympia/constants/promoted.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
('VERIFIED', 9, 'Verified'),
)

DEACTIVATED_LEGACY_IDS = [
PROMOTED_GROUP_CHOICES.SPONSORED,
PROMOTED_GROUP_CHOICES.VERIFIED,
]

BADGED_API_NAME = 'badged' # Special alias for all badged groups


### START LEGACY PROMOTION GROUPS ###
### DO NOT EDIT THIS SECTION ###

_PromotedSuperClass = namedtuple(
'_PromotedSuperClass',
Expand Down Expand Up @@ -88,20 +98,6 @@ def __bool__(self):
high_profile_rating=True,
)

# Obsolete, never used in production, only there to prevent us from re-using
# the ids. Both these classes used to have specific properties set that were
# removed since they are not supposed to be used anyway.
_SPONSORED = PromotedClass(
id=PROMOTED_GROUP_CHOICES.SPONSORED,
name='Sponsored',
api_name=PROMOTED_GROUP_CHOICES.SPONSORED.api_value,
)
_VERIFIED = PromotedClass(
id=PROMOTED_GROUP_CHOICES.VERIFIED,
name='Verified',
api_name=PROMOTED_GROUP_CHOICES.VERIFIED.api_value,
)

LINE = PromotedClass(
id=PROMOTED_GROUP_CHOICES.LINE,
name=_('By Firefox'),
Expand Down Expand Up @@ -151,6 +147,9 @@ def __bool__(self):

# _VERIFIED and _SPONSORED should not be included, they are no longer valid
# promoted groups.
# This data should be kept in sync with the new PromotedGroup model.
# If this list changes, we should update the relevant PromotedGroup instances
# via a data migration to add/remove the "active" field.
PROMOTED_GROUPS = [
NOT_PROMOTED,
RECOMMENDED,
Expand All @@ -161,10 +160,11 @@ def __bool__(self):
]

BADGED_GROUPS = [group for group in PROMOTED_GROUPS if group.badged]
BADGED_API_NAME = 'badged' # Special alias for all badged groups

PROMOTED_GROUPS_BY_ID = {p.id: p for p in PROMOTED_GROUPS}
PROMOTED_API_NAME_TO_IDS = {
**{p.api_name: [p.id] for p in PROMOTED_GROUPS if p},
BADGED_API_NAME: [p.id for p in BADGED_GROUPS],
}

### END LEGACY PROMOTION GROUPS ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Generated by Django 4.2.18 on 2025-02-04 19:21


from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import olympia.amo.models

from olympia.constants.promoted import PROMOTED_GROUPS_BY_ID

def create_promoted_groups(apps, schema_editor):
PromotedGroup = apps.get_model('promoted', 'PromotedGroup')
# Import legacy promoted groups from constants

# Loop over all groups (active and inactive) from PROMOTED_GROUPS_BY_ID
for group in PROMOTED_GROUPS_BY_ID.values():
PromotedGroup.objects.create(
legacy_id=group.id,
name=group.name,
api_name=group.api_name,
search_ranking_bump=group.search_ranking_bump,
listed_pre_review=group.listed_pre_review,
unlisted_pre_review=group.unlisted_pre_review,
admin_review=group.admin_review,
badged=group.badged,
autograph_signing_states=group.autograph_signing_states,
can_primary_hero=group.can_primary_hero,
immediate_approval=group.immediate_approval,
flag_for_human_review=group.flag_for_human_review,
can_be_compatible_with_all_fenix_versions=group.can_be_compatible_with_all_fenix_versions,
high_profile=group.high_profile,
high_profile_rating=group.high_profile_rating,
# We only include actively promoted groups so we can hardcode to true
active=True,
)


def reverse_promoted_groups(apps, schema_editor):
PromotedGroup = apps.get_model('promoted', 'PromotedGroup')
PromotedGroup.objects.all().delete()


def create_promoted_addon_promotions(apps, schema_editor):
PromotedAddonPromotion = apps.get_model('promoted', 'PromotedAddonPromotion')
PromotedAddon = apps.get_model('promoted', 'PromotedAddon')
PromotedGroup = apps.get_model('promoted', 'PromotedGroup')

for promoted_addon in PromotedAddon.objects.all():
PromotedAddonPromotion.objects.create(
promoted_group=PromotedGroup.objects.get(legacy_id=promoted_addon.group_id),
addon=promoted_addon.addon,
application_id=promoted_addon.application_id,
legacy_promoted_addon=promoted_addon,
)


def reverse_promoted_addon_promotions(apps, schema_editor):
PromotedAddonPromotion = apps.get_model('promoted', 'PromotedAddonPromotion')
PromotedAddonPromotion.objects.all().delete()


def create_promoted_addon_versions(apps, schema_editor):
PromotedAddonVersion = apps.get_model('promoted', 'PromotedAddonVersion')
PromotedApproval = apps.get_model('promoted', 'PromotedApproval')
PromotedGroup = apps.get_model('promoted', 'PromotedGroup')

for promoted_approval in PromotedApproval.objects.all():
PromotedAddonVersion.objects.create(
promoted_group=PromotedGroup.objects.get(legacy_id=promoted_approval.group_id),
version=promoted_approval.version,
application_id=promoted_approval.application_id,
)


def reverse_promoted_addon_versions(apps, schema_editor):
PromotedAddonVersion = apps.get_model('promoted', 'PromotedAddonVersion')
PromotedAddonVersion.objects.all().delete()

class Migration(migrations.Migration):

dependencies = [
('promoted', '0021_auto_20240919_0952'),
]

operations = [
migrations.CreateModel(
name='PromotedGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('legacy_id', models.SmallIntegerField(choices=[(0, 'Not Promoted'), (1, 'Recommended'), (4, 'By Firefox'), (5, 'Spotlight'), (6, 'Strategic'), (7, 'Notable'), (8, 'Sponsored'), (9, 'Verified')], help_text='The legacy ID from back when promoted groups were static classes')),
('name', models.CharField(help_text='Human-readable name for the promotion group.', max_length=255)),
('api_name', models.CharField(help_text='Programmatic API name for the promotion group.', max_length=100)),
('search_ranking_bump', models.FloatField(default=0.0, help_text='Boost value used to influence search ranking for add-ons in this group.')),
('listed_pre_review', models.BooleanField(default=False, help_text='Indicates if listed versions require pre-review.')),
('unlisted_pre_review', models.BooleanField(default=False, help_text='Indicates if unlisted versions require pre-review.')),
('admin_review', models.BooleanField(default=False, help_text='Specifies whether the promotion requires administrative review.')),
('badged', models.BooleanField(default=False, help_text='Specifies if the add-on receives a badge upon promotion.')),
('autograph_signing_states', models.JSONField(default=dict, help_text='Mapping of application shorthand to autograph signing states.')),
('can_primary_hero', models.BooleanField(default=False, help_text='Determines if the add-on can be featured in a primary hero shelf.')),
('immediate_approval', models.BooleanField(default=False, help_text='If true, add-ons are auto-approved upon saving.')),
('flag_for_human_review', models.BooleanField(default=False, help_text='If true, add-ons are flagged for manual human review.')),
('can_be_compatible_with_all_fenix_versions', models.BooleanField(default=False, help_text='Determines compatibility with all Fenix (Android) versions.')),
('high_profile', models.BooleanField(default=False, help_text='Indicates if the add-on is high-profile for review purposes.')),
('high_profile_rating', models.BooleanField(default=False, help_text='Indicates if developer replies are treated as high-profile.')),
('active', models.BooleanField(default=False, help_text='Marks whether this promotion group is active (inactive groups are considered obsolete).')),
],
),
migrations.CreateModel(
name='PromotedAddonPromotion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(blank=True, default=django.utils.timezone.now, editable=False)),
('modified', models.DateTimeField(auto_now=True)),
('application_id', models.SmallIntegerField(choices=[(1, 'Firefox'), (61, 'Firefox for Android')], verbose_name='Application')),
('addon', models.ForeignKey(help_text='Add-on id this item will point to (If you do not know the id, paste the slug instead and it will be transformed automatically for you. If you have access to the add-on admin page, you can use the magnifying glass to see all available add-ons.', on_delete=django.db.models.deletion.CASCADE, to='addons.addon')),
('legacy_promoted_addon', models.ForeignKey(help_text='The legacy promoted addon this instance belongs to.', null=True, on_delete=django.db.models.deletion.CASCADE, to='promoted.promotedaddon')),
('promoted_group', models.ForeignKey(help_text='Can be set to Not Promoted to disable promotion without deleting it. Note: changing the group does *not* change approvals of versions.', on_delete=django.db.models.deletion.CASCADE, to='promoted.promotedgroup')),
],
bases=(olympia.amo.models.SaveUpdateMixin, models.Model),
),
migrations.CreateModel(
name='PromotedAddonVersion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(blank=True, default=django.utils.timezone.now, editable=False)),
('modified', models.DateTimeField(auto_now=True)),
('application_id', models.SmallIntegerField(choices=[(1, 'Firefox'), (61, 'Firefox for Android')], default=None, null=True, verbose_name='Application')),
('promoted_group', models.ForeignKey(help_text='Can be set to Not Promoted to disable promotion without deleting it. Note: changing the group does *not* change ', on_delete=django.db.models.deletion.CASCADE, to='promoted.promotedgroup')),
('version', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promoted_versions', to='versions.version')),
('legacy_promoted_approval', models.ForeignKey(help_text='The legacy promoted approval this instance belongs to.', null=True, on_delete=django.db.models.deletion.CASCADE, to='promoted.promotedapproval')),
],
bases=(olympia.amo.models.SaveUpdateMixin, models.Model),
),
migrations.AddConstraint(
model_name='promotedaddonversion',
constraint=models.UniqueConstraint(fields=('promoted_group', 'version', 'application_id'), name='unique_promoted_addon_version'),
),
migrations.AddConstraint(
model_name='promotedaddonpromotion',
constraint=models.UniqueConstraint(fields=('promoted_group', 'addon', 'application_id'), name='unique_promoted_addon_application'),
),
migrations.AlterField(
model_name='promotedaddon',
name='group_id',
field=models.SmallIntegerField(choices=[(0, 'Not Promoted'), (1, 'Recommended'), (4, 'By Firefox'), (5, 'Spotlight'), (6, 'Strategic'), (7, 'Notable'), (8, 'Sponsored'), (9, 'Verified')], default=0, help_text='Can be set to Not Promoted to disable promotion without deleting it. Note: changing the group does *not* change approvals of versions.', verbose_name='Group'),
),
migrations.AlterField(
model_name='promotedapproval',
name='group_id',
field=models.SmallIntegerField(choices=[(0, 'Not Promoted'), (1, 'Recommended'), (4, 'By Firefox'), (5, 'Spotlight'), (6, 'Strategic'), (7, 'Notable'), (8, 'Sponsored'), (9, 'Verified')], null=True, verbose_name='Group'),
),
# Finally we can populate the new tables with legacy data.
# signals are used to sync the data after the migration has run.
migrations.RunPython(create_promoted_groups, reverse_promoted_groups),
migrations.RunPython(create_promoted_addon_promotions, reverse_promoted_addon_promotions),
migrations.RunPython(create_promoted_addon_versions, reverse_promoted_addon_versions),
]
Loading

0 comments on commit 2358f05

Please sign in to comment.