Skip to content

Commit 2ea8b4d

Browse files
committed
Switch reads of AnnotationModeration to Annotation.moderation_status
After: 1/ Adding the new column 2/ Concurrently writing both the old table and the new column 3/ Backfilling the new column based on AnnotationModeration we are ready ot switch the reads over the new column. This commit doesn't introduce new functionality or any behavior changes.
1 parent 0391d9d commit 2ea8b4d

16 files changed

+57
-82
lines changed

h/models/__init__.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from h.models.activation import Activation
2121
from h.models.annotation import Annotation
2222
from h.models.annotation_metadata import AnnotationMetadata
23-
from h.models.annotation_moderation import AnnotationModeration
2423
from h.models.annotation_slim import AnnotationSlim
2524
from h.models.auth_client import AuthClient
2625
from h.models.auth_ticket import AuthTicket
@@ -33,6 +32,7 @@
3332
from h.models.group import Group, GroupMembership, GroupMembershipRoles
3433
from h.models.group_scope import GroupScope
3534
from h.models.job import Job
35+
from h.models.legacy_annotation_moderation import _LegacyAnnotationModeration
3636
from h.models.mention import Mention
3737
from h.models.moderation_log import ModerationLog
3838
from h.models.notification import Notification
@@ -49,7 +49,6 @@
4949
__all__ = (
5050
"Activation",
5151
"Annotation",
52-
"AnnotationModeration",
5352
"AnnotationSlim",
5453
"AuthClient",
5554
"AuthTicket",

h/models/annotation.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,12 @@ def authority(self):
280280
return split_user(self.userid)["domain"]
281281

282282
@property
283-
def is_hidden(self):
283+
def is_hidden(self) -> bool:
284284
"""Check if this annotation id is hidden."""
285-
286-
return self.moderation is not None
285+
return bool(
286+
self.moderation_status
287+
and self.moderation_status != ModerationStatus.APPROVED
288+
)
287289

288290
def __repr__(self):
289291
return f"<Annotation {self.id}>"

h/models/annotation_moderation.py renamed to h/models/legacy_annotation_moderation.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
from h.db.mixins import Timestamps
55

66

7-
class AnnotationModeration(Base, Timestamps):
7+
class _LegacyAnnotationModeration(Base, Timestamps):
88
"""
99
A flag for a moderated and hidden annotation.
1010
1111
This means that the annotation is violating the community guidelines and
1212
should be hidden from other users.
13+
14+
This model/table is deprecated and replaced by Annoation.moderation_status column
15+
and the ModerationLog table.
1316
"""
1417

1518
__tablename__ = "annotation_moderation"
@@ -27,12 +30,12 @@ class AnnotationModeration(Base, Timestamps):
2730
annotation = sa.orm.relationship(
2831
"Annotation",
2932
backref=sa.orm.backref(
30-
"moderation",
33+
"_moderation",
3134
uselist=False,
3235
cascade="all, delete-orphan",
3336
passive_deletes=True,
3437
),
3538
)
3639

3740
def __repr__(self):
38-
return f"<AnnotationModeration annotation_id={self.annotation_id}>"
41+
return f"<_LegacyAnnotationModeration annotation_id={self.annotation_id}>"

h/search/index.py

-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ def _eager_loaded_annotations(session):
149149
models.Document.document_uris
150150
),
151151
subqueryload(models.Annotation.document).subqueryload(models.Document.meta),
152-
subqueryload(models.Annotation.moderation),
153152
subqueryload(models.Annotation.thread).load_only(models.Annotation.id),
154153
)
155154

h/services/annotation_json.py

-2
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,6 @@ def present_all_for_user(self, annotation_ids, user: User):
168168
eager_load=[
169169
# Optimise access to the document
170170
Annotation.document,
171-
# Optimise the check used for "hidden" above
172-
Annotation.moderation,
173171
# Optimise the permissions check for MODERATE permissions,
174172
# which ultimately depends on group permissions, causing a
175173
# group lookup for every annotation without this

h/services/annotation_moderation.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from h.models import Annotation, AnnotationModeration, User
1+
from h.models import Annotation, User
22
from h.models.annotation import ModerationStatus
33
from h.models.moderation_log import ModerationLog
44

@@ -7,7 +7,7 @@ class AnnotationModerationService:
77
def __init__(self, session):
88
self._session = session
99

10-
def all_hidden(self, annotation_ids):
10+
def all_hidden(self, annotation_ids: str) -> set[str]:
1111
"""
1212
Check which of the given annotation ids is hidden.
1313
@@ -17,10 +17,10 @@ def all_hidden(self, annotation_ids):
1717
if not annotation_ids:
1818
return set()
1919

20-
query = self._session.query(AnnotationModeration.annotation_id).filter(
21-
AnnotationModeration.annotation_id.in_(annotation_ids)
20+
query = self._session.query(Annotation).filter(
21+
Annotation.id.in_(annotation_ids)
2222
)
23-
return {m.annotation_id for m in query}
23+
return {a.id for a in query if a.is_hidden}
2424

2525
def set_status(
2626
self, annotation: Annotation, user: User, status: ModerationStatus | None

h/services/annotation_write.py

+5-16
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from collections.abc import Callable
22
from datetime import datetime
33

4-
from sqlalchemy import exists, select
4+
from sqlalchemy import select
55
from sqlalchemy.dialects.postgresql import insert
66
from sqlalchemy.orm import Session
77

88
from h import i18n
9-
from h.models import Annotation, AnnotationModeration, AnnotationSlim, User
9+
from h.models import Annotation, AnnotationSlim, User
1010
from h.models.document import update_document_metadata
1111
from h.schemas import ValidationError
1212
from h.security import Permission
@@ -168,7 +168,6 @@ def update_annotation(
168168
def hide(self, annotation: Annotation, user: User):
169169
"""Hides an annotation marking it it as "moderated"."""
170170
if not annotation.is_hidden:
171-
annotation.moderation = AnnotationModeration()
172171
self._moderation_service.set_status(
173172
annotation, user, Annotation.ModerationStatus.DENIED
174173
)
@@ -177,11 +176,10 @@ def hide(self, annotation: Annotation, user: User):
177176

178177
def unhide(self, annotation: Annotation, user: User):
179178
"""Remove the moderation status of an annotation."""
180-
annotation.moderation = None
181-
self.upsert_annotation_slim(annotation)
182179
self._moderation_service.set_status(
183180
annotation, user, Annotation.ModerationStatus.APPROVED
184181
)
182+
self.upsert_annotation_slim(annotation)
185183

186184
@staticmethod
187185
def change_document(db, old_document_ids, new_document):
@@ -235,7 +233,7 @@ def _validate_group(self, annotation: Annotation, enforce_write_permission=True)
235233
+ _("Annotations for this target URI are not allowed in this group")
236234
)
237235

238-
def upsert_annotation_slim(self, annotation):
236+
def upsert_annotation_slim(self, annotation: Annotation) -> None:
239237
self._db.flush() # See the last model changes in the transaction
240238

241239
user_id = self._db.scalar(
@@ -247,15 +245,6 @@ def upsert_annotation_slim(self, annotation):
247245
# The AnnotationSlim records will get deleted by a cascade, no need to do anything here.
248246
return
249247

250-
moderated = self._db.scalar(
251-
select(
252-
exists(
253-
select(AnnotationModeration.id).where(
254-
AnnotationModeration.annotation_id == annotation.id
255-
)
256-
)
257-
)
258-
)
259248
stmt = insert(AnnotationSlim).values(
260249
[
261250
{
@@ -270,7 +259,7 @@ def upsert_annotation_slim(self, annotation):
270259
# Fields of AnnotationSlim
271260
"group_id": annotation.group.id,
272261
"user_id": user_id,
273-
"moderated": moderated,
262+
"moderated": annotation.is_hidden,
274263
}
275264
]
276265
)

tests/common/factories/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from tests.common.factories.activation import Activation
44
from tests.common.factories.annotation import Annotation
55
from tests.common.factories.annotation_metadata import AnnotationMetadata
6-
from tests.common.factories.annotation_moderation import AnnotationModeration
76
from tests.common.factories.annotation_slim import AnnotationSlim
87
from tests.common.factories.auth_client import AuthClient, ConfidentialAuthClient
98
from tests.common.factories.auth_ticket import AuthTicket

tests/common/factories/annotation_moderation.py

-14
This file was deleted.

tests/unit/h/models/annotation_moderation_test.py

-8
This file was deleted.

tests/unit/h/models/annotation_test.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
from h.db.types import URLSafeUUID
6-
from h.models.annotation import Annotation
6+
from h.models.annotation import Annotation, ModerationStatus
77

88

99
def test_parent_id_of_direct_reply():
@@ -234,13 +234,20 @@ def subreply(self, factories, root, reply):
234234
return factories.Annotation(references=[root.id, reply.id])
235235

236236

237-
@pytest.mark.parametrize("has_moderation", (True, False))
238-
def test_is_hidden(factories, has_moderation):
237+
@pytest.mark.parametrize(
238+
"moderation_status,hidden",
239+
[
240+
(None, False),
241+
(ModerationStatus.APPROVED, False),
242+
(ModerationStatus.DENIED, True),
243+
],
244+
)
245+
def test_is_hidden(factories, moderation_status, hidden):
239246
annotation = factories.Annotation(
240-
moderation=factories.AnnotationModeration() if has_moderation else None
247+
moderation_status=moderation_status,
241248
)
242249

243-
assert annotation.is_hidden == has_moderation
250+
assert annotation.is_hidden == hidden
244251

245252

246253
def test_uuid(factories):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from h.models.legacy_annotation_moderation import _LegacyAnnotationModeration
2+
3+
4+
class TestAnnotationModeration:
5+
def test___repr__(self):
6+
moderation = _LegacyAnnotationModeration(annotation_id=123)
7+
8+
assert repr(moderation) == "<_LegacyAnnotationModeration annotation_id=123>"

tests/unit/h/services/annotation_json_test.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ def test_present_all_for_user(
204204
ids=sentinel.annotation_ids,
205205
eager_load=[
206206
Annotation.document,
207-
Annotation.moderation,
208207
Annotation.group,
209208
Annotation.mentions,
210209
],
@@ -244,15 +243,14 @@ def user(self, factories):
244243
@pytest.fixture
245244
def annotation(self, factories):
246245
return factories.Annotation(
247-
moderation=None,
248246
groupid="NOT WORLD",
249247
tags=["some-tags"],
250248
slim=factories.AnnotationSlim(),
251249
)
252250

253251
@pytest.fixture
254-
def with_hidden_annotation(self, annotation, factories):
255-
annotation.moderation = factories.AnnotationModeration()
252+
def with_hidden_annotation(self, annotation):
253+
annotation.moderation_status = Annotation.ModerationStatus.DENIED
256254

257255
@pytest.fixture(autouse=True)
258256
def Identity(self, patch):

tests/unit/h/services/annotation_moderation_test.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99

1010
class TestAnnotationModerationService:
11-
def test_all_hidden_lists_moderated_annotation_ids(self, svc, mods):
12-
ids = [m.annotation.id for m in mods[0:-1]]
11+
def test_all_hidden_lists_moderated_annotation_ids(
12+
self, svc, moderated_annotations
13+
):
14+
ids = [a.id for a in moderated_annotations[0:-1]]
1315
assert svc.all_hidden(ids) == set(ids)
1416

1517
def test_all_hidden_skips_non_moderated_annotations(self, svc, factories):
@@ -68,8 +70,10 @@ def test_update_status_initializes_moderation_status(
6870
assert annotation.moderation_status == moderation_status
6971

7072
@pytest.fixture(autouse=True)
71-
def mods(self, factories):
72-
return factories.AnnotationModeration.create_batch(3)
73+
def moderated_annotations(self, factories):
74+
return factories.Annotation.create_batch(
75+
3, moderation_status=ModerationStatus.DENIED
76+
)
7377

7478
@pytest.fixture
7579
def annotation(self, factories):

tests/unit/h/services/annotation_read_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def test_get_annotations_by_id(self, svc, factories, reverse):
2929
def test_get_annotations_by_id_with_no_input(self, svc):
3030
assert not svc.get_annotations_by_id(ids=[])
3131

32-
@pytest.mark.parametrize("attribute", ("document", "moderation", "group"))
32+
@pytest.mark.parametrize("attribute", ("document", "group"))
3333
def test_get_annotations_by_id_preloading(
3434
self, svc, factories, db_session, query_counter, attribute
3535
):

tests/unit/h/services/annotation_write_test.py

+4-13
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55
from h_matchers import Any
66

7-
from h.models import Annotation, AnnotationModeration, AnnotationSlim, User
7+
from h.models import Annotation, AnnotationSlim, User
88
from h.schemas import ValidationError
99
from h.security import Permission
1010
from h.services.annotation_write import AnnotationWriteService, service_factory
@@ -100,6 +100,7 @@ def test_update_annotation(
100100
update_document_metadata,
101101
queue_service,
102102
_validate_group, # noqa: PT019
103+
moderation_service,
103104
):
104105
then = datetime.now() - timedelta(days=1) # noqa: DTZ005
105106
annotation.extra = {"key": "value"}
@@ -130,6 +131,7 @@ def test_update_annotation(
130131
{"uri": 1},
131132
updated=anno.updated,
132133
)
134+
moderation_service.update_status.assert_called_once_with(anno)
133135

134136
queue_service.add_by_id.assert_called_once_with(
135137
"sync_annotation",
@@ -219,35 +221,24 @@ def test__validate_group_with_url_not_in_scopes(
219221
svc._validate_group(annotation) # noqa: SLF001
220222

221223
def test_hide_hides_the_annotation(self, annotation, svc, user, moderation_service):
222-
annotation.moderation = None
223-
224224
svc.hide(annotation, user)
225225

226-
assert annotation.is_hidden
227226
moderation_service.set_status.assert_called_once_with(
228227
annotation, user, Annotation.ModerationStatus.DENIED
229228
)
230229

231230
def test_hide_does_not_modify_an_already_hidden_annotation(
232231
self, annotation, svc, user, moderation_service
233232
):
234-
moderation = AnnotationModeration()
235-
annotation.moderation = moderation
233+
annotation.moderation_status = Annotation.ModerationStatus.DENIED
236234

237235
svc.hide(annotation, user)
238236

239-
assert annotation.is_hidden
240-
# It's the same one not a new one
241-
assert annotation.moderation == moderation
242237
moderation_service.set_status.assert_not_called()
243238

244239
def test_unhide(self, annotation, svc, user, moderation_service):
245-
moderation = AnnotationModeration()
246-
annotation.moderation = moderation
247-
248240
svc.unhide(annotation, user)
249241

250-
assert not annotation.is_hidden
251242
moderation_service.set_status.assert_called_once_with(
252243
annotation, user, Annotation.ModerationStatus.APPROVED
253244
)

0 commit comments

Comments
 (0)