2020-02-11 18:17:21 -05:00
|
|
|
''' models for storing different kinds of Activities '''
|
2020-03-29 20:40:51 -04:00
|
|
|
from django.utils import timezone
|
2020-02-11 18:17:21 -05:00
|
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
|
|
from django.db import models
|
|
|
|
from model_utils.managers import InheritanceManager
|
|
|
|
|
2020-09-21 11:10:37 -04:00
|
|
|
from bookwyrm import activitypub
|
2020-09-17 16:09:11 -04:00
|
|
|
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
|
2020-12-13 16:03:17 -05:00
|
|
|
from .base_model import BookWyrmModel
|
2020-11-30 17:24:31 -05:00
|
|
|
from . import fields
|
|
|
|
from .fields import image_serializer
|
2020-11-20 11:14:16 -05:00
|
|
|
|
2020-09-21 11:16:34 -04:00
|
|
|
class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
2020-02-16 21:22:01 -05:00
|
|
|
''' any post, like a reply to a review, etc '''
|
2020-11-30 17:24:31 -05:00
|
|
|
user = fields.ForeignKey(
|
|
|
|
'User', on_delete=models.PROTECT, activitypub_field='attributedTo')
|
2020-12-16 19:47:05 -05:00
|
|
|
content = fields.HtmlField(blank=True, null=True)
|
2020-11-30 17:24:31 -05:00
|
|
|
mention_users = fields.TagField('User', related_name='mention_user')
|
|
|
|
mention_books = fields.TagField('Edition', related_name='mention_book')
|
2020-02-15 17:38:46 -05:00
|
|
|
local = models.BooleanField(default=True)
|
2020-12-12 21:00:39 -05:00
|
|
|
content_warning = fields.CharField(
|
|
|
|
max_length=150, blank=True, null=True, activitypub_field='summary')
|
2020-12-13 16:03:17 -05:00
|
|
|
privacy = fields.PrivacyField(max_length=255)
|
2020-11-30 17:24:31 -05:00
|
|
|
sensitive = fields.BooleanField(default=False)
|
2020-12-13 14:15:42 -05:00
|
|
|
# created date is different than publish date because of federated posts
|
2020-11-30 17:24:31 -05:00
|
|
|
published_date = fields.DateTimeField(
|
|
|
|
default=timezone.now, activitypub_field='published')
|
2020-10-08 15:32:45 -04:00
|
|
|
deleted = models.BooleanField(default=False)
|
2020-10-20 21:50:39 -04:00
|
|
|
deleted_date = models.DateTimeField(blank=True, null=True)
|
2020-02-19 02:26:42 -05:00
|
|
|
favorites = models.ManyToManyField(
|
|
|
|
'User',
|
|
|
|
symmetrical=False,
|
|
|
|
through='Favorite',
|
|
|
|
through_fields=('status', 'user'),
|
|
|
|
related_name='user_favorites'
|
|
|
|
)
|
2020-11-30 17:24:31 -05:00
|
|
|
reply_parent = fields.ForeignKey(
|
2020-02-15 14:13:49 -05:00
|
|
|
'self',
|
|
|
|
null=True,
|
2020-11-30 17:24:31 -05:00
|
|
|
on_delete=models.PROTECT,
|
|
|
|
activitypub_field='inReplyTo',
|
2020-02-15 14:13:49 -05:00
|
|
|
)
|
|
|
|
objects = InheritanceManager()
|
|
|
|
|
2020-09-17 16:02:52 -04:00
|
|
|
activity_serializer = activitypub.Note
|
2020-12-07 21:28:42 -05:00
|
|
|
serialize_reverse_fields = [('attachments', 'attachment')]
|
|
|
|
deserialize_reverse_fields = [('attachments', 'attachment')]
|
2020-09-17 16:02:52 -04:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def replies(cls, status):
|
|
|
|
''' load all replies to a status. idk if there's a better way
|
|
|
|
to write this so it's just a property '''
|
2020-12-13 14:15:42 -05:00
|
|
|
return cls.objects.filter(
|
|
|
|
reply_parent=status
|
|
|
|
).select_subclasses().order_by('published_date')
|
2020-09-17 16:02:52 -04:00
|
|
|
|
2020-09-28 18:57:31 -04:00
|
|
|
@property
|
|
|
|
def status_type(self):
|
|
|
|
''' expose the type of status for the ui using activity type '''
|
|
|
|
return self.activity_serializer.__name__
|
|
|
|
|
2020-09-17 16:02:52 -04:00
|
|
|
def to_replies(self, **kwargs):
|
|
|
|
''' helper function for loading AP serialized replies to a status '''
|
|
|
|
return self.to_ordered_collection(
|
|
|
|
self.replies(self),
|
|
|
|
remote_id='%s/replies' % self.remote_id,
|
|
|
|
**kwargs
|
|
|
|
)
|
2020-05-09 21:55:45 -04:00
|
|
|
|
2020-12-12 21:00:39 -05:00
|
|
|
def to_activity(self, pure=False):# pylint: disable=arguments-differ
|
2020-10-08 15:32:45 -04:00
|
|
|
''' return tombstone if the status is deleted '''
|
|
|
|
if self.deleted:
|
|
|
|
return activitypub.Tombstone(
|
|
|
|
id=self.remote_id,
|
|
|
|
url=self.remote_id,
|
2020-10-30 18:22:20 -04:00
|
|
|
deleted=self.deleted_date.isoformat(),
|
|
|
|
published=self.deleted_date.isoformat()
|
2020-10-08 15:32:45 -04:00
|
|
|
).serialize()
|
2020-11-30 17:24:31 -05:00
|
|
|
activity = ActivitypubMixin.to_activity(self)
|
|
|
|
activity['replies'] = self.to_replies()
|
|
|
|
|
|
|
|
# "pure" serialization for non-bookwyrm instances
|
2020-12-16 18:59:42 -05:00
|
|
|
if pure and hasattr(self, 'pure_content'):
|
2020-11-30 17:24:31 -05:00
|
|
|
activity['content'] = self.pure_content
|
|
|
|
if 'name' in activity:
|
|
|
|
activity['name'] = self.pure_name
|
|
|
|
activity['type'] = self.pure_type
|
|
|
|
activity['attachment'] = [
|
|
|
|
image_serializer(b.cover) for b in self.mention_books.all() \
|
|
|
|
if b.cover]
|
|
|
|
if hasattr(self, 'book'):
|
|
|
|
activity['attachment'].append(
|
|
|
|
image_serializer(self.book.cover)
|
|
|
|
)
|
|
|
|
return activity
|
|
|
|
|
2020-10-08 15:32:45 -04:00
|
|
|
|
2020-11-01 12:16:49 -05:00
|
|
|
def save(self, *args, **kwargs):
|
2020-11-04 19:28:32 -05:00
|
|
|
''' update user active time '''
|
2020-11-24 14:25:07 -05:00
|
|
|
if self.user.local:
|
|
|
|
self.user.last_active_date = timezone.now()
|
|
|
|
self.user.save()
|
|
|
|
return super().save(*args, **kwargs)
|
2020-10-08 15:32:45 -04:00
|
|
|
|
|
|
|
|
2020-10-30 18:22:20 -04:00
|
|
|
class GeneratedNote(Status):
|
2020-09-28 20:26:15 -04:00
|
|
|
''' these are app-generated messages about user activity '''
|
|
|
|
@property
|
2020-11-30 17:24:31 -05:00
|
|
|
def pure_content(self):
|
2020-09-28 20:26:15 -04:00
|
|
|
''' indicate the book in question for mastodon (or w/e) users '''
|
|
|
|
message = self.content
|
|
|
|
books = ', '.join(
|
2020-11-30 17:24:31 -05:00
|
|
|
'<a href="%s">"%s"</a>' % (book.remote_id, book.title) \
|
2020-10-08 15:32:45 -04:00
|
|
|
for book in self.mention_books.all()
|
2020-09-28 20:26:15 -04:00
|
|
|
)
|
2020-11-30 17:24:31 -05:00
|
|
|
return '%s %s %s' % (self.user.display_name, message, books)
|
2020-09-28 20:26:15 -04:00
|
|
|
|
|
|
|
activity_serializer = activitypub.GeneratedNote
|
2020-11-30 17:24:31 -05:00
|
|
|
pure_type = 'Note'
|
2020-09-28 20:26:15 -04:00
|
|
|
|
2020-05-09 21:55:45 -04:00
|
|
|
|
2020-03-21 19:50:49 -04:00
|
|
|
class Comment(Status):
|
|
|
|
''' like a review but without a rating and transient '''
|
2020-12-03 16:14:04 -05:00
|
|
|
book = fields.ForeignKey(
|
|
|
|
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
|
2020-03-21 19:50:49 -04:00
|
|
|
|
2020-05-09 21:55:45 -04:00
|
|
|
@property
|
2020-11-30 17:24:31 -05:00
|
|
|
def pure_content(self):
|
2020-09-17 16:02:52 -04:00
|
|
|
''' indicate the book in question for mastodon (or w/e) users '''
|
|
|
|
return self.content + '<br><br>(comment on <a href="%s">"%s"</a>)' % \
|
2020-11-13 12:47:35 -05:00
|
|
|
(self.book.remote_id, self.book.title)
|
2020-09-17 16:02:52 -04:00
|
|
|
|
|
|
|
activity_serializer = activitypub.Comment
|
2020-11-30 17:24:31 -05:00
|
|
|
pure_type = 'Note'
|
2020-05-09 21:55:45 -04:00
|
|
|
|
|
|
|
|
2020-04-08 12:40:47 -04:00
|
|
|
class Quotation(Status):
|
|
|
|
''' like a review but without a rating and transient '''
|
2020-12-16 19:47:05 -05:00
|
|
|
quote = fields.HtmlField()
|
2020-12-03 16:14:04 -05:00
|
|
|
book = fields.ForeignKey(
|
|
|
|
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
|
2020-03-21 19:50:49 -04:00
|
|
|
|
2020-05-09 21:55:45 -04:00
|
|
|
@property
|
2020-11-30 17:24:31 -05:00
|
|
|
def pure_content(self):
|
2020-09-17 16:02:52 -04:00
|
|
|
''' indicate the book in question for mastodon (or w/e) users '''
|
2020-11-04 17:01:28 -05:00
|
|
|
return '"%s"<br>-- <a href="%s">"%s"</a><br><br>%s' % (
|
2020-09-17 16:02:52 -04:00
|
|
|
self.quote,
|
2020-11-13 12:47:35 -05:00
|
|
|
self.book.remote_id,
|
2020-09-17 16:02:52 -04:00
|
|
|
self.book.title,
|
|
|
|
self.content,
|
|
|
|
)
|
|
|
|
|
|
|
|
activity_serializer = activitypub.Quotation
|
2020-11-30 17:24:31 -05:00
|
|
|
pure_type = 'Note'
|
2020-05-09 21:55:45 -04:00
|
|
|
|
|
|
|
|
2020-02-15 14:13:49 -05:00
|
|
|
class Review(Status):
|
|
|
|
''' a book review '''
|
2020-11-30 17:24:31 -05:00
|
|
|
name = fields.CharField(max_length=255, null=True)
|
2020-12-03 16:14:04 -05:00
|
|
|
book = fields.ForeignKey(
|
|
|
|
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
|
2020-11-30 17:24:31 -05:00
|
|
|
rating = fields.IntegerField(
|
2020-04-01 14:22:22 -04:00
|
|
|
default=None,
|
|
|
|
null=True,
|
|
|
|
blank=True,
|
|
|
|
validators=[MinValueValidator(1), MaxValueValidator(5)]
|
2020-02-15 14:13:49 -05:00
|
|
|
)
|
|
|
|
|
2020-09-17 16:02:52 -04:00
|
|
|
@property
|
2020-11-30 17:24:31 -05:00
|
|
|
def pure_name(self):
|
2020-09-17 16:02:52 -04:00
|
|
|
''' clarify review names for mastodon serialization '''
|
2020-10-26 18:10:32 -04:00
|
|
|
if self.rating:
|
2020-12-13 14:15:42 -05:00
|
|
|
#pylint: disable=bad-string-format-type
|
2020-10-26 18:10:32 -04:00
|
|
|
return 'Review of "%s" (%d stars): %s' % (
|
|
|
|
self.book.title,
|
|
|
|
self.rating,
|
|
|
|
self.name
|
|
|
|
)
|
|
|
|
return 'Review of "%s": %s' % (
|
2020-09-17 16:02:52 -04:00
|
|
|
self.book.title,
|
|
|
|
self.name
|
|
|
|
)
|
2020-03-07 16:29:57 -05:00
|
|
|
|
2020-05-09 21:55:45 -04:00
|
|
|
@property
|
2020-11-30 17:24:31 -05:00
|
|
|
def pure_content(self):
|
2020-09-17 16:02:52 -04:00
|
|
|
''' indicate the book in question for mastodon (or w/e) users '''
|
|
|
|
return self.content + '<br><br>(<a href="%s">"%s"</a>)' % \
|
2020-11-13 12:47:35 -05:00
|
|
|
(self.book.remote_id, self.book.title)
|
2020-09-17 16:02:52 -04:00
|
|
|
|
|
|
|
activity_serializer = activitypub.Review
|
2020-11-30 17:24:31 -05:00
|
|
|
pure_type = 'Article'
|
2020-05-09 21:55:45 -04:00
|
|
|
|
|
|
|
|
2020-09-21 11:16:34 -04:00
|
|
|
class Favorite(ActivitypubMixin, BookWyrmModel):
|
2020-02-19 02:26:42 -05:00
|
|
|
''' fav'ing a post '''
|
2020-11-30 17:24:31 -05:00
|
|
|
user = fields.ForeignKey(
|
|
|
|
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
|
|
|
status = fields.ForeignKey(
|
|
|
|
'Status', on_delete=models.PROTECT, activitypub_field='object')
|
2020-09-17 16:02:52 -04:00
|
|
|
|
|
|
|
activity_serializer = activitypub.Like
|
|
|
|
|
2020-11-01 12:16:49 -05:00
|
|
|
def save(self, *args, **kwargs):
|
2020-11-04 19:28:32 -05:00
|
|
|
''' update user active time '''
|
2020-11-01 12:16:49 -05:00
|
|
|
self.user.last_active_date = timezone.now()
|
|
|
|
self.user.save()
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
2020-02-19 03:22:55 -05:00
|
|
|
class Meta:
|
2020-09-17 16:02:52 -04:00
|
|
|
''' can't fav things twice '''
|
2020-02-19 03:22:55 -05:00
|
|
|
unique_together = ('user', 'status')
|
|
|
|
|
2020-02-21 01:19:19 -05:00
|
|
|
|
2020-03-30 10:13:32 -04:00
|
|
|
class Boost(Status):
|
|
|
|
''' boost'ing a post '''
|
2020-11-30 17:24:31 -05:00
|
|
|
boosted_status = fields.ForeignKey(
|
2020-03-30 10:13:32 -04:00
|
|
|
'Status',
|
|
|
|
on_delete=models.PROTECT,
|
2020-11-30 17:24:31 -05:00
|
|
|
related_name='boosters',
|
|
|
|
activitypub_field='object',
|
|
|
|
)
|
2020-09-17 16:02:52 -04:00
|
|
|
|
2020-12-15 14:15:06 -05:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
''' the user field is "actor" here instead of "attributedTo" '''
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
reserve_fields = ['user', 'boosted_status']
|
|
|
|
self.simple_fields = [f for f in self.simple_fields if \
|
|
|
|
f.name in reserve_fields]
|
|
|
|
self.activity_fields = self.simple_fields
|
|
|
|
self.many_to_many_fields = []
|
|
|
|
self.image_fields = []
|
|
|
|
self.deserialize_reverse_fields = []
|
|
|
|
|
2020-09-28 21:25:05 -04:00
|
|
|
activity_serializer = activitypub.Boost
|
2020-04-01 17:55:32 -04:00
|
|
|
|
2020-03-30 10:13:32 -04:00
|
|
|
# This constraint can't work as it would cross tables.
|
|
|
|
# class Meta:
|
|
|
|
# unique_together = ('user', 'boosted_status')
|
|
|
|
|
2020-09-17 16:02:52 -04:00
|
|
|
|
2020-09-21 11:16:34 -04:00
|
|
|
class ReadThrough(BookWyrmModel):
|
2020-04-02 14:05:10 -04:00
|
|
|
''' Store progress through a book in the database. '''
|
|
|
|
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
2020-12-11 19:57:38 -05:00
|
|
|
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
2020-04-02 14:05:10 -04:00
|
|
|
pages_read = models.IntegerField(
|
|
|
|
null=True,
|
|
|
|
blank=True)
|
|
|
|
start_date = models.DateTimeField(
|
|
|
|
blank=True,
|
|
|
|
null=True)
|
|
|
|
finish_date = models.DateTimeField(
|
|
|
|
blank=True,
|
|
|
|
null=True)
|
|
|
|
|
2020-11-01 12:16:49 -05:00
|
|
|
def save(self, *args, **kwargs):
|
2020-11-04 19:28:32 -05:00
|
|
|
''' update user active time '''
|
2020-11-01 12:16:49 -05:00
|
|
|
self.user.last_active_date = timezone.now()
|
|
|
|
self.user.save()
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
2020-04-02 14:05:10 -04:00
|
|
|
|
2020-03-13 09:38:09 -04:00
|
|
|
NotificationType = models.TextChoices(
|
2020-04-20 12:10:19 -04:00
|
|
|
'NotificationType',
|
2020-11-01 13:13:51 -05:00
|
|
|
'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
|
2020-03-13 09:38:09 -04:00
|
|
|
|
2020-09-21 11:16:34 -04:00
|
|
|
class Notification(BookWyrmModel):
|
2020-03-07 17:50:29 -05:00
|
|
|
''' you've been tagged, liked, followed, etc '''
|
|
|
|
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
|
|
|
related_book = models.ForeignKey(
|
2020-03-30 17:12:18 -04:00
|
|
|
'Edition', on_delete=models.PROTECT, null=True)
|
2020-03-07 17:50:29 -05:00
|
|
|
related_user = models.ForeignKey(
|
|
|
|
'User',
|
|
|
|
on_delete=models.PROTECT, null=True, related_name='related_user')
|
|
|
|
related_status = models.ForeignKey(
|
|
|
|
'Status', on_delete=models.PROTECT, null=True)
|
2020-04-22 07:43:10 -04:00
|
|
|
related_import = models.ForeignKey(
|
|
|
|
'ImportJob', on_delete=models.PROTECT, null=True)
|
2020-03-07 17:50:29 -05:00
|
|
|
read = models.BooleanField(default=False)
|
2020-03-13 09:38:09 -04:00
|
|
|
notification_type = models.CharField(
|
|
|
|
max_length=255, choices=NotificationType.choices)
|
2020-09-17 16:02:52 -04:00
|
|
|
|
2020-03-13 09:38:09 -04:00
|
|
|
class Meta:
|
2020-09-17 16:02:52 -04:00
|
|
|
''' checks if notifcation is in enum list for valid types '''
|
2020-03-13 09:38:09 -04:00
|
|
|
constraints = [
|
|
|
|
models.CheckConstraint(
|
|
|
|
check=models.Q(notification_type__in=NotificationType.values),
|
|
|
|
name="notification_type_valid",
|
|
|
|
)
|
2020-03-07 17:50:29 -05:00
|
|
|
]
|