-
Notifications
You must be signed in to change notification settings - Fork 442
Moderation proof of concept implementation #9461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
e1a888f
to
fe7c4a7
Compare
fe7c4a7
to
f87c9b0
Compare
@@ -5,13 +5,16 @@ | |||
import sqlalchemy as sa | |||
|
|||
|
|||
class Timestamps: | |||
class CreatedMixin: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Splitting this in two, we not always need updated but if we have created we most likely want also updated.
@@ -1,7 +1,12 @@ | |||
from typing import Literal | |||
|
|||
AnnotationAction = Literal["create", "update", "delete"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember why I did this but I reckon I was looking for possible values
down_revision = "c8f748cbfb8f" | ||
|
||
|
||
def upgrade() -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This migrates existing AnnotationModeration rows to the moderation_status column.
""" | ||
) | ||
) | ||
print("\tUpdated annotations as DENIED:", result.rowcount) # noqa: T201 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Also backfill moderation_log based on AnnotationModeration.
This is not going to be possible as we don't have the user who hide them.
We could:
- Make user nullable on moderation_log.
- Just don't migrate these to moderation_log
- Pick one user, the creator of the group? Could be confusing on the future.
nullable=True, | ||
), | ||
) | ||
op.add_column( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- AnnotationSlim has a boolean
moderated
column. I'm going to keep it like that but take into consideration the new Annotation.moderation_status to keep them in sync.
We should have a spike/discussion about AnnotationSlim. We did some work there but we haven't committed to move the codebase over that table.
My current thinking is that we should:
- Move annotation_metadata to be related to annotation.id
- Make a assessment of current slow queries, existing indexes in annotation and make the necessary adjustments.
- Move existing queries of AnnotationSlim back to Annotation.
I don't think we should block the pre-moderation work over this but it something we could work in parallel or right afterwards.
@@ -224,13 +243,10 @@ def _validate_group(self, annotation: Annotation, enforce_write_permission=True) | |||
+ _("Annotations for this target URI are not allowed in this group") | |||
) | |||
|
|||
def upsert_annotation_slim(self, annotation): | |||
def upsert_annotation_slim(self, annotation: Annotation, user: User): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are passing user
to various methods above now, pass it here to and avoid this query.
permission=Permission.Annotation.MODERATE, | ||
) | ||
def change_annotation_moderation_status(context, request): | ||
status = Annotation.ModerationStatus(request.json_body["moderation_status"].upper()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This endpoint needs the mechanism to avoid updates if the annotation the client saw was updated.
@view_config( | ||
route_name="group_edit_moderation", | ||
request_method="GET", | ||
permission=Permission.Group.EDIT, # TODO permission for moderation? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exposing the new endpoints should be based on:
- A new feature flag
- Whether or not you are moderator.
assert svc.all_hidden([]) == set() | ||
|
||
# fmt: off | ||
@pytest.mark.parametrize("action,is_private,pre_moderation_enabled,existing_status,expected_status", [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the test that don't require shared changing.
We should test those here too here for coverage but I wanted to make sure all looked good so those are tested as func test here.
from h.services.annotation_moderation import ( | ||
AnnotationModerationService, | ||
annotation_moderation_service_factory, | ||
) | ||
|
||
|
||
class TestAnnotationModerationServiceAllHidden: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored this to one test class per target class instead of one test class per method.
f87c9b0
to
a6671c5
Compare
@@ -47,7 +47,7 @@ def asdict(self): | |||
def _add_hidden(self, result): | |||
# Mark an annotation as hidden if it and all of it's children have been | |||
# moderated and hidden. | |||
parents_and_replies = [self.annotation.id] + self.annotation.thread_ids # noqa: RUF005 | |||
parents_and_replies = [self.annotation.id, *self.annotation.thread_ids] | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO This method below is not reading from the new status column yet.
This has the effect that for example single annotation threads are not hidden from the response (but their content is hidden).
(True, ModerationStatus.SPAM, ModerationStatus.SPAM), | ||
], | ||
) | ||
def test_sharing_a_private_annotation( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using func test for this cases that require a visibility change.
This would be the edit view, calling the anno write service and that calling the moderation service.
These same cases need to be unit tested but I wanted to see if everything was wired correctly.
@@ -32,9 +33,43 @@ def create(context, request): | |||
permission=Permission.Annotation.MODERATE, | |||
) | |||
def delete(context, request): | |||
request.find_service(AnnotationWriteService).unhide(context.annotation) | |||
request.find_service(AnnotationWriteService).unhide( | |||
context.annotation, request.user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding user to these to keep the audit log up to date.
def _present_for_user(service, annotation, user): | ||
annotation_json = service.present_for_user(annotation, user, hide_moderated=False) | ||
|
||
annotation_json["moderation_status"] = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is duplicated on the group_annotations endpoint as well. We'll need a better way to deal with this.
def list_annotations(context: GroupContext, request): | ||
group = context.group | ||
params = validate_query_params(PaginationQueryParamsSchema(), request.params) | ||
page_number = params["page[number]"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested any of the pagination.
).scalars() | ||
|
||
return sorted(annotations, key=lambda annotation: ids.index(annotation.id)) | ||
|
||
@staticmethod | ||
def _annotation_search_query( | ||
def annotation_search_query( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making this public and adding a bunch of parameters.
return {m.annotation_id for m in query} | ||
|
||
def set_status( | ||
self, annotation: Annotation, user: User, status: ModerationStatus | None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we set an status (almost) unconditionally.
We log it on the moderation_log table as well.
) | ||
annotation.moderation_status = status | ||
|
||
def update_status( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we might change the status on creation/updates
Hypothesis's ~2017 Moderation Feature
ERD: Moderation
This PR:
Migrate AnnotationModeration
Every existing annotation will start with a null value there.
Creates a new moderation_log table.
Updates the current hide/unhide feature to also take into account the new column.
We'll set it to DENIED when hiding and to APPROVED when "`unhidden".
We'll also create an audit record on moderation_log for it.
Migration to backfill "DENIED" based on the existing AnnotationModeration rows.
TODO Migration to backfill moderation_log based on AnnotationModeration.
TODO switch reads off AnnotationModeration to the new moderation_status.
Pre moderated group creation
pre_moderated
toggle.Moderation queue