Skip to content

Commit

Permalink
Merge branch 'refs/heads/main' into feat/async-environment-clone
Browse files Browse the repository at this point in the history
# Conflicts:
#	api/environments/models.py
  • Loading branch information
matthewelwell committed May 23, 2024
2 parents e7c5d1a + 59ddfba commit 2a662af
Show file tree
Hide file tree
Showing 31 changed files with 1,184 additions and 354 deletions.
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.116.0"
".": "2.116.3"
}
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## [2.116.3](https://github.com/Flagsmith/flagsmith/compare/v2.116.2...v2.116.3) (2024-05-22)


### Bug Fixes

* **versioning:** webhooks not triggered when new version published ([#3953](https://github.com/Flagsmith/flagsmith/issues/3953)) ([fb2191b](https://github.com/Flagsmith/flagsmith/commit/fb2191b34fef9b6d45d7eda17e22b77d826d4a19))

## [2.116.2](https://github.com/Flagsmith/flagsmith/compare/v2.116.1...v2.116.2) (2024-05-22)


### Bug Fixes

* **versioning:** segment overrides limit ([#4007](https://github.com/Flagsmith/flagsmith/issues/4007)) ([918b731](https://github.com/Flagsmith/flagsmith/commit/918b73148e180d91e794ea8b840310b61ffe6300))

## [2.116.1](https://github.com/Flagsmith/flagsmith/compare/v2.116.0...v2.116.1) (2024-05-21)


### Bug Fixes

* **versioning:** fix cloning environments using v2 versioning ([#3999](https://github.com/Flagsmith/flagsmith/issues/3999)) ([eef02fb](https://github.com/Flagsmith/flagsmith/commit/eef02fb75de85a75b169561b9055b533f3c71bfb))

## [2.116.0](https://github.com/Flagsmith/flagsmith/compare/v2.115.0...v2.116.0) (2024-05-20)


Expand Down
1 change: 1 addition & 0 deletions api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ def feature_external_resource(feature: Feature) -> FeatureExternalResource:
url="https://github.com/userexample/example-project-repo/issues/11",
type="GITHUB_ISSUE",
feature=feature,
metadata='{"status": "open"}',
)


Expand Down
2 changes: 1 addition & 1 deletion api/environments/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def clone_environment_feature_states(
# Grab the latest feature versions from the source environment.
latest_environment_feature_versions = (
EnvironmentFeatureVersion.objects.get_latest_versions_as_queryset(
environment=source
environment_id=source.id
)
)

Expand Down
26 changes: 23 additions & 3 deletions api/environments/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from django.db.models import Count
from django.db.models import Count, Q
from django.utils.decorators import method_decorator
from drf_yasg import openapi
from drf_yasg.utils import no_body, swagger_auto_schema
Expand All @@ -17,6 +17,7 @@
NestedEnvironmentPermissions,
)
from environments.sdk.schemas import SDKEnvironmentDocumentModel
from features.versioning.models import EnvironmentFeatureVersion
from features.versioning.tasks import (
disable_v2_versioning,
enable_v2_versioning,
Expand Down Expand Up @@ -108,9 +109,28 @@ def get_queryset(self):
queryset = Environment.objects.all()

if self.action == "retrieve":
queryset = queryset.annotate(
total_segment_overrides=Count("feature_segments")
# Since we don't have the environment at this stage, we would need to query the database
# regardless, so it seems worthwhile to just query the database for the latest versions
# and use their existence as a proxy to whether v2 feature versioning is enabled.
latest_versions = EnvironmentFeatureVersion.objects.get_latest_versions_by_environment_api_key(
environment_api_key=self.kwargs["api_key"]
)
if latest_versions:
# if there are latest versions (and hence v2 feature versioning is enabled), then
# we need to ensure that we're only counting the feature segments for those
# latest versions against the limits.
queryset = queryset.annotate(
total_segment_overrides=Count(
"feature_segments",
filter=Q(
feature_segments__environment_feature_version__in=latest_versions
),
)
)
else:
queryset = queryset.annotate(
total_segment_overrides=Count("feature_segments")
)

return queryset

Expand Down
37 changes: 14 additions & 23 deletions api/features/feature_external_resources/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
from dataclasses import asdict

from django.db import models
from django_lifecycle import (
Expand All @@ -10,8 +9,7 @@
)

from features.models import Feature, FeatureState
from integrations.github.github import GithubData, generate_data
from integrations.github.tasks import call_github_app_webhook_for_feature_state
from integrations.github.github import call_github_task
from organisations.models import Organisation
from webhooks.webhooks import WebhookEventType

Expand Down Expand Up @@ -50,43 +48,36 @@ class Meta:
def execute_after_save_actions(self):
# Add a comment to GitHub Issue/PR when feature is linked to the GH external resource
if (
github_configuration := Organisation.objects.prefetch_related(
"github_config"
)
Organisation.objects.prefetch_related("github_config")
.get(id=self.feature.project.organisation_id)
.github_config.first()
):
feature_states = FeatureState.objects.filter(
feature_id=self.feature_id, identity_id__isnull=True
)
feature_data: GithubData = generate_data(
github_configuration=github_configuration,
feature=self.feature,
call_github_task(
organisation_id=self.feature.project.organisation_id,
type=WebhookEventType.FEATURE_EXTERNAL_RESOURCE_ADDED.value,
feature=self.feature,
segment_name=None,
url=None,
feature_states=feature_states,
)

call_github_app_webhook_for_feature_state.delay(
args=(asdict(feature_data),),
)

@hook(BEFORE_DELETE)
def execute_before_save_actions(self) -> None:
# Add a comment to GitHub Issue/PR when feature is unlinked to the GH external resource
if (
github_configuration := Organisation.objects.prefetch_related(
"github_config"
)
Organisation.objects.prefetch_related("github_config")
.get(id=self.feature.project.organisation_id)
.github_config.first()
):
feature_data: GithubData = generate_data(
github_configuration=github_configuration,
feature=self.feature,

call_github_task(
organisation_id=self.feature.project.organisation_id,
type=WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value,
feature=self.feature,
segment_name=None,
url=self.url,
)

call_github_app_webhook_for_feature_state.delay(
args=(asdict(feature_data),),
feature_states=None,
)
20 changes: 20 additions & 0 deletions api/features/feature_external_resources/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from features.models import Feature
from features.permissions import FeatureExternalResourcePermissions
from integrations.github.client import get_github_issue_pr_title_and_state
from organisations.models import Organisation

from .models import FeatureExternalResource
Expand All @@ -25,6 +26,25 @@ def get_queryset(self):
features_pk = self.kwargs["feature_pk"]
return FeatureExternalResource.objects.filter(feature=features_pk)

# Override get list view to add github issue/pr name to each linked external resource
def list(self, request, *args, **kwargs) -> Response:
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
data = serializer.data

# get organisation id from feature and get feature from validated data
organisation_id = get_object_or_404(
Feature.objects.filter(id=self.kwargs["feature_pk"]),
).project.organisation_id

for resource in data if isinstance(data, list) else []:
if resource_url := resource.get("url"):
resource["metadata"] = get_github_issue_pr_title_and_state(
organisation_id=organisation_id, resource_url=resource_url
)

return Response(data={"results": data})

def create(self, request, *args, **kwargs):
feature = get_object_or_404(
Feature.objects.filter(
Expand Down
6 changes: 4 additions & 2 deletions api/features/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ def get_live_feature_states(

qs_filter = Q(environment=environment, deleted_at__isnull=True)
if environment.use_v2_feature_versioning:
latest_versions = EnvironmentFeatureVersion.objects.get_latest_versions(
environment
latest_versions = (
EnvironmentFeatureVersion.objects.get_latest_versions_by_environment_id(
environment.id
)
)
latest_version_uuids = [efv.uuid for efv in latest_versions]

Expand Down
46 changes: 29 additions & 17 deletions api/features/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import typing
import uuid
from copy import deepcopy
from dataclasses import asdict

from core.models import (
AbstractBaseExportableModel,
Expand All @@ -23,6 +22,7 @@
from django.utils import timezone
from django_lifecycle import (
AFTER_CREATE,
AFTER_DELETE,
AFTER_SAVE,
BEFORE_CREATE,
BEFORE_SAVE,
Expand Down Expand Up @@ -74,7 +74,6 @@
STRING,
)
from features.versioning.models import EnvironmentFeatureVersion
from integrations.github.models import GithubConfiguration
from metadata.models import Metadata
from projects.models import Project
from projects.tags.models import Tag
Expand Down Expand Up @@ -139,10 +138,7 @@ class Meta:

@hook(AFTER_SAVE)
def create_github_comment(self) -> None:
from integrations.github.github import GithubData, generate_data
from integrations.github.tasks import (
call_github_app_webhook_for_feature_state,
)
from integrations.github.github import call_github_task
from webhooks.webhooks import WebhookEventType

if (
Expand All @@ -151,19 +147,14 @@ def create_github_comment(self) -> None:
and self.project.organisation.github_config.exists()
and self.deleted_at
):
github_configuration = GithubConfiguration.objects.get(
organisation_id=self.project.organisation_id
)

feature_data: GithubData = generate_data(
github_configuration=github_configuration,
feature=self,
call_github_task(
organisation_id=self.project.organisation_id,
type=WebhookEventType.FLAG_DELETED.value,
feature_states=[],
)

call_github_app_webhook_for_feature_state.delay(
args=(asdict(feature_data),),
feature=self,
segment_name=None,
url=None,
feature_states=None,
)

@hook(AFTER_CREATE)
Expand Down Expand Up @@ -219,6 +210,7 @@ def get_next_segment_priority(feature):


class FeatureSegment(
LifecycleModelMixin,
AbstractBaseExportableModel,
OrderedModelBase,
abstract_base_auditable_model_factory(["uuid"]),
Expand Down Expand Up @@ -406,6 +398,26 @@ def get_delete_log_message(self, history_instance) -> typing.Optional[str]:
def _get_environment(self) -> "Environment":
return self.environment

@hook(AFTER_DELETE)
def create_github_comment(self) -> None:
from integrations.github.github import call_github_task
from webhooks.webhooks import WebhookEventType

if (
self.feature.external_resources.exists()
and self.feature.project.github_project.exists()
and self.feature.project.organisation.github_config.exists()
):

call_github_task(
self.feature.project.organisation_id,
WebhookEventType.SEGMENT_OVERRIDE_DELETED.value,
self.feature,
self.segment.name,
None,
None,
)


class FeatureState(
SoftDeleteExportableModel,
Expand Down
26 changes: 9 additions & 17 deletions api/features/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import typing
from dataclasses import asdict
from datetime import datetime

import django.core.exceptions
Expand All @@ -13,9 +12,7 @@
from environments.sdk.serializers_mixins import (
HideSensitiveFieldsSerializerMixin,
)
from integrations.github.github import GithubData, generate_data
from integrations.github.models import GithubConfiguration
from integrations.github.tasks import call_github_app_webhook_for_feature_state
from integrations.github.github import call_github_task
from metadata.serializers import MetadataSerializer, SerializerWithMetadata
from projects.models import Project
from users.serializers import (
Expand Down Expand Up @@ -474,23 +471,18 @@ def save(self, **kwargs):
and feature_state.environment.project.github_project.exists()
and feature_state.environment.project.organisation.github_config.exists()
):
github_configuration = GithubConfiguration.objects.get(
organisation_id=feature_state.environment.project.organisation_id
)
feature_states = []
feature_states.append(feature_state)
feature_data: GithubData = generate_data(
github_configuration=github_configuration,
feature=feature_state.feature,
type=WebhookEventType.FLAG_UPDATED.value,
feature_states=feature_states,
)

call_github_app_webhook_for_feature_state.delay(
args=(asdict(feature_data),),
call_github_task(
organisation_id=feature_state.feature.project.organisation_id,
type=WebhookEventType.FLAG_UPDATED.value,
feature=feature_state.feature,
segment_name=None,
url=None,
feature_states=[feature_state],
)

return response

except django.core.exceptions.ValidationError as e:
raise serializers.ValidationError(str(e))

Expand Down
Loading

0 comments on commit 2a662af

Please sign in to comment.