Skip to content

Commit c8b7f5c

Browse files
committed
feat(comment-replies): add single threading on comments
1 parent 23f0010 commit c8b7f5c

File tree

23 files changed

+1224
-50
lines changed

23 files changed

+1224
-50
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,7 @@ target/
6969
# node modules
7070
**/*/node_modules
7171
node_modules
72-
.python-version
72+
.python-version
73+
74+
# Claude
75+
Claude.md

invenio_requests/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,10 @@
132132

133133
REQUESTS_REVIEWERS_MAX_NUMBER = 15
134134
"""Maximum number of reviewers allowed for a request."""
135+
136+
REQUESTS_COMMENT_PREVIEW_LIMIT = 5
137+
"""Number of most recent child comments to inline in parent's search index.
138+
139+
This limits the size of indexed documents when comments have many replies.
140+
Additional replies can be loaded via pagination.
141+
"""

invenio_requests/customizations/event_types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ def payload_schema():
4646
payload_required = False
4747
"""Require the event payload."""
4848

49+
allow_children = False
50+
"""Allow this event type to have children (parent-child relationships).
51+
52+
If True, events of this type can have a parent_id and children.
53+
If False, attempting to set a parent_id will raise an error.
54+
"""
55+
4956
def __init__(self, payload=None):
5057
"""Constructor."""
5158
self.payload = payload or {}
@@ -115,6 +122,8 @@ class LogEventType(EventType):
115122
"""Log event type."""
116123

117124
type_id = "L"
125+
allow_children = True
126+
"""Allow log events to preserve parent-child structure (e.g., deleted comments)."""
118127

119128
def payload_schema():
120129
"""Return payload schema as a dictionary."""
@@ -162,6 +171,9 @@ class CommentEventType(EventType):
162171

163172
type_id = "C"
164173

174+
allow_children = True
175+
"""Comments support parent-child relationships (replies to comments)."""
176+
165177
def payload_schema():
166178
"""Return payload schema as a dictionary."""
167179
# we need to import here because of circular imports

invenio_requests/errors.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,36 @@ def __init__(self, action, reason=None):
4747
"""
4848
reason = reason or "Could not execute the action"
4949
super().__init__(action, reason)
50+
51+
52+
class ChildrenNotSupportedError(Exception):
53+
"""Exception raised when children are attempted on an event type that doesn't support it."""
54+
55+
def __init__(self, event_type, message=None):
56+
"""Constructor.
57+
58+
:param event_type: The event type that doesn't support children.
59+
:param message: Optional custom error message.
60+
"""
61+
self.event_type = event_type
62+
self.message = message or (
63+
f"Event type '{event_type}' does not support children. "
64+
"Only event types with allow_children=True can have parent-child relationships."
65+
)
66+
super().__init__(self.message)
67+
68+
69+
class NestedChildrenNotAllowedError(Exception):
70+
"""Exception raised when attempting to create nested children (reply to a reply)."""
71+
72+
def __init__(self, message=None):
73+
"""Constructor.
74+
75+
:param message: Optional custom error message.
76+
"""
77+
self.message = message or (
78+
"Nested children are not allowed. "
79+
"You cannot reply to a comment that is already a reply. "
80+
"Only one level of parent-child relationships is supported."
81+
)
82+
super().__init__(self.message)

invenio_requests/records/api.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@
1010

1111
from enum import Enum
1212
from functools import partial
13+
from werkzeug.utils import cached_property
1314

1415
from invenio_records.dumpers import SearchDumper
1516
from invenio_records.systemfields import ConstantField, DictField, ModelField
1617
from invenio_records_resources.records.api import Record
1718
from invenio_records_resources.records.systemfields import IndexField
1819

1920
from ..customizations import RequestState as State
20-
from .dumpers import CalculatedFieldDumperExt, GrantTokensDumperExt
21+
from .dumpers import (
22+
CalculatedFieldDumperExt,
23+
GrantTokensDumperExt,
24+
ParentChildDumperExt,
25+
)
2126
from .models import RequestEventModel, RequestMetadata
2227
from .systemfields import (
2328
EntityReferenceField,
@@ -51,6 +56,13 @@ class RequestEvent(Record):
5156

5257
model_cls = RequestEventModel
5358

59+
dumper = SearchDumper(
60+
extensions=[
61+
ParentChildDumperExt(),
62+
]
63+
)
64+
"""Search dumper with parent-child relationship extension."""
65+
5466
# Systemfields
5567
metadata = None
5668

@@ -83,6 +95,22 @@ class RequestEvent(Record):
8395
created_by = EntityReferenceField("created_by", check_referenced)
8496
"""Who created the event."""
8597

98+
parent_id = DictField("parent_id")
99+
"""The parent event ID for parent-child relationships."""
100+
101+
parent_child = DictField("parent_child")
102+
"""OpenSearch join relationship for parent-child queries."""
103+
104+
def pre_commit(self):
105+
"""Hook called before committing the record.
106+
107+
Validates that children are allowed for this event type.
108+
"""
109+
from .validators import validate_children_allowed
110+
111+
validate_children_allowed(self)
112+
super().pre_commit()
113+
86114

87115
class Request(Record):
88116
"""A generic request record."""

invenio_requests/records/dumpers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
from .calculated import CalculatedFieldDumperExt
1111
from .granttokens import GrantTokensDumperExt
12+
from .parentchild import ParentChildDumperExt
1213

1314
__all__ = (
1415
"CalculatedFieldDumperExt",
1516
"GrantTokensDumperExt",
17+
"ParentChildDumperExt",
1618
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2025 CERN.
4+
#
5+
# Invenio-Requests is free software; you can redistribute it and/or modify
6+
# it under the terms of the MIT License; see LICENSE file for more details.
7+
8+
"""Search dumper for OpenSearch join relationships."""
9+
10+
from invenio_records.dumpers import SearchDumperExt
11+
12+
13+
class ParentChildDumperExt(SearchDumperExt):
14+
"""Search dumper extension for OpenSearch join relationships.
15+
16+
This dumper sets the join relationship field for parent-child documents:
17+
- Parent events: {"name": "parent"}
18+
- Child events (replies): {"name": "child", "parent": parent_id}
19+
20+
It also handles routing to ensure child documents are indexed on the
21+
same shard as their parent (required for join queries to work).
22+
"""
23+
24+
def dump(self, record, data):
25+
"""Dump the join relationship data.
26+
27+
Sets the parent_child field based on whether the record
28+
is a parent event or a child (reply).
29+
"""
30+
if record.parent_id:
31+
# This is a child event (reply)
32+
data["parent_child"] = {"name": "child", "parent": str(record.parent_id)}
33+
else:
34+
# This is a parent event
35+
data["parent_child"] = {"name": "parent"}
36+
37+
def load(self, data, record_cls):
38+
"""Load the data.
39+
40+
The join relationship is only used in the search index,
41+
not in the record data, so we remove it when loading.
42+
"""
43+
data.pop("parent_child", None)

invenio_requests/records/jsonschemas/requestevents/requestevent-v1.0.0.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
},
2222
"type": {
2323
"type": "string"
24+
},
25+
"parent_id": {
26+
"$ref": "local://definitions-v1.0.0.json#/identifier"
2427
}
2528
}
2629
}

invenio_requests/records/mappings/os-v1/requestevents/requestevent-v1.0.0.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@
4747
"payload": {
4848
"type": "object",
4949
"enabled": false
50+
},
51+
"parent_id": {
52+
"type": "keyword"
53+
},
54+
"parent_child": {
55+
"type": "join",
56+
"relations": {
57+
"parent": "child"
58+
}
5059
}
5160
}
5261
}

invenio_requests/records/mappings/os-v2/requestevents/requestevent-v1.0.0.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@
4747
"payload": {
4848
"type": "object",
4949
"enabled": false
50+
},
51+
"parent_id": {
52+
"type": "keyword"
53+
},
54+
"parent_child": {
55+
"type": "join",
56+
"relations": {
57+
"parent": "child"
58+
}
5059
}
5160
}
5261
}

0 commit comments

Comments
 (0)