Skip to content

Commit 423d264

Browse files
committed
Merge branch 'refs/heads/main' into feat/async-environment-clone
# Conflicts: # api/environments/models.py
2 parents e7c5d1a + 59ddfba commit 423d264

31 files changed

+1184
-354
lines changed

.release-please-manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "2.116.0"
2+
".": "2.116.3"
33
}

CHANGELOG.md

+21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## [2.116.3](https://github.com/Flagsmith/flagsmith/compare/v2.116.2...v2.116.3) (2024-05-22)
4+
5+
6+
### Bug Fixes
7+
8+
* **versioning:** webhooks not triggered when new version published ([#3953](https://github.com/Flagsmith/flagsmith/issues/3953)) ([fb2191b](https://github.com/Flagsmith/flagsmith/commit/fb2191b34fef9b6d45d7eda17e22b77d826d4a19))
9+
10+
## [2.116.2](https://github.com/Flagsmith/flagsmith/compare/v2.116.1...v2.116.2) (2024-05-22)
11+
12+
13+
### Bug Fixes
14+
15+
* **versioning:** segment overrides limit ([#4007](https://github.com/Flagsmith/flagsmith/issues/4007)) ([918b731](https://github.com/Flagsmith/flagsmith/commit/918b73148e180d91e794ea8b840310b61ffe6300))
16+
17+
## [2.116.1](https://github.com/Flagsmith/flagsmith/compare/v2.116.0...v2.116.1) (2024-05-21)
18+
19+
20+
### Bug Fixes
21+
22+
* **versioning:** fix cloning environments using v2 versioning ([#3999](https://github.com/Flagsmith/flagsmith/issues/3999)) ([eef02fb](https://github.com/Flagsmith/flagsmith/commit/eef02fb75de85a75b169561b9055b533f3c71bfb))
23+
324
## [2.116.0](https://github.com/Flagsmith/flagsmith/compare/v2.115.0...v2.116.0) (2024-05-20)
425

526

api/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@ def feature_external_resource(feature: Feature) -> FeatureExternalResource:
971971
url="https://github.com/userexample/example-project-repo/issues/11",
972972
type="GITHUB_ISSUE",
973973
feature=feature,
974+
metadata='{"status": "open"}',
974975
)
975976

976977

api/environments/tasks.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def clone_environment_feature_states(
7575
# Grab the latest feature versions from the source environment.
7676
latest_environment_feature_versions = (
7777
EnvironmentFeatureVersion.objects.get_latest_versions_as_queryset(
78-
environment=source
78+
environment=source.id
7979
)
8080
)
8181

api/environments/views.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from django.db.models import Count
3+
from django.db.models import Count, Q
44
from django.utils.decorators import method_decorator
55
from drf_yasg import openapi
66
from drf_yasg.utils import no_body, swagger_auto_schema
@@ -17,6 +17,7 @@
1717
NestedEnvironmentPermissions,
1818
)
1919
from environments.sdk.schemas import SDKEnvironmentDocumentModel
20+
from features.versioning.models import EnvironmentFeatureVersion
2021
from features.versioning.tasks import (
2122
disable_v2_versioning,
2223
enable_v2_versioning,
@@ -108,9 +109,28 @@ def get_queryset(self):
108109
queryset = Environment.objects.all()
109110

110111
if self.action == "retrieve":
111-
queryset = queryset.annotate(
112-
total_segment_overrides=Count("feature_segments")
112+
# Since we don't have the environment at this stage, we would need to query the database
113+
# regardless, so it seems worthwhile to just query the database for the latest versions
114+
# and use their existence as a proxy to whether v2 feature versioning is enabled.
115+
latest_versions = EnvironmentFeatureVersion.objects.get_latest_versions_by_environment_api_key(
116+
environment_api_key=self.kwargs["api_key"]
113117
)
118+
if latest_versions:
119+
# if there are latest versions (and hence v2 feature versioning is enabled), then
120+
# we need to ensure that we're only counting the feature segments for those
121+
# latest versions against the limits.
122+
queryset = queryset.annotate(
123+
total_segment_overrides=Count(
124+
"feature_segments",
125+
filter=Q(
126+
feature_segments__environment_feature_version__in=latest_versions
127+
),
128+
)
129+
)
130+
else:
131+
queryset = queryset.annotate(
132+
total_segment_overrides=Count("feature_segments")
133+
)
114134

115135
return queryset
116136

api/features/feature_external_resources/models.py

+14-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from dataclasses import asdict
32

43
from django.db import models
54
from django_lifecycle import (
@@ -10,8 +9,7 @@
109
)
1110

1211
from features.models import Feature, FeatureState
13-
from integrations.github.github import GithubData, generate_data
14-
from integrations.github.tasks import call_github_app_webhook_for_feature_state
12+
from integrations.github.github import call_github_task
1513
from organisations.models import Organisation
1614
from webhooks.webhooks import WebhookEventType
1715

@@ -50,43 +48,36 @@ class Meta:
5048
def execute_after_save_actions(self):
5149
# Add a comment to GitHub Issue/PR when feature is linked to the GH external resource
5250
if (
53-
github_configuration := Organisation.objects.prefetch_related(
54-
"github_config"
55-
)
51+
Organisation.objects.prefetch_related("github_config")
5652
.get(id=self.feature.project.organisation_id)
5753
.github_config.first()
5854
):
5955
feature_states = FeatureState.objects.filter(
6056
feature_id=self.feature_id, identity_id__isnull=True
6157
)
62-
feature_data: GithubData = generate_data(
63-
github_configuration=github_configuration,
64-
feature=self.feature,
58+
call_github_task(
59+
organisation_id=self.feature.project.organisation_id,
6560
type=WebhookEventType.FEATURE_EXTERNAL_RESOURCE_ADDED.value,
61+
feature=self.feature,
62+
segment_name=None,
63+
url=None,
6664
feature_states=feature_states,
6765
)
6866

69-
call_github_app_webhook_for_feature_state.delay(
70-
args=(asdict(feature_data),),
71-
)
72-
7367
@hook(BEFORE_DELETE)
7468
def execute_before_save_actions(self) -> None:
7569
# Add a comment to GitHub Issue/PR when feature is unlinked to the GH external resource
7670
if (
77-
github_configuration := Organisation.objects.prefetch_related(
78-
"github_config"
79-
)
71+
Organisation.objects.prefetch_related("github_config")
8072
.get(id=self.feature.project.organisation_id)
8173
.github_config.first()
8274
):
83-
feature_data: GithubData = generate_data(
84-
github_configuration=github_configuration,
85-
feature=self.feature,
75+
76+
call_github_task(
77+
organisation_id=self.feature.project.organisation_id,
8678
type=WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value,
79+
feature=self.feature,
80+
segment_name=None,
8781
url=self.url,
88-
)
89-
90-
call_github_app_webhook_for_feature_state.delay(
91-
args=(asdict(feature_data),),
82+
feature_states=None,
9283
)

api/features/feature_external_resources/views.py

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from features.models import Feature
1010
from features.permissions import FeatureExternalResourcePermissions
11+
from integrations.github.client import get_github_issue_pr_title_and_state
1112
from organisations.models import Organisation
1213

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

29+
# Override get list view to add github issue/pr name to each linked external resource
30+
def list(self, request, *args, **kwargs) -> Response:
31+
queryset = self.get_queryset()
32+
serializer = self.get_serializer(queryset, many=True)
33+
data = serializer.data
34+
35+
# get organisation id from feature and get feature from validated data
36+
organisation_id = get_object_or_404(
37+
Feature.objects.filter(id=self.kwargs["feature_pk"]),
38+
).project.organisation_id
39+
40+
for resource in data if isinstance(data, list) else []:
41+
if resource_url := resource.get("url"):
42+
resource["metadata"] = get_github_issue_pr_title_and_state(
43+
organisation_id=organisation_id, resource_url=resource_url
44+
)
45+
46+
return Response(data={"results": data})
47+
2848
def create(self, request, *args, **kwargs):
2949
feature = get_object_or_404(
3050
Feature.objects.filter(

api/features/managers.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ def get_live_feature_states(
3333

3434
qs_filter = Q(environment=environment, deleted_at__isnull=True)
3535
if environment.use_v2_feature_versioning:
36-
latest_versions = EnvironmentFeatureVersion.objects.get_latest_versions(
37-
environment
36+
latest_versions = (
37+
EnvironmentFeatureVersion.objects.get_latest_versions_by_environment_id(
38+
environment.id
39+
)
3840
)
3941
latest_version_uuids = [efv.uuid for efv in latest_versions]
4042

api/features/models.py

+29-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import typing
66
import uuid
77
from copy import deepcopy
8-
from dataclasses import asdict
98

109
from core.models import (
1110
AbstractBaseExportableModel,
@@ -23,6 +22,7 @@
2322
from django.utils import timezone
2423
from django_lifecycle import (
2524
AFTER_CREATE,
25+
AFTER_DELETE,
2626
AFTER_SAVE,
2727
BEFORE_CREATE,
2828
BEFORE_SAVE,
@@ -74,7 +74,6 @@
7474
STRING,
7575
)
7676
from features.versioning.models import EnvironmentFeatureVersion
77-
from integrations.github.models import GithubConfiguration
7877
from metadata.models import Metadata
7978
from projects.models import Project
8079
from projects.tags.models import Tag
@@ -139,10 +138,7 @@ class Meta:
139138

140139
@hook(AFTER_SAVE)
141140
def create_github_comment(self) -> None:
142-
from integrations.github.github import GithubData, generate_data
143-
from integrations.github.tasks import (
144-
call_github_app_webhook_for_feature_state,
145-
)
141+
from integrations.github.github import call_github_task
146142
from webhooks.webhooks import WebhookEventType
147143

148144
if (
@@ -151,19 +147,14 @@ def create_github_comment(self) -> None:
151147
and self.project.organisation.github_config.exists()
152148
and self.deleted_at
153149
):
154-
github_configuration = GithubConfiguration.objects.get(
155-
organisation_id=self.project.organisation_id
156-
)
157150

158-
feature_data: GithubData = generate_data(
159-
github_configuration=github_configuration,
160-
feature=self,
151+
call_github_task(
152+
organisation_id=self.project.organisation_id,
161153
type=WebhookEventType.FLAG_DELETED.value,
162-
feature_states=[],
163-
)
164-
165-
call_github_app_webhook_for_feature_state.delay(
166-
args=(asdict(feature_data),),
154+
feature=self,
155+
segment_name=None,
156+
url=None,
157+
feature_states=None,
167158
)
168159

169160
@hook(AFTER_CREATE)
@@ -219,6 +210,7 @@ def get_next_segment_priority(feature):
219210

220211

221212
class FeatureSegment(
213+
LifecycleModelMixin,
222214
AbstractBaseExportableModel,
223215
OrderedModelBase,
224216
abstract_base_auditable_model_factory(["uuid"]),
@@ -406,6 +398,26 @@ def get_delete_log_message(self, history_instance) -> typing.Optional[str]:
406398
def _get_environment(self) -> "Environment":
407399
return self.environment
408400

401+
@hook(AFTER_DELETE)
402+
def create_github_comment(self) -> None:
403+
from integrations.github.github import call_github_task
404+
from webhooks.webhooks import WebhookEventType
405+
406+
if (
407+
self.feature.external_resources.exists()
408+
and self.feature.project.github_project.exists()
409+
and self.feature.project.organisation.github_config.exists()
410+
):
411+
412+
call_github_task(
413+
self.feature.project.organisation_id,
414+
WebhookEventType.SEGMENT_OVERRIDE_DELETED.value,
415+
self.feature,
416+
self.segment.name,
417+
None,
418+
None,
419+
)
420+
409421

410422
class FeatureState(
411423
SoftDeleteExportableModel,

api/features/serializers.py

+9-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import typing
2-
from dataclasses import asdict
32
from datetime import datetime
43

54
import django.core.exceptions
@@ -13,9 +12,7 @@
1312
from environments.sdk.serializers_mixins import (
1413
HideSensitiveFieldsSerializerMixin,
1514
)
16-
from integrations.github.github import GithubData, generate_data
17-
from integrations.github.models import GithubConfiguration
18-
from integrations.github.tasks import call_github_app_webhook_for_feature_state
15+
from integrations.github.github import call_github_task
1916
from metadata.serializers import MetadataSerializer, SerializerWithMetadata
2017
from projects.models import Project
2118
from users.serializers import (
@@ -474,23 +471,18 @@ def save(self, **kwargs):
474471
and feature_state.environment.project.github_project.exists()
475472
and feature_state.environment.project.organisation.github_config.exists()
476473
):
477-
github_configuration = GithubConfiguration.objects.get(
478-
organisation_id=feature_state.environment.project.organisation_id
479-
)
480-
feature_states = []
481-
feature_states.append(feature_state)
482-
feature_data: GithubData = generate_data(
483-
github_configuration=github_configuration,
484-
feature=feature_state.feature,
485-
type=WebhookEventType.FLAG_UPDATED.value,
486-
feature_states=feature_states,
487-
)
488474

489-
call_github_app_webhook_for_feature_state.delay(
490-
args=(asdict(feature_data),),
475+
call_github_task(
476+
organisation_id=feature_state.feature.project.organisation_id,
477+
type=WebhookEventType.FLAG_UPDATED.value,
478+
feature=feature_state.feature,
479+
segment_name=None,
480+
url=None,
481+
feature_states=[feature_state],
491482
)
492483

493484
return response
485+
494486
except django.core.exceptions.ValidationError as e:
495487
raise serializers.ValidationError(str(e))
496488

0 commit comments

Comments
 (0)