-
Notifications
You must be signed in to change notification settings - Fork 516
Expand file tree
/
Copy pathserializers.py
More file actions
686 lines (566 loc) · 25.5 KB
/
serializers.py
File metadata and controls
686 lines (566 loc) · 25.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
from datetime import datetime
from typing import Any
import django.core.exceptions
from common.features.multivariate.serializers import (
MultivariateFeatureStateValueSerializer,
)
from common.features.serializers import (
CreateSegmentOverrideFeatureStateSerializer,
FeatureStateValueSerializer,
)
from drf_writable_nested import ( # type: ignore[attr-defined]
WritableNestedModelSerializer,
)
from drf_yasg.utils import swagger_serializer_method # type: ignore[import-untyped]
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied
from app_analytics.serializers import LabelsQuerySerializerMixin, LabelsSerializer
from environments.identities.models import Identity
from environments.sdk.serializers_mixins import (
HideSensitiveFieldsSerializerMixin,
)
from integrations.github.constants import GitHubEventType
from integrations.github.github import call_github_task
from metadata.serializers import (
MetadataSerializer,
with_metadata,
)
from projects.code_references.serializers import (
FeatureFlagCodeReferencesRepositoryCountSerializer,
)
from projects.models import Project
from users.serializers import (
UserIdsSerializer,
UserListSerializer,
UserPermissionGroupSummarySerializer,
)
from util.drf_writable_nested.serializers import (
DeleteBeforeUpdateWritableNestedModelSerializer,
)
from .constants import INTERSECTION, UNION
from .feature_segments.limits import (
SEGMENT_OVERRIDE_LIMIT_EXCEEDED_MESSAGE,
exceeds_segment_override_limit,
)
from .feature_segments.serializers import (
CustomCreateSegmentOverrideFeatureSegmentSerializer,
)
from .models import Feature, FeatureState
from .multivariate.serializers import NestedMultivariateFeatureOptionSerializer
class FeatureStateSerializerSmall(serializers.ModelSerializer): # type: ignore[type-arg]
feature_state_value = serializers.SerializerMethodField()
class Meta:
model = FeatureState
fields = (
"id",
"feature_state_value",
"environment",
"identity",
"feature_segment",
"enabled",
)
def get_feature_state_value(self, obj): # type: ignore[no-untyped-def]
return obj.get_feature_state_value(identity=self.context.get("identity"))
class FeatureQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
search = serializers.CharField(required=False)
sort_field = serializers.ChoiceField(
choices=("created_date", "name"), default="created_date"
)
sort_direction = serializers.ChoiceField(choices=("ASC", "DESC"), default="ASC")
tags = serializers.CharField(
required=False,
help_text=(
"Comma separated list of tag ids to filter on (AND with "
"INTERSECTION, and OR with UNION via tag_strategy)"
),
)
tag_strategy = serializers.ChoiceField(
choices=(UNION, INTERSECTION), default=INTERSECTION
)
is_archived = serializers.BooleanField(required=False)
environment = serializers.IntegerField(
required=False,
help_text="Integer ID of the environment to view features in the context of.",
)
segment = serializers.IntegerField(
required=False,
help_text="Integer ID of the segment to retrieve segment overrides for.",
)
is_enabled = serializers.BooleanField(
allow_null=True,
required=False,
default=None,
help_text="Boolean value to filter features as enabled or disabled.",
)
value_search = serializers.CharField(
required=False,
default=None,
help_text="Value of type int, string, or boolean to filter features based on their values",
)
owners = serializers.CharField(
required=False,
help_text="Comma separated list of owner ids to filter on",
)
group_owners = serializers.CharField(
required=False,
help_text="Comma separated list of group owner ids to filter on",
)
def validate_owners(self, owners: str) -> list[int]:
try:
return [int(owner_id.strip()) for owner_id in owners.split(",")]
except ValueError:
raise serializers.ValidationError("Owner IDs must be integers.")
def validate_group_owners(self, group_owners: str) -> list[int]:
try:
return [
int(group_owner_id.strip())
for group_owner_id in group_owners.split(",")
]
except ValueError:
raise serializers.ValidationError("Group owner IDs must be integers.")
def validate_tags(self, tags: str) -> list[int]:
try:
return [int(tag_id.strip()) for tag_id in tags.split(",")]
except ValueError:
raise serializers.ValidationError("Tag IDs must be integers.")
class CreateFeatureSerializer(DeleteBeforeUpdateWritableNestedModelSerializer):
multivariate_options = NestedMultivariateFeatureOptionSerializer(
many=True, required=False
)
owners = UserListSerializer(many=True, read_only=True)
group_owners = UserPermissionGroupSummarySerializer(many=True, read_only=True)
environment_feature_state = serializers.SerializerMethodField()
segment_feature_state = serializers.SerializerMethodField()
num_segment_overrides = serializers.SerializerMethodField(
help_text="Number of segment overrides that exist for the given feature "
"in the environment provided by the `environment` query parameter."
)
num_identity_overrides = serializers.SerializerMethodField(
help_text="Number of identity overrides that exist for the given feature "
"in the environment provided by the `environment` query parameter. "
"Note: will return null for Edge enabled projects."
)
is_num_identity_overrides_complete = serializers.SerializerMethodField(
help_text="A boolean that indicates whether there are more"
" identity overrides than are being listed, if `False`. This field is "
"`True` when querying overrides data for a features list page and "
"exact data has been returned."
)
last_modified_in_any_environment = serializers.SerializerMethodField(
help_text="Datetime representing the last time that the feature was modified "
"in any environment in the given project. Note: requires feature "
"versioning v2 enabled on the environment."
)
last_modified_in_current_environment = serializers.SerializerMethodField(
help_text="Datetime representing the last time that the feature was modified "
"in any environment in the current environment. Note: requires that "
"the environment query parameter is passed and feature versioning v2 "
"enabled on the environment."
)
class Meta:
model = Feature
fields = (
"id",
"name",
"type",
"default_enabled",
"initial_value",
"created_date",
"description",
"tags",
"multivariate_options",
"is_archived",
"owners",
"group_owners",
"uuid",
"project",
"environment_feature_state",
"segment_feature_state",
"num_segment_overrides",
"num_identity_overrides",
"is_num_identity_overrides_complete",
"is_server_key_only",
"last_modified_in_any_environment",
"last_modified_in_current_environment",
)
read_only_fields = ("feature_segments", "created_date", "uuid", "project")
def to_internal_value(self, data): # type: ignore[no-untyped-def]
if data.get("initial_value") and not isinstance(data["initial_value"], str):
data["initial_value"] = str(data["initial_value"])
return super(CreateFeatureSerializer, self).to_internal_value(data)
def create(self, validated_data: dict) -> Feature: # type: ignore[type-arg]
project = self.context["project"]
self.validate_project_features_limit(project)
# Add the default(User creating the feature) owner of the feature
# NOTE: pop the user before passing the data to create
user = validated_data.pop("user", None)
instance = super(CreateFeatureSerializer, self).create(validated_data) # type: ignore[no-untyped-call]
if user and getattr(user, "is_master_api_key_user", False) is False:
instance.owners.add(user)
return instance # type: ignore[no-any-return]
def validate_project_features_limit(self, project: Project) -> None:
if project.features.count() >= project.max_features_allowed:
raise serializers.ValidationError(
{
"project": "The Project has reached the maximum allowed features limit."
}
)
def validate_multivariate_options(self, multivariate_options): # type: ignore[no-untyped-def]
if multivariate_options:
user = self.context["request"].user
project = self.context.get("project")
if user.is_authenticated and not (
project and user.is_project_admin(project)
):
raise PermissionDenied(
"User must be project admin to modify / create MV options."
)
total_percentage_allocation = sum(
mv_option.get("default_percentage_allocation", 100)
for mv_option in multivariate_options
)
if total_percentage_allocation > 100:
raise serializers.ValidationError("Invalid percentage allocation")
return multivariate_options
def validate_name(self, name: str): # type: ignore[no-untyped-def]
view = self.context["view"]
project = self.context["project"]
feature_name_regex = project.feature_name_regex
if not project.is_feature_name_valid(name):
raise serializers.ValidationError(
f"Feature name must match regex: {feature_name_regex}"
)
unique_filters = {
"project__id": view.kwargs.get("project_pk"),
"name__iexact": name,
}
existing_feature_queryset = Feature.objects.filter(**unique_filters)
if self.instance:
existing_feature_queryset = existing_feature_queryset.exclude(
id=self.instance.id # type: ignore[union-attr]
)
if existing_feature_queryset.exists():
raise serializers.ValidationError(
"Feature with that name already exists for this "
"project. Note that feature names are case "
"insensitive."
)
return name
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
view = self.context["view"]
project_id = str(view.kwargs.get("project_pk"))
if not project_id.isdigit():
raise serializers.ValidationError("Invalid project ID.")
# If tags selected check they from the same Project as Feature Project
if any(tag.project_id != int(project_id) for tag in attrs.get("tags", [])):
raise serializers.ValidationError(
"Selected Tags must be from the same Project as current Feature"
)
return attrs
@swagger_serializer_method( # type: ignore[misc]
serializer_or_field=FeatureStateSerializerSmall(allow_null=True)
)
def get_environment_feature_state( # type: ignore[return]
self, instance: Feature
) -> dict[str, Any] | None:
if (feature_states := self.context.get("feature_states")) and (
feature_state := feature_states.get(instance.id)
):
return FeatureStateSerializerSmall(instance=feature_state).data
@swagger_serializer_method( # type: ignore[misc]
serializer_or_field=FeatureStateSerializerSmall(allow_null=True)
)
def get_segment_feature_state( # type: ignore[return]
self, instance: Feature
) -> dict[str, Any] | None:
if (segment_feature_states := self.context.get("segment_feature_states")) and (
segment_feature_state := segment_feature_states.get(instance.id)
):
return FeatureStateSerializerSmall(instance=segment_feature_state).data
def get_num_segment_overrides(self, instance: Feature) -> int:
try:
return self.context["overrides_data"][instance.id].num_segment_overrides # type: ignore[no-any-return]
except (KeyError, AttributeError):
return 0
def get_num_identity_overrides(self, instance: Feature) -> int | None:
try:
return self.context["overrides_data"][instance.id].num_identity_overrides # type: ignore[no-any-return]
except (KeyError, AttributeError):
return None
def get_is_num_identity_overrides_complete(self, instance: Feature) -> bool | None:
try:
return self.context["overrides_data"][ # type: ignore[no-any-return]
instance.id
].is_num_identity_overrides_complete
except (KeyError, AttributeError):
return None
def get_last_modified_in_any_environment(
self, instance: Feature
) -> datetime | None:
return getattr(instance, "last_modified_in_any_environment", None)
def get_last_modified_in_current_environment(
self, instance: Feature
) -> datetime | None:
return getattr(instance, "last_modified_in_current_environment", None)
@with_metadata(
lambda self, attrs: (
self.instance.project if self.instance else self.context["project"]
).organisation
)
class FeatureSerializerWithMetadata(CreateFeatureSerializer):
metadata = MetadataSerializer(required=False, many=True)
code_references_counts = FeatureFlagCodeReferencesRepositoryCountSerializer(
many=True,
read_only=True,
)
class Meta(CreateFeatureSerializer.Meta):
fields = CreateFeatureSerializer.Meta.fields + ( # type: ignore[assignment]
"metadata",
"code_references_counts",
)
class UpdateFeatureSerializerWithMetadata(FeatureSerializerWithMetadata):
"""prevent users from changing certain values after creation"""
class Meta(FeatureSerializerWithMetadata.Meta):
read_only_fields = FeatureSerializerWithMetadata.Meta.read_only_fields + ( # type: ignore[assignment]
"default_enabled",
"initial_value",
"name",
)
class ListFeatureSerializer(FeatureSerializerWithMetadata):
# This exists purely to reduce the conflicts for the EE repository
# which has some extra behaviour here to support Oracle DB.
pass
class UpdateFeatureSerializer(ListFeatureSerializer):
"""prevent users from changing certain values after creation"""
class Meta(ListFeatureSerializer.Meta):
read_only_fields = ListFeatureSerializer.Meta.read_only_fields + ( # type: ignore[assignment]
"default_enabled",
"initial_value",
"name",
)
class FeatureSerializer(serializers.ModelSerializer): # type: ignore[type-arg]
class Meta:
model = Feature
fields = (
"id",
"name",
"created_date",
"description",
"initial_value",
"default_enabled",
"type",
)
writeonly_fields = ("initial_value", "default_enabled")
class SDKFeatureSerializer(HideSensitiveFieldsSerializerMixin, FeatureSerializer):
sensitive_fields = (
"created_date",
"description",
"initial_value",
"default_enabled",
)
class FeatureStateSerializerFull(serializers.ModelSerializer): # type: ignore[type-arg]
feature = FeatureSerializer()
feature_state_value = serializers.SerializerMethodField()
class Meta:
model = FeatureState
fields = (
"id",
"feature",
"feature_state_value",
"environment",
"identity",
"feature_segment",
"enabled",
)
def get_feature_state_value(self, obj): # type: ignore[no-untyped-def]
return obj.get_feature_state_value(identity=self.context.get("identity"))
class FeatureOwnerInputSerializer(UserIdsSerializer):
def add_owners(self, feature: Feature): # type: ignore[no-untyped-def]
user_ids = self.validated_data["user_ids"]
feature.owners.add(*user_ids)
def remove_users(self, feature: Feature): # type: ignore[no-untyped-def]
user_ids = self.validated_data["user_ids"]
feature.owners.remove(*user_ids)
class FeatureGroupOwnerInputSerializer(serializers.Serializer): # type: ignore[type-arg]
group_ids = serializers.ListField(child=serializers.IntegerField())
def add_group_owners(self, feature: Feature): # type: ignore[no-untyped-def]
group_ids = self.validated_data["group_ids"]
feature.group_owners.add(*group_ids)
def remove_group_owners(self, feature: Feature): # type: ignore[no-untyped-def]
group_ids = self.validated_data["group_ids"]
feature.group_owners.remove(*group_ids)
class ProjectFeatureSerializer(serializers.ModelSerializer): # type: ignore[type-arg]
owners = UserListSerializer(many=True, read_only=True)
group_owners = UserPermissionGroupSummarySerializer(many=True, read_only=True)
class Meta:
model = Feature
fields = (
"id",
"name",
"created_date",
"description",
"initial_value",
"default_enabled",
"type",
"owners",
"group_owners",
"is_server_key_only",
)
writeonly_fields = ("initial_value", "default_enabled")
class SDKFeatureStateSerializer(
HideSensitiveFieldsSerializerMixin, FeatureStateSerializerFull
):
feature = SDKFeatureSerializer()
sensitive_fields = (
"id",
"environment",
"identity",
"feature_segment",
)
class FeatureStateSerializerBasic(WritableNestedModelSerializer):
feature_state_value = serializers.SerializerMethodField()
multivariate_feature_state_values = MultivariateFeatureStateValueSerializer(
many=True, required=False
)
identifier = serializers.CharField(
required=False,
help_text="Can be passed as an alternative to `identity`",
)
class Meta:
model = FeatureState
fields = "__all__"
read_only_fields = ("version", "created_at", "updated_at", "status")
def get_feature_state_value(self, obj): # type: ignore[no-untyped-def]
return obj.get_feature_state_value(identity=self.context.get("identity"))
def save(self, **kwargs): # type: ignore[no-untyped-def]
try:
response = super().save(**kwargs) # type: ignore[no-untyped-call]
feature_state = self.instance
if (
not feature_state.identity_id # type: ignore[union-attr]
and feature_state.feature.external_resources.exists() # type: ignore[union-attr]
and feature_state.environment.project.github_project.exists() # type: ignore[union-attr]
and feature_state.environment.project.organisation.github_config.exists() # type: ignore[union-attr]
):
call_github_task(
organisation_id=feature_state.feature.project.organisation_id, # type: ignore[union-attr]
type=GitHubEventType.FLAG_UPDATED.value,
feature=feature_state.feature, # type: ignore[union-attr]
segment_name=None,
url=None,
feature_states=[feature_state],
)
return response
except django.core.exceptions.ValidationError as e:
raise serializers.ValidationError(str(e))
def validate_feature(self, feature): # type: ignore[no-untyped-def]
if self.instance and self.instance.feature_id != feature.id: # type: ignore[union-attr]
raise serializers.ValidationError(
"Cannot change the feature of a feature state"
)
return feature
def validate_environment(self, environment): # type: ignore[no-untyped-def]
if self.instance and self.instance.environment_id != environment.id: # type: ignore[union-attr]
raise serializers.ValidationError(
"Cannot change the environment of a feature state"
)
return environment
def validate(self, attrs): # type: ignore[no-untyped-def]
environment = attrs.get("environment") or self.context["environment"]
identity = attrs.get("identity")
feature_segment = attrs.get("feature_segment")
identifier = attrs.pop("identifier", None)
feature = attrs.get("feature")
if feature and feature.project_id != environment.project_id:
error = {"feature": "Feature does not exist in project"}
raise serializers.ValidationError(error)
if identifier:
try:
identity = Identity.objects.get(
identifier=identifier, environment=environment
)
attrs["identity"] = identity
except Identity.DoesNotExist:
raise serializers.ValidationError("Invalid identifier")
if identity and not identity.environment == environment:
raise serializers.ValidationError("Identity does not exist in environment.")
if feature_segment and not feature_segment.environment == environment:
raise serializers.ValidationError(
"Feature Segment does not belong to environment."
)
mv_values = attrs.get("multivariate_feature_state_values", [])
if sum([v.get("percentage_allocation", 0) for v in mv_values]) > 100:
raise serializers.ValidationError(
"Multivariate percentage values exceed 100%."
)
return attrs
class FeatureStateSerializerWithIdentity(FeatureStateSerializerBasic):
class _IdentitySerializer(serializers.ModelSerializer): # type: ignore[type-arg]
class Meta:
model = Identity
fields = ("id", "identifier")
identity = _IdentitySerializer()
class FeatureStateSerializerCreate(serializers.ModelSerializer): # type: ignore[type-arg]
class Meta:
model = FeatureState
fields = ("feature", "enabled")
class FeatureInfluxDataSerializer(serializers.Serializer): # type: ignore[type-arg]
events_list = serializers.ListSerializer(child=serializers.DictField()) # type: ignore[var-annotated]
class FeatureEvaluationDataSerializer(serializers.Serializer): # type: ignore[type-arg]
day = serializers.CharField()
count = serializers.IntegerField()
labels = LabelsSerializer(allow_null=True)
class GetInfluxDataQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
period = serializers.CharField(required=False, default="24h")
environment_id = serializers.CharField(required=True)
aggregate_every = serializers.CharField(required=False, default="24h")
class GetUsageDataQuerySerializer(LabelsQuerySerializerMixin, serializers.Serializer): # type: ignore[type-arg]
period = serializers.IntegerField(
required=False,
default=30,
help_text="number of days",
source="period_days",
)
environment_id = serializers.IntegerField(required=True)
class WritableNestedFeatureStateSerializer(FeatureStateSerializerBasic):
feature_state_value = FeatureStateValueSerializer(required=False) # type: ignore[assignment]
class Meta(FeatureStateSerializerBasic.Meta):
extra_kwargs = {"environment": {"required": True}}
class SegmentAssociatedFeatureStateSerializer(serializers.ModelSerializer): # type: ignore[type-arg]
class Meta:
model = FeatureState
fields = ("id", "feature", "environment")
class AssociatedFeaturesQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
environment = serializers.IntegerField(required=False)
class SDKFeatureStatesQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
feature = serializers.CharField(
required=False, help_text="Name of the feature to get the state of"
)
class EnvironmentFeatureStatesQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
segment = serializers.IntegerField(
required=False,
help_text="ID of the segment to filter segment overrides by.",
)
class CustomCreateSegmentOverrideFeatureStateSerializer(
CreateSegmentOverrideFeatureStateSerializer
):
validate_override_limit = True
feature_segment = CustomCreateSegmentOverrideFeatureSegmentSerializer(
required=False, allow_null=True
)
def _get_save_kwargs(self, field_name): # type: ignore[no-untyped-def]
kwargs = super()._get_save_kwargs(field_name) # type: ignore[no-untyped-call]
if field_name == "feature_segment":
kwargs["feature"] = self.context.get("feature")
kwargs["environment"] = self.context.get("environment")
kwargs["environment_feature_version"] = self.context.get(
"environment_feature_version"
)
return kwargs
def create(self, validated_data: dict) -> FeatureState: # type: ignore[type-arg]
environment = validated_data["environment"]
if self.validate_override_limit and exceeds_segment_override_limit(environment):
raise serializers.ValidationError(
{"environment": SEGMENT_OVERRIDE_LIMIT_EXCEEDED_MESSAGE}
)
return super().create(validated_data) # type: ignore[no-any-return,no-untyped-call]