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