Skip to content

Commit c866fb0

Browse files
Improve page & profile deletion
1 parent 8a2621e commit c866fb0

File tree

9 files changed

+63
-56
lines changed

9 files changed

+63
-56
lines changed

config/management/commands/fs2import.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
if TYPE_CHECKING: # pragma: no cover
1919
from argparse import ArgumentParser
20-
from xml.etree.ElementTree import Element # nosec: B405
20+
from xml.etree.ElementTree import Element
2121

2222

2323
class Command(BaseCommand):

docs/modules/config.management.commands.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ config.management.commands package
99
Submodules
1010
----------
1111

12+
config.management.commands.clearcache module
13+
--------------------------------------------
14+
15+
.. automodule:: config.management.commands.clearcache
16+
:members:
17+
:undoc-members:
18+
:show-inheritance:
19+
1220
config.management.commands.createsuperuser module
1321
-------------------------------------------------
1422

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ skips = [
163163
"B104", # 0.0.0.0 is only used in debug mode
164164
"B301", # pickled data is verified with HMAC
165165
"B403", # pickled data is verified with HMAC
166+
"B405", # covered by B314 (xml parse)
166167
"B703", # covered by B308 (mark_safe)
167168
]
168169

reader/models.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
from pathlib import PurePath
1818
from shutil import rmtree
1919
from threading import Lock, Thread
20-
from typing import Any, List, Tuple, Union
21-
from xml.etree import ElementTree as ET # nosec: B405
20+
from typing import Any, Dict, List, Optional, Tuple, Union
21+
from xml.etree import ElementTree as ET
2222
from zipfile import ZipFile
2323

2424
from django.conf import settings
@@ -545,7 +545,7 @@ def comicinfo(self) -> ET.Element:
545545
web.text = f'{scheme}://{domain}{self.get_absolute_url()}'
546546

547547
count = ET.SubElement(root, 'PageCount')
548-
count.text = str(len(self.pages.all()))
548+
count.text = str(self.pages.count())
549549

550550
language = ET.SubElement(root, 'LanguageISO')
551551
language.text = 'en'
@@ -701,6 +701,13 @@ class Meta:
701701
),
702702
)
703703

704+
def delete(self, using: Optional[str] = None,
705+
keep_parents: bool = False) -> Tuple[int, Dict[str, int]]:
706+
if self.image:
707+
# XXX: can't use self.image.delete() for some reason
708+
self.image.storage.delete(self.image.name)
709+
return super().delete()
710+
704711
def get_absolute_url(self) -> str:
705712
"""
706713
Get the absolute URL of the object.

reader/receivers.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -129,19 +129,6 @@ def complete_series(sender: Type[Chapter], instance: Chapter, **kwargs):
129129
instance.series.save(update_fields=('status',))
130130

131131

132-
@receiver(signals.post_delete, sender=Page)
133-
def remove_page(sender: Type[Page], instance: Page, **kwargs):
134-
"""
135-
Receive a signal when a page has been deleted.
136-
137-
Remove the image file of the page.
138-
139-
:param sender: The model class that sent the signal.
140-
:param instance: The instance of the model.
141-
"""
142-
instance.image.storage.delete(instance.image.name)
143-
144-
145132
@receiver(signals.post_save, sender=Chapter)
146133
def clear_chapter_cache(sender: Type[Chapter], instance:
147134
Chapter, created: bool, **kwargs):
@@ -201,5 +188,5 @@ def track_view(sender: Type[WSGIHandler], environ:
201188

202189
__all__ = [
203190
'redirect_series', 'redirect_chapter',
204-
'complete_series', 'remove_page', 'track_view'
191+
'complete_series', 'track_view'
205192
]

reader/tests/test_models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from os.path import exists
2+
13
from django.core.exceptions import ValidationError
24
from django.urls import reverse
35

@@ -178,6 +180,13 @@ def test_create(self):
178180
assert str(page) == 'My Series - 1/0.5 #001'
179181
assert hash(page) > 0
180182

183+
def test_delete(self):
184+
page = self.create_page()
185+
path = page.image.path
186+
assert exists(path)
187+
page.delete()
188+
assert not exists(path)
189+
181190
def test_get_absolute_url(self):
182191
page = self.create_page()
183192
assert page.get_absolute_url() == reverse('reader:page', kwargs={

reader/tests/test_receivers.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from os.path import exists
21
from typing import List, Tuple
32

43
from django.conf import settings
@@ -88,20 +87,6 @@ def test_complete(self):
8887
assert self.series.status == 'completed'
8988

9089

91-
class TestRemovePage(ReaderTestBase):
92-
def setup_method(self):
93-
super().setup_method()
94-
series = Series.objects.create(title='series')
95-
chapter = series.chapters.create(title='Chapter', number=1)
96-
self.page = chapter.pages.create(number=1, image=get_test_image())
97-
98-
def test_delete(self):
99-
path = self.page.image.path
100-
assert exists(path)
101-
self.page.delete()
102-
assert not exists(path)
103-
104-
10590
class TestClearChapterCache(ReaderTestBase):
10691
def setup_method(self):
10792
super().setup_method()

users/models.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from hashlib import blake2b
77
from pathlib import PurePath
88
from secrets import token_hex
9+
from typing import Dict, Optional, Tuple
910

1011
from django.conf import settings
1112
from django.contrib.auth.models import User
@@ -80,6 +81,29 @@ def save(self, *args, **kwargs):
8081
).hexdigest()
8182
super().save(*args, **kwargs)
8283

84+
def delete(self, using: Optional[str] = None,
85+
keep_parents: bool = False) -> Tuple[int, Dict[str, int]]:
86+
"""Delete or anonymize data associated with the user."""
87+
if not keep_parents:
88+
self.user.username = f'ANON-{token_hex(8)}'
89+
self.user.email = ''
90+
self.user.first_name = ''
91+
self.user.last_name = ''
92+
self.user.is_active = False
93+
self.user.last_login = None
94+
self.user.date_joined = dt.fromtimestamp(0, tz=tz.utc)
95+
self.user.save(update_fields=(
96+
'username', 'first_name', 'last_name',
97+
'is_active', 'email', 'date_joined', 'last_login'
98+
))
99+
self.user.bookmarks.all().delete()
100+
self.user.emailaddress_set.all().delete() # type: ignore
101+
self.user.socialaccount_set.all().delete() # type: ignore
102+
ApiKey.objects.filter(user_id=self.user.id).delete()
103+
if self.avatar:
104+
self.avatar.delete()
105+
return super().delete()
106+
83107
def get_absolute_url(self) -> str:
84108
"""
85109
Get the absolute URL of the object.
@@ -97,27 +121,6 @@ def get_directory(self) -> PurePath:
97121
"""
98122
return PurePath('users', str(self.id))
99123

100-
def delete(self):
101-
"""Delete or anonymize data associated with the user."""
102-
self.user.username = f'ANON-{token_hex(8)}'
103-
self.user.email = ''
104-
self.user.first_name = ''
105-
self.user.last_name = ''
106-
self.user.is_active = False
107-
self.user.last_login = None
108-
self.user.date_joined = dt.fromtimestamp(0, tz=tz.utc)
109-
self.user.save(update_fields=(
110-
'username', 'first_name', 'last_name',
111-
'is_active', 'email', 'date_joined', 'last_login'
112-
))
113-
self.user.bookmarks.all().delete()
114-
self.user.emailaddress_set.all().delete()
115-
self.user.socialaccount_set.all().delete()
116-
ApiKey.objects.filter(user_id=self.user.id).delete()
117-
if self.avatar:
118-
self.avatar.delete()
119-
super().delete()
120-
121124
def export(self) -> dict:
122125
"""Export the data associated with the user."""
123126
bookmarks = self.user.bookmarks.values(

users/tests/test_models.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from os.path import exists
2+
13
from django.contrib.auth.models import User
24
from django.db import IntegrityError
35

46
from pytest import raises
57

8+
from MangAdventure.tests.utils import get_test_image
9+
610
from reader.models import Series
711
from users.models import ApiKey, Bookmark, UserProfile, _avatar_uploader
812

@@ -35,7 +39,9 @@ def test_integrity(self):
3539
class TestUserProfile(UsersTestBase):
3640
def setup_method(self):
3741
super().setup_method()
38-
self.profile = UserProfile.objects.create(user=self.user, bio='Test')
42+
self.profile = UserProfile.objects.create(
43+
user=self.user, bio='Test', avatar=get_test_image()
44+
)
3945

4046
def test_create(self):
4147
"""Test object creation & relations."""
@@ -57,8 +63,9 @@ def test_export(self):
5763

5864
def test_delete(self):
5965
"""Test account deletion."""
66+
path = self.profile.avatar.path
6067
self.profile.delete()
61-
self.user.refresh_from_db()
68+
assert not exists(path)
6269
assert self.user.email == ''
6370
assert self.user.first_name == ''
6471
assert self.user.last_name == ''

0 commit comments

Comments
 (0)