forked from mirrors/bookwyrm
9940abfd81
This is in preparation for removing a user and their lists when the group owner blocks them. Remove the user via models.group Remove the lists via models.list
179 lines
5.9 KiB
Python
179 lines
5.9 KiB
Python
""" do book related things with other users """
|
|
from django.apps import apps
|
|
from django.db import models, IntegrityError, transaction
|
|
from django.db.models import Q
|
|
from bookwyrm.settings import DOMAIN
|
|
from .base_model import BookWyrmModel
|
|
from . import fields
|
|
from .relationship import UserBlocks
|
|
|
|
class Group(BookWyrmModel):
|
|
"""A group of users"""
|
|
|
|
name = fields.CharField(max_length=100)
|
|
user = fields.ForeignKey("User", on_delete=models.CASCADE)
|
|
description = fields.TextField(blank=True, null=True)
|
|
privacy = fields.PrivacyField()
|
|
|
|
def get_remote_id(self):
|
|
"""don't want the user to be in there in this case"""
|
|
return f"https://{DOMAIN}/group/{self.id}"
|
|
|
|
@classmethod
|
|
def followers_filter(cls, queryset, viewer):
|
|
"""Override filter for "followers" privacy level to allow non-following group members to see the existence of groups and group lists"""
|
|
|
|
return queryset.exclude(
|
|
~Q( # user isn't following and it isn't their own status and they are not a group member
|
|
Q(user__followers=viewer) | Q(user=viewer) | Q(memberships__user=viewer)
|
|
),
|
|
privacy="followers", # and the status of the group is followers only
|
|
)
|
|
|
|
@classmethod
|
|
def direct_filter(cls, queryset, viewer):
|
|
"""Override filter for "direct" privacy level to allow group members to see the existence of groups and group lists"""
|
|
|
|
return queryset.exclude(~Q(memberships__user=viewer), privacy="direct")
|
|
|
|
class GroupMember(models.Model):
|
|
"""Users who are members of a group"""
|
|
|
|
created_date = models.DateTimeField(auto_now_add=True)
|
|
updated_date = models.DateTimeField(auto_now=True)
|
|
group = models.ForeignKey(
|
|
"Group", on_delete=models.CASCADE, related_name="memberships"
|
|
)
|
|
user = models.ForeignKey(
|
|
"User", on_delete=models.CASCADE, related_name="memberships"
|
|
)
|
|
|
|
class Meta:
|
|
"""Users can only have one membership per group"""
|
|
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["group", "user"], name="unique_membership")
|
|
]
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""don't let a user invite someone who blocked them"""
|
|
# blocking in either direction is a no-go
|
|
if UserBlocks.objects.filter(
|
|
Q(
|
|
user_subject=self.group.user,
|
|
user_object=self.user,
|
|
)
|
|
| Q(
|
|
user_subject=self.user,
|
|
user_object=self.group.user,
|
|
)
|
|
).exists():
|
|
raise IntegrityError()
|
|
# accepts and requests are handled by the GroupInvitation model
|
|
super().save(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def from_request(cls, join_request):
|
|
"""converts a join request into a member relationship"""
|
|
|
|
# remove the invite
|
|
join_request.delete()
|
|
|
|
# make a group member
|
|
return cls.objects.create(
|
|
user=join_request.user,
|
|
group=join_request.group,
|
|
)
|
|
|
|
@classmethod
|
|
def remove(cls, owner, user):
|
|
"""remove a user from a group"""
|
|
|
|
memberships = cls.objects.filter(group__user=owner, user=user).all()
|
|
for m in memberships:
|
|
# remove this user
|
|
m.delete()
|
|
|
|
|
|
class GroupMemberInvitation(models.Model):
|
|
"""adding a user to a group requires manual confirmation"""
|
|
|
|
created_date = models.DateTimeField(auto_now_add=True)
|
|
group = models.ForeignKey(
|
|
"Group", on_delete=models.CASCADE, related_name="user_invitations"
|
|
)
|
|
user = models.ForeignKey(
|
|
"User", on_delete=models.CASCADE, related_name="group_invitations"
|
|
)
|
|
|
|
class Meta:
|
|
"""Users can only have one outstanding invitation per group"""
|
|
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["group", "user"], name="unique_invitation")
|
|
]
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""make sure the membership doesn't already exist"""
|
|
# if there's an invitation for a membership that already exists, accept it
|
|
# without changing the local database state
|
|
if GroupMember.objects.filter(user=self.user, group=self.group).exists():
|
|
self.accept()
|
|
return
|
|
|
|
# blocking in either direction is a no-go
|
|
if UserBlocks.objects.filter(
|
|
Q(
|
|
user_subject=self.group.user,
|
|
user_object=self.user,
|
|
)
|
|
| Q(
|
|
user_subject=self.user,
|
|
user_object=self.group.user,
|
|
)
|
|
).exists():
|
|
raise IntegrityError()
|
|
|
|
# make an invitation
|
|
super().save(*args, **kwargs)
|
|
|
|
# now send the invite
|
|
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
|
notification_type = "INVITE"
|
|
model.objects.create(
|
|
user=self.user,
|
|
related_user=self.group.user,
|
|
related_group=self.group,
|
|
notification_type=notification_type,
|
|
)
|
|
|
|
def accept(self):
|
|
"""turn this request into the real deal"""
|
|
|
|
with transaction.atomic():
|
|
GroupMember.from_request(self)
|
|
|
|
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
|
# tell the group owner
|
|
model.objects.create(
|
|
user=self.group.user,
|
|
related_user=self.user,
|
|
related_group=self.group,
|
|
notification_type="ACCEPT",
|
|
)
|
|
|
|
# let the other members know about it
|
|
for membership in self.group.memberships.all():
|
|
member = membership.user
|
|
if member not in (self.user, self.group.user):
|
|
model.objects.create(
|
|
user=member,
|
|
related_user=self.user,
|
|
related_group=self.group,
|
|
notification_type="JOIN",
|
|
)
|
|
|
|
def reject(self):
|
|
"""generate a Reject for this membership request"""
|
|
|
|
self.delete()
|