-
Notifications
You must be signed in to change notification settings - Fork 426
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
Record user deletions in a new DB table #8721
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
def repr_(obj, attrs): | ||
class_name = type(obj).__name__ | ||
attrs = {attrname: getattr(obj, attrname) for attrname in attrs} | ||
return f"{class_name}({', '.join(f'{name}={value!r}' for name, value in attrs.items())})" | ||
Comment on lines
+1
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used this code in a couple of places now so moved it to a helper function. Longer-term I think we should move all h's models to SQLAlchemy's dataclasses support (which we use in Via) then we won't need this. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from datetime import datetime | ||
|
||
from sqlalchemy import func | ||
from sqlalchemy.orm import Mapped, mapped_column | ||
|
||
from h.db import Base | ||
from h.models import helpers | ||
|
||
|
||
class UserDeletion(Base): | ||
"""A record of a user account that was deleted.""" | ||
|
||
__tablename__ = "user_deletion" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||
|
||
userid: Mapped[str] | ||
"""The userid of the user who was deleted.""" | ||
|
||
requested_at: Mapped[datetime] = mapped_column( | ||
server_default=func.now(), # pylint:disable=not-callable | ||
) | ||
"""The time at which the user deletion was requested.""" | ||
|
||
requested_by: Mapped[str] | ||
"""The userid of the user who requested the deletion.""" | ||
|
||
tag: Mapped[str] | ||
"""Just a string 'tag' field. | ||
|
||
For example different views that delete users might put different tag | ||
values here. | ||
""" | ||
|
||
registered_date: Mapped[datetime] | ||
"""The time when the deleted user account was first registered.""" | ||
|
||
num_annotations: Mapped[int] | ||
"""The number of annotations that the deleted user account had.""" | ||
|
||
def __repr__(self) -> str: | ||
return helpers.repr_( | ||
self, | ||
[ | ||
"id", | ||
"userid", | ||
"requested_at", | ||
"requested_by", | ||
"tag", | ||
"registered_date", | ||
"num_annotations", | ||
], | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import sqlalchemy as sa | ||
|
||
from h.models import Annotation, Group, Token, User | ||
from h.models import Annotation, Group, Token, User, UserDeletion | ||
from h.services.annotation_delete import AnnotationDeleteService | ||
|
||
|
||
|
@@ -13,7 +13,7 @@ def __init__( | |
self._db = db_session | ||
self._annotation_delete_service = annotation_delete_service | ||
|
||
def delete_user(self, user: User): | ||
def delete_user(self, user: User, requested_by: User, tag: str): | ||
""" | ||
Delete a user with all their group memberships and annotations. | ||
|
||
|
@@ -46,6 +46,20 @@ def delete_user(self, user: User): | |
else: | ||
self._db.delete(group) | ||
|
||
self._db.add( | ||
UserDeletion( | ||
userid=user.userid, | ||
requested_by=requested_by.userid, | ||
tag=tag, | ||
registered_date=user.registered_date, | ||
num_annotations=self._db.scalar( | ||
sa.select( | ||
sa.func.count(Annotation.id) # pylint:disable=not-callable | ||
).where(Annotation.userid == user.userid) | ||
), | ||
Comment on lines
+55
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Querying the annotation table here. Any chance we might want to move this to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not yet (if you want to get the exact number), just created: #8727 to finish that. |
||
) | ||
) | ||
|
||
self._db.delete(user) | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from factory import Faker, LazyAttribute | ||
|
||
from h import models | ||
|
||
from .base import ModelFactory | ||
|
||
|
||
class UserDeletion(ModelFactory): | ||
class Meta: | ||
model = models.UserDeletion | ||
exclude = ("username", "requesting_username") | ||
|
||
username = Faker("user_name") | ||
userid = LazyAttribute(lambda o: f"acct:{o.username}@example.com") | ||
requesting_username = Faker("user_name") | ||
requested_by = LazyAttribute(lambda o: f"acct:{o.requesting_username}@example.com") | ||
tag = "factory" | ||
registered_date = Faker("date_time") | ||
num_annotations = Faker("random_int") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from unittest.mock import Mock | ||
|
||
from h.models import helpers | ||
|
||
|
||
def test_repr_(): | ||
obj = Mock(foo="FOO", bar="BAR") | ||
|
||
assert helpers.repr_(obj, ["foo", "bar"]) == "Mock(foo='FOO', bar='BAR')" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,29 @@ | ||
from datetime import datetime | ||
import pytest | ||
|
||
from h.models.job import Job | ||
|
||
def test___repr__(factories, helpers): | ||
job = factories.Job() | ||
repr_ = repr(job) | ||
|
||
class TestJob: | ||
def test___repr__(self): | ||
job = Job( | ||
id=42, | ||
name="job_name", | ||
enqueued_at=datetime( | ||
year=2024, | ||
month=5, | ||
day=8, | ||
hour=11, | ||
minute=51, | ||
second=23, | ||
), | ||
scheduled_at=datetime( | ||
year=2024, | ||
month=6, | ||
day=1, | ||
hour=0, | ||
minute=0, | ||
second=0, | ||
), | ||
expires_at=datetime( | ||
year=2025, | ||
month=1, | ||
day=1, | ||
hour=0, | ||
minute=0, | ||
second=0, | ||
), | ||
priority=3, | ||
tag="job_tag", | ||
kwargs={"foo": "FOO", "bar": "BAR"}, | ||
) | ||
helpers.repr_.assert_called_once_with( | ||
job, | ||
[ | ||
"id", | ||
"name", | ||
"enqueued_at", | ||
"scheduled_at", | ||
"expires_at", | ||
"priority", | ||
"tag", | ||
"kwargs", | ||
], | ||
) | ||
assert repr_ == helpers.repr_.return_value | ||
|
||
assert ( | ||
repr(job) | ||
== "Job(id=42, name='job_name', enqueued_at=datetime.datetime(2024, 5, 8, 11, 51, 23), scheduled_at=datetime.datetime(2024, 6, 1, 0, 0), expires_at=datetime.datetime(2025, 1, 1, 0, 0), priority=3, tag='job_tag', kwargs={'foo': 'FOO', 'bar': 'BAR'})" | ||
) | ||
|
||
@pytest.fixture(autouse=True) | ||
def helpers(mocker): | ||
helpers = mocker.patch("h.models.job.helpers") | ||
# __repr__() needs to return a string or repr() raises. | ||
helpers.repr_.return_value = "test_string_representation" | ||
return helpers |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import pytest | ||
|
||
|
||
def test___repr__(factories, helpers): | ||
user_deletion = factories.UserDeletion() | ||
repr_ = repr(user_deletion) | ||
|
||
helpers.repr_.assert_called_once_with( | ||
user_deletion, | ||
[ | ||
"id", | ||
"userid", | ||
"requested_at", | ||
"requested_by", | ||
"tag", | ||
"registered_date", | ||
"num_annotations", | ||
], | ||
) | ||
assert repr_ == helpers.repr_.return_value | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def helpers(mocker): | ||
helpers = mocker.patch("h.models.user_deletion.helpers") | ||
# __repr__() needs to return a string or repr() raises. | ||
helpers.repr_.return_value = "test_string_representation" | ||
return helpers |
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.
Nobody uses this command so I'm just deleting it rather than updating it to write to (or skip) the new user deletions log.