eventsourcing 9.3.0__py3-none-any.whl → 9.3.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/application.py +0 -10
- eventsourcing/domain.py +88 -87
- eventsourcing/examples/aggregate4/domainmodel.py +28 -14
- eventsourcing/examples/contentmanagementsystem/test_system.py +113 -119
- eventsourcing/examples/searchablecontent/test_application.py +5 -4
- eventsourcing/examples/searchablecontent/test_recorder.py +5 -4
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +5 -8
- eventsourcing/postgres.py +22 -28
- eventsourcing/system.py +0 -10
- eventsourcing/tests/docs_tests/test_docs.py +10 -10
- eventsourcing/tests/domain_tests/test_aggregate.py +0 -21
- eventsourcing/tests/persistence.py +0 -3
- eventsourcing/tests/persistence_tests/test_postgres.py +106 -104
- eventsourcing/tests/system_tests/test_runner.py +17 -17
- eventsourcing/tests/system_tests/test_system.py +4 -1
- {eventsourcing-9.3.0.dist-info → eventsourcing-9.3.0a1.dist-info}/METADATA +4 -6
- {eventsourcing-9.3.0.dist-info → eventsourcing-9.3.0a1.dist-info}/RECORD +19 -20
- {eventsourcing-9.3.0.dist-info → eventsourcing-9.3.0a1.dist-info}/WHEEL +1 -1
- eventsourcing-9.3.0.dist-info/AUTHORS +0 -10
- {eventsourcing-9.3.0.dist-info → eventsourcing-9.3.0a1.dist-info}/LICENSE +0 -0
eventsourcing/application.py
CHANGED
|
@@ -915,16 +915,6 @@ class AggregateNotFoundError(EventSourcingError):
|
|
|
915
915
|
"""
|
|
916
916
|
|
|
917
917
|
|
|
918
|
-
class AggregateNotFound(AggregateNotFoundError): # noqa: N818
|
|
919
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
920
|
-
warn(
|
|
921
|
-
"AggregateNotFound is deprecated, use AggregateNotFoundError instead",
|
|
922
|
-
DeprecationWarning,
|
|
923
|
-
stacklevel=2,
|
|
924
|
-
)
|
|
925
|
-
super().__init__(*args, **kwargs)
|
|
926
|
-
|
|
927
|
-
|
|
928
918
|
class EventSourcedLog(Generic[TDomainEvent]):
|
|
929
919
|
"""
|
|
930
920
|
Constructs a sequence of domain events, like an aggregate.
|
eventsourcing/domain.py
CHANGED
|
@@ -941,7 +941,6 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
941
941
|
setattr(cls, base_event_name, base_event_cls)
|
|
942
942
|
|
|
943
943
|
# Make sure all events defined on aggregate subclass the base event class.
|
|
944
|
-
created_event_classes: Dict[str, Type[CanInitAggregate]] = {}
|
|
945
944
|
for name, value in tuple(cls.__dict__.items()):
|
|
946
945
|
if name == base_event_name:
|
|
947
946
|
# Don't subclass the base event class again.
|
|
@@ -956,20 +955,31 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
956
955
|
):
|
|
957
956
|
sub_class = cls._define_event_class(name, (value, base_event_cls), None)
|
|
958
957
|
setattr(cls, name, sub_class)
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
958
|
+
|
|
959
|
+
# Identify or define the aggregate's "created" event class.
|
|
960
|
+
created_event_class: Type[CanInitAggregate] | None = None
|
|
961
|
+
|
|
962
|
+
# Has the "created" event class been indicated with '_created_event_class'.
|
|
963
|
+
if "_created_event_class" in cls.__dict__:
|
|
964
|
+
created_event_class = cls.__dict__["_created_event_class"]
|
|
965
|
+
if isinstance(created_event_class, type) and issubclass(
|
|
966
|
+
created_event_class, CanInitAggregate
|
|
967
|
+
):
|
|
968
|
+
# We just subclassed the event classes, so reassign this.
|
|
969
|
+
created_event_class = getattr(cls, created_event_class.__name__)
|
|
970
|
+
assert created_event_class
|
|
971
|
+
cls._created_event_class = created_event_class
|
|
972
|
+
else:
|
|
973
|
+
msg = (
|
|
974
|
+
f"{created_event_class} not subclass of {CanInitAggregate.__name__}"
|
|
975
|
+
)
|
|
976
|
+
raise TypeError(msg)
|
|
962
977
|
|
|
963
978
|
# Disallow using both '_created_event_class' and 'created_event_name'.
|
|
964
|
-
created_event_class: Type[CanInitAggregate] | None = cls.__dict__.get(
|
|
965
|
-
"_created_event_class"
|
|
966
|
-
)
|
|
967
979
|
if created_event_class and created_event_name:
|
|
968
980
|
msg = "Can't use both '_created_event_class' and 'created_event_name'"
|
|
969
981
|
raise TypeError(msg)
|
|
970
982
|
|
|
971
|
-
# Identify or define the aggregate's "created" event class.
|
|
972
|
-
|
|
973
983
|
# Is the init method decorated with a CommandMethodDecorator?
|
|
974
984
|
if isinstance(cls.__dict__.get("__init__"), CommandMethodDecorator):
|
|
975
985
|
init_decorator: CommandMethodDecorator = cls.__dict__["__init__"]
|
|
@@ -977,20 +987,31 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
977
987
|
# Set the original method on the class (un-decorate __init__).
|
|
978
988
|
cls.__init__ = init_decorator.decorated_method # type: ignore
|
|
979
989
|
|
|
980
|
-
# Disallow using both 'created_event_name' and
|
|
990
|
+
# Disallow using both 'created_event_name' and '_created_event_class'.
|
|
981
991
|
if created_event_name:
|
|
982
992
|
msg = "Can't use both 'created_event_name' and decorator on __init__"
|
|
983
993
|
raise TypeError(msg)
|
|
984
|
-
# Disallow using both '_created_event_class' and decorator on __init__.
|
|
985
994
|
if created_event_class:
|
|
986
995
|
msg = "Can't use both '_created_event_class' and decorator on __init__"
|
|
987
996
|
raise TypeError(msg)
|
|
988
997
|
|
|
989
|
-
# Does the decorator specify a "
|
|
998
|
+
# Does the decorator specify a "create" event class?
|
|
990
999
|
if init_decorator.given_event_cls:
|
|
991
|
-
created_event_class =
|
|
992
|
-
|
|
1000
|
+
created_event_class = getattr(
|
|
1001
|
+
cls, init_decorator.given_event_cls.__name__
|
|
993
1002
|
)
|
|
1003
|
+
if isinstance(created_event_class, type) and issubclass(
|
|
1004
|
+
created_event_class, CanInitAggregate
|
|
1005
|
+
):
|
|
1006
|
+
assert created_event_class
|
|
1007
|
+
cls._created_event_class = created_event_class
|
|
1008
|
+
else:
|
|
1009
|
+
msg = (
|
|
1010
|
+
f"{created_event_class} not subclass of "
|
|
1011
|
+
f"{CanInitAggregate.__name__}"
|
|
1012
|
+
)
|
|
1013
|
+
raise TypeError(msg)
|
|
1014
|
+
|
|
994
1015
|
# Does the decorator specify a "created" event name?
|
|
995
1016
|
elif init_decorator.event_cls_name:
|
|
996
1017
|
created_event_name = init_decorator.event_cls_name
|
|
@@ -1007,85 +1028,65 @@ class MetaAggregate(type, Generic[TAggregate]):
|
|
|
1007
1028
|
_init_mentions_id.add(cls)
|
|
1008
1029
|
break
|
|
1009
1030
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
)
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
if created_event_name and len(created_event_classes) == 1:
|
|
1037
|
-
base_created_event_cls = next(iter(created_event_classes.values()))
|
|
1038
|
-
else:
|
|
1039
|
-
for base_cls in cls.__mro__:
|
|
1040
|
-
if base_cls is cls:
|
|
1041
|
-
continue
|
|
1042
|
-
base_created_event_cls = base_cls.__dict__.get(
|
|
1043
|
-
"_created_event_class",
|
|
1044
|
-
base_cls.__dict__.get("Created"),
|
|
1045
|
-
)
|
|
1046
|
-
if base_created_event_cls:
|
|
1047
|
-
break
|
|
1048
|
-
else: # pragma: no cover
|
|
1049
|
-
msg = "Can't decide base class for new 'created' event class"
|
|
1050
|
-
raise TypeError(msg)
|
|
1051
|
-
|
|
1052
|
-
if not created_event_name:
|
|
1053
|
-
created_event_name = base_created_event_cls.__name__
|
|
1054
|
-
|
|
1055
|
-
# Disallow init method from having variable params if
|
|
1056
|
-
# we are using it to define a "created" event class.
|
|
1057
|
-
try:
|
|
1058
|
-
init_method = cls.__dict__["__init__"]
|
|
1059
|
-
except KeyError:
|
|
1060
|
-
init_method = None
|
|
1061
|
-
else:
|
|
1031
|
+
# If no "created" event class has been specified, find or create one.
|
|
1032
|
+
if created_event_class is None:
|
|
1033
|
+
# Discover all the "created" event classes already defined.
|
|
1034
|
+
created_event_classes: Dict[str, Type[AggregateCreated]] = {}
|
|
1035
|
+
for name, value in tuple(cls.__dict__.items()):
|
|
1036
|
+
if isinstance(value, type) and issubclass(value, AggregateCreated):
|
|
1037
|
+
created_event_classes[name] = value
|
|
1038
|
+
|
|
1039
|
+
# Is a "created" event class already defined that matches the name?
|
|
1040
|
+
if created_event_name in created_event_classes:
|
|
1041
|
+
cls._created_event_class = created_event_classes[created_event_name]
|
|
1042
|
+
|
|
1043
|
+
# If there is only one class defined, and we have no name, use it.
|
|
1044
|
+
elif len(created_event_classes) == 1 and not created_event_name:
|
|
1045
|
+
cls._created_event_class = next(iter(created_event_classes.values()))
|
|
1046
|
+
|
|
1047
|
+
# If there are no "created" event classes already defined, or a name is
|
|
1048
|
+
# specified that hasn't matched, then define a "created" event class.
|
|
1049
|
+
elif len(created_event_classes) == 0 or created_event_name:
|
|
1050
|
+
# If no "created" event name has been specified, use default name.
|
|
1051
|
+
if not created_event_name:
|
|
1052
|
+
# This is safe because len(created_event_classes) == 0.
|
|
1053
|
+
created_event_name = "Created"
|
|
1054
|
+
|
|
1055
|
+
# Disallow init method from having variable params if
|
|
1056
|
+
# we are using it to define a "created" event class.
|
|
1062
1057
|
try:
|
|
1063
|
-
|
|
1064
|
-
except
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1058
|
+
init_method = cls.__dict__["__init__"]
|
|
1059
|
+
except KeyError:
|
|
1060
|
+
init_method = None
|
|
1061
|
+
else:
|
|
1062
|
+
try:
|
|
1063
|
+
_check_no_variable_params(init_method)
|
|
1064
|
+
except TypeError:
|
|
1065
|
+
raise
|
|
1066
|
+
|
|
1067
|
+
# Define a "created" event class for this aggregate.
|
|
1068
|
+
if issubclass(cls.Created, base_event_cls):
|
|
1069
|
+
# Don't subclass from base event class twice.
|
|
1070
|
+
bases: Tuple[Type[CanMutateAggregate], ...] = (cls.Created,)
|
|
1071
|
+
else:
|
|
1072
|
+
bases = (cls.Created, base_event_cls)
|
|
1073
|
+
event_cls = cls._define_event_class(
|
|
1076
1074
|
created_event_name,
|
|
1077
1075
|
bases,
|
|
1078
1076
|
init_method,
|
|
1079
|
-
)
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
# Set the event class as an attribute of the aggregate class.
|
|
1080
|
+
setattr(cls, created_event_name, event_cls)
|
|
1081
|
+
|
|
1082
|
+
# Remember which is the "created" event class.
|
|
1083
|
+
cls._created_event_class = cast(Type[AggregateCreated], event_cls)
|
|
1083
1084
|
|
|
1084
|
-
if created_event_class:
|
|
1085
|
-
cls._created_event_class = created_event_class
|
|
1086
|
-
else:
|
|
1087
1085
|
# Prepare to disallow ambiguity of choice between created event classes.
|
|
1088
|
-
|
|
1086
|
+
else:
|
|
1087
|
+
aggregate_has_many_created_event_classes[cls] = list(
|
|
1088
|
+
created_event_classes
|
|
1089
|
+
)
|
|
1089
1090
|
|
|
1090
1091
|
# Prepare the subsequent event classes.
|
|
1091
1092
|
for attr_name, attr_value in tuple(cls.__dict__.items()):
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
4
|
+
from collections import defaultdict
|
|
3
5
|
from dataclasses import dataclass
|
|
4
6
|
from datetime import datetime, timezone
|
|
5
|
-
from typing import Any, Iterable, List, Type, TypeVar, cast
|
|
7
|
+
from typing import Any, ClassVar, Dict, Iterable, List, Type, TypeVar, cast
|
|
6
8
|
from uuid import UUID, uuid4
|
|
7
9
|
|
|
8
10
|
from eventsourcing.dispatch import singledispatchmethod
|
|
@@ -27,7 +29,6 @@ class Aggregate:
|
|
|
27
29
|
id: UUID
|
|
28
30
|
version: int
|
|
29
31
|
created_on: datetime
|
|
30
|
-
_pending_events: List[DomainEvent]
|
|
31
32
|
|
|
32
33
|
def __init__(self, event: DomainEvent):
|
|
33
34
|
self.id = event.originator_id
|
|
@@ -46,15 +47,15 @@ class Aggregate:
|
|
|
46
47
|
timestamp=event_class.create_timestamp(),
|
|
47
48
|
)
|
|
48
49
|
new_event = event_class(**kwargs)
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
50
|
+
self.apply(new_event)
|
|
51
|
+
self.pending_events.append(new_event)
|
|
51
52
|
|
|
52
53
|
@singledispatchmethod
|
|
53
|
-
def
|
|
54
|
+
def apply(self, event: DomainEvent) -> None:
|
|
54
55
|
"""Applies event to aggregate."""
|
|
55
56
|
|
|
56
57
|
def collect_events(self) -> List[DomainEvent]:
|
|
57
|
-
events, self.
|
|
58
|
+
events, self.pending_events = self.pending_events, []
|
|
58
59
|
return events
|
|
59
60
|
|
|
60
61
|
@classmethod
|
|
@@ -63,12 +64,25 @@ class Aggregate:
|
|
|
63
64
|
_: TAggregate | None,
|
|
64
65
|
events: Iterable[DomainEvent],
|
|
65
66
|
) -> TAggregate | None:
|
|
66
|
-
aggregate
|
|
67
|
-
aggregate._pending_events = []
|
|
67
|
+
aggregate = object.__new__(cls)
|
|
68
68
|
for event in events:
|
|
69
|
-
aggregate.
|
|
69
|
+
aggregate.apply(event)
|
|
70
70
|
return aggregate
|
|
71
71
|
|
|
72
|
+
@property
|
|
73
|
+
def pending_events(self) -> List[DomainEvent]:
|
|
74
|
+
return type(self).__pending_events[id(self)]
|
|
75
|
+
|
|
76
|
+
@pending_events.setter
|
|
77
|
+
def pending_events(self, pending_events: List[DomainEvent]) -> None:
|
|
78
|
+
type(self).__pending_events[id(self)] = pending_events
|
|
79
|
+
|
|
80
|
+
__pending_events: ClassVar[Dict[int, List[DomainEvent]]] = defaultdict(list)
|
|
81
|
+
|
|
82
|
+
def __del__(self) -> None:
|
|
83
|
+
with contextlib.suppress(KeyError):
|
|
84
|
+
type(self).__pending_events.pop(id(self))
|
|
85
|
+
|
|
72
86
|
|
|
73
87
|
class Dog(Aggregate):
|
|
74
88
|
@dataclass(frozen=True)
|
|
@@ -88,27 +102,27 @@ class Dog(Aggregate):
|
|
|
88
102
|
name=name,
|
|
89
103
|
)
|
|
90
104
|
dog = cast(Dog, cls.projector(None, [event]))
|
|
91
|
-
dog.
|
|
105
|
+
dog.pending_events.append(event)
|
|
92
106
|
return dog
|
|
93
107
|
|
|
94
108
|
def add_trick(self, trick: str) -> None:
|
|
95
109
|
self.trigger_event(self.TrickAdded, trick=trick)
|
|
96
110
|
|
|
97
111
|
@singledispatchmethod
|
|
98
|
-
def
|
|
112
|
+
def apply(self, event: DomainEvent) -> None:
|
|
99
113
|
"""Applies event to aggregate."""
|
|
100
114
|
|
|
101
|
-
@
|
|
115
|
+
@apply.register(Registered)
|
|
102
116
|
def _(self, event: Registered) -> None:
|
|
103
117
|
super().__init__(event)
|
|
104
118
|
self.name = event.name
|
|
105
119
|
self.tricks: List[str] = []
|
|
106
120
|
|
|
107
|
-
@
|
|
121
|
+
@apply.register(TrickAdded)
|
|
108
122
|
def _(self, event: TrickAdded) -> None:
|
|
109
123
|
self.tricks.append(event.trick)
|
|
110
124
|
self.version = event.originator_version
|
|
111
125
|
|
|
112
|
-
@
|
|
126
|
+
@apply.register(Snapshot)
|
|
113
127
|
def _(self, event: Snapshot) -> None:
|
|
114
128
|
self.__dict__.update(event.state)
|
|
@@ -23,119 +23,112 @@ class ContentManagementSystemTestCase(TestCase):
|
|
|
23
23
|
env: ClassVar[Dict[str, str]] = {}
|
|
24
24
|
|
|
25
25
|
def test_system(self) -> None:
|
|
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
|
-
# Search for alternative words, expect two results.
|
|
133
|
-
page_ids = search_index_app.search("rose OR zebra")
|
|
134
|
-
pages = [
|
|
135
|
-
content_management_app.get_page_by_id(page_id) for page_id in page_ids
|
|
136
|
-
]
|
|
137
|
-
self.assertEqual(2, len(pages))
|
|
138
|
-
self.assertEqual(["animals", "plants"], sorted(p["slug"] for p in pages))
|
|
26
|
+
runner = SingleThreadedRunner(system=ContentManagementSystem(), env=self.env)
|
|
27
|
+
runner.start()
|
|
28
|
+
|
|
29
|
+
content_management_app = runner.get(ContentManagementApplication)
|
|
30
|
+
search_index_app = runner.get(SearchIndexApplication)
|
|
31
|
+
|
|
32
|
+
# Set user_id context variable.
|
|
33
|
+
user_id = uuid4()
|
|
34
|
+
user_id_cvar.set(user_id)
|
|
35
|
+
|
|
36
|
+
# Create empty pages.
|
|
37
|
+
content_management_app.create_page(title="Animals", slug="animals")
|
|
38
|
+
content_management_app.create_page(title="Plants", slug="plants")
|
|
39
|
+
content_management_app.create_page(title="Minerals", slug="minerals")
|
|
40
|
+
|
|
41
|
+
# Search, expect no results.
|
|
42
|
+
self.assertEqual(0, len(search_index_app.search("cat")))
|
|
43
|
+
self.assertEqual(0, len(search_index_app.search("rose")))
|
|
44
|
+
self.assertEqual(0, len(search_index_app.search("calcium")))
|
|
45
|
+
|
|
46
|
+
# Update the pages.
|
|
47
|
+
content_management_app.update_body(slug="animals", body="cat")
|
|
48
|
+
content_management_app.update_body(slug="plants", body="rose")
|
|
49
|
+
content_management_app.update_body(slug="minerals", body="calcium")
|
|
50
|
+
|
|
51
|
+
# Search for single words.
|
|
52
|
+
page_ids = search_index_app.search("cat")
|
|
53
|
+
self.assertEqual(1, len(page_ids))
|
|
54
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
55
|
+
self.assertEqual(page["slug"], "animals")
|
|
56
|
+
self.assertEqual(page["body"], "cat")
|
|
57
|
+
|
|
58
|
+
page_ids = search_index_app.search("rose")
|
|
59
|
+
self.assertEqual(1, len(page_ids))
|
|
60
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
61
|
+
self.assertEqual(page["slug"], "plants")
|
|
62
|
+
self.assertEqual(page["body"], "rose")
|
|
63
|
+
|
|
64
|
+
page_ids = search_index_app.search("calcium")
|
|
65
|
+
self.assertEqual(1, len(page_ids))
|
|
66
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
67
|
+
self.assertEqual(page["slug"], "minerals")
|
|
68
|
+
self.assertEqual(page["body"], "calcium")
|
|
69
|
+
|
|
70
|
+
self.assertEqual(len(search_index_app.search("dog")), 0)
|
|
71
|
+
self.assertEqual(len(search_index_app.search("bluebell")), 0)
|
|
72
|
+
self.assertEqual(len(search_index_app.search("zinc")), 0)
|
|
73
|
+
|
|
74
|
+
# Update the pages again.
|
|
75
|
+
content_management_app.update_body(slug="animals", body="cat dog zebra")
|
|
76
|
+
content_management_app.update_body(slug="plants", body="bluebell rose jasmine")
|
|
77
|
+
content_management_app.update_body(slug="minerals", body="iron zinc calcium")
|
|
78
|
+
|
|
79
|
+
# Search for single words.
|
|
80
|
+
page_ids = search_index_app.search("cat")
|
|
81
|
+
self.assertEqual(1, len(page_ids))
|
|
82
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
83
|
+
self.assertEqual(page["slug"], "animals")
|
|
84
|
+
self.assertEqual(page["body"], "cat dog zebra")
|
|
85
|
+
|
|
86
|
+
page_ids = search_index_app.search("rose")
|
|
87
|
+
self.assertEqual(1, len(page_ids))
|
|
88
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
89
|
+
self.assertEqual(page["slug"], "plants")
|
|
90
|
+
self.assertEqual(page["body"], "bluebell rose jasmine")
|
|
91
|
+
|
|
92
|
+
page_ids = search_index_app.search("calcium")
|
|
93
|
+
self.assertEqual(1, len(page_ids))
|
|
94
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
95
|
+
self.assertEqual(page["slug"], "minerals")
|
|
96
|
+
self.assertEqual(page["body"], "iron zinc calcium")
|
|
97
|
+
|
|
98
|
+
page_ids = search_index_app.search("dog")
|
|
99
|
+
self.assertEqual(1, len(page_ids))
|
|
100
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
101
|
+
self.assertEqual(page["slug"], "animals")
|
|
102
|
+
self.assertEqual(page["body"], "cat dog zebra")
|
|
103
|
+
|
|
104
|
+
page_ids = search_index_app.search("bluebell")
|
|
105
|
+
self.assertEqual(1, len(page_ids))
|
|
106
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
107
|
+
self.assertEqual(page["slug"], "plants")
|
|
108
|
+
self.assertEqual(page["body"], "bluebell rose jasmine")
|
|
109
|
+
|
|
110
|
+
page_ids = search_index_app.search("zinc")
|
|
111
|
+
self.assertEqual(1, len(page_ids))
|
|
112
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
113
|
+
self.assertEqual(page["slug"], "minerals")
|
|
114
|
+
self.assertEqual(page["body"], "iron zinc calcium")
|
|
115
|
+
|
|
116
|
+
# Search for multiple words in same page.
|
|
117
|
+
page_ids = search_index_app.search("dog cat")
|
|
118
|
+
self.assertEqual(1, len(page_ids))
|
|
119
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
120
|
+
self.assertEqual(page["slug"], "animals")
|
|
121
|
+
self.assertEqual(page["body"], "cat dog zebra")
|
|
122
|
+
|
|
123
|
+
# Search for multiple words in same page, expect no results.
|
|
124
|
+
page_ids = search_index_app.search("rose zebra")
|
|
125
|
+
self.assertEqual(0, len(page_ids))
|
|
126
|
+
|
|
127
|
+
# Search for alternative words, expect two results.
|
|
128
|
+
page_ids = search_index_app.search("rose OR zebra")
|
|
129
|
+
pages = [content_management_app.get_page_by_id(page_id) for page_id in page_ids]
|
|
130
|
+
self.assertEqual(2, len(pages))
|
|
131
|
+
self.assertEqual(["animals", "plants"], sorted(p["slug"] for p in pages))
|
|
139
132
|
|
|
140
133
|
|
|
141
134
|
class TestWithSQLite(ContentManagementSystemTestCase):
|
|
@@ -164,17 +157,18 @@ class TestWithPostgres(ContentManagementSystemTestCase):
|
|
|
164
157
|
super().tearDown()
|
|
165
158
|
|
|
166
159
|
def drop_tables(self) -> None:
|
|
167
|
-
|
|
160
|
+
db = PostgresDatastore(
|
|
168
161
|
self.env["POSTGRES_DBNAME"],
|
|
169
162
|
self.env["POSTGRES_HOST"],
|
|
170
163
|
self.env["POSTGRES_PORT"],
|
|
171
164
|
self.env["POSTGRES_USER"],
|
|
172
165
|
self.env["POSTGRES_PASSWORD"],
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
166
|
+
)
|
|
167
|
+
drop_postgres_table(db, "public.contentmanagementapplication_events")
|
|
168
|
+
drop_postgres_table(db, "public.pages_projection_example")
|
|
169
|
+
drop_postgres_table(db, "public.searchindexapplication_events")
|
|
170
|
+
drop_postgres_table(db, "public.searchindexapplication_tracking")
|
|
171
|
+
db.close()
|
|
178
172
|
|
|
179
173
|
|
|
180
174
|
del ContentManagementSystemTestCase
|
|
@@ -96,15 +96,16 @@ class TestWithPostgres(SearchableContentApplicationTestCase):
|
|
|
96
96
|
super().tearDown()
|
|
97
97
|
|
|
98
98
|
def drop_tables(self) -> None:
|
|
99
|
-
|
|
99
|
+
db = PostgresDatastore(
|
|
100
100
|
os.environ["POSTGRES_DBNAME"],
|
|
101
101
|
os.environ["POSTGRES_HOST"],
|
|
102
102
|
os.environ["POSTGRES_PORT"],
|
|
103
103
|
os.environ["POSTGRES_USER"],
|
|
104
104
|
os.environ["POSTGRES_PASSWORD"],
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
)
|
|
106
|
+
drop_postgres_table(db, "public.searchablecontentapplication_events")
|
|
107
|
+
drop_postgres_table(db, "public.pages_projection_example")
|
|
108
|
+
db.close()
|
|
108
109
|
|
|
109
110
|
|
|
110
111
|
del SearchableContentApplicationTestCase
|
|
@@ -54,15 +54,16 @@ class TestWithPostgres(SearchableContentRecorderTestCase):
|
|
|
54
54
|
super().tearDown()
|
|
55
55
|
|
|
56
56
|
def drop_tables(self) -> None:
|
|
57
|
-
|
|
57
|
+
db = PostgresDatastore(
|
|
58
58
|
os.environ["POSTGRES_DBNAME"],
|
|
59
59
|
os.environ["POSTGRES_HOST"],
|
|
60
60
|
os.environ["POSTGRES_PORT"],
|
|
61
61
|
os.environ["POSTGRES_USER"],
|
|
62
62
|
os.environ["POSTGRES_PASSWORD"],
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
)
|
|
64
|
+
drop_postgres_table(db, "public.searchablecontentapplication_events")
|
|
65
|
+
drop_postgres_table(db, "public.pages_projection_example")
|
|
66
|
+
db.close()
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
del SearchableContentRecorderTestCase
|