diff --git a/backend/analytics_server/mhq/api/resources/settings_resource.py b/backend/analytics_server/mhq/api/resources/settings_resource.py index 192ea2ed2..42a824b86 100644 --- a/backend/analytics_server/mhq/api/resources/settings_resource.py +++ b/backend/analytics_server/mhq/api/resources/settings_resource.py @@ -1,10 +1,11 @@ from mhq.service.settings.models import ( ConfigurationSettings, - DefaultSyncDaysSetting, IncidentSettings, ExcludedPRsSetting, IncidentTypesSetting, IncidentSourcesSetting, + DefaultSyncDaysSetting, + IncidentPrsSetting, ) from mhq.store.models import EntityType @@ -55,6 +56,13 @@ def _add_setting_data(config_settings: ConfigurationSettings, response): "default_sync_days": config_settings.specific_settings.default_sync_days } + if isinstance(config_settings.specific_settings, IncidentPrsSetting): + response["setting"] = { + "include_revert_prs": config_settings.specific_settings.include_revert_prs, + "title_filters": config_settings.specific_settings.title_filters, + "head_branch_filters": config_settings.specific_settings.head_branch_filters, + } + # ADD NEW API ADAPTER HERE return response diff --git a/backend/analytics_server/mhq/service/incidents/incident_filter.py b/backend/analytics_server/mhq/service/incidents/incident_filter.py index f2bc8c7a2..8ed916190 100644 --- a/backend/analytics_server/mhq/service/incidents/incident_filter.py +++ b/backend/analytics_server/mhq/service/incidents/incident_filter.py @@ -1,9 +1,11 @@ from typing import Dict, List, Any, Optional +from mhq.store.models.incidents.enums import IncidentType from mhq.store.models.settings.configuration_settings import SettingType from mhq.service.settings.configuration_settings import ( get_settings_service, IncidentSettings, IncidentTypesSetting, + IncidentPrsSetting, ) from mhq.store.models.incidents import IncidentFilter @@ -106,4 +108,19 @@ def __incident_type_setting(self) -> List[str]: incident_types = [] if setting and isinstance(setting, IncidentTypesSetting): incident_types = setting.incident_types + + if SettingType.INCIDENT_PRS_SETTING in self.setting_types: + incident_prs_setting: Optional[IncidentPrsSetting] = ( + self.setting_type_to_settings_map.get(SettingType.INCIDENT_PRS_SETTING) + ) + if ( + isinstance(incident_prs_setting, IncidentPrsSetting) + and not incident_prs_setting.include_revert_prs + ): + incident_types = [ + incident_type + for incident_type in incident_types + if incident_type != IncidentType.REVERT_PR + ] + return incident_types diff --git a/backend/analytics_server/mhq/service/settings/configuration_settings.py b/backend/analytics_server/mhq/service/settings/configuration_settings.py index 41975312d..b5c5bf379 100644 --- a/backend/analytics_server/mhq/service/settings/configuration_settings.py +++ b/backend/analytics_server/mhq/service/settings/configuration_settings.py @@ -4,11 +4,12 @@ from mhq.service.settings.default_settings_data import get_default_setting_data from mhq.service.settings.models import ( ConfigurationSettings, - DefaultSyncDaysSetting, ExcludedPRsSetting, IncidentSettings, IncidentSourcesSetting, IncidentTypesSetting, + DefaultSyncDaysSetting, + IncidentPrsSetting, ) from mhq.store.models.core.users import Users from mhq.store.models.incidents import IncidentSource, IncidentType @@ -66,6 +67,15 @@ def _adapt_default_sync_days_setting_from_setting_data(self, data: Dict[str, any default_sync_days=data.get("default_sync_days", None) ) + def _adapt_incident_prs_setting_setting_from_setting_data( + self, data: Dict[str, any] + ): + return IncidentPrsSetting( + include_revert_prs=data.get("include_revert_prs", True), + title_filters=data.get("title_filters", []), + head_branch_filters=data.get("head_branch_filters", []), + ) + # ADD NEW DICT TO DATACLASS ADAPTERS HERE def _handle_config_setting_from_db_setting( @@ -88,6 +98,11 @@ def _handle_config_setting_from_db_setting( if setting_type == SettingType.DEFAULT_SYNC_DAYS_SETTING: return self._adapt_default_sync_days_setting_from_setting_data(setting_data) + if setting_type == SettingType.INCIDENT_PRS_SETTING: + return self._adapt_incident_prs_setting_setting_from_setting_data( + setting_data + ) + # ADD NEW HANDLE FROM DB SETTINGS HERE raise Exception(f"Invalid Setting Type: {setting_type}") @@ -131,7 +146,14 @@ def get_or_set_default_settings( setting = self.get_settings(setting_type, entity_type, entity_id) if not setting: - setting = self.save_settings(setting_type, entity_type, entity_id) + try: + setting = self.save_settings(setting_type, entity_type, entity_id) + except Exception as e: + if "UniqueViolation" in str(e) and "Settings_pkey" in str(e): + # If another concurrent request already created the settings, fetch that settings + setting = self.get_settings(setting_type, entity_type, entity_id) + else: + raise e return setting @@ -182,6 +204,13 @@ def _adapt_default_sync_days_setting_from_json(self, data: Dict[str, any]): default_sync_days=data.get("default_sync_days", None) ) + def _adapt_incident_prs_setting_setting_from_json(self, data: Dict[str, any]): + return IncidentPrsSetting( + include_revert_prs=data.get("include_revert_prs", True), + title_filters=data.get("title_filters", []), + head_branch_filters=data.get("head_branch_filters", []), + ) + # ADD NEW DICT TO API ADAPTERS HERE def _handle_config_setting_from_json_data( @@ -204,6 +233,9 @@ def _handle_config_setting_from_json_data( if setting_type == SettingType.DEFAULT_SYNC_DAYS_SETTING: return self._adapt_default_sync_days_setting_from_json(setting_data) + if setting_type == SettingType.INCIDENT_PRS_SETTING: + return self._adapt_incident_prs_setting_setting_from_json(setting_data) + # ADD NEW HANDLE FROM JSON DATA HERE raise Exception(f"Invalid Setting Type: {setting_type}") @@ -242,6 +274,15 @@ def _adapt_default_sync_days_setting_json_data( ): return {"default_sync_days": specific_setting.default_sync_days} + def _adapt_incident_prs_setting_setting_json_data( + self, specific_setting: IncidentPrsSetting + ): + return { + "include_revert_prs": specific_setting.include_revert_prs, + "title_filters": specific_setting.title_filters, + "head_branch_filters": specific_setting.head_branch_filters, + } + # ADD NEW DATACLASS TO JSON DATA ADAPTERS HERE def _handle_config_setting_to_db_setting( @@ -273,6 +314,11 @@ def _handle_config_setting_to_db_setting( ): return self._adapt_default_sync_days_setting_json_data(specific_setting) + if setting_type == SettingType.INCIDENT_PRS_SETTING and isinstance( + specific_setting, IncidentPrsSetting + ): + return self._adapt_incident_prs_setting_setting_json_data(specific_setting) + # ADD NEW HANDLE TO DB SETTINGS HERE raise Exception(f"Invalid Setting Type: {setting_type}") diff --git a/backend/analytics_server/mhq/service/settings/default_settings_data.py b/backend/analytics_server/mhq/service/settings/default_settings_data.py index 8d133b1c5..0cebe9da7 100644 --- a/backend/analytics_server/mhq/service/settings/default_settings_data.py +++ b/backend/analytics_server/mhq/service/settings/default_settings_data.py @@ -29,6 +29,13 @@ def get_default_setting_data(setting_type: SettingType): if setting_type == SettingType.DEFAULT_SYNC_DAYS_SETTING: return {"default_sync_days": 31} + if setting_type == SettingType.INCIDENT_PRS_SETTING: + return { + "include_revert_prs": True, + "title_filters": [], + "head_branch_filters": [], + } + # ADD NEW DEFAULT SETTING HERE raise Exception(f"Invalid Setting Type: {setting_type}") diff --git a/backend/analytics_server/mhq/service/settings/models.py b/backend/analytics_server/mhq/service/settings/models.py index 8522d79b8..0a55dda6c 100644 --- a/backend/analytics_server/mhq/service/settings/models.py +++ b/backend/analytics_server/mhq/service/settings/models.py @@ -46,6 +46,13 @@ class DefaultSyncDaysSetting(BaseSetting): default_sync_days: int +@dataclass +class IncidentPrsSetting(BaseSetting): + include_revert_prs: bool + title_filters: List[str] + head_branch_filters: List[str] + + # ADD NEW SETTING CLASS HERE # Sample Future Settings diff --git a/backend/analytics_server/mhq/service/settings/setting_type_validator.py b/backend/analytics_server/mhq/service/settings/setting_type_validator.py index 008e4a9fa..7d71ba7ee 100644 --- a/backend/analytics_server/mhq/service/settings/setting_type_validator.py +++ b/backend/analytics_server/mhq/service/settings/setting_type_validator.py @@ -19,6 +19,9 @@ def settings_type_validator(setting_type: str): if setting_type == SettingType.DEFAULT_SYNC_DAYS_SETTING.value: return SettingType.DEFAULT_SYNC_DAYS_SETTING + if setting_type == SettingType.INCIDENT_PRS_SETTING.value: + return SettingType.INCIDENT_PRS_SETTING + # ADD NEW VALIDATOR HERE raise BadRequest(f"Invalid Setting Type: {setting_type}") diff --git a/backend/analytics_server/mhq/store/models/settings/configuration_settings.py b/backend/analytics_server/mhq/store/models/settings/configuration_settings.py index 4f81eae72..27050b605 100644 --- a/backend/analytics_server/mhq/store/models/settings/configuration_settings.py +++ b/backend/analytics_server/mhq/store/models/settings/configuration_settings.py @@ -16,6 +16,7 @@ class SettingType(Enum): INCIDENT_TYPES_SETTING = "INCIDENT_TYPES_SETTING" INCIDENT_SOURCES_SETTING = "INCIDENT_SOURCES_SETTING" EXCLUDED_PRS_SETTING = "EXCLUDED_PRS_SETTING" + INCIDENT_PRS_SETTING = "INCIDENT_PRS_SETTING" DEFAULT_SYNC_DAYS_SETTING = "DEFAULT_SYNC_DAYS_SETTING" # ADD NEW SETTING TYPE ENUM HERE diff --git a/backend/analytics_server/tests/service/Incidents/test_incident_types_setting.py b/backend/analytics_server/tests/service/Incidents/test_incident_types_setting.py new file mode 100644 index 000000000..0e8659fe9 --- /dev/null +++ b/backend/analytics_server/tests/service/Incidents/test_incident_types_setting.py @@ -0,0 +1,131 @@ +from mhq.service.incidents.incident_filter import ConfigurationsIncidentFilterProcessor +from mhq.store.models.incidents import IncidentFilter +from mhq.store.models.settings.configuration_settings import SettingType +from mhq.service.settings.configuration_settings import ( + IncidentTypesSetting, + IncidentPrsSetting, +) +from mhq.store.models.incidents.enums import IncidentType +from mhq.store.models.settings import EntityType + + +def test_get_incident_types_when_only_types_setting_present(): + setting_types = [SettingType.INCIDENT_TYPES_SETTING] + setting_type_to_settings_map = { + SettingType.INCIDENT_TYPES_SETTING: IncidentTypesSetting( + incident_types=[ + IncidentType.INCIDENT, + IncidentType.ALERT, + IncidentType.REVERT_PR, + ] + ) + } + + incident_filter = ConfigurationsIncidentFilterProcessor( + incident_filter=IncidentFilter(), + entity_type=EntityType.TEAM, + entity_id="team_id", + setting_types=setting_types, + setting_type_to_settings_map=setting_type_to_settings_map, + ).apply() + + expected_incident_types = [ + IncidentType.INCIDENT, + IncidentType.ALERT, + IncidentType.REVERT_PR, + ] + + assert incident_filter.incident_types == expected_incident_types + + +def test_get_incident_types_when_types_setting_is_empty(): + setting_types = [SettingType.INCIDENT_TYPES_SETTING] + setting_type_to_settings_map = { + SettingType.INCIDENT_TYPES_SETTING: IncidentTypesSetting(incident_types=[]) + } + + incident_filter = ConfigurationsIncidentFilterProcessor( + incident_filter=IncidentFilter(), + entity_type=EntityType.TEAM, + entity_id="dummy_id", + setting_types=setting_types, + setting_type_to_settings_map=setting_type_to_settings_map, + ).apply() + + assert incident_filter.incident_types == [] + + +def test_get_incident_types_when_only_prs_setting_present_returns_none(): + setting_types = [SettingType.INCIDENT_PRS_SETTING] + incident_prs_setting = IncidentPrsSetting( + include_revert_prs=True, title_filters=[], head_branch_filters=[] + ) + setting_type_to_settings_map = { + SettingType.INCIDENT_PRS_SETTING: incident_prs_setting + } + + incident_filter = ConfigurationsIncidentFilterProcessor( + incident_filter=IncidentFilter(), + entity_type=EntityType.TEAM, + entity_id="team_id", + setting_types=setting_types, + setting_type_to_settings_map=setting_type_to_settings_map, + ).apply() + + assert incident_filter.incident_types is None + + +def test_get_incident_types_when_both_types_and_prs_settings_present_and_includes_revert_prs(): + setting_types = [ + SettingType.INCIDENT_TYPES_SETTING, + SettingType.INCIDENT_PRS_SETTING, + ] + incident_prs_setting = IncidentPrsSetting( + include_revert_prs=True, title_filters=[], head_branch_filters=[] + ) + setting_type_to_settings_map = { + SettingType.INCIDENT_TYPES_SETTING: IncidentTypesSetting( + incident_types=[IncidentType.INCIDENT, IncidentType.REVERT_PR] + ), + SettingType.INCIDENT_PRS_SETTING: incident_prs_setting, + } + + incident_filter = ConfigurationsIncidentFilterProcessor( + incident_filter=IncidentFilter(), + entity_type=EntityType.TEAM, + entity_id="team_id", + setting_types=setting_types, + setting_type_to_settings_map=setting_type_to_settings_map, + ).apply() + + expected_incident_types = [IncidentType.INCIDENT, IncidentType.REVERT_PR] + + assert incident_filter.incident_types == expected_incident_types + + +def test_get_incident_types_when_both_settings_present_and_not_includes_revert_prs(): + setting_types = [ + SettingType.INCIDENT_TYPES_SETTING, + SettingType.INCIDENT_PRS_SETTING, + ] + incident_prs_setting = IncidentPrsSetting( + include_revert_prs=False, title_filters=[], head_branch_filters=[] + ) + setting_type_to_settings_map = { + SettingType.INCIDENT_TYPES_SETTING: IncidentTypesSetting( + incident_types=[IncidentType.INCIDENT, IncidentType.REVERT_PR] + ), + SettingType.INCIDENT_PRS_SETTING: incident_prs_setting, + } + + incident_filter = ConfigurationsIncidentFilterProcessor( + incident_filter=IncidentFilter(), + entity_type=EntityType.TEAM, + entity_id="team_id", + setting_types=setting_types, + setting_type_to_settings_map=setting_type_to_settings_map, + ).apply() + + expected_incident_types = [IncidentType.INCIDENT] + + assert incident_filter.incident_types == expected_incident_types