diff --git a/experimenter/experimenter/experiments/api/v5/serializers.py b/experimenter/experimenter/experiments/api/v5/serializers.py index 4d68c0827b..e1fb11ea96 100644 --- a/experimenter/experimenter/experiments/api/v5/serializers.py +++ b/experimenter/experimenter/experiments/api/v5/serializers.py @@ -19,6 +19,7 @@ from rest_framework import serializers from experimenter.experiments.constants import ( + FirefoxLabs, NimbusConstants, TargetingMultipleKintoCollectionsError, ) @@ -1757,12 +1758,9 @@ def _validate_firefox_labs(self, data): if not data.get("is_firefox_labs_opt_in"): return data - min_version = NimbusExperiment.Version.parse(data.get("firefox_min_version")) - required_min_version = NimbusExperiment.FIREFOX_LABS_MIN_VERSION.get( - self.instance.application - ) + application_config = self.instance.application_config - if required_min_version is None: + if (firefox_labs := application_config.firefox_labs) is None: raise serializers.ValidationError( { "is_firefox_labs_opt_in": ( @@ -1770,13 +1768,15 @@ def _validate_firefox_labs(self, data): ), } ) - required_min_version = NimbusExperiment.Version.parse(required_min_version) - if min_version < required_min_version: + + firefox_min_version = NimbusExperiment.Version.parse(data["firefox_min_version"]) + + if firefox_min_version < firefox_labs.min_supported_version: raise serializers.ValidationError( { "firefox_min_version": ( NimbusExperiment.ERROR_FIREFOX_LABS_MIN_VERSION.format( - version=required_min_version + version=firefox_labs.min_supported_version ) ), } @@ -1789,28 +1789,20 @@ def _validate_firefox_labs(self, data): errors = { field: [NimbusExperiment.ERROR_FIREFOX_LABS_REQUIRED_FIELD] - for field in ( - "firefox_labs_title", - "firefox_labs_description", - "firefox_labs_group", - ) - if not len((data.get(field) or "").strip()) + for field in firefox_labs.required_fields + if (field_value := data.get(field)) is None or not len(field_value.strip()) } group = data.get("firefox_labs_group") - if required_min_version := NimbusExperiment.FIREFOX_LABS_GROUP_AVAILABILITY[ - self.instance.application - ].get(group): - required_min_version = NimbusExperiment.Version.parse(required_min_version) - if min_version < required_min_version: - raise serializers.ValidationError( - { - "firefox_labs_group": ( - NimbusExperiment.ERROR_FIREFOX_LABS_GROUP_MIN_VERSION.format( - version=required_min_version - ) - ), - } + if group and firefox_labs.supports_groups: + # Unsupported groups are handled by the form field's choices. + if ( + group_min_version := firefox_labs.groups.get(group) + ) and firefox_min_version < group_min_version: + errors.setdefault("firefox_labs_group", []).append( + NimbusExperiment.ERROR_FIREFOX_LABS_GROUP_MIN_VERSION.format( + version=group_min_version, + ) ) if description_links := ( @@ -1823,7 +1815,15 @@ def _validate_firefox_labs(self, data): NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_JSON, ] else: - if isinstance(description_links_obj, dict): + if ( + not isinstance(description_links_obj, dict) + and description_links_obj is not None + ): + errors["firefox_labs_description_links"] = [ + NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_JSON, + ] + + elif isinstance(description_links_obj, dict): if not all( is_valid_http_url(value) for value in description_links_obj.values() @@ -1832,13 +1832,24 @@ def _validate_firefox_labs(self, data): NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_HTTP_URLS, ] - elif ( - not isinstance(description_links_obj, dict) - and description_links_obj is not None - ): - errors["firefox_labs_description_links"] = [ - NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_JSON, - ] + if ( + firefox_labs.supported_description_links + is not FirefoxLabs.ARBITRARY_KEYS + ): + unsupported_keys = set(description_links_obj.keys()).difference( + firefox_labs.supported_description_links + ) + + if unsupported_keys: + errors.setdefault( + "firefox_labs_description_links", [] + ).append( + NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_UNSUPPORTED_KEYS.format( + keys=", ".join( + sorted(firefox_labs.supported_description_links) + ) + ) + ) if errors: raise serializers.ValidationError(errors) diff --git a/experimenter/experimenter/experiments/constants.py b/experimenter/experimenter/experiments/constants.py index 23a8fd895a..cba517cd5d 100644 --- a/experimenter/experimenter/experiments/constants.py +++ b/experimenter/experimenter/experiments/constants.py @@ -5,10 +5,13 @@ from enum import Enum from typing import TYPE_CHECKING, Optional +import packaging.version from django.conf import settings from django.db import models from mozilla_nimbus_schemas.experimenter_apis.experiments import RandomizationUnit -from packaging import version +from typing_extensions import sentinel + +from experimenter.experiments.versions import Version if TYPE_CHECKING: # pragma: no cover from experimenter.experiments.models import NimbusExperiment @@ -74,6 +77,114 @@ def __init__(self, target_collections): self.target_collections = target_collections +@dataclass +class FirefoxLabs: + """Information about Firefox Labs support for a given application.""" + + class Groups(models.TextChoices): + CUSTOMIZE_BROWSING = "experimental-features-group-customize-browsing" + WEBPAGE_DISPLAY = "experimental-features-group-webpage-display" + DEVELOPER_TOOLS = "experimental-features-group-developer-tools" + PRODUCTIVITY = "experimental-features-group-productivity" + NEWTAB_WIDGETS = "experimental-features-group-newtab-widgets" + + #: This value indicates support for arbitrary description links. + ARBITRARY_KEYS = sentinel("ARBITRARY_KEYS") + + #: The set of description links keys available to nimbus-sdk-based + #: applications. + SDK_DESCRIPTION_LINKS = ("feedback",) + + #: The minimum application version that supports Firefox Labs. + min_supported_version: packaging.version.Version + + #: The available Firefox Labs groups for this application. + #: + #: A value of `None` indicates that the application does not support groups. + #: + #: Each group maps to the first version that it was available in. + groups: dict[str, packaging.version.Version] | None + + #: The supported keys for `firefox_labs_description_keys`. + #: + #: Firefox Desktop supports arbitrary description links, but nimbus-sdk + # based applications are limited to `SDK_DESCRIPTION_LINKS`. + supported_description_links: tuple[str] | ARBITRARY_KEYS + + #: The `min_supported_version` field, but unparsed. + #: + #: It only exists for tests. + _unparsed_min_supported_version: str + + #: This `groups` field, but unparsed. + #: + #: It only exists for tests. + _unparsed_groups: dict[str, str] | None + + def __init__( + self, + *, + min_supported_version: str, + groups: dict[str, str] | None = None, + supported_description_links: tuple[str] | ARBITRARY_KEYS = ARBITRARY_KEYS, + ): + self._unparsed_min_supported_version = min_supported_version + self.min_supported_version = Version.parse(min_supported_version) + + if groups is None: + self._unparsed_groups = self.groups = None + else: + self._unparsed_groups = groups + self.groups = { + group: Version.parse(min_supported_version) + for (group, min_supported_version) in groups.items() + } + + self.supported_description_links = supported_description_links + + @property + def supports_groups(self) -> bool: + """Whether this application supports the firefox_labs_groups field.""" + return self.groups is not None + + @property + def required_fields(self) -> list[str]: + """The required Firefox Labs fields for this application.""" + required_fields = ["firefox_labs_title", "firefox_labs_description"] + + if self.supports_groups: + required_fields.append("firefox_labs_group") + + return required_fields + + @property + def group_choices(self) -> list[tuple[str, str]]: + """The available group choices for this application. + + If the application does not support groups, this will be an empty list. + """ + if not self.supports_groups: + return [] + + return [ + choice for choice in FirefoxLabs.Groups.choices if choice[0] in self.groups + ] + + def available_groups_in_version( + self, + version: packaging.version.Version, + ) -> list[str]: + """Return the groups available in a given version.""" + if not self.supports_groups: + return [] + + return [ + group + for (group, min_required_version) in self.groups.items() + if min_required_version <= version + ] + + @dataclass class ApplicationConfig: name: str @@ -87,6 +198,11 @@ class ApplicationConfig: kinto_collections_by_feature_id: Optional[dict[str, str]] = field(default=None) targeting_context_file_name: Optional[str] = field(default=None) + #: The Firefox Labs configuration for the application. + #: + #: If None, this application will not support Firefox Labs. + firefox_labs: FirefoxLabs | None = field(default=None) + def get_kinto_collection_for_experiment(self, experiment: NimbusExperiment) -> str: if self.kinto_collections_by_feature_id is not None: return self.get_kinto_collection_for_feature_ids( @@ -165,6 +281,16 @@ def kinto_collections(self) -> set[str]: }, preview_collection=settings.KINTO_COLLECTION_NIMBUS_PREVIEW, targeting_context_file_name="TargetingContextRecorder.sys.mjs", + firefox_labs=FirefoxLabs( + min_supported_version=Version.FIREFOX_137, + groups={ + FirefoxLabs.Groups.CUSTOMIZE_BROWSING: Version.FIREFOX_137, + FirefoxLabs.Groups.WEBPAGE_DISPLAY: Version.FIREFOX_137, + FirefoxLabs.Groups.DEVELOPER_TOOLS: Version.FIREFOX_137, + FirefoxLabs.Groups.PRODUCTIVITY: Version.FIREFOX_143_B3, + FirefoxLabs.Groups.NEWTAB_WIDGETS: Version.FIREFOX_151, + }, + ), ) APPLICATION_CONFIG_FENIX = ApplicationConfig( @@ -181,6 +307,10 @@ def kinto_collections(self) -> set[str]: is_web=False, preview_collection=settings.KINTO_COLLECTION_NIMBUS_PREVIEW, targeting_context_file_name="RecordedNimbusContext.kt", + firefox_labs=FirefoxLabs( + min_supported_version=Version.FIREFOX_154, + supported_description_links=FirefoxLabs.SDK_DESCRIPTION_LINKS, + ), ) APPLICATION_CONFIG_IOS = ApplicationConfig( @@ -424,391 +554,7 @@ class HomeTypeChoices(models.TextChoices): EXPERIMENT = "Experiment", "🔬 Experiment" LABS = "Labs", "🧪 Labs" - class Version(models.TextChoices): - @staticmethod - def parse(version_str): - return version.parse(version_str.replace("!", "0")) - - NO_VERSION = "" - FIREFOX_11 = "11.!" - FIREFOX_12 = "12.!" - FIREFOX_13 = "13.!" - FIREFOX_14 = "14.!" - FIREFOX_15 = "15.!" - FIREFOX_16 = "16.!" - FIREFOX_17 = "17.!" - FIREFOX_18 = "18.!" - FIREFOX_19 = "19.!" - FIREFOX_20 = "20.!" - FIREFOX_21 = "21.!" - FIREFOX_22 = "22.!" - FIREFOX_23 = "23.!" - FIREFOX_24 = "24.!" - FIREFOX_25 = "25.!" - FIREFOX_26 = "26.!" - FIREFOX_27 = "27.!" - FIREFOX_28 = "28.!" - FIREFOX_29 = "29.!" - FIREFOX_30 = "30.!" - FIREFOX_31 = "31.!" - FIREFOX_32 = "32.!" - FIREFOX_33 = "33.!" - FIREFOX_34 = "34.!" - FIREFOX_35 = "35.!" - FIREFOX_36 = "36.!" - FIREFOX_37 = "37.!" - FIREFOX_38 = "38.!" - FIREFOX_39 = "39.!" - FIREFOX_40 = "40.!" - FIREFOX_41 = "41.!" - FIREFOX_42 = "42.!" - FIREFOX_43 = "43.!" - FIREFOX_44 = "44.!" - FIREFOX_45 = "45.!" - FIREFOX_46 = "46.!" - FIREFOX_47 = "47.!" - FIREFOX_48 = "48.!" - FIREFOX_49 = "49.!" - FIREFOX_50 = "50.!" - FIREFOX_51 = "51.!" - FIREFOX_52 = "52.!" - FIREFOX_53 = "53.!" - FIREFOX_54 = "54.!" - FIREFOX_55 = "55.!" - FIREFOX_56 = "56.!" - FIREFOX_57 = "57.!" - FIREFOX_58 = "58.!" - FIREFOX_59 = "59.!" - FIREFOX_60 = "60.!" - FIREFOX_61 = "61.!" - FIREFOX_62 = "62.!" - FIREFOX_63 = "63.!" - FIREFOX_64 = "64.!" - FIREFOX_65 = "65.!" - FIREFOX_66 = "66.!" - FIREFOX_67 = "67.!" - FIREFOX_68 = "68.!" - FIREFOX_69 = "69.!" - FIREFOX_70 = "70.!" - FIREFOX_71 = "71.!" - FIREFOX_72 = "72.!" - FIREFOX_73 = "73.!" - FIREFOX_74 = "74.!" - FIREFOX_75 = "75.!" - FIREFOX_76 = "76.!" - FIREFOX_77 = "77.!" - FIREFOX_78 = "78.!" - FIREFOX_79 = "79.!" - FIREFOX_80 = "80.!" - FIREFOX_81 = "81.!" - FIREFOX_82 = "82.!" - FIREFOX_83 = "83.!" - FIREFOX_84 = "84.!" - FIREFOX_85 = "85.!" - FIREFOX_86 = "86.!" - FIREFOX_87 = "87.!" - FIREFOX_88 = "88.!" - FIREFOX_89 = "89.!" - FIREFOX_90 = "90.!" - FIREFOX_91 = "91.!" - FIREFOX_92 = "92.!" - FIREFOX_9201 = "92.0.1" - FIREFOX_93 = "93.!" - FIREFOX_94 = "94.!" - FIREFOX_95 = "95.!" - FIREFOX_96 = "96.!" - FIREFOX_9601 = "96.0.1" - FIREFOX_9602 = "96.0.2" - FIREFOX_97 = "97.!" - FIREFOX_98 = "98.!" - FIREFOX_9830 = "98.3.0" - FIREFOX_99 = "99.!" - FIREFOX_9910 = "99.1.0" - FIREFOX_100 = "100.!" - FIREFOX_101 = "101.!" - FIREFOX_102 = "102.!" - FIREFOX_103 = "103.!" - FIREFOX_10301 = "103.0.1" - FIREFOX_104 = "104.!" - FIREFOX_105 = "105.!" - FIREFOX_10501 = "105.0.1" - FIREFOX_10502 = "105.0.2" - FIREFOX_10503 = "105.0.3" - FIREFOX_106 = "106.!" - FIREFOX_10601 = "106.0.1" - FIREFOX_10602 = "106.0.2" - FIREFOX_107 = "107.!" - FIREFOX_108 = "108.!" - FIREFOX_109 = "109.!" - FIREFOX_110 = "110.!" - FIREFOX_111 = "111.!" - FIREFOX_111_0_1 = "111.0.1" - FIREFOX_112 = "112.!" - FIREFOX_113 = "113.!" - FIREFOX_113_0_1 = "113.0.1" - FIREFOX_114 = "114.!" - FIREFOX_114_3_0 = "114.3.0" - FIREFOX_115 = "115.!" - FIREFOX_115_0_2 = "115.0.2" - FIREFOX_115_7 = "115.7.0" - FIREFOX_115_25 = "115.25.0" - FIREFOX_116 = "116.!" - FIREFOX_116_0_1 = "116.0.1" - FIREFOX_116_2_0 = "116.2.0" - FIREFOX_116_3_0 = "116.3.0" - FIREFOX_117 = "117.!" - FIREFOX_118 = "118.!" - FIREFOX_118_0_1 = "118.0.1" - FIREFOX_118_0_2 = "118.0.2" - FIREFOX_119 = "119.!" - FIREFOX_120 = "120.!" - FIREFOX_121 = "121.!" - FIREFOX_121_0_1 = "121.0.1" - FIREFOX_122 = "122.!" - FIREFOX_122_1_0 = "122.1.0" - FIREFOX_122_2_0 = "122.2.0" - FIREFOX_123 = "123.!" - FIREFOX_123_0_1 = "123.0.1" - FIREFOX_124 = "124.!" - FIREFOX_124_2_0 = "124.2.0" - FIREFOX_124_3_0 = "124.3.0" - FIREFOX_125 = "125.!" - FIREFOX_125_0_1 = "125.0.1" - FIREFOX_125_0_2 = "125.0.2" - FIREFOX_125_1_0 = "125.1.0" - FIREFOX_125_2_0 = "125.2.0" - FIREFOX_126 = "126.!" - FIREFOX_126_1_0 = "126.1.0" - FIREFOX_126_2_0 = "126.2.0" - FIREFOX_127 = "127.!" - FIREFOX_127_0_1 = "127.0.1" - FIREFOX_127_0_2 = "127.0.2" - FIREFOX_128 = "128.!" - FIREFOX_128_12 = "128.12.0" - FIREFOX_129 = "129.!" - FIREFOX_130 = "130.!" - FIREFOX_130_0_1 = "130.0.1" - FIREFOX_131 = "131.!" - FIREFOX_131_B4 = "131.0b4" - FIREFOX_131_0_3 = "131.0.3" - FIREFOX_131_1_0 = "131.1.0" - FIREFOX_131_2_0 = "131.2.0" - FIREFOX_132 = "132.!" - FIREFOX_132_B6 = "132.0b6" - FIREFOX_133 = "133.!" - FIREFOX_133_B8 = "133.0b8" - FIREFOX_133_0_1 = "133.0.1" - FIREFOX_134 = "134.!" - FIREFOX_134_1_0 = "134.1.0" - FIREFOX_135 = "135.!" - FIREFOX_135_0_1 = "135.0.1" - FIREFOX_135_1_0 = "135.1.0" - FIREFOX_136 = "136.!" - FIREFOX_136_0_2 = "136.0.2" - FIREFOX_137 = "137.!" - FIREFOX_137_1_0 = "137.1.0" - FIREFOX_137_2_0 = "137.2.0" - FIREFOX_138 = "138.!" - FIREFOX_138_B3 = "138.0b3" - FIREFOX_138_1_0 = "138.1.0" - FIREFOX_138_2_0 = "138.2.0" - FIREFOX_138_0_3 = "138.0.3" - FIREFOX_139 = "139.!" - FIREFOX_139_1_0 = "139.1.0" - FIREFOX_139_2_0 = "139.2.0" - FIREFOX_139_0_4 = "139.0.4" - FIREFOX_140 = "140.!" - FIREFOX_140_0_1 = "140.0.1" - FIREFOX_140_0_2 = "140.0.2" - FIREFOX_140_0_3 = "140.0.3" - FIREFOX_140_0_4 = "140.0.4" - FIREFOX_140_1_0 = "140.1.0" - FIREFOX_140_2_0 = "140.2.0" - FIREFOX_140_3_0 = "140.3.0" - FIREFOX_140_4_0 = "140.4.0" - FIREFOX_141 = "141.!" - FIREFOX_141_0_1 = "141.0.1" - FIREFOX_141_0_2 = "141.0.2" - FIREFOX_141_0_3 = "141.0.3" - FIREFOX_141_0_4 = "141.0.4" - FIREFOX_141_1_0 = "141.1.0" - FIREFOX_141_2_0 = "141.2.0" - FIREFOX_141_3_0 = "141.3.0" - FIREFOX_141_4_0 = "141.4.0" - FIREFOX_142 = "142.!" - FIREFOX_142_0_1 = "142.0.1" - FIREFOX_142_0_2 = "142.0.2" - FIREFOX_142_0_3 = "142.0.3" - FIREFOX_142_0_4 = "142.0.4" - FIREFOX_142_1_0 = "142.1.0" - FIREFOX_142_2_0 = "142.2.0" - FIREFOX_142_3_0 = "142.3.0" - FIREFOX_142_4_0 = "142.4.0" - FIREFOX_143 = "143.!" - FIREFOX_143_B3 = "143.0b3" - FIREFOX_143_B4 = "143.0b4" - FIREFOX_143_0_1 = "143.0.1" - FIREFOX_143_0_2 = "143.0.2" - FIREFOX_143_0_3 = "143.0.3" - FIREFOX_143_0_4 = "143.0.4" - FIREFOX_143_1_0 = "143.1.0" - FIREFOX_143_1_1 = "143.1.1" - FIREFOX_143_2_0 = "143.2.0" - FIREFOX_143_3_0 = "143.3.0" - FIREFOX_143_4_0 = "143.4.0" - FIREFOX_144 = "144.!" - FIREFOX_144_0_1 = "144.0.1" - FIREFOX_144_0_2 = "144.0.2" - FIREFOX_144_0_3 = "144.0.3" - FIREFOX_144_0_4 = "144.0.4" - FIREFOX_144_1_0 = "144.1.0" - FIREFOX_144_2_0 = "144.2.0" - FIREFOX_144_3_0 = "144.3.0" - FIREFOX_144_4_0 = "144.4.0" - FIREFOX_145 = "145.!" - FIREFOX_145_0_1 = "145.0.1" - FIREFOX_145_0_2 = "145.0.2" - FIREFOX_145_0_3 = "145.0.3" - FIREFOX_145_0_4 = "145.0.4" - FIREFOX_145_1_0 = "145.1.0" - FIREFOX_145_2_0 = "145.2.0" - FIREFOX_145_3_0 = "145.3.0" - FIREFOX_145_4_0 = "145.4.0" - FIREFOX_146 = "146.!" - FIREFOX_146_0_1 = "146.0.1" - FIREFOX_146_0_2 = "146.0.2" - FIREFOX_146_0_3 = "146.0.3" - FIREFOX_146_0_4 = "146.0.4" - FIREFOX_146_1_0 = "146.1.0" - FIREFOX_146_2_0 = "146.2.0" - FIREFOX_146_3_0 = "146.3.0" - FIREFOX_146_4_0 = "146.4.0" - FIREFOX_147 = "147.!" - FIREFOX_147_0_1 = "147.0.1" - FIREFOX_147_0_2 = "147.0.2" - FIREFOX_147_0_3 = "147.0.3" - FIREFOX_147_0_4 = "147.0.4" - FIREFOX_147_1_0 = "147.1.0" - FIREFOX_147_2_0 = "147.2.0" - FIREFOX_147_3_0 = "147.3.0" - FIREFOX_147_4_0 = "147.4.0" - FIREFOX_148 = "148.!" - FIREFOX_148_0_1 = "148.0.1" - FIREFOX_148_0_2 = "148.0.2" - FIREFOX_148_0_3 = "148.0.3" - FIREFOX_148_0_4 = "148.0.4" - FIREFOX_148_1_0 = "148.1.0" - FIREFOX_148_2_0 = "148.2.0" - FIREFOX_148_3_0 = "148.3.0" - FIREFOX_148_4_0 = "148.4.0" - FIREFOX_149 = "149.!" - FIREFOX_149_0_1 = "149.0.1" - FIREFOX_149_0_2 = "149.0.2" - FIREFOX_149_0_3 = "149.0.3" - FIREFOX_149_0_4 = "149.0.4" - FIREFOX_149_1_0 = "149.1.0" - FIREFOX_149_2_0 = "149.2.0" - FIREFOX_149_3_0 = "149.3.0" - FIREFOX_149_4_0 = "149.4.0" - FIREFOX_150 = "150.!" - FIREFOX_150_0_1 = "150.0.1" - FIREFOX_150_0_2 = "150.0.2" - FIREFOX_150_0_3 = "150.0.3" - FIREFOX_150_0_4 = "150.0.4" - FIREFOX_150_1_0 = "150.1.0" - FIREFOX_150_2_0 = "150.2.0" - FIREFOX_150_3_0 = "150.3.0" - FIREFOX_150_4_0 = "150.4.0" - FIREFOX_151 = "151.!" - FIREFOX_151_0_1 = "151.0.1" - FIREFOX_151_0_2 = "151.0.2" - FIREFOX_151_0_3 = "151.0.3" - FIREFOX_151_0_4 = "151.0.4" - FIREFOX_151_1_0 = "151.1.0" - FIREFOX_151_2_0 = "151.2.0" - FIREFOX_151_3_0 = "151.3.0" - FIREFOX_151_4_0 = "151.4.0" - FIREFOX_152 = "152.!" - FIREFOX_152_0_1 = "152.0.1" - FIREFOX_152_0_2 = "152.0.2" - FIREFOX_152_0_3 = "152.0.3" - FIREFOX_152_0_4 = "152.0.4" - FIREFOX_152_1_0 = "152.1.0" - FIREFOX_152_2_0 = "152.2.0" - FIREFOX_152_3_0 = "152.3.0" - FIREFOX_152_4_0 = "152.4.0" - FIREFOX_153 = "153.!" - FIREFOX_153_0_1 = "153.0.1" - FIREFOX_153_0_2 = "153.0.2" - FIREFOX_153_0_3 = "153.0.3" - FIREFOX_153_0_4 = "153.0.4" - FIREFOX_153_1_0 = "153.1.0" - FIREFOX_153_2_0 = "153.2.0" - FIREFOX_153_3_0 = "153.3.0" - FIREFOX_153_4_0 = "153.4.0" - FIREFOX_154 = "154.!" - FIREFOX_154_0_1 = "154.0.1" - FIREFOX_154_0_2 = "154.0.2" - FIREFOX_154_0_3 = "154.0.3" - FIREFOX_154_0_4 = "154.0.4" - FIREFOX_154_1_0 = "154.1.0" - FIREFOX_154_2_0 = "154.2.0" - FIREFOX_154_3_0 = "154.3.0" - FIREFOX_154_4_0 = "154.4.0" - FIREFOX_155 = "155.!" - FIREFOX_155_0_1 = "155.0.1" - FIREFOX_155_0_2 = "155.0.2" - FIREFOX_155_0_3 = "155.0.3" - FIREFOX_155_0_4 = "155.0.4" - FIREFOX_155_1_0 = "155.1.0" - FIREFOX_155_2_0 = "155.2.0" - FIREFOX_155_3_0 = "155.3.0" - FIREFOX_155_4_0 = "155.4.0" - FIREFOX_156 = "156.!" - FIREFOX_156_0_1 = "156.0.1" - FIREFOX_156_0_2 = "156.0.2" - FIREFOX_156_0_3 = "156.0.3" - FIREFOX_156_0_4 = "156.0.4" - FIREFOX_156_1_0 = "156.1.0" - FIREFOX_156_2_0 = "156.2.0" - FIREFOX_156_3_0 = "156.3.0" - FIREFOX_156_4_0 = "156.4.0" - FIREFOX_157 = "157.!" - FIREFOX_157_0_1 = "157.0.1" - FIREFOX_157_0_2 = "157.0.2" - FIREFOX_157_0_3 = "157.0.3" - FIREFOX_157_0_4 = "157.0.4" - FIREFOX_157_1_0 = "157.1.0" - FIREFOX_157_2_0 = "157.2.0" - FIREFOX_157_3_0 = "157.3.0" - FIREFOX_157_4_0 = "157.4.0" - FIREFOX_158 = "158.!" - FIREFOX_158_0_1 = "158.0.1" - FIREFOX_158_0_2 = "158.0.2" - FIREFOX_158_0_3 = "158.0.3" - FIREFOX_158_0_4 = "158.0.4" - FIREFOX_158_1_0 = "158.1.0" - FIREFOX_158_2_0 = "158.2.0" - FIREFOX_158_3_0 = "158.3.0" - FIREFOX_158_4_0 = "158.4.0" - FIREFOX_159 = "159.!" - FIREFOX_159_0_1 = "159.0.1" - FIREFOX_159_0_2 = "159.0.2" - FIREFOX_159_0_3 = "159.0.3" - FIREFOX_159_0_4 = "159.0.4" - FIREFOX_159_1_0 = "159.1.0" - FIREFOX_159_2_0 = "159.2.0" - FIREFOX_159_3_0 = "159.3.0" - FIREFOX_159_4_0 = "159.4.0" - FIREFOX_160 = "160.!" - FIREFOX_160_0_1 = "160.0.1" - FIREFOX_160_0_2 = "160.0.2" - FIREFOX_160_0_3 = "160.0.3" - FIREFOX_160_0_4 = "160.0.4" - FIREFOX_160_1_0 = "160.1.0" - FIREFOX_160_2_0 = "160.2.0" - FIREFOX_160_3_0 = "160.3.0" - FIREFOX_160_4_0 = "160.4.0" + Version = Version class EmailType(models.TextChoices): EXPERIMENT_END = "experiment end" @@ -849,12 +595,7 @@ class AnalysisWindow(models.TextChoices): WEEKLY = "weekly", "Weekly" OVERALL = "overall", "Overall" - class FirefoxLabsGroups(models.TextChoices): - CUSTOMIZE_BROWSING = "experimental-features-group-customize-browsing" - WEBPAGE_DISPLAY = "experimental-features-group-webpage-display" - DEVELOPER_TOOLS = "experimental-features-group-developer-tools" - PRODUCTIVITY = "experimental-features-group-productivity" - NEWTAB_WIDGETS = "experimental-features-group-newtab-widgets" + FirefoxLabs = FirefoxLabs EMAIL_EXPERIMENT_END_SUBJECT = "Action required: Please turn off your Experiment" EMAIL_ENROLLMENT_END_SUBJECT = "Action required: Please end experiment enrollment" @@ -1136,20 +877,6 @@ class FirefoxLabsGroups(models.TextChoices): OBSERVATION = "Observation" ENROLLMENT = "Enrollment" - FIREFOX_LABS_MIN_VERSION = { - Application.DESKTOP: Version.FIREFOX_137, - } - - FIREFOX_LABS_GROUP_AVAILABILITY = { - Application.DESKTOP: { - FirefoxLabsGroups.CUSTOMIZE_BROWSING: Version.FIREFOX_137, - FirefoxLabsGroups.WEBPAGE_DISPLAY: Version.FIREFOX_137, - FirefoxLabsGroups.DEVELOPER_TOOLS: Version.FIREFOX_137, - FirefoxLabsGroups.PRODUCTIVITY: Version.FIREFOX_143_B3, - FirefoxLabsGroups.NEWTAB_WIDGETS: Version.FIREFOX_151, - }, - } - ERROR_FIREFOX_LABS_MIN_VERSION = ( "Firefox Labs requires at least version " "{version.major}.{version.minor}.{version.micro}." @@ -1160,16 +887,26 @@ class FirefoxLabsGroups(models.TextChoices): "{version.micro}" ) + ERROR_FIREFOX_LABS_GROUPS_UNSUPPORTED = ( + "This application does not support Firefox Labs groups." + ) + ERROR_FIREFOX_LABS_UNSUPPORTED_APPLICATION = ( "This application does not support Firefox Labs." ) + ERROR_FIREFOX_LABS_UNKNOWN_GROUP = "This group is not supported" + ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_JSON = ( "Firefox Labs description links must be a JSON object or null." ) ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_HTTP_URLS = ( "Firefox Labs description links values must be HTTP(S) URLs." ) + ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_UNSUPPORTED_KEYS = ( + "Firefox Labs only supports the following description links for this " + "application: {keys}" + ) ERROR_FIREFOX_LABS_REQUIRED_FIELD = "This field is requried for Firefox Labs Opt-Ins." ERROR_FIREFOX_LABS_ROLLOUT_REQUIRED = "Firefox Labs opt-ins must be rollouts." diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index 75af68d3ff..b3054f09b3 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -490,7 +490,7 @@ def default_firefox_version_parsed(): blank=True, null=True, max_length=255, - choices=NimbusConstants.FirefoxLabsGroups.choices, + choices=NimbusConstants.FirefoxLabs.Groups.choices, ) requires_restart = models.BooleanField( ( diff --git a/experimenter/experimenter/experiments/tests/api/v5/test_serializers/test_nimbus_ready_for_review_serializer.py b/experimenter/experimenter/experiments/tests/api/v5/test_serializers/test_nimbus_ready_for_review_serializer.py index bebfeb933d..a45f20c066 100644 --- a/experimenter/experimenter/experiments/tests/api/v5/test_serializers/test_nimbus_ready_for_review_serializer.py +++ b/experimenter/experimenter/experiments/tests/api/v5/test_serializers/test_nimbus_ready_for_review_serializer.py @@ -4,6 +4,7 @@ from typing import Literal, Optional, Union from unittest.mock import patch +import packaging.version from django.test import TestCase from parameterized import parameterized @@ -12,7 +13,11 @@ LocaleFactory, ) from experimenter.experiments.api.v5.serializers import NimbusReviewSerializer -from experimenter.experiments.constants import NimbusConstants +from experimenter.experiments.constants import ( + ApplicationConfig, + FirefoxLabs, + NimbusConstants, +) from experimenter.experiments.models import NimbusExperiment, NimbusFeatureVersion from experimenter.experiments.tests.api.v5.test_serializers import ( mock_targeting_manifests, @@ -78,8 +83,20 @@ """ -class TestNimbusReviewSerializerSingleFeature(MockFmlErrorMixin, TestCase): +class GetReviewSerializerMixin: + def get_review_serializer(self, experiment): + return NimbusReviewSerializer( + experiment, + data=NimbusReviewSerializer(experiment, context={"user": self.user}).data, + context={"user": self.user}, + ) + + +class TestNimbusReviewSerializerSingleFeature( + GetReviewSerializerMixin, MockFmlErrorMixin, TestCase +): maxDiff = None + longMessage = True def setUp(self): super().setUp() @@ -2382,7 +2399,6 @@ def test_targeting_exclude_require_mutally_exclusive(self): @parameterized.expand((False, True)) def test_setpref_rollout_warning(self, prevent_pref_conflicts): - self.maxDiff = None experiment = NimbusExperimentFactory.create_with_lifecycle( NimbusExperimentFactory.Lifecycles.CREATED, firefox_min_version=NimbusExperiment.Version.FIREFOX_105, @@ -3007,98 +3023,160 @@ def test_empty_segments(self): self.assertTrue(serializer.is_valid(), serializer.errors) + @parameterized.expand( + NimbusConstants.APPLICATION_CONFIGS[application] + for application in sorted( + set(NimbusConstants.APPLICATION_CONFIGS) + - {NimbusConstants.Application.DESKTOP, NimbusConstants.Application.FENIX} + ) + ) + def test_firefox_labs_unsupported_applications( + self, application_config: ApplicationConfig + ): + experiment_kwargs = {} + + if not application_config.is_web: + experiment_kwargs["firefox_min_version"] = NimbusConstants.Version.FIREFOX_137 + + experiment = NimbusExperimentFactory.create_with_lifecycle( + NimbusExperimentFactory.Lifecycles.CREATED, + application=application_config.slug, + is_rollout=True, + **experiment_kwargs, + ) + + # We cannot pass experiment_fields to the factory due to it enforcing + # Firefox Labs support at an application level. + experiment.is_firefox_labs_opt_in = True + experiment.firefox_labs_title = "title" + experiment.firefox_labs_description = "desc" + experiment.firefox_labs_group = FirefoxLabs.Groups.CUSTOMIZE_BROWSING + experiment.save() + + serializer = self.get_review_serializer(experiment) + + self.assertFalse(serializer.is_valid()) + self.assertDictEqual( + serializer.errors, + { + "is_firefox_labs_opt_in": [ + NimbusExperiment.ERROR_FIREFOX_LABS_UNSUPPORTED_APPLICATION, + ] + }, + ) + @parameterized.expand( [ ( - NimbusExperiment.Application.DESKTOP, - NimbusExperiment.Version.FIREFOX_137, - None, - ), - ( - NimbusExperiment.Application.DESKTOP, - NimbusExperiment.Version.FIREFOX_136, - { - "firefox_min_version": [ - "Firefox Labs requires at least version 137.0.0." - ] - }, + NimbusConstants.Application.DESKTOP, + NimbusConstants.Version.FIREFOX_136, + FirefoxLabs.Groups.CUSTOMIZE_BROWSING, + False, ), ( - NimbusExperiment.Application.FENIX, - NimbusExperiment.Version.FIREFOX_137, - { - "is_firefox_labs_opt_in": [ - NimbusExperiment.ERROR_FIREFOX_LABS_UNSUPPORTED_APPLICATION - ] - }, + NimbusConstants.Application.DESKTOP, + NimbusConstants.Version.FIREFOX_137, + FirefoxLabs.Groups.CUSTOMIZE_BROWSING, + True, ), ( - NimbusExperiment.Application.IOS, - NimbusExperiment.Version.FIREFOX_137, - { - "is_firefox_labs_opt_in": [ - NimbusExperiment.ERROR_FIREFOX_LABS_UNSUPPORTED_APPLICATION - ] - }, + NimbusConstants.Application.FENIX, + NimbusConstants.Version.FIREFOX_153, + None, + False, ), ( - NimbusExperiment.Application.FXA, - NimbusExperiment.Version.NO_VERSION, - { - "is_firefox_labs_opt_in": [ - NimbusExperiment.ERROR_FIREFOX_LABS_UNSUPPORTED_APPLICATION - ] - }, + NimbusConstants.Application.FENIX, + NimbusConstants.Version.FIREFOX_154, + None, + True, ), ] ) - def test_firefox_labs_applications_verions( - self, application, version, expected_errors + def test_firefox_labs_supported_applications( + self, + application: str, + firefox_min_version: str, + firefox_labs_group: str | None, + expected_valid: bool, ): experiment = NimbusExperimentFactory.create_with_lifecycle( NimbusExperimentFactory.Lifecycles.CREATED, application=application, - firefox_min_version=version, + firefox_min_version=firefox_min_version, + is_rollout=True, is_firefox_labs_opt_in=True, firefox_labs_title="title", - firefox_labs_description="description", - firefox_labs_group=NimbusExperiment.FirefoxLabsGroups.CUSTOMIZE_BROWSING, - is_rollout=True, + firefox_labs_description="desc", + firefox_labs_group=firefox_labs_group, ) - serializer = NimbusReviewSerializer( - experiment, - data=NimbusReviewSerializer(experiment, context={"user": self.user}).data, - context={"user": self.user}, - ) + serializer = self.get_review_serializer(experiment) - self.assertEqual( - not bool(expected_errors), - serializer.is_valid(), - serializer.errors, - ) - if expected_errors: - self.assertEqual(expected_errors, serializer.errors) + self.assertEqual(serializer.is_valid(), expected_valid, serializer.errors) + if not expected_valid: + firefox_labs = experiment.application_config.firefox_labs + self.assertDictEqual( + serializer.errors, + { + "firefox_min_version": [ + NimbusConstants.ERROR_FIREFOX_LABS_MIN_VERSION.format( + version=firefox_labs.min_supported_version, + ) + ] + }, + ) @parameterized.expand( [ - (True, None), ( - False, - {"is_rollout": [NimbusExperiment.ERROR_FIREFOX_LABS_ROLLOUT_REQUIRED]}, - ), + application, + application_config.firefox_labs, + is_rollout, + expected_errors, + ) + for ( + application, + application_config, + ) in NimbusExperiment.APPLICATION_CONFIGS.items() + if application_config.firefox_labs + for (is_rollout, expected_errors) in ( + (True, None), + ( + False, + { + "is_rollout": [ + NimbusExperiment.ERROR_FIREFOX_LABS_ROLLOUT_REQUIRED + ] + }, + ), + ) ] ) - def test_firefox_labs_rollout_required(self, is_rollout, expected_errors): + def test_firefox_labs_rollout_required( + self, + application: str, + firefox_labs: FirefoxLabs, + is_rollout: bool, + expected_errors: dict[str, list[str]] | None, + ): + experiment_kwargs = {} + if firefox_labs.supports_groups: + experiment_kwargs["firefox_labs_group"] = ( + firefox_labs.available_groups_in_version( + firefox_labs.min_supported_version + )[0] + ) + experiment = NimbusExperimentFactory.create_with_lifecycle( NimbusExperimentFactory.Lifecycles.CREATED, - application=NimbusExperiment.Application.DESKTOP, - firefox_min_version=NimbusExperiment.Version.FIREFOX_137, - firefox_labs_group=NimbusExperiment.FirefoxLabsGroups.CUSTOMIZE_BROWSING, + application=application, + firefox_min_version=firefox_labs._unparsed_min_supported_version, firefox_labs_title="title", firefox_labs_description="description", is_firefox_labs_opt_in=True, is_rollout=is_rollout, + **experiment_kwargs, ) serializer = NimbusReviewSerializer( @@ -3113,19 +3191,16 @@ def test_firefox_labs_rollout_required(self, is_rollout, expected_errors): serializer.errors, ) if expected_errors: - self.assertEqual(expected_errors, serializer.errors) + self.assertEqual(serializer.errors, expected_errors) @parameterized.expand( [ - ({}, None), ( { "is_firefox_labs_opt_in": True, "firefox_labs_title": "title", "firefox_labs_description": "description", - "firefox_labs_group": ( - NimbusExperiment.FirefoxLabsGroups.CUSTOMIZE_BROWSING - ), + "firefox_labs_group": FirefoxLabs.Groups.CUSTOMIZE_BROWSING, }, None, ), @@ -3133,9 +3208,7 @@ def test_firefox_labs_rollout_required(self, is_rollout, expected_errors): { "is_firefox_labs_opt_in": True, "firefox_labs_description": "description", - "firefox_labs_group": ( - NimbusExperiment.FirefoxLabsGroups.CUSTOMIZE_BROWSING - ), + "firefox_labs_group": FirefoxLabs.Groups.CUSTOMIZE_BROWSING, }, ["firefox_labs_title"], ), @@ -3143,9 +3216,7 @@ def test_firefox_labs_rollout_required(self, is_rollout, expected_errors): { "is_firefox_labs_opt_in": True, "firefox_labs_title": "title", - "firefox_labs_group": ( - NimbusExperiment.FirefoxLabsGroups.CUSTOMIZE_BROWSING - ), + "firefox_labs_group": FirefoxLabs.Groups.CUSTOMIZE_BROWSING, }, ["firefox_labs_description"], ), @@ -3162,9 +3233,7 @@ def test_firefox_labs_rollout_required(self, is_rollout, expected_errors): "is_firefox_labs_opt_in": True, "firefox_labs_title": "", "firefox_labs_description": "", - "firefox_labs_group": ( - NimbusExperiment.FirefoxLabsGroups.CUSTOMIZE_BROWSING - ), + "firefox_labs_group": FirefoxLabs.Groups.CUSTOMIZE_BROWSING, }, ["firefox_labs_title", "firefox_labs_description"], ), @@ -3173,16 +3242,16 @@ def test_firefox_labs_rollout_required(self, is_rollout, expected_errors): "is_firefox_labs_opt_in": True, "firefox_labs_title": " ", "firefox_labs_description": " ", - "firefox_labs_group": ( - NimbusExperiment.FirefoxLabsGroups.CUSTOMIZE_BROWSING - ), + "firefox_labs_group": FirefoxLabs.Groups.CUSTOMIZE_BROWSING, }, ["firefox_labs_title", "firefox_labs_description"], ), ] ) - def test_firefox_labs_required_fields( - self, experiment_fields, expected_required_fields + def test_firefox_labs_desktop_required_fields( + self, + experiment_fields: dict[str, str], + expected_required_fields: list[str], ): experiment = NimbusExperimentFactory.create_with_lifecycle( NimbusExperimentFactory.Lifecycles.CREATED, @@ -3193,17 +3262,117 @@ def test_firefox_labs_required_fields( # We cannot pass experiment_fields to the factory due to it enforcing # required fields when is_firefox_labs=True. + experiment.is_firefox_labs_opt_in = True for field, value in experiment_fields.items(): setattr(experiment, field, value) experiment.save() - serializer = NimbusReviewSerializer( - experiment, - data=NimbusReviewSerializer(experiment, context={"user": self.user}).data, - context={"user": self.user}, + serializer = self.get_review_serializer(experiment) + + self.assertEqual( + not bool(expected_required_fields), + serializer.is_valid(), + serializer.errors, + ) + if expected_required_fields: + expected_errors = { + field: [NimbusExperiment.ERROR_FIREFOX_LABS_REQUIRED_FIELD] + for field in expected_required_fields + } + self.assertDictEqual(expected_errors, serializer.errors) + + @parameterized.expand( + chain.from_iterable( + [ + ( + application, + firefox_min_version, + { + "is_firefox_labs_opt_in": True, + "firefox_labs_title": "title", + "firefox_labs_description": "description", + }, + None, + ), + ( + application, + firefox_min_version, + { + "is_firefox_labs_opt_in": True, + "firefox_labs_description": "description", + }, + ["firefox_labs_title"], + ), + ( + application, + firefox_min_version, + { + "is_firefox_labs_opt_in": True, + "firefox_labs_title": "title", + }, + ["firefox_labs_description"], + ), + ( + application, + firefox_min_version, + { + "is_firefox_labs_opt_in": True, + "firefox_labs_title": "title", + "firefox_labs_description": "description", + }, + [], + ), + ( + application, + firefox_min_version, + { + "is_firefox_labs_opt_in": True, + "firefox_labs_title": "", + "firefox_labs_description": "", + }, + ["firefox_labs_title", "firefox_labs_description"], + ), + ( + application, + firefox_min_version, + { + "is_firefox_labs_opt_in": True, + "firefox_labs_title": " ", + "firefox_labs_description": " ", + }, + ["firefox_labs_title", "firefox_labs_description"], + ), + ] + for (application, firefox_min_version) in ( + (NimbusConstants.Application.FENIX, NimbusConstants.Version.FIREFOX_154), + ) + ) + ) + def test_firefox_labs_sdk_required_fields( + self, + application: str, + firefox_min_version: str, + experiment_fields: dict[str, str], + expected_required_fields: list[str], + ): + experiment = NimbusExperimentFactory.create_with_lifecycle( + NimbusExperimentFactory.Lifecycles.CREATED, + application=application, + firefox_min_version=firefox_min_version, + is_rollout=True, ) + # We cannot pass experiment_fields to the factory due to it enforcing + # required fields when is_firefox_labs=True. + experiment.is_firefox_labs_opt_in = True + for field, value in experiment_fields.items(): + setattr(experiment, field, value) + + experiment.save() + + serializer = self.get_review_serializer(experiment) + self.assertEqual( not bool(expected_required_fields), serializer.is_valid(), @@ -3214,40 +3383,42 @@ def test_firefox_labs_required_fields( field: [NimbusExperiment.ERROR_FIREFOX_LABS_REQUIRED_FIELD] for field in expected_required_fields } - self.assertEqual(expected_errors, serializer.errors) + self.assertDictEqual(expected_errors, serializer.errors) @parameterized.expand( chain( + # All available groups work at their mimimum supported version. ( - (application, group, required_version, None) - for application, available_groups in ( - NimbusExperiment.FIREFOX_LABS_GROUP_AVAILABILITY.items() + (group, required_version, None) + for (group, required_version) in ( + NimbusConstants.APPLICATION_CONFIGS[ + NimbusConstants.Application.DESKTOP + ].firefox_labs._unparsed_groups.items() ) - for group, required_version in available_groups.items() ), + # Groups added after 137 will throw an error when used before their + # minimum supported version. ( - ( - application, - group, - NimbusExperiment.Version.FIREFOX_137, - NimbusExperiment.ERROR_FIREFOX_LABS_GROUP_MIN_VERSION.format( - version=NimbusExperiment.Version.parse(required_version), - ), - ) - for application, available_groups in ( - NimbusExperiment.FIREFOX_LABS_GROUP_AVAILABILITY.items() + (group, NimbusConstants.Version.FIREFOX_137, required_version) + for (group, required_version) in ( + NimbusConstants.APPLICATION_CONFIGS[ + NimbusConstants.Application.DESKTOP + ].firefox_labs.groups.items() ) - for group, required_version in available_groups.items() - if required_version != NimbusExperiment.Version.FIREFOX_137 + if required_version + > NimbusConstants.Version.parse(NimbusConstants.Version.FIREFOX_137) ), - ) + ), ) - def test_firefox_labs_group_availability( - self, application, firefox_labs_group, firefox_min_version, expected_error + def test_firefox_labs_desktop_group_availability( + self, + firefox_labs_group: str, + firefox_min_version: str, + error_required_version: packaging.version.Version | None, ): experiment = NimbusExperimentFactory.create_with_lifecycle( NimbusExperimentFactory.Lifecycles.CREATED, - application=application, + application=NimbusExperiment.Application.DESKTOP, firefox_min_version=firefox_min_version, is_rollout=True, is_firefox_labs_opt_in=True, @@ -3256,85 +3427,93 @@ def test_firefox_labs_group_availability( firefox_labs_group=firefox_labs_group, ) - serializer = NimbusReviewSerializer( - experiment, - data=NimbusReviewSerializer(experiment, context={"user": self.user}).data, - context={"user": self.user}, - ) - + serializer = self.get_review_serializer(experiment) self.assertEqual( - not bool(expected_error), - serializer.is_valid(), - serializer.errors, + not bool(error_required_version), serializer.is_valid(), serializer.errors ) - if expected_error: - self.assertEqual( - {"firefox_labs_group": [expected_error]}, + + if error_required_version: + self.assertDictEqual( serializer.errors, - expected_error, + { + "firefox_labs_group": [ + NimbusConstants.ERROR_FIREFOX_LABS_GROUP_MIN_VERSION.format( + version=error_required_version + ), + ] + }, ) @parameterized.expand( - [ + ( + application, + application_config.firefox_labs, + firefox_labs_description_links, + expected_error, + ) + for ( + application, + application_config, + ) in NimbusConstants.APPLICATION_CONFIGS.items() + if application_config.firefox_labs + for (firefox_labs_description_links, expected_error) in [ (None, None), ("", None), ("null", None), ("{}", None), ( - json.dumps( - { - "foo": "https://mozilla.org", - } - ), + json.dumps({"feedback": "https://mozilla.org"}), None, ), ("1", NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_JSON), ("hello", NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_JSON), ("[]", NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_JSON), ( - json.dumps({"hello": "world"}), + json.dumps({"feedback": "hello, world"}), NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_HTTP_URLS, ), ( - json.dumps({"foo": "chrome:///bogus.xhtml"}), + json.dumps({"feedback": "chrome:///bogus.xhtml"}), NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_HTTP_URLS, ), ( - json.dumps({"foo": "resource:///bogus.xhtml"}), + json.dumps({"feedback": "resource:///bogus.xhtml"}), NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_HTTP_URLS, ), ( - json.dumps({"foo": "about:blank"}), + json.dumps({"feedback": "about:blank"}), NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_HTTP_URLS, ), ( - json.dumps({"foo": 1}), + json.dumps({"feedback": 1}), NimbusExperiment.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_HTTP_URLS, ), ] ) def test_firefox_labs_description_links( self, - firefox_labs_description_links, - expected_error, + application: str, + firefox_labs: FirefoxLabs, + firefox_labs_description_links: str, + expected_error: str | None, ): experiment = NimbusExperimentFactory.create_with_lifecycle( NimbusExperimentFactory.Lifecycles.CREATED, - application=NimbusExperiment.Application.DESKTOP, - firefox_min_version=NimbusExperiment.Version.FIREFOX_137, + application=application, + firefox_min_version=firefox_labs._unparsed_min_supported_version, is_rollout=True, is_firefox_labs_opt_in=True, firefox_labs_title="title", firefox_labs_description="description", + firefox_labs_group=( + FirefoxLabs.Groups.CUSTOMIZE_BROWSING + if firefox_labs.supports_groups + else None + ), firefox_labs_description_links=firefox_labs_description_links, - firefox_labs_group=NimbusExperiment.FirefoxLabsGroups.CUSTOMIZE_BROWSING, ) - serializer = NimbusReviewSerializer( - experiment, - data=NimbusReviewSerializer(experiment, context={"user": self.user}).data, - context={"user": self.user}, - ) + serializer = self.get_review_serializer(experiment) self.assertEqual( not bool(expected_error), @@ -3342,10 +3521,68 @@ def test_firefox_labs_description_links( serializer.errors, ) if expected_error: - self.assertEqual( - {"firefox_labs_description_links": [expected_error]}, + self.assertDictEqual( + serializer.errors, + { + "firefox_labs_description_links": [expected_error], + }, + ) + + @parameterized.expand( + ( + application, + NimbusConstants.APPLICATION_CONFIGS[application].firefox_labs, + key, + expected_error, + ) + for application in (NimbusConstants.Application.FENIX,) + for (key, expected_error) in ( + ("feedback", True), + ("connect", False), + ("unsupported", False), + ) + ) + def test_firefox_labs_sdk_description_links_keys( + self, + application: str, + firefox_labs: FirefoxLabs, + key: str, + expected_valid: bool, + ): + experiment = NimbusExperimentFactory.create_with_lifecycle( + NimbusExperimentFactory.Lifecycles.CREATED, + application=application, + firefox_min_version=firefox_labs._unparsed_min_supported_version, + is_rollout=True, + is_firefox_labs_opt_in=True, + firefox_labs_title="title", + firefox_labs_description="description", + firefox_labs_description_links=json.dumps( + { + key: "https://mozilla.org", + } + ), + ) + + serializer = self.get_review_serializer(experiment) + + self.assertEqual( + serializer.is_valid(), + expected_valid, + serializer.errors, + ) + if not expected_valid: + self.assertDictEqual( serializer.errors, - expected_error, + { + "firefox_labs_description_links": [ + NimbusConstants.ERROR_FIREFOX_LABS_DESCRIPTION_LINKS_UNSUPPORTED_KEYS.format( + keys=",".join( + sorted(firefox_labs.supported_description_links) + ) + ), + ] + }, ) def test_invalid_targeting_expr(self): diff --git a/experimenter/experimenter/experiments/tests/factories.py b/experimenter/experimenter/experiments/tests/factories.py index 7d987dc4a2..ed9fb6f3ef 100644 --- a/experimenter/experimenter/experiments/tests/factories.py +++ b/experimenter/experimenter/experiments/tests/factories.py @@ -1063,14 +1063,19 @@ def create( ) if kwargs.get("is_firefox_labs_opt_in", False): - for field in ( - "firefox_labs_title", - "firefox_labs_description", - "firefox_labs_group", - ): + application = kwargs.get("application", NimbusExperimentFactory.application) + firefox_labs = NimbusExperiment.APPLICATION_CONFIGS[application].firefox_labs + + if not firefox_labs: + raise factory.FactoryError( + f"The application {application} does not support Firefox Labs" + ) + + for field in firefox_labs.required_fields: if kwargs.get(field) is None: raise factory.FactoryError( - f"The field {field} is required when is_firefox_labs_opt_in=True" + f"The field {field} is required for application {application} " + "when is_firefox_labs_opt_in=True" ) experiment = super().create(*args, **kwargs) diff --git a/experimenter/experimenter/experiments/versions.py b/experimenter/experimenter/experiments/versions.py new file mode 100644 index 0000000000..54768bb68d --- /dev/null +++ b/experimenter/experimenter/experiments/versions.py @@ -0,0 +1,389 @@ +import packaging.version +from django.db import models + + +class Version(models.TextChoices): + @staticmethod + def parse(version_str: str) -> packaging.version.Version: + return packaging.version.parse(version_str.replace("!", "0")) + + NO_VERSION = "" + FIREFOX_11 = "11.!" + FIREFOX_12 = "12.!" + FIREFOX_13 = "13.!" + FIREFOX_14 = "14.!" + FIREFOX_15 = "15.!" + FIREFOX_16 = "16.!" + FIREFOX_17 = "17.!" + FIREFOX_18 = "18.!" + FIREFOX_19 = "19.!" + FIREFOX_20 = "20.!" + FIREFOX_21 = "21.!" + FIREFOX_22 = "22.!" + FIREFOX_23 = "23.!" + FIREFOX_24 = "24.!" + FIREFOX_25 = "25.!" + FIREFOX_26 = "26.!" + FIREFOX_27 = "27.!" + FIREFOX_28 = "28.!" + FIREFOX_29 = "29.!" + FIREFOX_30 = "30.!" + FIREFOX_31 = "31.!" + FIREFOX_32 = "32.!" + FIREFOX_33 = "33.!" + FIREFOX_34 = "34.!" + FIREFOX_35 = "35.!" + FIREFOX_36 = "36.!" + FIREFOX_37 = "37.!" + FIREFOX_38 = "38.!" + FIREFOX_39 = "39.!" + FIREFOX_40 = "40.!" + FIREFOX_41 = "41.!" + FIREFOX_42 = "42.!" + FIREFOX_43 = "43.!" + FIREFOX_44 = "44.!" + FIREFOX_45 = "45.!" + FIREFOX_46 = "46.!" + FIREFOX_47 = "47.!" + FIREFOX_48 = "48.!" + FIREFOX_49 = "49.!" + FIREFOX_50 = "50.!" + FIREFOX_51 = "51.!" + FIREFOX_52 = "52.!" + FIREFOX_53 = "53.!" + FIREFOX_54 = "54.!" + FIREFOX_55 = "55.!" + FIREFOX_56 = "56.!" + FIREFOX_57 = "57.!" + FIREFOX_58 = "58.!" + FIREFOX_59 = "59.!" + FIREFOX_60 = "60.!" + FIREFOX_61 = "61.!" + FIREFOX_62 = "62.!" + FIREFOX_63 = "63.!" + FIREFOX_64 = "64.!" + FIREFOX_65 = "65.!" + FIREFOX_66 = "66.!" + FIREFOX_67 = "67.!" + FIREFOX_68 = "68.!" + FIREFOX_69 = "69.!" + FIREFOX_70 = "70.!" + FIREFOX_71 = "71.!" + FIREFOX_72 = "72.!" + FIREFOX_73 = "73.!" + FIREFOX_74 = "74.!" + FIREFOX_75 = "75.!" + FIREFOX_76 = "76.!" + FIREFOX_77 = "77.!" + FIREFOX_78 = "78.!" + FIREFOX_79 = "79.!" + FIREFOX_80 = "80.!" + FIREFOX_81 = "81.!" + FIREFOX_82 = "82.!" + FIREFOX_83 = "83.!" + FIREFOX_84 = "84.!" + FIREFOX_85 = "85.!" + FIREFOX_86 = "86.!" + FIREFOX_87 = "87.!" + FIREFOX_88 = "88.!" + FIREFOX_89 = "89.!" + FIREFOX_90 = "90.!" + FIREFOX_91 = "91.!" + FIREFOX_92 = "92.!" + FIREFOX_9201 = "92.0.1" + FIREFOX_93 = "93.!" + FIREFOX_94 = "94.!" + FIREFOX_95 = "95.!" + FIREFOX_96 = "96.!" + FIREFOX_9601 = "96.0.1" + FIREFOX_9602 = "96.0.2" + FIREFOX_97 = "97.!" + FIREFOX_98 = "98.!" + FIREFOX_9830 = "98.3.0" + FIREFOX_99 = "99.!" + FIREFOX_9910 = "99.1.0" + FIREFOX_100 = "100.!" + FIREFOX_101 = "101.!" + FIREFOX_102 = "102.!" + FIREFOX_103 = "103.!" + FIREFOX_10301 = "103.0.1" + FIREFOX_104 = "104.!" + FIREFOX_105 = "105.!" + FIREFOX_10501 = "105.0.1" + FIREFOX_10502 = "105.0.2" + FIREFOX_10503 = "105.0.3" + FIREFOX_106 = "106.!" + FIREFOX_10601 = "106.0.1" + FIREFOX_10602 = "106.0.2" + FIREFOX_107 = "107.!" + FIREFOX_108 = "108.!" + FIREFOX_109 = "109.!" + FIREFOX_110 = "110.!" + FIREFOX_111 = "111.!" + FIREFOX_111_0_1 = "111.0.1" + FIREFOX_112 = "112.!" + FIREFOX_113 = "113.!" + FIREFOX_113_0_1 = "113.0.1" + FIREFOX_114 = "114.!" + FIREFOX_114_3_0 = "114.3.0" + FIREFOX_115 = "115.!" + FIREFOX_115_0_2 = "115.0.2" + FIREFOX_115_7 = "115.7.0" + FIREFOX_115_25 = "115.25.0" + FIREFOX_116 = "116.!" + FIREFOX_116_0_1 = "116.0.1" + FIREFOX_116_2_0 = "116.2.0" + FIREFOX_116_3_0 = "116.3.0" + FIREFOX_117 = "117.!" + FIREFOX_118 = "118.!" + FIREFOX_118_0_1 = "118.0.1" + FIREFOX_118_0_2 = "118.0.2" + FIREFOX_119 = "119.!" + FIREFOX_120 = "120.!" + FIREFOX_121 = "121.!" + FIREFOX_121_0_1 = "121.0.1" + FIREFOX_122 = "122.!" + FIREFOX_122_1_0 = "122.1.0" + FIREFOX_122_2_0 = "122.2.0" + FIREFOX_123 = "123.!" + FIREFOX_123_0_1 = "123.0.1" + FIREFOX_124 = "124.!" + FIREFOX_124_2_0 = "124.2.0" + FIREFOX_124_3_0 = "124.3.0" + FIREFOX_125 = "125.!" + FIREFOX_125_0_1 = "125.0.1" + FIREFOX_125_0_2 = "125.0.2" + FIREFOX_125_1_0 = "125.1.0" + FIREFOX_125_2_0 = "125.2.0" + FIREFOX_126 = "126.!" + FIREFOX_126_1_0 = "126.1.0" + FIREFOX_126_2_0 = "126.2.0" + FIREFOX_127 = "127.!" + FIREFOX_127_0_1 = "127.0.1" + FIREFOX_127_0_2 = "127.0.2" + FIREFOX_128 = "128.!" + FIREFOX_128_12 = "128.12.0" + FIREFOX_129 = "129.!" + FIREFOX_130 = "130.!" + FIREFOX_130_0_1 = "130.0.1" + FIREFOX_131 = "131.!" + FIREFOX_131_B4 = "131.0b4" + FIREFOX_131_0_3 = "131.0.3" + FIREFOX_131_1_0 = "131.1.0" + FIREFOX_131_2_0 = "131.2.0" + FIREFOX_132 = "132.!" + FIREFOX_132_B6 = "132.0b6" + FIREFOX_133 = "133.!" + FIREFOX_133_B8 = "133.0b8" + FIREFOX_133_0_1 = "133.0.1" + FIREFOX_134 = "134.!" + FIREFOX_134_1_0 = "134.1.0" + FIREFOX_135 = "135.!" + FIREFOX_135_0_1 = "135.0.1" + FIREFOX_135_1_0 = "135.1.0" + FIREFOX_136 = "136.!" + FIREFOX_136_0_2 = "136.0.2" + FIREFOX_137 = "137.!" + FIREFOX_137_1_0 = "137.1.0" + FIREFOX_137_2_0 = "137.2.0" + FIREFOX_138 = "138.!" + FIREFOX_138_B3 = "138.0b3" + FIREFOX_138_1_0 = "138.1.0" + FIREFOX_138_2_0 = "138.2.0" + FIREFOX_138_0_3 = "138.0.3" + FIREFOX_139 = "139.!" + FIREFOX_139_1_0 = "139.1.0" + FIREFOX_139_2_0 = "139.2.0" + FIREFOX_139_0_4 = "139.0.4" + FIREFOX_140 = "140.!" + FIREFOX_140_0_1 = "140.0.1" + FIREFOX_140_0_2 = "140.0.2" + FIREFOX_140_0_3 = "140.0.3" + FIREFOX_140_0_4 = "140.0.4" + FIREFOX_140_1_0 = "140.1.0" + FIREFOX_140_2_0 = "140.2.0" + FIREFOX_140_3_0 = "140.3.0" + FIREFOX_140_4_0 = "140.4.0" + FIREFOX_141 = "141.!" + FIREFOX_141_0_1 = "141.0.1" + FIREFOX_141_0_2 = "141.0.2" + FIREFOX_141_0_3 = "141.0.3" + FIREFOX_141_0_4 = "141.0.4" + FIREFOX_141_1_0 = "141.1.0" + FIREFOX_141_2_0 = "141.2.0" + FIREFOX_141_3_0 = "141.3.0" + FIREFOX_141_4_0 = "141.4.0" + FIREFOX_142 = "142.!" + FIREFOX_142_0_1 = "142.0.1" + FIREFOX_142_0_2 = "142.0.2" + FIREFOX_142_0_3 = "142.0.3" + FIREFOX_142_0_4 = "142.0.4" + FIREFOX_142_1_0 = "142.1.0" + FIREFOX_142_2_0 = "142.2.0" + FIREFOX_142_3_0 = "142.3.0" + FIREFOX_142_4_0 = "142.4.0" + FIREFOX_143 = "143.!" + FIREFOX_143_B3 = "143.0b3" + FIREFOX_143_B4 = "143.0b4" + FIREFOX_143_0_1 = "143.0.1" + FIREFOX_143_0_2 = "143.0.2" + FIREFOX_143_0_3 = "143.0.3" + FIREFOX_143_0_4 = "143.0.4" + FIREFOX_143_1_0 = "143.1.0" + FIREFOX_143_1_1 = "143.1.1" + FIREFOX_143_2_0 = "143.2.0" + FIREFOX_143_3_0 = "143.3.0" + FIREFOX_143_4_0 = "143.4.0" + FIREFOX_144 = "144.!" + FIREFOX_144_0_1 = "144.0.1" + FIREFOX_144_0_2 = "144.0.2" + FIREFOX_144_0_3 = "144.0.3" + FIREFOX_144_0_4 = "144.0.4" + FIREFOX_144_1_0 = "144.1.0" + FIREFOX_144_2_0 = "144.2.0" + FIREFOX_144_3_0 = "144.3.0" + FIREFOX_144_4_0 = "144.4.0" + FIREFOX_145 = "145.!" + FIREFOX_145_0_1 = "145.0.1" + FIREFOX_145_0_2 = "145.0.2" + FIREFOX_145_0_3 = "145.0.3" + FIREFOX_145_0_4 = "145.0.4" + FIREFOX_145_1_0 = "145.1.0" + FIREFOX_145_2_0 = "145.2.0" + FIREFOX_145_3_0 = "145.3.0" + FIREFOX_145_4_0 = "145.4.0" + FIREFOX_146 = "146.!" + FIREFOX_146_0_1 = "146.0.1" + FIREFOX_146_0_2 = "146.0.2" + FIREFOX_146_0_3 = "146.0.3" + FIREFOX_146_0_4 = "146.0.4" + FIREFOX_146_1_0 = "146.1.0" + FIREFOX_146_2_0 = "146.2.0" + FIREFOX_146_3_0 = "146.3.0" + FIREFOX_146_4_0 = "146.4.0" + FIREFOX_147 = "147.!" + FIREFOX_147_0_1 = "147.0.1" + FIREFOX_147_0_2 = "147.0.2" + FIREFOX_147_0_3 = "147.0.3" + FIREFOX_147_0_4 = "147.0.4" + FIREFOX_147_1_0 = "147.1.0" + FIREFOX_147_2_0 = "147.2.0" + FIREFOX_147_3_0 = "147.3.0" + FIREFOX_147_4_0 = "147.4.0" + FIREFOX_148 = "148.!" + FIREFOX_148_0_1 = "148.0.1" + FIREFOX_148_0_2 = "148.0.2" + FIREFOX_148_0_3 = "148.0.3" + FIREFOX_148_0_4 = "148.0.4" + FIREFOX_148_1_0 = "148.1.0" + FIREFOX_148_2_0 = "148.2.0" + FIREFOX_148_3_0 = "148.3.0" + FIREFOX_148_4_0 = "148.4.0" + FIREFOX_149 = "149.!" + FIREFOX_149_0_1 = "149.0.1" + FIREFOX_149_0_2 = "149.0.2" + FIREFOX_149_0_3 = "149.0.3" + FIREFOX_149_0_4 = "149.0.4" + FIREFOX_149_1_0 = "149.1.0" + FIREFOX_149_2_0 = "149.2.0" + FIREFOX_149_3_0 = "149.3.0" + FIREFOX_149_4_0 = "149.4.0" + FIREFOX_150 = "150.!" + FIREFOX_150_0_1 = "150.0.1" + FIREFOX_150_0_2 = "150.0.2" + FIREFOX_150_0_3 = "150.0.3" + FIREFOX_150_0_4 = "150.0.4" + FIREFOX_150_1_0 = "150.1.0" + FIREFOX_150_2_0 = "150.2.0" + FIREFOX_150_3_0 = "150.3.0" + FIREFOX_150_4_0 = "150.4.0" + FIREFOX_151 = "151.!" + FIREFOX_151_0_1 = "151.0.1" + FIREFOX_151_0_2 = "151.0.2" + FIREFOX_151_0_3 = "151.0.3" + FIREFOX_151_0_4 = "151.0.4" + FIREFOX_151_1_0 = "151.1.0" + FIREFOX_151_2_0 = "151.2.0" + FIREFOX_151_3_0 = "151.3.0" + FIREFOX_151_4_0 = "151.4.0" + FIREFOX_152 = "152.!" + FIREFOX_152_0_1 = "152.0.1" + FIREFOX_152_0_2 = "152.0.2" + FIREFOX_152_0_3 = "152.0.3" + FIREFOX_152_0_4 = "152.0.4" + FIREFOX_152_1_0 = "152.1.0" + FIREFOX_152_2_0 = "152.2.0" + FIREFOX_152_3_0 = "152.3.0" + FIREFOX_152_4_0 = "152.4.0" + FIREFOX_153 = "153.!" + FIREFOX_153_0_1 = "153.0.1" + FIREFOX_153_0_2 = "153.0.2" + FIREFOX_153_0_3 = "153.0.3" + FIREFOX_153_0_4 = "153.0.4" + FIREFOX_153_1_0 = "153.1.0" + FIREFOX_153_2_0 = "153.2.0" + FIREFOX_153_3_0 = "153.3.0" + FIREFOX_153_4_0 = "153.4.0" + FIREFOX_154 = "154.!" + FIREFOX_154_0_1 = "154.0.1" + FIREFOX_154_0_2 = "154.0.2" + FIREFOX_154_0_3 = "154.0.3" + FIREFOX_154_0_4 = "154.0.4" + FIREFOX_154_1_0 = "154.1.0" + FIREFOX_154_2_0 = "154.2.0" + FIREFOX_154_3_0 = "154.3.0" + FIREFOX_154_4_0 = "154.4.0" + FIREFOX_155 = "155.!" + FIREFOX_155_0_1 = "155.0.1" + FIREFOX_155_0_2 = "155.0.2" + FIREFOX_155_0_3 = "155.0.3" + FIREFOX_155_0_4 = "155.0.4" + FIREFOX_155_1_0 = "155.1.0" + FIREFOX_155_2_0 = "155.2.0" + FIREFOX_155_3_0 = "155.3.0" + FIREFOX_155_4_0 = "155.4.0" + FIREFOX_156 = "156.!" + FIREFOX_156_0_1 = "156.0.1" + FIREFOX_156_0_2 = "156.0.2" + FIREFOX_156_0_3 = "156.0.3" + FIREFOX_156_0_4 = "156.0.4" + FIREFOX_156_1_0 = "156.1.0" + FIREFOX_156_2_0 = "156.2.0" + FIREFOX_156_3_0 = "156.3.0" + FIREFOX_156_4_0 = "156.4.0" + FIREFOX_157 = "157.!" + FIREFOX_157_0_1 = "157.0.1" + FIREFOX_157_0_2 = "157.0.2" + FIREFOX_157_0_3 = "157.0.3" + FIREFOX_157_0_4 = "157.0.4" + FIREFOX_157_1_0 = "157.1.0" + FIREFOX_157_2_0 = "157.2.0" + FIREFOX_157_3_0 = "157.3.0" + FIREFOX_157_4_0 = "157.4.0" + FIREFOX_158 = "158.!" + FIREFOX_158_0_1 = "158.0.1" + FIREFOX_158_0_2 = "158.0.2" + FIREFOX_158_0_3 = "158.0.3" + FIREFOX_158_0_4 = "158.0.4" + FIREFOX_158_1_0 = "158.1.0" + FIREFOX_158_2_0 = "158.2.0" + FIREFOX_158_3_0 = "158.3.0" + FIREFOX_158_4_0 = "158.4.0" + FIREFOX_159 = "159.!" + FIREFOX_159_0_1 = "159.0.1" + FIREFOX_159_0_2 = "159.0.2" + FIREFOX_159_0_3 = "159.0.3" + FIREFOX_159_0_4 = "159.0.4" + FIREFOX_159_1_0 = "159.1.0" + FIREFOX_159_2_0 = "159.2.0" + FIREFOX_159_3_0 = "159.3.0" + FIREFOX_159_4_0 = "159.4.0" + FIREFOX_160 = "160.!" + FIREFOX_160_0_1 = "160.0.1" + FIREFOX_160_0_2 = "160.0.2" + FIREFOX_160_0_3 = "160.0.3" + FIREFOX_160_0_4 = "160.0.4" + FIREFOX_160_1_0 = "160.1.0" + FIREFOX_160_2_0 = "160.2.0" + FIREFOX_160_3_0 = "160.3.0" + FIREFOX_160_4_0 = "160.4.0" diff --git a/experimenter/experimenter/nimbus_ui/forms.py b/experimenter/experimenter/nimbus_ui/forms.py index e7d8b8eb79..e8d3903a98 100644 --- a/experimenter/experimenter/nimbus_ui/forms.py +++ b/experimenter/experimenter/nimbus_ui/forms.py @@ -713,7 +713,6 @@ class NimbusBranchesForm(NimbusChangeLogFormMixin, forms.ModelForm): ) firefox_labs_group = forms.ChoiceField( required=False, - choices=NimbusExperiment.FirefoxLabsGroups.choices, widget=forms.Select(attrs={"class": "form-select"}), ) requires_restart = forms.BooleanField( @@ -807,6 +806,9 @@ def __init__(self, *args, **kwargs): for field in self.update_on_change_fields: self.fields[field].widget.attrs.update(update_on_change_attrs) + if firefox_labs := self.instance.application_config.firefox_labs: + self.fields["firefox_labs_group"].choices = firefox_labs.group_choices + self.was_labs_opt_in = self.instance.is_firefox_labs_opt_in @property diff --git a/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/edit_branches.html b/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/edit_branches.html index deafa3ecf4..2380ecffec 100644 --- a/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/edit_branches.html +++ b/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/edit_branches.html @@ -316,7 +316,7 @@