Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion api/api/urls/future.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from django.urls import path

from features.future.views import FlagAPIView
from features.future.views import FlagAPIView, SegmentOverrideAPIView

app_name = "future"

Expand All @@ -17,4 +17,10 @@
FlagAPIView.as_view(),
name="flag",
),
path(
"environments/<str:environment_key>/features/<int:feature_id>"
"/segment-overrides/<int:segment_id>/",
SegmentOverrideAPIView.as_view(),
name="segment-override",
),
]
8 changes: 7 additions & 1 deletion api/features/future/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""https://docs.flagsmith.com/managing-flags/updating-flags"""

from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.exceptions import APIException, NotFound


class ChangeRequestsEnabledError(APIException):
Expand All @@ -23,3 +23,9 @@ class DuplicatePriorityError(APIException):

status_code = status.HTTP_400_BAD_REQUEST
default_detail = "Segment overrides must not share a priority."


class SegmentOverrideNotFoundError(NotFound):
"""Raised where a flag serves a segment nothing of its own."""

default_detail = "Segment override not found."
7 changes: 7 additions & 0 deletions api/features/future/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,10 @@ def check_update_permissions(
raise NotFound()
if any(property_name in properties for property_name in denied):
raise PermissionDenied()


def check_segment_overrides_permissions(
user: UserABC, environment: Environment
) -> None:
"""Authorise a caller to write a flag's segment overrides."""
check_update_permissions(user, environment, {"segment_overrides": None})
78 changes: 66 additions & 12 deletions api/features/future/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

from api_keys.user import APIKeyUser
from environments.models import Environment
from features.future.exceptions import DuplicatePriorityError
from features.future.exceptions import (
DuplicatePriorityError,
SegmentOverrideNotFoundError,
)
from features.future.mappers import (
map_environment_default,
map_segment_override,
Expand Down Expand Up @@ -70,6 +73,27 @@ def _get_feature_states_to_write(
)


def _publish_version(
version: EnvironmentFeatureVersion, author: FFAdminUser | APIKeyUser
) -> None:
# `UserABC.__subclasshook__` matches any user against `APIKeyUser`
published_by = author if isinstance(author, FFAdminUser) else None
version.publish(
published_by=published_by,
published_by_api_key=None if published_by else author.key,
)


def _get_overrides_by_segment_id(
feature_states: Sequence[FeatureState],
) -> dict[int, FeatureState]:
return {
feature_segment.segment_id: feature_state
for feature_state in feature_states
if (feature_segment := feature_state.feature_segment) is not None
Comment thread
emyller marked this conversation as resolved.
}


def _write_variants(feature_state: FeatureState, variants: Sequence[Variant]) -> None:
weighted = {
multivariate_value.multivariate_feature_option_id: multivariate_value
Expand Down Expand Up @@ -299,22 +323,13 @@ def update_flag(
feature=feature,
version=version,
environment_default=environment_default,
overrides={
feature_segment.segment_id: feature_state
for feature_state in feature_states
if (feature_segment := feature_state.feature_segment) is not None
},
overrides=_get_overrides_by_segment_id(feature_states),
changes=override_changes,
replace=replace,
)

if version is not None:
# `UserABC.__subclasshook__` matches any user against `APIKeyUser`
published_by = author if isinstance(author, FFAdminUser) else None
version.publish(
published_by=published_by,
published_by_api_key=None if published_by else author.key,
)
_publish_version(version, author)

logger.info(
"flag.updated",
Expand All @@ -330,6 +345,45 @@ def update_flag(
return get_flag(environment=environment, feature=feature)


def delete_segment_override(
*,
environment: Environment,
feature: Feature,
segment_id: int,
author: FFAdminUser | APIKeyUser,
) -> UpdateFlagResponse:
"""Remove a flag's override for one segment, leaving the rest of the flag alone."""
with transaction.atomic():
version = _create_draft_version(environment, feature)
feature_states = _get_feature_states_to_write(environment, feature, version)

if segment_id not in _get_overrides_by_segment_id(feature_states):
raise SegmentOverrideNotFoundError()

_delete_segment_overrides(
environment=environment,
feature=feature,
version=version,
segment_ids=[segment_id],
)

if version is not None:
_publish_version(version, author)

logger.info(
"flag.updated",
organisation__id=environment.project.organisation_id,
project__id=environment.project_id,
environment__id=environment.id,
feature__id=feature.id,
segment_overrides__created__segment__ids=[],
segment_overrides__updated__segment__ids=[],
segment_overrides__deleted__segment__ids=[segment_id],
)

return get_flag(environment=environment, feature=feature)


def get_flag(*, environment: Environment, feature: Feature) -> UpdateFlagResponse:
"""Read what a flag serves in an environment."""
feature_states = _get_feature_states(environment, feature)
Expand Down
66 changes: 53 additions & 13 deletions api/features/future/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
from features.future.exceptions import ChangeRequestsEnabledError
from features.future.permissions import (
check_read_permissions,
check_segment_overrides_permissions,
check_update_permissions,
)
from features.future.serializers import UpdateFlagSerializer
from features.future.services import get_flag, update_flag
from features.future.services import delete_segment_override, get_flag, update_flag
from features.future.types import UpdateFlagRequest, UpdateFlagResponse
from features.models import Feature

Expand All @@ -41,6 +42,22 @@ def _get_feature(environment: Environment, feature_id: int) -> Feature:
raise NotFound() from None


def _check_change_requests_disabled(environment: Environment, feature: Feature) -> None:
"""Refuse to write a flag that can only be changed by a change request."""
if not environment.is_workflow_enabled:
return
api_error = ChangeRequestsEnabledError()
logger.warning(
"flag.update_rejected",
organisation__id=environment.project.organisation_id,
project__id=environment.project_id,
environment__id=environment.id,
feature__id=feature.id,
reason=api_error.default_code,
)
raise api_error


class FlagAPIView(APIView):
"""Read or update what a flag serves in an environment."""

Expand Down Expand Up @@ -96,18 +113,7 @@ def _update_flag(
environment = _get_environment(environment_key)
check_update_permissions(request.user, environment, request.data)
feature = _get_feature(environment, feature_id)

if environment.is_workflow_enabled:
api_error = ChangeRequestsEnabledError()
logger.warning(
"flag.update_rejected",
organisation__id=environment.project.organisation_id,
project__id=environment.project_id,
environment__id=environment.id,
feature__id=feature.id,
reason=api_error.default_code,
)
raise api_error
_check_change_requests_disabled(environment, feature)

serializer = UpdateFlagSerializer(
data=request.data,
Expand All @@ -124,3 +130,37 @@ def _update_flag(
author=request.user,
)
)


class SegmentOverrideAPIView(APIView):
"""Remove what a flag serves to a segment in an environment."""

permission_classes = [IsAuthenticated]

@extend_schema(
# Responds with the flag, like the other methods, rather than no content.
responses={200: UpdateFlagResponse},
tags=["experimental"],
description=(
"Remove the flag's override for the segment, "
"leaving the rest of the flag as it is."
),
)
def delete(
self, request: Request, environment_key: str, feature_id: int, segment_id: int
) -> Response:
assert not isinstance(request.user, AnonymousUser)

environment = _get_environment(environment_key)
check_segment_overrides_permissions(request.user, environment)
feature = _get_feature(environment, feature_id)
_check_change_requests_disabled(environment, feature)

return Response(
delete_segment_override(
environment=environment,
feature=feature,
segment_id=segment_id,
author=request.user,
)
)
Loading
Loading