Skip to content

Commit 84e6450

Browse files
committed
global: add Request.last_reply systemfield
* Adds a computed and search-indexed field for getting the last reply (i.e. the last comment event) for a request.
1 parent 90249b0 commit 84e6450

File tree

14 files changed

+488
-97
lines changed

14 files changed

+488
-97
lines changed

invenio_requests/records/api.py

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
EventTypeField,
2525
ExpiredStateCalculatedField,
2626
IdentityField,
27+
LastReply,
2728
RequestStateCalculatedField,
2829
RequestStatusField,
2930
RequestTypeField,
@@ -38,9 +39,56 @@
3839
)
3940

4041

42+
class RequestEventFormat(Enum):
43+
"""Comment/content format enum."""
44+
45+
HTML = "html"
46+
47+
48+
class RequestEvent(Record):
49+
"""A Request Event."""
50+
51+
model_cls = RequestEventModel
52+
53+
# Systemfields
54+
metadata = None
55+
56+
schema = ConstantField("$schema", "local://requestevents/requestevent-v1.0.0.json")
57+
"""The JSON Schema to use for validation."""
58+
59+
request = ModelField(dump=False)
60+
"""The request."""
61+
62+
request_id = DictField("request_id")
63+
"""The data-layer id of the related Request."""
64+
65+
type = EventTypeField("type")
66+
"""Request event type system field."""
67+
68+
index = IndexField(
69+
"requestevents-requestevent-v1.0.0", search_alias="requestevents"
70+
)
71+
"""The ES index used."""
72+
73+
id = ModelField("id")
74+
"""The data-layer id."""
75+
76+
check_referenced = partial(
77+
check_allowed_references,
78+
lambda r: True, # for system process for now
79+
lambda r: ["user", "email"], # only users for now
80+
)
81+
82+
created_by = EntityReferenceField("created_by", check_referenced)
83+
"""Who created the event."""
84+
85+
4186
class Request(Record):
4287
"""A generic request record."""
4388

89+
event_cls = RequestEvent
90+
"""The event class used for request events."""
91+
4492
model_cls = RequestMetadata
4593
"""The model class for the request."""
4694

@@ -101,46 +149,5 @@ class Request(Record):
101149
is_expired = ExpiredStateCalculatedField("expires_at")
102150
"""Whether or not the request is already expired."""
103151

104-
105-
class RequestEventFormat(Enum):
106-
"""Comment/content format enum."""
107-
108-
HTML = "html"
109-
110-
111-
class RequestEvent(Record):
112-
"""A Request Event."""
113-
114-
model_cls = RequestEventModel
115-
116-
# Systemfields
117-
metadata = None
118-
119-
schema = ConstantField("$schema", "local://requestevents/requestevent-v1.0.0.json")
120-
"""The JSON Schema to use for validation."""
121-
122-
request = ModelField(dump=False)
123-
"""The request."""
124-
125-
request_id = DictField("request_id")
126-
"""The data-layer id of the related Request."""
127-
128-
type = EventTypeField("type")
129-
"""Request event type system field."""
130-
131-
index = IndexField(
132-
"requestevents-requestevent-v1.0.0", search_alias="requestevents"
133-
)
134-
"""The ES index used."""
135-
136-
id = ModelField("id")
137-
"""The data-layer id."""
138-
139-
check_referenced = partial(
140-
check_allowed_references,
141-
lambda r: True, # for system process for now
142-
lambda r: ["user", "email"], # only users for now
143-
)
144-
145-
created_by = EntityReferenceField("created_by", check_referenced)
146-
"""Who created the event."""
152+
last_reply = LastReply()
153+
"""The complete last reply event in the request."""

invenio_requests/records/dumpers/calculated.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
"""Search dumpers for the is_expired state of requests."""
99

10-
1110
from invenio_records.dumpers import SearchDumperExt
1211

1312

invenio_requests/records/mappings/os-v1/requests/request-v1.0.0.json

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"dynamic_templates": [
55
{
66
"creators": {
7-
"path_match": "created_by.*",
7+
"path_match": "*.created_by.*",
88
"mapping": {
99
"type": "keyword"
1010
}
@@ -33,8 +33,7 @@
3333
"type": "keyword"
3434
}
3535
}
36-
}
37-
],
36+
} ],
3837
"properties": {
3938
"$schema": {
4039
"type": "keyword",
@@ -105,6 +104,43 @@
105104
"reviewers": {
106105
"type": "object",
107106
"dynamic": "true"
107+
},
108+
"last_reply": {
109+
"type": "object",
110+
"properties": {
111+
"$schema": {
112+
"type": "keyword"
113+
},
114+
"created": {
115+
"type": "date"
116+
},
117+
"updated": {
118+
"type": "date"
119+
},
120+
"uuid": {
121+
"type": "keyword"
122+
},
123+
"version_id": {
124+
"type": "keyword"
125+
},
126+
"created_by": {
127+
"type": "object",
128+
"dynamic": "true"
129+
},
130+
"id": {
131+
"type": "keyword"
132+
},
133+
"type": {
134+
"type": "keyword"
135+
},
136+
"request_id": {
137+
"type": "keyword"
138+
},
139+
"payload": {
140+
"type": "object",
141+
"enabled": false
142+
}
143+
}
108144
}
109145
}
110146
}

invenio_requests/records/mappings/os-v2/requests/request-v1.0.0.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"dynamic_templates": [
55
{
66
"creators": {
7-
"path_match": "created_by.*",
7+
"path_match": "*.created_by.*",
88
"mapping": {
99
"type": "keyword"
1010
}
@@ -105,6 +105,43 @@
105105
"reviewers": {
106106
"type": "object",
107107
"dynamic": "true"
108+
},
109+
"last_reply": {
110+
"type": "object",
111+
"properties": {
112+
"$schema": {
113+
"type": "keyword"
114+
},
115+
"created": {
116+
"type": "date"
117+
},
118+
"updated": {
119+
"type": "date"
120+
},
121+
"uuid": {
122+
"type": "keyword"
123+
},
124+
"version_id": {
125+
"type": "keyword"
126+
},
127+
"created_by": {
128+
"type": "object",
129+
"dynamic": "true"
130+
},
131+
"id": {
132+
"type": "keyword"
133+
},
134+
"type": {
135+
"type": "keyword"
136+
},
137+
"request_id": {
138+
"type": "keyword"
139+
},
140+
"payload": {
141+
"type": "object",
142+
"enabled": false
143+
}
144+
}
108145
}
109146
}
110147
}

invenio_requests/records/mappings/v7/requests/request-v1.0.0.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"dynamic_templates": [
55
{
66
"creators": {
7-
"path_match": "created_by.*",
7+
"path_match": "*.created_by.*",
88
"mapping": {
99
"type": "keyword"
1010
}
@@ -105,6 +105,43 @@
105105
"reviewers": {
106106
"type": "object",
107107
"dynamic": "true"
108+
},
109+
"last_reply": {
110+
"type": "object",
111+
"properties": {
112+
"$schema": {
113+
"type": "keyword"
114+
},
115+
"created": {
116+
"type": "date"
117+
},
118+
"updated": {
119+
"type": "date"
120+
},
121+
"uuid": {
122+
"type": "keyword"
123+
},
124+
"version_id": {
125+
"type": "keyword"
126+
},
127+
"created_by": {
128+
"type": "object",
129+
"dynamic": "true"
130+
},
131+
"id": {
132+
"type": "keyword"
133+
},
134+
"type": {
135+
"type": "keyword"
136+
},
137+
"request_id": {
138+
"type": "keyword"
139+
},
140+
"payload": {
141+
"type": "object",
142+
"enabled": false
143+
}
144+
}
108145
}
109146
}
110147
}

invenio_requests/records/systemfields/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
"""Systemfields for request records."""
99

10+
from .computed import LastReply
1011
from .entity_reference import EntityReferenceField
1112
from .event_type import EventTypeField
1213
from .expired_state import ExpiredStateCalculatedField
@@ -20,6 +21,7 @@
2021
"EventTypeField",
2122
"ExpiredStateCalculatedField",
2223
"IdentityField",
24+
"LastReply",
2325
"RequestStateCalculatedField",
2426
"RequestStatusField",
2527
"RequestTypeField",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
"""Computed system fields for requests."""
9+
10+
from invenio_db import db
11+
from invenio_records_resources.records.systemfields.calculated import (
12+
CalculatedField,
13+
)
14+
15+
from ...customizations import CommentEventType
16+
17+
18+
class LastReply(CalculatedField):
19+
"""System field for getting the last reply event."""
20+
21+
def __init__(self, key=None, use_cache=True):
22+
"""Constructor."""
23+
super().__init__(key=key, use_cache=use_cache)
24+
25+
def calculate(self, record):
26+
"""Fetch the last reply event."""
27+
# TODO: This logic should be pushed up to the `CalculatedField` class. If we the
28+
# cache has a key for the system field, even if the value is `None`, it means
29+
# that the value was already calculated or explicitly cached, e.g. in
30+
# `post_load`.
31+
obj_cache = getattr(record, "_obj_cache", None)
32+
if obj_cache is not None and self.attr_name in obj_cache:
33+
return obj_cache[self.attr_name]
34+
35+
RequestEvent = record.event_cls
36+
RequestEventModel = RequestEvent.model_cls
37+
38+
last_comment = (
39+
db.session.query(RequestEventModel)
40+
.filter(
41+
RequestEventModel.request_id == record.id,
42+
RequestEventModel.type == CommentEventType.type_id,
43+
)
44+
.order_by(RequestEventModel.created.desc())
45+
.first()
46+
)
47+
48+
if last_comment:
49+
return RequestEvent(data=last_comment.data, model=last_comment)
50+
51+
return None
52+
53+
def pre_dump(self, record, data, dumper=None):
54+
"""Called after a record is dumped."""
55+
last_reply = getattr(record, self.attr_name)
56+
if last_reply:
57+
data[self.attr_name] = last_reply.dumps()
58+
else:
59+
data[self.attr_name] = None
60+
61+
def post_load(self, record, data, loader=None):
62+
"""Called after a record was loaded."""
63+
RequestEvent = record.event_cls
64+
65+
record.pop(self.attr_name, None) # Remove the attribute from the record
66+
last_reply_dump = data.pop(self.attr_name, None)
67+
last_reply = None
68+
if last_reply_dump:
69+
last_reply = RequestEvent.loads(last_reply_dump)
70+
self._set_cache(record, last_reply)

0 commit comments

Comments
 (0)