From cc776b1d342e22b232c0dda0f36cccfda25e0785 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Fri, 24 Jan 2025 16:55:40 +0100
Subject: [PATCH 01/22] Add MentionService
---
h/services/__init__.py | 3 +++
h/services/annotation_write.py | 6 +++++
h/services/mention.py | 40 ++++++++++++++++++++++++++++++++++
3 files changed, 49 insertions(+)
create mode 100644 h/services/mention.py
diff --git a/h/services/__init__.py b/h/services/__init__.py
index 3ed27124858..451e0b912ed 100644
--- a/h/services/__init__.py
+++ b/h/services/__init__.py
@@ -42,6 +42,9 @@ def includeme(config): # pragma: no cover
config.register_service_factory(
"h.services.annotation_write.service_factory", iface=AnnotationWriteService
)
+ config.register_service_factory(
+ "h.services.mention.service_factory", name="mention"
+ )
# Other services
config.register_service_factory(
diff --git a/h/services/annotation_write.py b/h/services/annotation_write.py
index 10d5497ecb9..868e4d65f1f 100644
--- a/h/services/annotation_write.py
+++ b/h/services/annotation_write.py
@@ -13,6 +13,7 @@
from h.services.annotation_metadata import AnnotationMetadataService
from h.services.annotation_read import AnnotationReadService
from h.services.job_queue import JobQueueService
+from h.services.mention import MentionService
from h.traversal.group import GroupContext
from h.util.group_scope import url_in_scope
@@ -29,12 +30,14 @@ def __init__(
queue_service: JobQueueService,
annotation_read_service: AnnotationReadService,
annotation_metadata_service: AnnotationMetadataService,
+ mention_service: MentionService,
):
self._db = db_session
self._has_permission = has_permission
self._queue_service = queue_service
self._annotation_read_service = annotation_read_service
self._annotation_metadata_service = annotation_metadata_service
+ self._mention_service = mention_service
def create_annotation(self, data: dict) -> Annotation:
"""
@@ -88,6 +91,8 @@ def create_annotation(self, data: dict) -> Annotation:
schedule_in=60,
)
+ self._mention_service.update_mentions(annotation)
+
return annotation
def update_annotation(
@@ -281,4 +286,5 @@ def service_factory(_context, request) -> AnnotationWriteService:
queue_service=request.find_service(name="queue_service"),
annotation_read_service=request.find_service(AnnotationReadService),
annotation_metadata_service=request.find_service(AnnotationMetadataService),
+ mention_service=request.find_service(MentionService),
)
diff --git a/h/services/mention.py b/h/services/mention.py
new file mode 100644
index 00000000000..bc0050c4787
--- /dev/null
+++ b/h/services/mention.py
@@ -0,0 +1,40 @@
+import re
+
+from sqlalchemy.orm import Session
+import sqlalchemy as sa
+
+from h.models import Annotation, Mention
+from h.services.user import UserService
+
+USERID_PAT = re.compile(r"data-userid=\"([^\"]+)\"")
+
+
+class MentionService:
+ """A service for managing user mentions."""
+
+ def __init__(self, session: Session, user_service: UserService):
+ self._session = session
+ self._user_service = user_service
+
+ def update_mentions(self, annotation: Annotation) -> None:
+ self._session.flush()
+
+ userids = set(self._parse_userids(annotation.text))
+ users = self._user_service.fetch_all(userids)
+ self._session.execute(
+ sa.delete(Mention).where(Mention.annotation_id == annotation.id)
+ )
+ for user in users:
+ mention = Mention(annotation_id=annotation.id, user_id=user.id)
+ self._session.add(mention)
+
+ @staticmethod
+ def _parse_userids(text: str) -> list[str]:
+ return USERID_PAT.findall(text)
+
+
+def service_factory(_context, request) -> MentionService:
+ """Return a MentionService instance for the passed context and request."""
+ return MentionService(
+ session=request.db, user_service=request.find_service(name="user")
+ )
From 461ea5ffccf76c3cc8a097340907883e1da78133 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Fri, 24 Jan 2025 18:41:08 +0100
Subject: [PATCH 02/22] Fix MentionService
---
h/services/__init__.py | 3 ++-
h/services/annotation_write.py | 2 ++
h/services/mention.py | 8 +++++---
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/h/services/__init__.py b/h/services/__init__.py
index 451e0b912ed..128f52e9458 100644
--- a/h/services/__init__.py
+++ b/h/services/__init__.py
@@ -12,6 +12,7 @@
)
from h.services.job_queue import JobQueueService
from h.services.subscription import SubscriptionService
+from h.services.mention import MentionService
def includeme(config): # pragma: no cover
@@ -43,7 +44,7 @@ def includeme(config): # pragma: no cover
"h.services.annotation_write.service_factory", iface=AnnotationWriteService
)
config.register_service_factory(
- "h.services.mention.service_factory", name="mention"
+ "h.services.mention.service_factory", iface=MentionService
)
# Other services
diff --git a/h/services/annotation_write.py b/h/services/annotation_write.py
index 868e4d65f1f..4e4e9fc13e1 100644
--- a/h/services/annotation_write.py
+++ b/h/services/annotation_write.py
@@ -156,6 +156,8 @@ def update_annotation(
force=not update_timestamp,
)
+ self._mention_service.update_mentions(annotation)
+
return annotation
def hide(self, annotation):
diff --git a/h/services/mention.py b/h/services/mention.py
index bc0050c4787..20870fa5576 100644
--- a/h/services/mention.py
+++ b/h/services/mention.py
@@ -2,12 +2,14 @@
from sqlalchemy.orm import Session
import sqlalchemy as sa
-
+import logging
from h.models import Annotation, Mention
from h.services.user import UserService
USERID_PAT = re.compile(r"data-userid=\"([^\"]+)\"")
+logger = logging.getLogger(__name__)
+
class MentionService:
"""A service for managing user mentions."""
@@ -22,10 +24,10 @@ def update_mentions(self, annotation: Annotation) -> None:
userids = set(self._parse_userids(annotation.text))
users = self._user_service.fetch_all(userids)
self._session.execute(
- sa.delete(Mention).where(Mention.annotation_id == annotation.id)
+ sa.delete(Mention).where(Mention.annotation_id == annotation.slim.id)
)
for user in users:
- mention = Mention(annotation_id=annotation.id, user_id=user.id)
+ mention = Mention(annotation_id=annotation.slim.id, user_id=user.id)
self._session.add(mention)
@staticmethod
From 0ce87038de58338530435dfad10b43c92d757219 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Fri, 24 Jan 2025 18:49:23 +0100
Subject: [PATCH 03/22] Add username to mention
---
.../bfc54f0844aa_add_username_to_mention.py | 23 +++++++++++++++++++
h/models/mention.py | 3 +++
h/services/__init__.py | 2 +-
h/services/mention.py | 11 ++++++---
4 files changed, 35 insertions(+), 4 deletions(-)
create mode 100644 h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
diff --git a/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py b/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
new file mode 100644
index 00000000000..66cf71d3c08
--- /dev/null
+++ b/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
@@ -0,0 +1,23 @@
+"""Add username column to mention table.
+
+Revision ID: bfc54f0844aa
+Revises: 39cc1025a3a2
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+revision = "bfc54f0844aa"
+down_revision = "39cc1025a3a2"
+
+
+def upgrade() -> None:
+ op.add_column("mention", sa.Column("username", sa.UnicodeText(), nullable=False))
+
+
+def downgrade() -> None:
+ op.drop_constraint(
+ op.f("uq__annotation_metadata__annotation_id"),
+ "annotation_metadata",
+ type_="unique",
+ )
diff --git a/h/models/mention.py b/h/models/mention.py
index 04afb6bfab6..27c0d3fce86 100644
--- a/h/models/mention.py
+++ b/h/models/mention.py
@@ -28,5 +28,8 @@ class Mention(Base, Timestamps): # pragma: nocover
"""FK to user.id"""
user = sa.orm.relationship("User")
+ username = sa.Column("username", sa.UnicodeText(), nullable=False)
+ """Current username of the user mentioned"""
+
def __repr__(self) -> str:
return helpers.repr_(self, ["id", "annotation_id", "user_id"])
diff --git a/h/services/__init__.py b/h/services/__init__.py
index 128f52e9458..2ed2efdf15d 100644
--- a/h/services/__init__.py
+++ b/h/services/__init__.py
@@ -11,8 +11,8 @@
BulkLMSStatsService,
)
from h.services.job_queue import JobQueueService
-from h.services.subscription import SubscriptionService
from h.services.mention import MentionService
+from h.services.subscription import SubscriptionService
def includeme(config): # pragma: no cover
diff --git a/h/services/mention.py b/h/services/mention.py
index 20870fa5576..38bc8d7b617 100644
--- a/h/services/mention.py
+++ b/h/services/mention.py
@@ -1,8 +1,9 @@
+import logging
import re
-from sqlalchemy.orm import Session
import sqlalchemy as sa
-import logging
+from sqlalchemy.orm import Session
+
from h.models import Annotation, Mention
from h.services.user import UserService
@@ -27,7 +28,11 @@ def update_mentions(self, annotation: Annotation) -> None:
sa.delete(Mention).where(Mention.annotation_id == annotation.slim.id)
)
for user in users:
- mention = Mention(annotation_id=annotation.slim.id, user_id=user.id)
+ mention = Mention(
+ annotation_id=annotation.slim.id,
+ user_id=user.id,
+ username=user.username,
+ )
self._session.add(mention)
@staticmethod
From 45be341e8c0bfcc8a26064b0c69d00bec6e308e2 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Mon, 27 Jan 2025 09:37:38 +0100
Subject: [PATCH 04/22] Optimize MentionService
---
h/models/annotation_slim.py | 2 ++
h/models/mention.py | 2 +-
h/services/annotation_json.py | 9 +++++++++
h/services/mention.py | 7 ++++++-
4 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/h/models/annotation_slim.py b/h/models/annotation_slim.py
index 6b5954dc5d7..a2b9226d8cd 100644
--- a/h/models/annotation_slim.py
+++ b/h/models/annotation_slim.py
@@ -96,5 +96,7 @@ class AnnotationSlim(Base):
)
group = sa.orm.relationship("Group")
+ mentions = sa.orm.relationship("Mention", back_populates="annotation")
+
def __repr__(self):
return helpers.repr_(self, ["id"])
diff --git a/h/models/mention.py b/h/models/mention.py
index 27c0d3fce86..63f4290854d 100644
--- a/h/models/mention.py
+++ b/h/models/mention.py
@@ -17,7 +17,7 @@ class Mention(Base, Timestamps): # pragma: nocover
nullable=False,
)
"""FK to annotation_slim.id"""
- annotation = sa.orm.relationship("AnnotationSlim")
+ annotation = sa.orm.relationship("AnnotationSlim", back_populates="mentions")
user_id: Mapped[int] = mapped_column(
sa.Integer,
diff --git a/h/services/annotation_json.py b/h/services/annotation_json.py
index a88f7368690..f76cf03f1f1 100644
--- a/h/services/annotation_json.py
+++ b/h/services/annotation_json.py
@@ -4,6 +4,7 @@
from h.presenters import DocumentJSONPresenter
from h.security import Identity, identity_permits
from h.security.permissions import Permission
+from h.services import MentionService
from h.services.annotation_read import AnnotationReadService
from h.services.flag import FlagService
from h.services.links import LinksService
@@ -22,6 +23,7 @@ def __init__(
links_service: LinksService,
flag_service: FlagService,
user_service: UserService,
+ mention_service: MentionService,
):
"""
Instantiate the service.
@@ -30,11 +32,13 @@ def __init__(
:param links_service: LinksService instance
:param flag_service: FlagService instance
:param user_service: UserService instance
+ :param mention_service: MentionService instance
"""
self._annotation_read_service = annotation_read_service
self._links_service = links_service
self._flag_service = flag_service
self._user_service = user_service
+ self._mention_service = mention_service
def present(self, annotation: Annotation):
"""
@@ -71,6 +75,7 @@ def present(self, annotation: Annotation):
"target": annotation.target,
"document": DocumentJSONPresenter(annotation.document).asdict(),
"links": self._links_service.get_all(annotation),
+ # ...
}
)
@@ -157,6 +162,9 @@ def present_all_for_user(self, annotation_ids, user: User):
# Optimise the user service `fetch()` call
self._user_service.fetch_all([annotation.userid for annotation in annotations])
+ # Optimise access to mentions
+ self._mention_service.fetch_all(annotations)
+
return [self.present_for_user(annotation, user) for annotation in annotations]
@classmethod
@@ -184,4 +192,5 @@ def factory(_context, request):
links_service=request.find_service(name="links"),
flag_service=request.find_service(name="flag"),
user_service=request.find_service(name="user"),
+ mention_service=request.find_service(MentionService),
)
diff --git a/h/services/mention.py b/h/services/mention.py
index 38bc8d7b617..08d836e5fa2 100644
--- a/h/services/mention.py
+++ b/h/services/mention.py
@@ -1,6 +1,6 @@
import logging
import re
-
+from typing import Sequence, Iterable
import sqlalchemy as sa
from sqlalchemy.orm import Session
@@ -39,6 +39,11 @@ def update_mentions(self, annotation: Annotation) -> None:
def _parse_userids(text: str) -> list[str]:
return USERID_PAT.findall(text)
+ def fetch_all(self, annotations: Iterable[Annotation]) -> Sequence[Mention]:
+ annotation_slim_ids = [annotation.slim.id for annotation in annotations]
+ stmt = sa.select(Mention).where(Mention.annotation_id.in_(annotation_slim_ids))
+ return self._session.execute(stmt).scalars().all()
+
def service_factory(_context, request) -> MentionService:
"""Return a MentionService instance for the passed context and request."""
From a1ce1564b9b9d62318245e85c0ff6b17b677fd6d Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Mon, 27 Jan 2025 10:04:15 +0100
Subject: [PATCH 05/22] Add MENTION_SCHEMA
---
h/presenters/mention_json.py | 17 +++++++++++++++++
h/schemas/annotation.py | 15 +++++++++++++++
h/services/annotation_json.py | 3 ++-
3 files changed, 34 insertions(+), 1 deletion(-)
create mode 100644 h/presenters/mention_json.py
diff --git a/h/presenters/mention_json.py b/h/presenters/mention_json.py
new file mode 100644
index 00000000000..e1b85fc338d
--- /dev/null
+++ b/h/presenters/mention_json.py
@@ -0,0 +1,17 @@
+from typing import Any
+
+from h.models import Mention
+
+
+class MentionJSONPresenter:
+ """Present a mention in the JSON format returned by API requests."""
+ def __init__(self, mention: Mention):
+ self._mention = mention
+
+ def asdict(self) -> dict[str, Any]:
+ return {
+ "userid": self._mention.annotation.user.userid,
+ "username": self._mention.username,
+ "display_name": self._mention.user.display_name,
+ "link": self._mention.user.uri,
+ }
diff --git a/h/schemas/annotation.py b/h/schemas/annotation.py
index a86ea64bdae..5651643102c 100644
--- a/h/schemas/annotation.py
+++ b/h/schemas/annotation.py
@@ -65,6 +65,17 @@ def _validate_wildcard_uri(node, value):
},
}
+MENTION_SCHEMA = {
+ "type": "object",
+ "properties": {
+ "userid": {"type": "string"},
+ "username": {"type": "string"},
+ "display_name": {"type": "string"},
+ "link": {"type": "string"},
+ },
+ "required": ["userid", "username"],
+}
+
class AnnotationSchema(JSONSchema):
"""Validate an annotation object."""
@@ -98,6 +109,10 @@ class AnnotationSchema(JSONSchema):
"text": {"type": "string"},
"uri": {"type": "string"},
"metadata": {"type": "object"},
+ "mentions": {
+ "type": "array",
+ "items": copy.deepcopy(MENTION_SCHEMA),
+ },
},
}
diff --git a/h/services/annotation_json.py b/h/services/annotation_json.py
index f76cf03f1f1..96302bf8eb6 100644
--- a/h/services/annotation_json.py
+++ b/h/services/annotation_json.py
@@ -2,6 +2,7 @@
from h.models import Annotation, User
from h.presenters import DocumentJSONPresenter
+from h.presenters.mention_json import MentionJSONPresenter
from h.security import Identity, identity_permits
from h.security.permissions import Permission
from h.services import MentionService
@@ -75,7 +76,7 @@ def present(self, annotation: Annotation):
"target": annotation.target,
"document": DocumentJSONPresenter(annotation.document).asdict(),
"links": self._links_service.get_all(annotation),
- # ...
+ "mentions": [MentionJSONPresenter(mention).asdict() for mention in annotation.slim.mentions],
}
)
From 0129a3c27810152ffbbf41edb07eb4fed60ebb36 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Mon, 27 Jan 2025 12:04:49 +0100
Subject: [PATCH 06/22] Fix migration
---
.../versions/bfc54f0844aa_add_username_to_mention.py | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py b/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
index 66cf71d3c08..15d10db7bfb 100644
--- a/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
+++ b/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
@@ -16,8 +16,4 @@ def upgrade() -> None:
def downgrade() -> None:
- op.drop_constraint(
- op.f("uq__annotation_metadata__annotation_id"),
- "annotation_metadata",
- type_="unique",
- )
+ op.drop_column("mention", "username")
From 72c06900493d114d56fb055ee070db6dcac27952 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Mon, 27 Jan 2025 12:47:35 +0100
Subject: [PATCH 07/22] Optimize mentions query
---
h/services/annotation_json.py | 10 ++++++----
h/services/annotation_read.py | 9 +++++++--
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/h/services/annotation_json.py b/h/services/annotation_json.py
index 96302bf8eb6..7b7ecebf171 100644
--- a/h/services/annotation_json.py
+++ b/h/services/annotation_json.py
@@ -1,6 +1,6 @@
from copy import deepcopy
-from h.models import Annotation, User
+from h.models import Annotation, User, AnnotationSlim
from h.presenters import DocumentJSONPresenter
from h.presenters.mention_json import MentionJSONPresenter
from h.security import Identity, identity_permits
@@ -146,6 +146,9 @@ def present_all_for_user(self, annotation_ids, user: User):
self._flag_service.all_flagged(user, annotation_ids)
self._flag_service.flag_counts(annotation_ids)
+ import logging
+ logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
+
annotations = self._annotation_read_service.get_annotations_by_id(
ids=annotation_ids,
eager_load=[
@@ -157,15 +160,14 @@ def present_all_for_user(self, annotation_ids, user: User):
# which ultimately depends on group permissions, causing a
# group lookup for every annotation without this
Annotation.group,
+ # Optimise access to the mentions
+ (Annotation.slim, AnnotationSlim.mentions),
],
)
# Optimise the user service `fetch()` call
self._user_service.fetch_all([annotation.userid for annotation in annotations])
- # Optimise access to mentions
- self._mention_service.fetch_all(annotations)
-
return [self.present_for_user(annotation, user) for annotation in annotations]
@classmethod
diff --git a/h/services/annotation_read.py b/h/services/annotation_read.py
index ad614883fd3..a72dc007333 100644
--- a/h/services/annotation_read.py
+++ b/h/services/annotation_read.py
@@ -1,7 +1,7 @@
from typing import Iterable, List, Optional
from sqlalchemy import select
-from sqlalchemy.orm import Query, Session, subqueryload
+from sqlalchemy.orm import Query, Session, subqueryload, selectinload
from h.db.types import InvalidUUID
from h.models import Annotation
@@ -54,7 +54,12 @@ def _annotation_search_query(
query = query.where(Annotation.id.in_(ids))
if eager_load:
- query = query.options(*(subqueryload(prop) for prop in eager_load))
+ for prop in eager_load:
+ if isinstance(prop, tuple) and len(prop) == 2:
+ parent, child = prop
+ query = query.options(subqueryload(parent).subqueryload(child))
+ else:
+ query = query.options(subqueryload(prop))
return query
From bd27f5a0185314193c1aea3a877cd88f85c2e9f8 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Mon, 27 Jan 2025 13:18:25 +0100
Subject: [PATCH 08/22] Fix format
---
h/presenters/mention_json.py | 1 +
h/services/annotation_json.py | 10 +++++++---
h/services/annotation_read.py | 2 +-
h/services/mention.py | 3 ++-
4 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/h/presenters/mention_json.py b/h/presenters/mention_json.py
index e1b85fc338d..1b45e3d0b3d 100644
--- a/h/presenters/mention_json.py
+++ b/h/presenters/mention_json.py
@@ -5,6 +5,7 @@
class MentionJSONPresenter:
"""Present a mention in the JSON format returned by API requests."""
+
def __init__(self, mention: Mention):
self._mention = mention
diff --git a/h/services/annotation_json.py b/h/services/annotation_json.py
index 7b7ecebf171..a31a7f72495 100644
--- a/h/services/annotation_json.py
+++ b/h/services/annotation_json.py
@@ -1,6 +1,6 @@
from copy import deepcopy
-from h.models import Annotation, User, AnnotationSlim
+from h.models import Annotation, AnnotationSlim, User
from h.presenters import DocumentJSONPresenter
from h.presenters.mention_json import MentionJSONPresenter
from h.security import Identity, identity_permits
@@ -76,7 +76,10 @@ def present(self, annotation: Annotation):
"target": annotation.target,
"document": DocumentJSONPresenter(annotation.document).asdict(),
"links": self._links_service.get_all(annotation),
- "mentions": [MentionJSONPresenter(mention).asdict() for mention in annotation.slim.mentions],
+ "mentions": [
+ MentionJSONPresenter(mention).asdict()
+ for mention in annotation.slim.mentions
+ ],
}
)
@@ -147,7 +150,8 @@ def present_all_for_user(self, annotation_ids, user: User):
self._flag_service.flag_counts(annotation_ids)
import logging
- logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
+
+ logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
annotations = self._annotation_read_service.get_annotations_by_id(
ids=annotation_ids,
diff --git a/h/services/annotation_read.py b/h/services/annotation_read.py
index a72dc007333..c49024dc493 100644
--- a/h/services/annotation_read.py
+++ b/h/services/annotation_read.py
@@ -1,7 +1,7 @@
from typing import Iterable, List, Optional
from sqlalchemy import select
-from sqlalchemy.orm import Query, Session, subqueryload, selectinload
+from sqlalchemy.orm import Query, Session, selectinload, subqueryload
from h.db.types import InvalidUUID
from h.models import Annotation
diff --git a/h/services/mention.py b/h/services/mention.py
index 08d836e5fa2..bff948dd7ef 100644
--- a/h/services/mention.py
+++ b/h/services/mention.py
@@ -1,6 +1,7 @@
import logging
import re
-from typing import Sequence, Iterable
+from typing import Iterable, Sequence
+
import sqlalchemy as sa
from sqlalchemy.orm import Session
From 6494f05071a86475ed9bc476d9ab8e239d86b1db Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Mon, 27 Jan 2025 15:23:25 +0100
Subject: [PATCH 09/22] Clean up
---
h/services/annotation_json.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/h/services/annotation_json.py b/h/services/annotation_json.py
index a31a7f72495..2a8bc7b9f9f 100644
--- a/h/services/annotation_json.py
+++ b/h/services/annotation_json.py
@@ -149,10 +149,6 @@ def present_all_for_user(self, annotation_ids, user: User):
self._flag_service.all_flagged(user, annotation_ids)
self._flag_service.flag_counts(annotation_ids)
- import logging
-
- logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
-
annotations = self._annotation_read_service.get_annotations_by_id(
ids=annotation_ids,
eager_load=[
From 06605bead145bfcdc005a21e162e59e0baeda339 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 09:15:07 +0100
Subject: [PATCH 10/22] Clean up
---
h/schemas/annotation.py | 15 ---------------
1 file changed, 15 deletions(-)
diff --git a/h/schemas/annotation.py b/h/schemas/annotation.py
index 5651643102c..a86ea64bdae 100644
--- a/h/schemas/annotation.py
+++ b/h/schemas/annotation.py
@@ -65,17 +65,6 @@ def _validate_wildcard_uri(node, value):
},
}
-MENTION_SCHEMA = {
- "type": "object",
- "properties": {
- "userid": {"type": "string"},
- "username": {"type": "string"},
- "display_name": {"type": "string"},
- "link": {"type": "string"},
- },
- "required": ["userid", "username"],
-}
-
class AnnotationSchema(JSONSchema):
"""Validate an annotation object."""
@@ -109,10 +98,6 @@ class AnnotationSchema(JSONSchema):
"text": {"type": "string"},
"uri": {"type": "string"},
"metadata": {"type": "object"},
- "mentions": {
- "type": "array",
- "items": copy.deepcopy(MENTION_SCHEMA),
- },
},
}
From 9d0c2c2e6745878a4113e3ec2f520c14252d6869 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 09:22:06 +0100
Subject: [PATCH 11/22] Update annotation.yaml with mentions
---
.../api-reference/schemas/annotation.yaml | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/docs/_extra/api-reference/schemas/annotation.yaml b/docs/_extra/api-reference/schemas/annotation.yaml
index 68e7f0253ca..8d5825b25ae 100644
--- a/docs/_extra/api-reference/schemas/annotation.yaml
+++ b/docs/_extra/api-reference/schemas/annotation.yaml
@@ -199,3 +199,24 @@ Annotation:
description: The annotation creator's display name
example: "Felicity Nunsun"
- type: null
+ mentions:
+ type: array
+ items:
+ type: object
+ properties:
+ userid:
+ type: string
+ pattern: "acct:^[A-Za-z0-9._]{3,30}@.*$"
+ description: user account ID in the format `"acct:@"`
+ example: "acct:felicity_nunsun@hypothes.is"
+ username:
+ type: string
+ description: The username of the user
+ display_name:
+ type: string
+ description: The display name of the user
+ link:
+ type: string
+ format: uri
+ description: The link to the user profile
+ description: An array of user mentions the annotation text
From 66f5a0d4fdee16d485063cd2aa75495762eee395 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 09:31:25 +0100
Subject: [PATCH 12/22] Clean up
---
.../bfc54f0844aa_add_username_to_mention.py | 19 -------------------
h/models/mention.py | 3 ---
2 files changed, 22 deletions(-)
delete mode 100644 h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
diff --git a/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py b/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
deleted file mode 100644
index 15d10db7bfb..00000000000
--- a/h/migrations/versions/bfc54f0844aa_add_username_to_mention.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Add username column to mention table.
-
-Revision ID: bfc54f0844aa
-Revises: 39cc1025a3a2
-"""
-
-import sqlalchemy as sa
-from alembic import op
-
-revision = "bfc54f0844aa"
-down_revision = "39cc1025a3a2"
-
-
-def upgrade() -> None:
- op.add_column("mention", sa.Column("username", sa.UnicodeText(), nullable=False))
-
-
-def downgrade() -> None:
- op.drop_column("mention", "username")
diff --git a/h/models/mention.py b/h/models/mention.py
index 63f4290854d..6e0a5e07912 100644
--- a/h/models/mention.py
+++ b/h/models/mention.py
@@ -28,8 +28,5 @@ class Mention(Base, Timestamps): # pragma: nocover
"""FK to user.id"""
user = sa.orm.relationship("User")
- username = sa.Column("username", sa.UnicodeText(), nullable=False)
- """Current username of the user mentioned"""
-
def __repr__(self) -> str:
return helpers.repr_(self, ["id", "annotation_id", "user_id"])
From 99769cd4e2558059b1bb689948f7bff406f803bf Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 09:51:39 +0100
Subject: [PATCH 13/22] Clean up
---
h/presenters/mention_json.py | 1 -
h/services/mention.py | 9 ++++-----
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/h/presenters/mention_json.py b/h/presenters/mention_json.py
index 1b45e3d0b3d..963077ab883 100644
--- a/h/presenters/mention_json.py
+++ b/h/presenters/mention_json.py
@@ -12,7 +12,6 @@ def __init__(self, mention: Mention):
def asdict(self) -> dict[str, Any]:
return {
"userid": self._mention.annotation.user.userid,
- "username": self._mention.username,
"display_name": self._mention.user.display_name,
"link": self._mention.user.uri,
}
diff --git a/h/services/mention.py b/h/services/mention.py
index bff948dd7ef..393a6eda44f 100644
--- a/h/services/mention.py
+++ b/h/services/mention.py
@@ -2,7 +2,7 @@
import re
from typing import Iterable, Sequence
-import sqlalchemy as sa
+from sqlalchemy import select, delete
from sqlalchemy.orm import Session
from h.models import Annotation, Mention
@@ -26,13 +26,12 @@ def update_mentions(self, annotation: Annotation) -> None:
userids = set(self._parse_userids(annotation.text))
users = self._user_service.fetch_all(userids)
self._session.execute(
- sa.delete(Mention).where(Mention.annotation_id == annotation.slim.id)
+ delete(Mention).where(Mention.annotation_id == annotation.slim.id)
)
for user in users:
mention = Mention(
annotation_id=annotation.slim.id,
- user_id=user.id,
- username=user.username,
+ user_id=user.id
)
self._session.add(mention)
@@ -42,7 +41,7 @@ def _parse_userids(text: str) -> list[str]:
def fetch_all(self, annotations: Iterable[Annotation]) -> Sequence[Mention]:
annotation_slim_ids = [annotation.slim.id for annotation in annotations]
- stmt = sa.select(Mention).where(Mention.annotation_id.in_(annotation_slim_ids))
+ stmt = select(Mention).where(Mention.annotation_id.in_(annotation_slim_ids))
return self._session.execute(stmt).scalars().all()
From 5bd068b7f46ed4519181a544a7ee6efb3f0cf3d6 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 09:53:44 +0100
Subject: [PATCH 14/22] Format
---
h/services/mention.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/h/services/mention.py b/h/services/mention.py
index 393a6eda44f..9f0310b4deb 100644
--- a/h/services/mention.py
+++ b/h/services/mention.py
@@ -2,7 +2,7 @@
import re
from typing import Iterable, Sequence
-from sqlalchemy import select, delete
+from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from h.models import Annotation, Mention
@@ -29,10 +29,7 @@ def update_mentions(self, annotation: Annotation) -> None:
delete(Mention).where(Mention.annotation_id == annotation.slim.id)
)
for user in users:
- mention = Mention(
- annotation_id=annotation.slim.id,
- user_id=user.id
- )
+ mention = Mention(annotation_id=annotation.slim.id, user_id=user.id)
self._session.add(mention)
@staticmethod
From d16bb034f29308a15d0b08a11a71fa984bf37c2a Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 13:15:24 +0100
Subject: [PATCH 15/22] Update mention to reference annotation
---
..._update_mention_to_reference_annotation.py | 39 +++++++++++++++++++
h/models/annotation.py | 2 +
h/models/annotation_slim.py | 2 -
h/models/mention.py | 12 +++---
h/services/mention.py | 9 +----
5 files changed, 49 insertions(+), 15 deletions(-)
create mode 100644 h/migrations/versions/022ea4bf4a27_update_mention_to_reference_annotation.py
diff --git a/h/migrations/versions/022ea4bf4a27_update_mention_to_reference_annotation.py b/h/migrations/versions/022ea4bf4a27_update_mention_to_reference_annotation.py
new file mode 100644
index 00000000000..e386e67e4ed
--- /dev/null
+++ b/h/migrations/versions/022ea4bf4a27_update_mention_to_reference_annotation.py
@@ -0,0 +1,39 @@
+"""Update mention to reference annotation.
+
+Revision ID: 022ea4bf4a27
+Revises: 39cc1025a3a2
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+from h.db import types
+
+revision = "022ea4bf4a27"
+down_revision = "39cc1025a3a2"
+
+
+def upgrade() -> None:
+ op.drop_column("mention", "annotation_id")
+ op.add_column(
+ "mention",
+ sa.Column(
+ "annotation_id",
+ types.URLSafeUUID(),
+ sa.ForeignKey("annotation.id", ondelete="CASCADE"),
+ nullable=False,
+ ),
+ )
+
+
+def downgrade() -> None:
+ op.drop_column("mention", "annotation_id")
+ op.add_column(
+ "mention",
+ sa.Column(
+ "annotation_id",
+ sa.INTEGER(),
+ sa.ForeignKey("annotation_slim.id", ondelete="CASCADE"),
+ nullable=False,
+ ),
+ )
diff --git a/h/models/annotation.py b/h/models/annotation.py
index 7254a4630f4..f6611edd01e 100644
--- a/h/models/annotation.py
+++ b/h/models/annotation.py
@@ -138,6 +138,8 @@ class Annotation(Base):
uselist=True,
)
+ mentions = sa.orm.relationship("Mention", back_populates="annotation")
+
@property
def uuid(self):
"""
diff --git a/h/models/annotation_slim.py b/h/models/annotation_slim.py
index a2b9226d8cd..6b5954dc5d7 100644
--- a/h/models/annotation_slim.py
+++ b/h/models/annotation_slim.py
@@ -96,7 +96,5 @@ class AnnotationSlim(Base):
)
group = sa.orm.relationship("Group")
- mentions = sa.orm.relationship("Mention", back_populates="annotation")
-
def __repr__(self):
return helpers.repr_(self, ["id"])
diff --git a/h/models/mention.py b/h/models/mention.py
index 6e0a5e07912..be52ba700ad 100644
--- a/h/models/mention.py
+++ b/h/models/mention.py
@@ -1,7 +1,7 @@
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
-from h.db import Base
+from h.db import Base, types
from h.db.mixins import Timestamps
from h.models import helpers
@@ -11,13 +11,13 @@ class Mention(Base, Timestamps): # pragma: nocover
id: Mapped[int] = mapped_column(sa.Integer, autoincrement=True, primary_key=True)
- annotation_id: Mapped[int] = mapped_column(
- sa.Integer,
- sa.ForeignKey("annotation_slim.id", ondelete="CASCADE"),
+ annotation_id: Mapped[types.URLSafeUUID] = mapped_column(
+ types.URLSafeUUID,
+ sa.ForeignKey("annotation.id", ondelete="CASCADE"),
nullable=False,
)
- """FK to annotation_slim.id"""
- annotation = sa.orm.relationship("AnnotationSlim", back_populates="mentions")
+ """FK to annotation.id"""
+ annotation = sa.orm.relationship("Annotation", back_populates="mentions")
user_id: Mapped[int] = mapped_column(
sa.Integer,
diff --git a/h/services/mention.py b/h/services/mention.py
index 9f0310b4deb..164bdd34dec 100644
--- a/h/services/mention.py
+++ b/h/services/mention.py
@@ -26,21 +26,16 @@ def update_mentions(self, annotation: Annotation) -> None:
userids = set(self._parse_userids(annotation.text))
users = self._user_service.fetch_all(userids)
self._session.execute(
- delete(Mention).where(Mention.annotation_id == annotation.slim.id)
+ delete(Mention).where(Mention.annotation_id == annotation.id)
)
for user in users:
- mention = Mention(annotation_id=annotation.slim.id, user_id=user.id)
+ mention = Mention(annotation_id=annotation.id, user_id=user.id)
self._session.add(mention)
@staticmethod
def _parse_userids(text: str) -> list[str]:
return USERID_PAT.findall(text)
- def fetch_all(self, annotations: Iterable[Annotation]) -> Sequence[Mention]:
- annotation_slim_ids = [annotation.slim.id for annotation in annotations]
- stmt = select(Mention).where(Mention.annotation_id.in_(annotation_slim_ids))
- return self._session.execute(stmt).scalars().all()
-
def service_factory(_context, request) -> MentionService:
"""Return a MentionService instance for the passed context and request."""
From 4f44ce487670f378f7ec86038e13f991f8c9110a Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 13:24:44 +0100
Subject: [PATCH 16/22] Clean up
---
h/presenters/mention_json.py | 2 +-
h/services/annotation_json.py | 4 ++--
h/services/annotation_read.py | 7 +------
3 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/h/presenters/mention_json.py b/h/presenters/mention_json.py
index 963077ab883..97e5067ce57 100644
--- a/h/presenters/mention_json.py
+++ b/h/presenters/mention_json.py
@@ -11,7 +11,7 @@ def __init__(self, mention: Mention):
def asdict(self) -> dict[str, Any]:
return {
- "userid": self._mention.annotation.user.userid,
+ "userid": self._mention.annotation.userid,
"display_name": self._mention.user.display_name,
"link": self._mention.user.uri,
}
diff --git a/h/services/annotation_json.py b/h/services/annotation_json.py
index 2a8bc7b9f9f..6190a5f2094 100644
--- a/h/services/annotation_json.py
+++ b/h/services/annotation_json.py
@@ -78,7 +78,7 @@ def present(self, annotation: Annotation):
"links": self._links_service.get_all(annotation),
"mentions": [
MentionJSONPresenter(mention).asdict()
- for mention in annotation.slim.mentions
+ for mention in annotation.mentions
],
}
)
@@ -161,7 +161,7 @@ def present_all_for_user(self, annotation_ids, user: User):
# group lookup for every annotation without this
Annotation.group,
# Optimise access to the mentions
- (Annotation.slim, AnnotationSlim.mentions),
+ Annotation.mentions,
],
)
diff --git a/h/services/annotation_read.py b/h/services/annotation_read.py
index c49024dc493..e224ab14912 100644
--- a/h/services/annotation_read.py
+++ b/h/services/annotation_read.py
@@ -54,12 +54,7 @@ def _annotation_search_query(
query = query.where(Annotation.id.in_(ids))
if eager_load:
- for prop in eager_load:
- if isinstance(prop, tuple) and len(prop) == 2:
- parent, child = prop
- query = query.options(subqueryload(parent).subqueryload(child))
- else:
- query = query.options(subqueryload(prop))
+ query = query.options(*(subqueryload(prop) for prop in eager_load))
return query
From 4dd1e6895641753fa83fb1b544d16b9ee0aaa5fa Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 16:08:53 +0100
Subject: [PATCH 17/22] Send mention email notifications
---
h/emails/__init__.py | 4 +-
h/emails/mention_notification.py | 36 +++++++++++
h/notification/mention.py | 61 +++++++++++++++++++
h/services/annotation_json.py | 2 +-
h/services/annotation_read.py | 2 +-
h/services/mention.py | 3 +-
h/subscribers.py | 27 +++++++-
.../emails/mention_notification.html.jinja2 | 26 ++++++++
.../emails/mention_notification.txt.jinja2 | 7 +++
9 files changed, 161 insertions(+), 7 deletions(-)
create mode 100644 h/emails/mention_notification.py
create mode 100644 h/notification/mention.py
create mode 100644 h/templates/emails/mention_notification.html.jinja2
create mode 100644 h/templates/emails/mention_notification.txt.jinja2
diff --git a/h/emails/__init__.py b/h/emails/__init__.py
index f4457355268..0404049722c 100644
--- a/h/emails/__init__.py
+++ b/h/emails/__init__.py
@@ -1,3 +1,3 @@
-from h.emails import reply_notification, reset_password, signup
+from h.emails import mention_notification, reply_notification, reset_password, signup
-__all__ = ("reply_notification", "reset_password", "signup")
+__all__ = ("reply_notification", "reset_password", "signup", "mention_notification")
diff --git a/h/emails/mention_notification.py b/h/emails/mention_notification.py
new file mode 100644
index 00000000000..447810b85d4
--- /dev/null
+++ b/h/emails/mention_notification.py
@@ -0,0 +1,36 @@
+from pyramid.renderers import render
+from pyramid.request import Request
+
+from h import links
+from h.notification.mention import Notification
+
+
+def generate(request: Request, notification: Notification):
+ context = {
+ "user_url": _get_user_url(notification.mentioning_user, request),
+ "user_display_name": notification.mentioning_user.display_name
+ or notification.mentioning_user.username,
+ "annotation_url": links.incontext_link(request, notification.annotation)
+ or request.route_url("annotation", id=notification.annotation.id),
+ "document_title": notification.document.title
+ or notification.annotation.target_uri,
+ "document_url": notification.annotation.target_uri,
+ "annotation": notification.annotation,
+ }
+
+ subject = f"{context['user_display_name']} has mentioned you in an annotation"
+ text = render(
+ "h:templates/emails/mention_notification.txt.jinja2", context, request=request
+ )
+ html = render(
+ "h:templates/emails/mention_notification.html.jinja2", context, request=request
+ )
+
+ return [notification.mentioned_user.email], subject, text, html
+
+
+def _get_user_url(user, request):
+ if user.authority == request.default_authority:
+ return request.route_url("stream.user_query", user=user.username)
+
+ return None
diff --git a/h/notification/mention.py b/h/notification/mention.py
new file mode 100644
index 00000000000..d122982977b
--- /dev/null
+++ b/h/notification/mention.py
@@ -0,0 +1,61 @@
+import logging
+from dataclasses import dataclass
+
+from h.models import Annotation, Document, User
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class Notification:
+ """A data structure representing a notification of a mention in an annotation."""
+
+ mentioning_user: User
+ mentioned_user: User
+ annotation: Annotation
+ document: Document
+
+
+def get_notifications(request, annotation: Annotation, action) -> list[Notification]:
+ # Only send notifications when new annotations are created
+ if action != "create":
+ return []
+
+ user_service = request.find_service(name="user")
+
+ # If the mentioning user doesn't exist (anymore), we can't send emails, but
+ # this would be super weird, so log a warning.
+ mentioning_user = user_service.fetch(annotation.userid)
+ if mentioning_user is None:
+ logger.warning(
+ "user who just mentioned another user no longer exists: %s",
+ annotation.userid,
+ )
+ return []
+
+ notifications = []
+ for mention in annotation.mentions:
+ # If the mentioning user doesn't exist (anymore), we can't send emails
+ mentioned_user = user_service.fetch(mention.user.userid)
+ if mentioned_user is None:
+ continue
+
+ # If mentioned user doesn't have an email address we can't email them.
+ if not mention.user.email:
+ continue
+
+ # Do not notify users about their own replies
+ if mentioning_user == mentioned_user:
+ continue
+
+ # If the annotation doesn't have a document, we can't send an email.
+ if annotation.document is None:
+ continue
+
+ notifications.append(
+ Notification(
+ mentioning_user, mentioned_user, annotation, annotation.document
+ )
+ )
+
+ return notifications
diff --git a/h/services/annotation_json.py b/h/services/annotation_json.py
index 6190a5f2094..1dc760b0ea1 100644
--- a/h/services/annotation_json.py
+++ b/h/services/annotation_json.py
@@ -1,6 +1,6 @@
from copy import deepcopy
-from h.models import Annotation, AnnotationSlim, User
+from h.models import Annotation, User
from h.presenters import DocumentJSONPresenter
from h.presenters.mention_json import MentionJSONPresenter
from h.security import Identity, identity_permits
diff --git a/h/services/annotation_read.py b/h/services/annotation_read.py
index e224ab14912..ad614883fd3 100644
--- a/h/services/annotation_read.py
+++ b/h/services/annotation_read.py
@@ -1,7 +1,7 @@
from typing import Iterable, List, Optional
from sqlalchemy import select
-from sqlalchemy.orm import Query, Session, selectinload, subqueryload
+from sqlalchemy.orm import Query, Session, subqueryload
from h.db.types import InvalidUUID
from h.models import Annotation
diff --git a/h/services/mention.py b/h/services/mention.py
index 164bdd34dec..138c6a31a7f 100644
--- a/h/services/mention.py
+++ b/h/services/mention.py
@@ -1,8 +1,7 @@
import logging
import re
-from typing import Iterable, Sequence
-from sqlalchemy import delete, select
+from sqlalchemy import delete
from sqlalchemy.orm import Session
from h.models import Annotation, Mention
diff --git a/h/subscribers.py b/h/subscribers.py
index 704d2f306cb..e8e9c078ae6 100644
--- a/h/subscribers.py
+++ b/h/subscribers.py
@@ -5,7 +5,7 @@
from h import __version__, emails
from h.events import AnnotationEvent
from h.exceptions import RealtimeMessageQueueError
-from h.notification import reply
+from h.notification import mention, reply
from h.services.annotation_read import AnnotationReadService
from h.tasks import mailer
@@ -89,3 +89,28 @@ def send_reply_notifications(event):
except OperationalError as err: # pragma: no cover
# We could not connect to rabbit! So carry on
report_exception(err)
+
+
+@subscriber(AnnotationEvent)
+def send_mention_notifications(event):
+ """Send mention notifications triggered by a mention event."""
+
+ request = event.request
+
+ with request.tm:
+ annotation = request.find_service(AnnotationReadService).get_annotation_by_id(
+ event.annotation_id,
+ )
+ notifications = mention.get_notifications(request, annotation, event.action)
+
+ if not notifications:
+ return
+
+ for notification in notifications:
+ send_params = emails.mention_notification.generate(request, notification)
+
+ try:
+ mailer.send.delay(*send_params)
+ except OperationalError as err: # pragma: no cover
+ # We could not connect to rabbit! So carry on
+ report_exception(err)
diff --git a/h/templates/emails/mention_notification.html.jinja2 b/h/templates/emails/mention_notification.html.jinja2
new file mode 100644
index 00000000000..eb7ce438f20
--- /dev/null
+++ b/h/templates/emails/mention_notification.html.jinja2
@@ -0,0 +1,26 @@
+
+ {% if user_url %}
+ {{ user_display_name }}
+ {% else %}
+ {{ user_display_name }}
+ {% endif %}
+ has
+ mentioned you
+ on
+ “{{ document_title }}”:
+
+
+
+ On
+ {{ annotation.updated | human_timestamp }}
+ {% if user_url %}
+ {{ user_display_name }}
+ {% else %}
+ {{ user_display_name }}
+ {% endif %}
+ commented:
+
+
+{{ annotation.text or "" }}
+
+View the thread and respond.
diff --git a/h/templates/emails/mention_notification.txt.jinja2 b/h/templates/emails/mention_notification.txt.jinja2
new file mode 100644
index 00000000000..66726b21ed0
--- /dev/null
+++ b/h/templates/emails/mention_notification.txt.jinja2
@@ -0,0 +1,7 @@
+{{ user_display_name }} has replied to your annotation on "{{ document_title }}":
+
+On {{ annotation.updated | human_timestamp }} {{ user_display_name }} replied:
+
+> {{ annotation.text or "" }}
+
+View the thread and respond: {{ annotation_url }}
From 9fd2ebef53676492bb3ba95c643cb4d882afd177 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 16:10:21 +0100
Subject: [PATCH 18/22] Update docstring
---
h/notification/mention.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/h/notification/mention.py b/h/notification/mention.py
index d122982977b..13126b3caa1 100644
--- a/h/notification/mention.py
+++ b/h/notification/mention.py
@@ -8,7 +8,7 @@
@dataclass
class Notification:
- """A data structure representing a notification of a mention in an annotation."""
+ """A data structure representing a mention notification in an annotation."""
mentioning_user: User
mentioned_user: User
From d089ea02b766d8bfc49303e1441e8bf0a5a1146d Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 16:11:21 +0100
Subject: [PATCH 19/22] Fix MentionJSONPresenter
---
h/presenters/mention_json.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/h/presenters/mention_json.py b/h/presenters/mention_json.py
index 97e5067ce57..106fc7afca1 100644
--- a/h/presenters/mention_json.py
+++ b/h/presenters/mention_json.py
@@ -12,6 +12,7 @@ def __init__(self, mention: Mention):
def asdict(self) -> dict[str, Any]:
return {
"userid": self._mention.annotation.userid,
+ "username": self._mention.user.username,
"display_name": self._mention.user.display_name,
"link": self._mention.user.uri,
}
From c5a2a27d15559e6d7fbe0f51d64898754f6b0776 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 16:41:22 +0100
Subject: [PATCH 20/22] Strip html tags
---
h/templates/emails/mention_notification.html.jinja2 | 2 +-
h/templates/emails/mention_notification.txt.jinja2 | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/h/templates/emails/mention_notification.html.jinja2 b/h/templates/emails/mention_notification.html.jinja2
index eb7ce438f20..17e29bf6140 100644
--- a/h/templates/emails/mention_notification.html.jinja2
+++ b/h/templates/emails/mention_notification.html.jinja2
@@ -21,6 +21,6 @@
commented:
-{{ annotation.text or "" }}
+{{ annotation.text|striptags or "" }}
View the thread and respond.
diff --git a/h/templates/emails/mention_notification.txt.jinja2 b/h/templates/emails/mention_notification.txt.jinja2
index 66726b21ed0..eca7e622be4 100644
--- a/h/templates/emails/mention_notification.txt.jinja2
+++ b/h/templates/emails/mention_notification.txt.jinja2
@@ -2,6 +2,6 @@
On {{ annotation.updated | human_timestamp }} {{ user_display_name }} replied:
-> {{ annotation.text or "" }}
+> {{ annotation.text|striptags or "" }}
View the thread and respond: {{ annotation_url }}
From 8fc7df111c0b742a6db86adbff167e91ff9ed911 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Wed, 29 Jan 2025 17:04:25 +0100
Subject: [PATCH 21/22] Strip html tags in replies
---
h/templates/emails/reply_notification.html.jinja2 | 4 ++--
h/templates/emails/reply_notification.txt.jinja2 | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/h/templates/emails/reply_notification.html.jinja2 b/h/templates/emails/reply_notification.html.jinja2
index e2553ef39b1..33e967b1660 100644
--- a/h/templates/emails/reply_notification.html.jinja2
+++ b/h/templates/emails/reply_notification.html.jinja2
@@ -21,7 +21,7 @@
commented:
-{{ parent.text or "" }}
+{{ parent.text|striptags or "" }}
On
@@ -34,7 +34,7 @@
replied:
-{{ reply.text or "" }}
+{{ reply.text|striptags or "" }}
View the thread and respond.
diff --git a/h/templates/emails/reply_notification.txt.jinja2 b/h/templates/emails/reply_notification.txt.jinja2
index 80fd0967271..eb2170871c9 100644
--- a/h/templates/emails/reply_notification.txt.jinja2
+++ b/h/templates/emails/reply_notification.txt.jinja2
@@ -2,11 +2,11 @@
On {{ parent.created | human_timestamp }} {{ parent_user_display_name }} commented:
-> {{ parent.text or "" }}
+> {{ parent.text|striptags or "" }}
On {{ reply.created | human_timestamp }} {{ reply_user_display_name }} replied:
-> {{ reply.text or "" }}
+> {{ reply.text|striptags or "" }}
View the thread and respond: {{ reply_url }}
From 740ff4942f463d9c3ef12da8f63e2195d79bc700 Mon Sep 17 00:00:00 2001
From: Misha Tomilov
Date: Thu, 30 Jan 2025 16:17:41 +0100
Subject: [PATCH 22/22] Fix striptags
---
h/templates/emails/mention_notification.html.jinja2 | 2 +-
h/templates/emails/mention_notification.txt.jinja2 | 2 +-
h/templates/emails/reply_notification.html.jinja2 | 4 ++--
h/templates/emails/reply_notification.txt.jinja2 | 4 ++--
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/h/templates/emails/mention_notification.html.jinja2 b/h/templates/emails/mention_notification.html.jinja2
index 17e29bf6140..47a5f251bbe 100644
--- a/h/templates/emails/mention_notification.html.jinja2
+++ b/h/templates/emails/mention_notification.html.jinja2
@@ -21,6 +21,6 @@
commented:
-{{ annotation.text|striptags or "" }}
+{{ (annotation.text or "")|striptags }}
View the thread and respond.
diff --git a/h/templates/emails/mention_notification.txt.jinja2 b/h/templates/emails/mention_notification.txt.jinja2
index eca7e622be4..5d0ffa5ae09 100644
--- a/h/templates/emails/mention_notification.txt.jinja2
+++ b/h/templates/emails/mention_notification.txt.jinja2
@@ -2,6 +2,6 @@
On {{ annotation.updated | human_timestamp }} {{ user_display_name }} replied:
-> {{ annotation.text|striptags or "" }}
+> {{ (annotation.text or "")|striptags }}
View the thread and respond: {{ annotation_url }}
diff --git a/h/templates/emails/reply_notification.html.jinja2 b/h/templates/emails/reply_notification.html.jinja2
index 33e967b1660..28b1e9eeae2 100644
--- a/h/templates/emails/reply_notification.html.jinja2
+++ b/h/templates/emails/reply_notification.html.jinja2
@@ -21,7 +21,7 @@
commented:
-{{ parent.text|striptags or "" }}
+{{ (parent.text or "")|striptags }}
On
@@ -34,7 +34,7 @@
replied:
-{{ reply.text|striptags or "" }}
+{{ (reply.text or "")|striptags }}
View the thread and respond.
diff --git a/h/templates/emails/reply_notification.txt.jinja2 b/h/templates/emails/reply_notification.txt.jinja2
index eb2170871c9..76ff756d04c 100644
--- a/h/templates/emails/reply_notification.txt.jinja2
+++ b/h/templates/emails/reply_notification.txt.jinja2
@@ -2,11 +2,11 @@
On {{ parent.created | human_timestamp }} {{ parent_user_display_name }} commented:
-> {{ parent.text|striptags or "" }}
+> {{ (parent.text or "")|striptags }}
On {{ reply.created | human_timestamp }} {{ reply_user_display_name }} replied:
-> {{ reply.text|striptags or "" }}
+> {{ (reply.text or "")|striptags }}
View the thread and respond: {{ reply_url }}