Merge pull request #1449 from bookwyrm-social/model-cleanup

Model cleanup
This commit is contained in:
Mouse Reeve 2021-09-22 11:20:39 -07:00 committed by GitHub
commit d5dba9e66d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 160 additions and 151 deletions

View file

@ -14,7 +14,6 @@ from .status import Review, ReviewRating
from .status import Boost from .status import Boost
from .attachment import Image from .attachment import Image
from .favorite import Favorite from .favorite import Favorite
from .notification import Notification
from .readthrough import ReadThrough, ProgressUpdate, ProgressMode from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
from .user import User, KeyPair, AnnualGoal from .user import User, KeyPair, AnnualGoal
@ -29,6 +28,8 @@ from .site import PasswordReset, InviteRequest
from .announcement import Announcement from .announcement import Announcement
from .antispam import EmailBlocklist, IPBlocklist from .antispam import EmailBlocklist, IPBlocklist
from .notification import Notification
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
activity_models = { activity_models = {
c[1].activity_serializer.__name__: c[1] c[1].activity_serializer.__name__: c[1]

View file

@ -1,7 +1,5 @@
""" like/fav/star a status """ """ like/fav/star a status """
from django.apps import apps
from django.db import models from django.db import models
from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from .activitypub_mixin import ActivityMixin from .activitypub_mixin import ActivityMixin
@ -29,38 +27,9 @@ class Favorite(ActivityMixin, BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
self.user.last_active_date = timezone.now() self.user.update_active_date()
self.user.save(broadcast=False, update_fields=["last_active_date"])
super().save(*args, **kwargs) super().save(*args, **kwargs)
if self.status.user.local and self.status.user != self.user:
notification_model = apps.get_model(
"bookwyrm.Notification", require_ready=True
)
notification_model.objects.create(
user=self.status.user,
notification_type="FAVORITE",
related_user=self.user,
related_status=self.status,
)
def delete(self, *args, **kwargs):
"""delete and delete notifications"""
# check for notification
if self.status.user.local:
notification_model = apps.get_model(
"bookwyrm.Notification", require_ready=True
)
notification = notification_model.objects.filter(
user=self.status.user,
related_user=self.user,
related_status=self.status,
notification_type="FAVORITE",
).first()
if notification:
notification.delete()
super().delete(*args, **kwargs)
class Meta: class Meta:
"""can't fav things twice""" """can't fav things twice"""

View file

@ -2,7 +2,6 @@
import re import re
import dateutil.parser import dateutil.parser
from django.apps import apps
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
@ -50,19 +49,6 @@ class ImportJob(models.Model):
) )
retry = models.BooleanField(default=False) retry = models.BooleanField(default=False)
def save(self, *args, **kwargs):
"""save and notify"""
super().save(*args, **kwargs)
if self.complete:
notification_model = apps.get_model(
"bookwyrm.Notification", require_ready=True
)
notification_model.objects.create(
user=self.user,
notification_type="IMPORT",
related_import=self,
)
class ImportItem(models.Model): class ImportItem(models.Model):
"""a single line of a csv being imported""" """a single line of a csv being imported"""

View file

@ -1,6 +1,8 @@
""" alert a user to activity """ """ alert a user to activity """
from django.db import models from django.db import models
from django.dispatch import receiver
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
from . import Boost, Favorite, ImportJob, Report, Status, User
NotificationType = models.TextChoices( NotificationType = models.TextChoices(
@ -53,3 +55,127 @@ class Notification(BookWyrmModel):
name="notification_type_valid", name="notification_type_valid",
) )
] ]
@receiver(models.signals.post_save, sender=Favorite)
# pylint: disable=unused-argument
def notify_on_fav(sender, instance, *args, **kwargs):
"""someone liked your content, you ARE loved"""
if not instance.status.user.local or instance.status.user == instance.user:
return
Notification.objects.create(
user=instance.status.user,
notification_type="FAVORITE",
related_user=instance.user,
related_status=instance.status,
)
@receiver(models.signals.post_delete, sender=Favorite)
# pylint: disable=unused-argument
def notify_on_unfav(sender, instance, *args, **kwargs):
"""oops, didn't like that after all"""
if not instance.status.user.local:
return
Notification.objects.filter(
user=instance.status.user,
related_user=instance.user,
related_status=instance.status,
notification_type="FAVORITE",
).delete()
@receiver(models.signals.post_save)
# pylint: disable=unused-argument
def notify_user_on_mention(sender, instance, *args, **kwargs):
"""creating and deleting statuses with @ mentions and replies"""
if not issubclass(sender, Status):
return
if instance.deleted:
Notification.objects.filter(related_status=instance).delete()
return
if (
instance.reply_parent
and instance.reply_parent.user != instance.user
and instance.reply_parent.user.local
):
Notification.objects.create(
user=instance.reply_parent.user,
notification_type="REPLY",
related_user=instance.user,
related_status=instance,
)
for mention_user in instance.mention_users.all():
# avoid double-notifying about this status
if not mention_user.local or (
instance.reply_parent and mention_user == instance.reply_parent.user
):
continue
Notification.objects.create(
user=mention_user,
notification_type="MENTION",
related_user=instance.user,
related_status=instance,
)
@receiver(models.signals.post_save, sender=Boost)
# pylint: disable=unused-argument
def notify_user_on_boost(sender, instance, *args, **kwargs):
"""boosting a status"""
if (
not instance.boosted_status.user.local
or instance.boosted_status.user == instance.user
):
return
Notification.objects.create(
user=instance.boosted_status.user,
related_status=instance.boosted_status,
related_user=instance.user,
notification_type="BOOST",
)
@receiver(models.signals.post_delete, sender=Boost)
# pylint: disable=unused-argument
def notify_user_on_unboost(sender, instance, *args, **kwargs):
"""unboosting a status"""
Notification.objects.filter(
user=instance.boosted_status.user,
related_status=instance.boosted_status,
related_user=instance.user,
notification_type="BOOST",
).delete()
@receiver(models.signals.post_save, sender=ImportJob)
# pylint: disable=unused-argument
def notify_user_on_import_complete(sender, instance, *args, **kwargs):
"""we imported your books! aren't you proud of us"""
if not instance.complete:
return
Notification.objects.create(
user=instance.user,
notification_type="IMPORT",
related_import=instance,
)
@receiver(models.signals.post_save, sender=Report)
# pylint: disable=unused-argument
def notify_admins_on_report(sender, instance, *args, **kwargs):
"""something is up, make sure the admins know"""
# moderators and superusers should be notified
admins = User.objects.filter(
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
| models.Q(is_superuser=True)
).all()
for admin in admins:
Notification.objects.create(
user=admin,
related_report=instance,
notification_type="REPORT",
)

View file

@ -2,7 +2,6 @@
from django.core import validators from django.core import validators
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import F, Q
from django.utils import timezone
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -30,8 +29,7 @@ class ReadThrough(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
self.user.last_active_date = timezone.now() self.user.update_active_date()
self.user.save(broadcast=False, update_fields=["last_active_date"])
super().save(*args, **kwargs) super().save(*args, **kwargs)
def create_update(self): def create_update(self):
@ -65,6 +63,5 @@ class ProgressUpdate(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
self.user.last_active_date = timezone.now() self.user.update_active_date()
self.user.save(broadcast=False, update_fields=["last_active_date"])
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

@ -1,5 +1,4 @@
""" flagged for moderation """ """ flagged for moderation """
from django.apps import apps
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import F, Q
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -16,23 +15,6 @@ class Report(BookWyrmModel):
statuses = models.ManyToManyField("Status", blank=True) statuses = models.ManyToManyField("Status", blank=True)
resolved = models.BooleanField(default=False) resolved = models.BooleanField(default=False)
def save(self, *args, **kwargs):
"""notify admins when a report is created"""
super().save(*args, **kwargs)
user_model = apps.get_model("bookwyrm.User", require_ready=True)
# moderators and superusers should be notified
admins = user_model.objects.filter(
Q(user_permissions__name__in=["moderate_user", "moderate_post"])
| Q(is_superuser=True)
).all()
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
for admin in admins:
notification_model.objects.create(
user=admin,
related_report=self,
notification_type="REPORT",
)
class Meta: class Meta:
"""don't let users report themselves""" """don't let users report themselves"""

View file

@ -67,40 +67,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
ordering = ("-published_date",) ordering = ("-published_date",)
def save(self, *args, **kwargs):
"""save and notify"""
super().save(*args, **kwargs)
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
if self.deleted:
notification_model.objects.filter(related_status=self).delete()
return
if (
self.reply_parent
and self.reply_parent.user != self.user
and self.reply_parent.user.local
):
notification_model.objects.create(
user=self.reply_parent.user,
notification_type="REPLY",
related_user=self.user,
related_status=self,
)
for mention_user in self.mention_users.all():
# avoid double-notifying about this status
if not mention_user.local or (
self.reply_parent and mention_user == self.reply_parent.user
):
continue
notification_model.objects.create(
user=mention_user,
notification_type="MENTION",
related_user=self.user,
related_status=self,
)
def delete(self, *args, **kwargs): # pylint: disable=unused-argument def delete(self, *args, **kwargs): # pylint: disable=unused-argument
""" "delete" a status""" """ "delete" a status"""
if hasattr(self, "boosted_status"): if hasattr(self, "boosted_status"):
@ -108,6 +74,10 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
return return
self.deleted = True self.deleted = True
# clear user content
self.content = None
if hasattr(self, "quotation"):
self.quotation = None # pylint: disable=attribute-defined-outside-init
self.deleted_date = timezone.now() self.deleted_date = timezone.now()
self.save() self.save()
@ -386,27 +356,6 @@ class Boost(ActivityMixin, Status):
return return
super().save(*args, **kwargs) super().save(*args, **kwargs)
if not self.boosted_status.user.local or self.boosted_status.user == self.user:
return
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
notification_model.objects.create(
user=self.boosted_status.user,
related_status=self.boosted_status,
related_user=self.user,
notification_type="BOOST",
)
def delete(self, *args, **kwargs):
"""delete and un-notify"""
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
notification_model.objects.filter(
user=self.boosted_status.user,
related_status=self.boosted_status,
related_user=self.user,
notification_type="BOOST",
).delete()
super().delete(*args, **kwargs)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""the user field is "actor" here instead of "attributedTo" """ """the user field is "actor" here instead of "attributedTo" """
@ -419,10 +368,6 @@ class Boost(ActivityMixin, Status):
self.image_fields = [] self.image_fields = []
self.deserialize_reverse_fields = [] self.deserialize_reverse_fields = []
# This constraint can't work as it would cross tables.
# class Meta:
# unique_together = ('user', 'boosted_status')
# pylint: disable=unused-argument # pylint: disable=unused-argument
@receiver(models.signals.post_save) @receiver(models.signals.post_save)

View file

@ -195,6 +195,11 @@ class User(OrderedCollectionPageMixin, AbstractUser):
queryset = queryset.exclude(blocks=viewer) queryset = queryset.exclude(blocks=viewer)
return queryset return queryset
def update_active_date(self):
"""this user is here! they are doing things!"""
self.last_active_date = timezone.now()
self.save(broadcast=False, update_fields=["last_active_date"])
def to_outbox(self, filter_type=None, **kwargs): def to_outbox(self, filter_type=None, **kwargs):
"""an ordered collection of statuses""" """an ordered collection of statuses"""
if filter_type: if filter_type:

View file

@ -101,7 +101,7 @@ class StatusViews(TestCase):
"""@mention a user in a post""" """@mention a user in a post"""
view = views.CreateStatus.as_view() view = views.CreateStatus.as_view()
user = models.User.objects.create_user( user = models.User.objects.create_user(
"rat@%s" % DOMAIN, f"rat@{DOMAIN}",
"rat@rat.com", "rat@rat.com",
"password", "password",
local=True, local=True,
@ -124,7 +124,7 @@ class StatusViews(TestCase):
self.assertEqual(list(status.mention_users.all()), [user]) self.assertEqual(list(status.mention_users.all()), [user])
self.assertEqual(models.Notification.objects.get().user, user) self.assertEqual(models.Notification.objects.get().user, user)
self.assertEqual( self.assertEqual(
status.content, '<p>hi <a href="%s">@rat</a></p>' % user.remote_id status.content, f'<p>hi <a href="{user.remote_id}">@rat</a></p>'
) )
def test_handle_status_reply_with_mentions(self, *_): def test_handle_status_reply_with_mentions(self, *_):
@ -224,13 +224,13 @@ class StatusViews(TestCase):
def test_find_mentions(self, *_): def test_find_mentions(self, *_):
"""detect and look up @ mentions of users""" """detect and look up @ mentions of users"""
user = models.User.objects.create_user( user = models.User.objects.create_user(
"nutria@%s" % DOMAIN, f"nutria@{DOMAIN}",
"nutria@nutria.com", "nutria@nutria.com",
"password", "password",
local=True, local=True,
localname="nutria", localname="nutria",
) )
self.assertEqual(user.username, "nutria@%s" % DOMAIN) self.assertEqual(user.username, f"nutria@{DOMAIN}")
self.assertEqual( self.assertEqual(
list(views.status.find_mentions("@nutria"))[0], ("@nutria", user) list(views.status.find_mentions("@nutria"))[0], ("@nutria", user)
@ -263,19 +263,19 @@ class StatusViews(TestCase):
self.assertEqual(list(views.status.find_mentions("@beep@beep.com")), []) self.assertEqual(list(views.status.find_mentions("@beep@beep.com")), [])
self.assertEqual( self.assertEqual(
list(views.status.find_mentions("@nutria@%s" % DOMAIN))[0], list(views.status.find_mentions(f"@nutria@{DOMAIN}"))[0],
("@nutria@%s" % DOMAIN, user), (f"@nutria@{DOMAIN}", user),
) )
def test_format_links_simple_url(self, *_): def test_format_links_simple_url(self, *_):
"""find and format urls into a tags""" """find and format urls into a tags"""
url = "http://www.fish.com/" url = "http://www.fish.com/"
self.assertEqual( self.assertEqual(
views.status.format_links(url), '<a href="%s">www.fish.com/</a>' % url views.status.format_links(url), f'<a href="{url}">www.fish.com/</a>'
) )
self.assertEqual( self.assertEqual(
views.status.format_links("(%s)" % url), views.status.format_links(f"({url})"),
'(<a href="%s">www.fish.com/</a>)' % url, f'(<a href="{url}">www.fish.com/</a>)',
) )
def test_format_links_paragraph_break(self, *_): def test_format_links_paragraph_break(self, *_):
@ -292,8 +292,8 @@ http://www.fish.com/"""
"""find and format urls into a tags""" """find and format urls into a tags"""
url = "http://www.fish.com/" url = "http://www.fish.com/"
self.assertEqual( self.assertEqual(
views.status.format_links("(%s)" % url), views.status.format_links(f"({url})"),
'(<a href="%s">www.fish.com/</a>)' % url, f'(<a href="{url}">www.fish.com/</a>)',
) )
def test_format_links_special_chars(self, *_): def test_format_links_special_chars(self, *_):
@ -301,27 +301,27 @@ http://www.fish.com/"""
url = "https://archive.org/details/dli.granth.72113/page/n25/mode/2up" url = "https://archive.org/details/dli.granth.72113/page/n25/mode/2up"
self.assertEqual( self.assertEqual(
views.status.format_links(url), views.status.format_links(url),
'<a href="%s">' f'<a href="{url}">'
"archive.org/details/dli.granth.72113/page/n25/mode/2up</a>" % url, "archive.org/details/dli.granth.72113/page/n25/mode/2up</a>",
) )
url = "https://openlibrary.org/search?q=arkady+strugatsky&mode=everything" url = "https://openlibrary.org/search?q=arkady+strugatsky&mode=everything"
self.assertEqual( self.assertEqual(
views.status.format_links(url), views.status.format_links(url),
'<a href="%s">openlibrary.org/search' f'<a href="{url}">openlibrary.org/search'
"?q=arkady+strugatsky&mode=everything</a>" % url, "?q=arkady+strugatsky&mode=everything</a>",
) )
url = "https://tech.lgbt/@bookwyrm" url = "https://tech.lgbt/@bookwyrm"
self.assertEqual( self.assertEqual(
views.status.format_links(url), '<a href="%s">tech.lgbt/@bookwyrm</a>' % url views.status.format_links(url), f'<a href="{url}">tech.lgbt/@bookwyrm</a>'
) )
url = "https://users.speakeasy.net/~lion/nb/book.pdf" url = "https://users.speakeasy.net/~lion/nb/book.pdf"
self.assertEqual( self.assertEqual(
views.status.format_links(url), views.status.format_links(url),
'<a href="%s">users.speakeasy.net/~lion/nb/book.pdf</a>' % url, f'<a href="{url}">users.speakeasy.net/~lion/nb/book.pdf</a>',
) )
url = "https://pkm.one/#/page/The%20Book%20which%20launched%20a%201000%20Note%20taking%20apps" url = "https://pkm.one/#/page/The%20Book%20launched%20a%201000%20Note%20apps"
self.assertEqual( self.assertEqual(
views.status.format_links(url), '<a href="%s">%s</a>' % (url, url[8:]) views.status.format_links(url), f'<a href="{url}">{url[8:]}</a>'
) )
def test_to_markdown(self, *_): def test_to_markdown(self, *_):

View file

@ -3,7 +3,6 @@ from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
@ -54,8 +53,7 @@ class Login(View):
if user is not None: if user is not None:
# successful login # successful login
login(request, user) login(request, user)
user.last_active_date = timezone.now() user.update_active_date()
user.save(broadcast=False, update_fields=["last_active_date"])
if request.POST.get("first_login"): if request.POST.get("first_login"):
return redirect("get-started-profile") return redirect("get-started-profile")
return redirect(request.GET.get("next", "/")) return redirect(request.GET.get("next", "/"))