Skip to content

Commit 33eb122

Browse files
authored
Merge pull request #832 from OpenTrafficCam/bug/9664-road-user-assignment-highlighting-is-not-correct-when-loading-ottrk-before-otflow
bug/9664-road-user-assignment-highlighting-is-not-correct-when-loading-ottrk-before-otflow
2 parents f039958 + 78bef19 commit 33eb122

7 files changed

Lines changed: 133 additions & 57 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ data/
44
tests/data/OTCamera19_FR20_2023-05-24*
55
tests/scripts/
66

7+
# Anthropic Claude
8+
.claude/rules/shared
9+
710
# Byte-compiled / optimized / DLL files
811
__pycache__/
912
*.py[cod]

OTAnalytics/application/application.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
EnableFilterTrackByDate,
3737
)
3838
from OTAnalytics.application.use_cases.flow_repository import AddFlow
39+
from OTAnalytics.application.use_cases.flow_repository import (
40+
is_flow_name_valid as check_flow_name_valid,
41+
)
3942
from OTAnalytics.application.use_cases.flow_statistics import (
4043
NumberOfTracksAssignedToEachFlow,
4144
)
@@ -282,7 +285,7 @@ def is_flow_name_valid(self, flow_name: str) -> bool:
282285
Returns:
283286
bool: True if a flow with the name already exists, False otherwise.
284287
"""
285-
return self._add_flow.is_flow_name_valid(flow_name)
288+
return check_flow_name_valid(flow_name, self._datastore.get_all_flows())
286289

287290
def add_flow(self, flow: Flow) -> None:
288291
self._add_flow(flow)

OTAnalytics/application/use_cases/flow_repository.py

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,41 +32,41 @@ def __call__(self, flow: Flow) -> None:
3232
Args:
3333
flow (Flow): the flow to be added.
3434
"""
35-
self.check_flow_already_exists(flow)
35+
check_flow_already_exists(flow, self._flow_repository.get_all())
3636

3737
self._flow_repository.add(flow)
3838

39-
def check_flow_already_exists(self, flow: Flow) -> None:
40-
if not self.is_flow_name_valid(flow.name):
41-
raise FlowAlreadyExists(
42-
f"A flow with the name {flow.name} already exists. "
43-
"Choose another name."
44-
)
45-
if not self.is_flow_id_valid(flow.id):
46-
raise FlowIdAlreadyExists(f"A flow with id {flow.id} already exists.")
47-
48-
if self.flow_with_same_start_end_section_exists(flow):
49-
raise FlowAlreadyExists(
50-
"Flow with same start and end section already exists."
51-
)
52-
53-
def flow_with_same_start_end_section_exists(self, flow: Flow) -> bool:
54-
existing_flows = self._flow_repository.get_all()
55-
for existing_flow in existing_flows:
56-
if existing_flow.start == flow.start and existing_flow.end == flow.end:
57-
return True
58-
return False
5939

60-
def is_flow_name_valid(self, flow_name: str) -> bool:
61-
if not flow_name:
62-
return False
63-
return all(
64-
stored_flow.name != flow_name
65-
for stored_flow in self._flow_repository.get_all()
40+
def check_flow_already_exists(given: Flow, existing_flows: Iterable[Flow]) -> None:
41+
if not is_flow_name_valid(given.name, existing_flows):
42+
raise FlowAlreadyExists(
43+
f"A flow with the name {given.name} already exists. " "Choose another name."
6644
)
45+
existing_flow_ids = {flow.id for flow in existing_flows}
46+
if not is_flow_id_valid(given.id, existing_flow_ids):
47+
raise FlowIdAlreadyExists(f"A flow with id {given.id} already exists.")
48+
49+
if flow_with_same_start_end_section_exists(given, existing_flows):
50+
raise FlowAlreadyExists("Flow with same start and end section already exists.")
51+
52+
53+
def flow_with_same_start_end_section_exists(
54+
given: Flow, existing_flows: Iterable[Flow]
55+
) -> bool:
56+
for existing_flow in existing_flows:
57+
if existing_flow.start == given.start and existing_flow.end == given.end:
58+
return True
59+
return False
60+
61+
62+
def is_flow_name_valid(flow_name: str, existing_flows: Iterable[Flow]) -> bool:
63+
if not flow_name:
64+
return False
65+
return all(stored_flow.name != flow_name for stored_flow in existing_flows)
66+
6767

68-
def is_flow_id_valid(self, flow_id: FlowId) -> bool:
69-
return not (flow_id in self._flow_repository.get_flow_ids())
68+
def is_flow_id_valid(given: FlowId, existing_ids: Iterable[FlowId]) -> bool:
69+
return given not in existing_ids
7070

7171

7272
class ClearAllFlows:
@@ -93,9 +93,24 @@ def get(self) -> list[Flow]:
9393

9494

9595
class AddAllFlows:
96-
def __init__(self, add_flow: AddFlow) -> None:
97-
self._add_flow = add_flow
96+
def __init__(self, flow_repository: FlowRepository) -> None:
97+
self._flow_repository = flow_repository
9898

9999
def add(self, flows: Iterable[Flow]) -> None:
100-
for flow in flows:
101-
self._add_flow(flow)
100+
flow_list = list(flows)
101+
102+
if not flow_list:
103+
return
104+
105+
if not flows_are_unique(flow_list):
106+
raise FlowAlreadyExists("Flows to be added are not unique.")
107+
108+
existing_flows = self._flow_repository.get_all()
109+
for flow in flow_list:
110+
check_flow_already_exists(flow, existing_flows)
111+
112+
self._flow_repository.add_all(flow_list)
113+
114+
115+
def flows_are_unique(flows: list[Flow]) -> bool:
116+
return len(flows) == len(set(flows))

OTAnalytics/application/use_cases/load_otflow.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from OTAnalytics.application.state import ConfigurationFile
77
from OTAnalytics.application.use_cases.event_repository import ClearAllEvents
88
from OTAnalytics.application.use_cases.flow_repository import (
9-
AddFlow,
9+
AddAllFlows,
1010
ClearAllFlows,
1111
FlowAlreadyExists,
1212
)
@@ -34,7 +34,7 @@ class LoadOtflow:
3434
clear_all_events (ClearAllEvents): use case to clear event repository.
3535
flow_parser (FlowParser): to parse sections and flows from file.
3636
add_section (AddSection): use case to add sections to section repository.
37-
add_flow (AddFlow): use case to add flows to flow repository.
37+
add_all_flows (AddAllFlows): use case to batch-add flows to flow repository.
3838
"""
3939

4040
def __init__(
@@ -44,15 +44,15 @@ def __init__(
4444
clear_all_events: ClearAllEvents,
4545
flow_parser: FlowParser,
4646
add_section: AddSection,
47-
add_flow: AddFlow,
47+
add_all_flows: AddAllFlows,
4848
deserialize: Deserializer,
4949
) -> None:
5050
self._clear_all_sections = clear_all_sections
5151
self._clear_all_flows = clear_all_flows
5252
self._clear_all_events = clear_all_events
5353
self._flow_parser = flow_parser
5454
self._add_section = add_section
55-
self._add_flow = add_flow
55+
self._add_all_flows = add_all_flows
5656
self._deserialize = deserialize
5757
self._subject = Subject[ConfigurationFile]()
5858

@@ -87,8 +87,7 @@ def _add_sections(self, sections: Iterable[Section]) -> None:
8787
self._add_section(section)
8888

8989
def _add_flows(self, flows: Iterable[Flow]) -> None:
90-
for flow in flows:
91-
self._add_flow(flow)
90+
self._add_all_flows.add(flows)
9291

9392
def register(self, observer: OBSERVER[ConfigurationFile]) -> None:
9493
self._subject.register(observer)

OTAnalytics/plugin_ui/base_application.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def load_otconfig(self) -> LoadOtconfig:
376376
self.project_updater,
377377
AddAllVideos(self.video_repository),
378378
AddAllSections(self.add_section),
379-
AddAllFlows(self.add_flow),
379+
self.add_all_flows,
380380
self.load_track_files,
381381
self.add_new_remark,
382382
parse_json,
@@ -491,6 +491,10 @@ def clear_all_flows(self) -> ClearAllFlows:
491491
def add_flow(self) -> AddFlow:
492492
return AddFlow(self.flow_repository)
493493

494+
@cached_property
495+
def add_all_flows(self) -> AddAllFlows:
496+
return AddAllFlows(self.flow_repository)
497+
494498
@cached_property
495499
def clear_all_sections(self) -> ClearAllSections:
496500
return ClearAllSections(self.section_repository)
@@ -802,7 +806,7 @@ def load_otflow(self) -> LoadOtflow:
802806
self.clear_all_events,
803807
self.flow_parser,
804808
self.add_section,
805-
self.add_flow,
809+
self.add_all_flows,
806810
parse_json,
807811
)
808812

tests/unit/OTAnalytics/application/use_cases/test_flow_repository.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def test_add_flow_with_different_names(
5757
assert flow_repository.add.call_args_list == [
5858
call(second_flow),
5959
]
60-
flow_repository.get_flow_ids.assert_called_once()
6160

6261
def test_add_flow_with_same_names(
6362
self, flow_repository: Mock, first_flow: Mock
@@ -113,12 +112,65 @@ def test_get(self) -> None:
113112

114113
class TestAddAllFlows:
115114
def test_add(self, first_flow: Flow, second_flow: Flow) -> None:
116-
add_flow = Mock()
117-
add_all_flows = AddAllFlows(add_flow)
115+
flow_repository = Mock(spec=FlowRepository)
116+
flow_repository.get_all.return_value = []
117+
add_all_flows = AddAllFlows(flow_repository)
118118

119119
add_all_flows.add([first_flow, second_flow])
120120

121-
assert add_flow.call_args_list == [
122-
call(first_flow),
123-
call(second_flow),
124-
]
121+
flow_repository.add_all.assert_called_once_with([first_flow, second_flow])
122+
123+
def test_add_empty_list_does_nothing(self) -> None:
124+
flow_repository = Mock(spec=FlowRepository)
125+
add_all_flows = AddAllFlows(flow_repository)
126+
127+
add_all_flows.add([])
128+
129+
flow_repository.get_all.assert_not_called()
130+
flow_repository.add_all.assert_not_called()
131+
132+
def test_add_raises_when_flows_not_unique(self, first_flow: Flow) -> None:
133+
flow_repository = Mock(spec=FlowRepository)
134+
add_all_flows = AddAllFlows(flow_repository)
135+
136+
with pytest.raises(FlowAlreadyExists):
137+
add_all_flows.add([first_flow, first_flow])
138+
139+
flow_repository.add_all.assert_not_called()
140+
141+
def test_add_raises_when_flow_already_exists_in_repository(
142+
self, first_flow: Flow, second_flow: Flow
143+
) -> None:
144+
flow_repository = Mock(spec=FlowRepository)
145+
flow_repository.get_all.return_value = [first_flow]
146+
add_all_flows = AddAllFlows(flow_repository)
147+
148+
with pytest.raises(FlowAlreadyExists):
149+
add_all_flows.add([second_flow, first_flow])
150+
151+
flow_repository.add_all.assert_not_called()
152+
153+
def test_add_raises_when_flows_added_are_not_unique(
154+
self, first_flow: Flow, second_flow: Flow
155+
) -> None:
156+
flow_repository = Mock(spec=FlowRepository)
157+
flow_repository.get_all.return_value = [first_flow]
158+
add_all_flows = AddAllFlows(flow_repository)
159+
160+
with pytest.raises(FlowAlreadyExists):
161+
add_all_flows.add([first_flow, first_flow])
162+
163+
flow_repository.add_all.assert_not_called()
164+
165+
def test_add_able_to_handle_exhaustable_iterables(
166+
self, first_flow: Flow, second_flow: Flow
167+
) -> None:
168+
flow_repository = Mock(spec=FlowRepository)
169+
flow_repository.get_all.return_value = []
170+
add_all_flows = AddAllFlows(flow_repository)
171+
172+
exhaustable_iterable = (_flow for _flow in [first_flow, second_flow])
173+
174+
add_all_flows.add(exhaustable_iterable)
175+
176+
flow_repository.add_all.assert_called_once_with([first_flow, second_flow])

tests/unit/OTAnalytics/application/use_cases/test_load_otflow.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from OTAnalytics.application.state import ConfigurationFile
99
from OTAnalytics.application.use_cases.event_repository import ClearAllEvents
1010
from OTAnalytics.application.use_cases.flow_repository import (
11-
AddFlow,
11+
AddAllFlows,
1212
ClearAllFlows,
1313
FlowAlreadyExists,
1414
)
@@ -31,7 +31,7 @@ class MockDependencies(TypedDict):
3131
clear_all_events: Mock
3232
flow_parser: Mock
3333
add_section: Mock
34-
add_flow: Mock
34+
add_all_flows: Mock
3535
deserialize: Mock
3636

3737

@@ -63,15 +63,15 @@ def mock_deps(
6363
)
6464

6565
add_section = Mock(spec=AddSection)
66-
add_flow = Mock(spec=AddFlow)
66+
add_all_flows = Mock(spec=AddAllFlows)
6767
deserializer = Mock()
6868
return {
6969
"clear_all_sections": clear_all_sections,
7070
"clear_all_flows": clear_all_flows,
7171
"clear_all_events": clear_all_events,
7272
"flow_parser": flow_parser,
7373
"add_section": add_section,
74-
"add_flow": add_flow,
74+
"add_all_flows": add_all_flows,
7575
"deserialize": deserializer,
7676
}
7777

@@ -96,7 +96,7 @@ def test_load_flow_file(
9696
call(mock_first_section),
9797
call(mock_second_section),
9898
]
99-
assert mock_deps["add_flow"].call_args_list == [call(mock_flow)]
99+
mock_deps["add_all_flows"].add.assert_called_once_with([mock_flow])
100100
mock_deps["clear_all_events"].assert_called_once()
101101
mock_deps["clear_all_flows"].assert_called_once()
102102
mock_deps["clear_all_sections"].assert_called_once()
@@ -117,7 +117,7 @@ def test_load_flow_file_invalid_section_file(
117117
load_flow_file(otflow_file)
118118

119119
mock_deps["add_section"].assert_called_once_with(mock_first_section)
120-
mock_deps["add_flow"].assert_not_called()
120+
mock_deps["add_all_flows"].add.assert_not_called()
121121
assert mock_deps["clear_all_sections"].call_count == 2
122122
assert mock_deps["clear_all_flows"].call_count == 2
123123
assert mock_deps["clear_all_events"].call_count == 2
@@ -130,7 +130,7 @@ def test_load_flow_file_invalid_flow_file(
130130
mock_second_section: Mock,
131131
mock_flow: Mock,
132132
) -> None:
133-
mock_deps["add_flow"].side_effect = FlowAlreadyExists
133+
mock_deps["add_all_flows"].add.side_effect = FlowAlreadyExists
134134
load_flow_file = LoadOtflow(**mock_deps)
135135
observer = Mock()
136136
load_flow_file.register(observer)
@@ -143,7 +143,7 @@ def test_load_flow_file_invalid_flow_file(
143143
call(mock_first_section),
144144
call(mock_second_section),
145145
]
146-
mock_deps["add_flow"].assert_called_once_with(mock_flow)
146+
mock_deps["add_all_flows"].add.assert_called_once_with([mock_flow])
147147
assert mock_deps["clear_all_sections"].call_count == 2
148148
assert mock_deps["clear_all_flows"].call_count == 2
149149
assert mock_deps["clear_all_events"].call_count == 2

0 commit comments

Comments
 (0)