-
Notifications
You must be signed in to change notification settings - Fork 438
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Process mention on annotation creation #9322
- Loading branch information
Showing
17 changed files
with
376 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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:<username>@<authority>"` | ||
example: "acct:[email protected]" | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
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.user.userid, | ||
"username": self._mention.username, | ||
"display_name": self._mention.user.display_name, | ||
"link": self._mention.user.uri, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from html.parser import HTMLParser | ||
|
||
|
||
class LinkParser(HTMLParser): | ||
def __init__(self): | ||
super().__init__() | ||
self._links = [] | ||
|
||
def handle_starttag(self, tag, attrs): | ||
if tag == "a": | ||
self._links.append(dict(attrs)) | ||
|
||
def get_links(self) -> list[dict]: | ||
return self._links | ||
|
||
|
||
def parse_html_links(html: str) -> list[dict]: | ||
parser = LinkParser() | ||
parser.feed(html) | ||
return parser.get_links() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import logging | ||
from collections import OrderedDict | ||
|
||
from sqlalchemy import delete | ||
from sqlalchemy.orm import Session | ||
|
||
from h.models import Annotation, Mention | ||
from h.services.html import parse_html_links | ||
from h.services.user import UserService | ||
|
||
MENTION_CLASS = "data-hyp-mention" | ||
MENTION_USERID = "data-userid" | ||
MENTION_LIMIT = 5 | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
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() | ||
|
||
# Only shared annotations can have mentions | ||
if not annotation.shared: | ||
return | ||
mentioning_user = self._user_service.fetch(annotation.userid) | ||
# NIPSA users do not send mentions | ||
if mentioning_user.nipsa: | ||
return | ||
|
||
mentioned_userids = OrderedDict.fromkeys(self._parse_userids(annotation.text)) | ||
mentioned_users = self._user_service.fetch_all(mentioned_userids) | ||
self._session.execute( | ||
delete(Mention).where(Mention.annotation_id == annotation.id) | ||
) | ||
|
||
for i, user in enumerate(mentioned_users): | ||
if i >= MENTION_LIMIT: | ||
logger.warning( | ||
"Annotation %s has more than %s mentions", | ||
annotation.id, | ||
MENTION_LIMIT, | ||
) | ||
break | ||
# NIPSA users do not receive mentions | ||
if user.nipsa: | ||
continue | ||
# Only allow mentions if the annotation is in the public group | ||
# or the annotation is in one of mentioned user's groups | ||
if not ( | ||
annotation.groupid == "__world__" or annotation.group in user.groups | ||
): | ||
continue | ||
|
||
mention = Mention( | ||
annotation_id=annotation.id, user_id=user.id, username=user.username | ||
) | ||
self._session.add(mention) | ||
|
||
@staticmethod | ||
def _parse_userids(text: str) -> list[str]: | ||
links = parse_html_links(text) | ||
return [ | ||
user_id | ||
for link in links | ||
if MENTION_CLASS in link and (user_id := link.get(MENTION_USERID)) | ||
] | ||
|
||
|
||
def 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") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import factory | ||
|
||
from h import models | ||
|
||
from .annotation import Annotation | ||
from .base import ModelFactory | ||
from .user import User | ||
|
||
|
||
class Mention(ModelFactory): | ||
class Meta: | ||
model = models.Mention | ||
sqlalchemy_session_persistence = "flush" | ||
|
||
annotation = factory.SubFactory(Annotation) | ||
user = factory.SubFactory(User) | ||
username = factory.LazyAttribute(lambda obj: obj.user.username) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
def test_repr(factories): | ||
annotation = factories.Annotation() | ||
mention = factories.Mention(annotation=annotation) | ||
|
||
assert ( | ||
repr(mention) | ||
== f"Mention(id={mention.id}, annotation_id={mention.annotation.id!r}, user_id={mention.user.id})" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import pytest | ||
|
||
from h.models import Mention | ||
from h.presenters.mention_json import MentionJSONPresenter | ||
|
||
|
||
class TestMentionJSONPresenter: | ||
def test_as_dict(self, user, annotation): | ||
mention = Mention(annotation=annotation, user=user, username=user.username) | ||
|
||
data = MentionJSONPresenter(mention).asdict() | ||
|
||
assert data == { | ||
"userid": user.userid, | ||
"username": user.username, | ||
"display_name": user.display_name, | ||
"link": user.uri, | ||
} | ||
|
||
@pytest.fixture | ||
def user(self, factories): | ||
return factories.User.build() | ||
|
||
@pytest.fixture | ||
def annotation(self, factories): | ||
return factories.Annotation.build() |
Oops, something went wrong.