Skip to content

Commit c405d6e

Browse files
emyllerclaudematthewelwell
authored
refactor(Segments): Clean up after Segment Change Request work (#6265)
Co-authored-by: Claude <[email protected]> Co-authored-by: Matthew Elwell <[email protected]>
1 parent dd28ed5 commit c405d6e

File tree

4 files changed

+86
-59
lines changed

4 files changed

+86
-59
lines changed

api/segments/managers.py

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.22 on 2025-11-11 00:08
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("segments", "0029_add_is_system_segment"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="historicalsegment",
15+
name="version",
16+
field=models.IntegerField(default=1, null=True),
17+
),
18+
migrations.AlterField(
19+
model_name="segment",
20+
name="version",
21+
field=models.IntegerField(default=1, null=True),
22+
),
23+
]

api/segments/models.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from django.db import models, transaction
1010
from django_lifecycle import ( # type: ignore[import-untyped]
1111
AFTER_CREATE,
12-
BEFORE_CREATE,
1312
LifecycleModelMixin,
1413
hook,
1514
)
@@ -30,13 +29,24 @@
3029
from metadata.models import Metadata
3130
from projects.models import Project
3231

33-
from .managers import LiveSegmentManager, SegmentManager
34-
3532
ModelT = typing.TypeVar("ModelT", bound=models.Model)
3633

3734
logger = logging.getLogger(__name__)
3835

3936

37+
class SegmentManager(SoftDeleteExportableManager):
38+
pass
39+
40+
41+
class LiveSegmentManager(SoftDeleteExportableManager):
42+
def get_queryset(self): # type: ignore[no-untyped-def]
43+
"""
44+
Returns only the canonical segments, which will always be
45+
the highest version.
46+
"""
47+
return super().get_queryset().filter(id=models.F("version_of"))
48+
49+
4050
class ConfiguredOrderManager(SoftDeleteExportableManager, models.Manager[ModelT]):
4151
setting_name: str
4252

@@ -87,8 +97,7 @@ class Segment(
8797
Feature, on_delete=models.CASCADE, related_name="segments", null=True
8898
)
8999

90-
# This defaults to 1 for newly created segments.
91-
version = models.IntegerField(null=True)
100+
version = models.IntegerField(default=1, null=True)
92101

93102
version_of = models.ForeignKey(
94103
"self",
@@ -126,18 +135,8 @@ def __str__(self): # type: ignore[no-untyped-def]
126135
def get_skip_create_audit_log(self) -> bool:
127136
if self.is_system_segment:
128137
return True
129-
try:
130-
if self.version_of_id and self.version_of_id != self.id:
131-
return True
132-
except Segment.DoesNotExist:
133-
return True
134-
135-
return False
136-
137-
@hook(BEFORE_CREATE, when="version_of", is_now=None)
138-
def set_default_version_to_one_if_new_segment(self): # type: ignore[no-untyped-def]
139-
if self.version is None:
140-
self.version = 1
138+
is_revision = bool(self.version_of_id and self.version_of_id != self.id)
139+
return is_revision
141140

142141
@hook(AFTER_CREATE, when="version_of", is_now=None)
143142
def set_version_of_to_self_if_none(self): # type: ignore[no-untyped-def]

api/tests/unit/segments/test_unit_segments_models.py

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from collections.abc import Callable
22
from typing import Any
3-
from unittest.mock import PropertyMock
43

54
import pytest
65
from django.core.exceptions import ValidationError
@@ -29,7 +28,15 @@ def test_Condition_str__returns_readable_representation_of_condition(
2928
assert result == "Condition for ALL rule for Segment - segment: foo EQUAL bar"
3029

3130

32-
def test_Condition_get_skip_create_audit_log__returns_true(
31+
@pytest.mark.parametrize(
32+
"delete",
33+
[
34+
lambda rule: rule.delete(),
35+
lambda rule: rule.hard_delete(),
36+
],
37+
)
38+
def test_Condition_get_skip_create_audit_log__rule_deleted__returns_true(
39+
delete: Callable[[SegmentRule], None],
3340
segment_rule: SegmentRule,
3441
) -> None:
3542
# Given
@@ -38,16 +45,45 @@ def test_Condition_get_skip_create_audit_log__returns_true(
3845
property="foo",
3946
operator=EQUAL,
4047
value="bar",
48+
created_with_segment=False,
4149
)
4250

4351
# When
44-
result = condition.get_skip_create_audit_log()
52+
delete(segment_rule)
4553

4654
# Then
47-
assert result is True
55+
assert condition.get_skip_create_audit_log() is True
4856

4957

50-
def test_manager_returns_only_highest_version_of_segments(
58+
@pytest.mark.parametrize(
59+
"delete",
60+
[
61+
lambda segment: segment.delete(),
62+
lambda segment: segment.hard_delete(),
63+
],
64+
)
65+
def test_Condition_get_skip_create_audit_log__segment_deleted__returns_true(
66+
delete: Callable[[Segment], None],
67+
segment: Segment,
68+
segment_rule: SegmentRule,
69+
) -> None:
70+
# Given
71+
condition = Condition.objects.create(
72+
rule=segment_rule,
73+
property="foo",
74+
operator=EQUAL,
75+
value="bar",
76+
created_with_segment=False,
77+
)
78+
79+
# When
80+
delete(segment)
81+
82+
# Then
83+
assert condition.get_skip_create_audit_log() is True
84+
85+
86+
def test_LiveSegmentManager__returns_only_highest_version_of_segments(
5187
segment: Segment,
5288
) -> None:
5389
# Given
@@ -108,24 +144,7 @@ def test_SegmentRule_get_skip_create_audit_log__returns_true(
108144
assert result is True
109145

110146

111-
def test_segment_get_skip_create_audit_log_when_exception(
112-
mocker: MockerFixture,
113-
segment: Segment,
114-
) -> None:
115-
# Given
116-
patched_segment = mocker.patch.object(
117-
Segment, "version_of_id", new_callable=PropertyMock
118-
)
119-
patched_segment.side_effect = Segment.DoesNotExist("Segment missing")
120-
121-
# When
122-
result = segment.get_skip_create_audit_log()
123-
124-
# Then
125-
assert result is True
126-
127-
128-
def test_delete_segment_only_schedules_one_task_for_audit_log_creation(
147+
def test_Segment_delete__multiple_rules_conditions__schedules_audit_log_task_once(
129148
mocker: MockerFixture, segment: Segment
130149
) -> None:
131150
# Given
@@ -143,11 +162,11 @@ def test_delete_segment_only_schedules_one_task_for_audit_log_creation(
143162
)
144163

145164
# When
146-
mocked_tasks = mocker.patch("core.signals.tasks")
165+
task = mocker.patch("core.signals.tasks.create_audit_log_from_historical_record")
147166
segment.delete()
148167

149168
# Then
150-
assert len(mocked_tasks.mock_calls) == 1
169+
assert task.delay.call_count == 1
151170

152171

153172
def test_Segment_clone__can_create_standalone_segment_clone(
@@ -264,7 +283,9 @@ def test_Segment_clone__segment_with_rules__returns_new_segment_with_copied_rule
264283
]
265284

266285

267-
def test_system_segment_get_skip_create_audit_log(system_segment: Segment) -> None:
286+
def test_Segment_get_skip_create_audit_log__system_segment__returns_true(
287+
system_segment: Segment,
288+
) -> None:
268289
# When
269290
result = system_segment.get_skip_create_audit_log()
270291

0 commit comments

Comments
 (0)