forked from mirrors/bookwyrm
disambiguate groups and prep for group invitations
- rename Group to BookwyrmGroup - create group memberships and invitations - adjust all model name references accordingly
This commit is contained in:
parent
66494e7788
commit
2f42161dda
10 changed files with 255 additions and 69 deletions
|
@ -297,7 +297,7 @@ class ListForm(CustomForm):
|
|||
|
||||
class GroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Group
|
||||
model = models.BookwyrmGroup
|
||||
fields = ["user", "privacy", "name", "description"]
|
||||
|
||||
class ReportForm(CustomForm):
|
||||
|
|
|
@ -21,7 +21,7 @@ from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
|||
from .report import Report, ReportComment
|
||||
from .federated_server import FederatedServer
|
||||
|
||||
from .group import Group, GroupMember
|
||||
from .group import BookwyrmGroup, BookwyrmGroupMember, GroupMemberInvitation
|
||||
|
||||
from .import_job import ImportJob, ImportItem
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
""" do book related things with other users """
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from django.db import models, IntegrityError, models, transaction
|
||||
from django.db.models import Q
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
from .relationship import UserBlocks
|
||||
# from .user import User
|
||||
|
||||
|
||||
class Group(BookWyrmModel):
|
||||
class BookwyrmGroup(BookWyrmModel):
|
||||
"""A group of users"""
|
||||
|
||||
name = fields.CharField(max_length=100)
|
||||
|
@ -16,27 +16,138 @@ class Group(BookWyrmModel):
|
|||
"User", on_delete=models.PROTECT)
|
||||
description = fields.TextField(blank=True, null=True)
|
||||
privacy = fields.PrivacyField()
|
||||
members = models.ManyToManyField(
|
||||
"User",
|
||||
symmetrical=False,
|
||||
through="GroupMember",
|
||||
through_fields=("group", "user"),
|
||||
related_name="bookwyrm_groups"
|
||||
)
|
||||
|
||||
def get_remote_id(self):
|
||||
"""don't want the user to be in there in this case"""
|
||||
return f"https://{DOMAIN}/group/{self.id}"
|
||||
|
||||
class GroupMember(models.Model):
|
||||
class BookwyrmGroupMember(models.Model):
|
||||
"""Users who are members of a group"""
|
||||
|
||||
group = models.ForeignKey("Group", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey("User", on_delete=models.CASCADE)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
updated_date = models.DateTimeField(auto_now=True)
|
||||
group = models.ForeignKey(
|
||||
"BookwyrmGroup",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="memberships"
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
"User",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="memberships"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["group", "user"], name="unique_member"
|
||||
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 BookwyrmGroupInvitation 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,
|
||||
)
|
||||
|
||||
|
||||
class GroupMemberInvitation(models.Model):
|
||||
"""adding a user to a group requires manual confirmation"""
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
group = models.ForeignKey(
|
||||
"BookwyrmGroup",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="user_invitations"
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
"User",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="group_invitations"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["group", "user"], name="unique_invitation"
|
||||
)
|
||||
]
|
||||
def save(self, *args, **kwargs): # pylint: disable=arguments-differ
|
||||
"""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 BookwyrmGroupMember.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():
|
||||
BookwyrmGroupMember.from_request(self)
|
||||
self.delete()
|
||||
|
||||
# let the other members know about it
|
||||
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||
for member in self.group.members.all:
|
||||
if member != self.user:
|
||||
model.objects.create(
|
||||
user=member,
|
||||
related_user=self.user,
|
||||
related_group=self.group,
|
||||
notification_type="ACCEPT",
|
||||
)
|
||||
|
||||
def reject(self):
|
||||
"""generate a Reject for this membership request"""
|
||||
|
||||
self.delete()
|
||||
|
||||
# TODO: send notification
|
|
@ -35,7 +35,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
|||
max_length=255, default="closed", choices=CurationType.choices
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
"Group",
|
||||
"BookwyrmGroup",
|
||||
on_delete=models.PROTECT,
|
||||
default=None,
|
||||
blank=True,
|
||||
|
@ -101,6 +101,9 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
|||
notification_type="ADD",
|
||||
)
|
||||
|
||||
# TODO: send a notification to all team members except the one who added the book
|
||||
# for team curated lists
|
||||
|
||||
class Meta:
|
||||
"""A book may only be placed into a list once,
|
||||
and each order in the list may be used only once"""
|
||||
|
|
|
@ -7,7 +7,7 @@ from . import Boost, Favorite, ImportJob, Report, Status, User
|
|||
|
||||
NotificationType = models.TextChoices(
|
||||
"NotificationType",
|
||||
"FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT",
|
||||
"FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT INVITE ACCEPT",
|
||||
)
|
||||
|
||||
|
||||
|
@ -19,6 +19,12 @@ class Notification(BookWyrmModel):
|
|||
related_user = models.ForeignKey(
|
||||
"User", on_delete=models.CASCADE, null=True, related_name="related_user"
|
||||
)
|
||||
related_group_member = models.ForeignKey(
|
||||
"User", on_delete=models.CASCADE, null=True, related_name="related_group_member"
|
||||
)
|
||||
related_group = models.ForeignKey(
|
||||
"BookwyrmGroup", on_delete=models.CASCADE, null=True, related_name="notifications"
|
||||
)
|
||||
related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True)
|
||||
related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True)
|
||||
related_list_item = models.ForeignKey(
|
||||
|
@ -37,6 +43,8 @@ class Notification(BookWyrmModel):
|
|||
user=self.user,
|
||||
related_book=self.related_book,
|
||||
related_user=self.related_user,
|
||||
related_group_member=self.related_group_member,
|
||||
related_group=self.related_group,
|
||||
related_status=self.related_status,
|
||||
related_import=self.related_import,
|
||||
related_list_item=self.related_list_item,
|
||||
|
|
|
@ -143,6 +143,11 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
property_fields = [("following_link", "following")]
|
||||
field_tracker = FieldTracker(fields=["name", "avatar"])
|
||||
|
||||
# @property
|
||||
# def bookwyrm_groups(self):
|
||||
# group_ids = bookwyrm_group_membership.values_list("user", flat=True)
|
||||
# return BookwyrmGroup.objects.in_bulk(group_ids).values()
|
||||
|
||||
@property
|
||||
def confirmation_link(self):
|
||||
"""helper for generating confirmation links"""
|
||||
|
|
|
@ -253,10 +253,13 @@ urlpatterns = [
|
|||
re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"),
|
||||
# groups
|
||||
re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"),
|
||||
re_path(r"^group/(?P<group_id>\d+)(.json)?/?$", views.Group.as_view(), name="group"),
|
||||
re_path(r"^group/(?P<group_id>\d+)(.json)?/?$", views.BookwyrmGroup.as_view(), name="group"),
|
||||
re_path(r"^group/(?P<group_id>\d+)/add-users/?$", views.FindUsers.as_view(), name="group-find-users"),
|
||||
re_path(r"^add-group-member/?$", views.add_member, name="add-group-member"),
|
||||
re_path(r"^remove-group-member/?$", views.remove_member, name="remove-group-member"),
|
||||
re_path(r"^add-group-member/?$", views.invite_member, name="invite-group-member"),
|
||||
re_path(r"^add-group-member/?$", views.uninvite_member, name="uninvite-group-member"),
|
||||
re_path(r"^add-group-member/?$", views.uninvite_member, name="uninvite-group-member"),
|
||||
re_path(r"^accept-group-invitation/?$", views.accept_membership, name="accept-group-invitation"),
|
||||
re_path(r"^reject-group-invitation/?$", views.reject_membership, name="reject-group-invitation"),
|
||||
# lists
|
||||
re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"),
|
||||
re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
|
||||
|
|
|
@ -32,7 +32,7 @@ from .follow import follow, unfollow
|
|||
from .follow import accept_follow_request, delete_follow_request
|
||||
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
||||
from .goal import Goal, hide_goal
|
||||
from .group import Group, UserGroups, FindUsers, add_member, remove_member
|
||||
from .group import BookwyrmGroup, UserGroups, FindUsers, invite_member, remove_member, uninvite_member, accept_membership, reject_membership
|
||||
from .import_data import Import, ImportStatus
|
||||
from .inbox import Inbox
|
||||
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
||||
|
|
|
@ -18,13 +18,13 @@ from .helpers import privacy_filter
|
|||
from .helpers import get_user_from_username
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
class Group(View):
|
||||
class BookwyrmGroup(View):
|
||||
"""group page"""
|
||||
|
||||
def get(self, request, group_id):
|
||||
"""display a group"""
|
||||
|
||||
group = get_object_or_404(models.Group, id=group_id)
|
||||
group = get_object_or_404(models.BookwyrmGroup, id=group_id)
|
||||
lists = models.List.objects.filter(group=group).order_by("-updated_date")
|
||||
lists = privacy_filter(request.user, lists)
|
||||
|
||||
|
@ -43,7 +43,7 @@ class Group(View):
|
|||
@method_decorator(login_required, name="dispatch")
|
||||
def post(self, request, group_id):
|
||||
"""edit a group"""
|
||||
user_group = get_object_or_404(models.Group, id=group_id)
|
||||
user_group = get_object_or_404(models.BookwyrmGroup, id=group_id)
|
||||
form = forms.GroupForm(request.POST, instance=user_group)
|
||||
if not form.is_valid():
|
||||
return redirect("group", user_group.id)
|
||||
|
@ -57,11 +57,12 @@ class UserGroups(View):
|
|||
def get(self, request, username):
|
||||
"""display a group"""
|
||||
user = get_user_from_username(request.user, username)
|
||||
groups = models.Group.objects.filter(members=user).order_by("-updated_date")
|
||||
# groups = privacy_filter(request.user, groups)
|
||||
groups = user.bookwyrmgroup_set.all() # follow the relationship backwards, nice
|
||||
paginated = Paginator(groups, 12)
|
||||
|
||||
data = {
|
||||
"groups": paginated.get_page(request.GET.get("page")),
|
||||
"is_self": request.user.id == user.id,
|
||||
"user": user,
|
||||
"group_form": forms.GroupForm(),
|
||||
"path": user.local_path + "/group",
|
||||
|
@ -77,7 +78,7 @@ class UserGroups(View):
|
|||
return redirect(request.user.local_path + "groups")
|
||||
group = form.save()
|
||||
# add the creator as a group member
|
||||
models.GroupMember.objects.create(group=group, user=request.user)
|
||||
models.BookwyrmGroupMember.objects.create(group=group, user=request.user)
|
||||
return redirect("group", group.id)
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -109,21 +110,22 @@ class FindUsers(View):
|
|||
request.user
|
||||
)
|
||||
|
||||
group = get_object_or_404(models.Group, id=group_id)
|
||||
group = get_object_or_404(models.BookwyrmGroup, id=group_id)
|
||||
|
||||
data["suggested_users"] = user_results
|
||||
data["group"] = group
|
||||
data["query"] = query
|
||||
data["requestor_is_manager"] = request.user == group.user
|
||||
data = {
|
||||
"suggested_users": user_results,
|
||||
"group": group,
|
||||
"query": query,
|
||||
"requestor_is_manager": request.user == group.user
|
||||
}
|
||||
return TemplateResponse(request, "groups/find_users.html", data)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def add_member(request):
|
||||
"""add a member to the group"""
|
||||
def invite_member(request):
|
||||
"""invite a member to the group"""
|
||||
|
||||
# TODO: if groups become AP values we need something like get_group_from_group_fullname
|
||||
group = get_object_or_404(models.Group, id=request.POST.get("group"))
|
||||
group = get_object_or_404(models.BookwyrmGroup, id=request.POST.get("group"))
|
||||
if not group:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
@ -135,28 +137,42 @@ def add_member(request):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
models.GroupMember.objects.create(
|
||||
group=group,
|
||||
user=user
|
||||
)
|
||||
models.GroupMemberInvitation.objects.create(
|
||||
user=user,
|
||||
group=group
|
||||
)
|
||||
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
# TODO: actually this needs to be associated with the user ACCEPTING AN INVITE!!! DOH!
|
||||
return redirect(user.local_path)
|
||||
|
||||
"""create a notification too"""
|
||||
# notify all team members when a user is added to the group
|
||||
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||
for team_member in group.members.all():
|
||||
if team_member.local and team_member != request.user:
|
||||
model.objects.create(
|
||||
user=team_member,
|
||||
related_user=request.user,
|
||||
related_group_member=user,
|
||||
related_group=group,
|
||||
notification_type="ADD",
|
||||
)
|
||||
@require_POST
|
||||
@login_required
|
||||
def uninvite_member(request):
|
||||
"""invite a member to the group"""
|
||||
|
||||
group = get_object_or_404(models.BookwyrmGroup, id=request.POST.get("group"))
|
||||
if not group:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
user = get_user_from_username(request.user, request.POST["user"])
|
||||
if not user:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if not group.user == request.user:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
invitation = models.GroupMemberInvitation.objects.get(
|
||||
user=user,
|
||||
group=group
|
||||
)
|
||||
|
||||
invitation.reject()
|
||||
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
return redirect(user.local_path)
|
||||
|
||||
|
@ -168,10 +184,11 @@ def remove_member(request):
|
|||
# TODO: send notification to user telling them they have been removed
|
||||
# TODO: remove yourself from a group!!!! (except owner)
|
||||
# FUTURE TODO: transfer ownership of group
|
||||
# THIS LOGIC SHOULD BE IN MODEL
|
||||
|
||||
# TODO: if groups become AP values we need something like get_group_from_group_fullname
|
||||
# group = get_object_or_404(models.Group, id=request.POST.get("group")) # NOTE: does this not work?
|
||||
group = models.Group.objects.get(id=request.POST["group"])
|
||||
group = models.BookwyrmGroup.objects.get(id=request.POST["group"])
|
||||
if not group:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
@ -183,11 +200,52 @@ def remove_member(request):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
membership = models.GroupMember.objects.get(group=group,user=user)
|
||||
membership = models.BookwyrmGroupMember.objects.get(group=group,user=user) # BUG: wrong
|
||||
membership.delete()
|
||||
|
||||
except IntegrityError:
|
||||
print("no integrity")
|
||||
pass
|
||||
|
||||
return redirect(user.local_path)
|
||||
return redirect(user.local_path)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def accept_membership(request):
|
||||
"""accept an invitation to join a group"""
|
||||
|
||||
group = models.BookwyrmGroup.objects.get(id=request.POST["group"])
|
||||
if not group:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
invite = models.GroupMemberInvitation.objects.get(group=group,user=request.user)
|
||||
if not invite:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
invite.accept()
|
||||
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
return redirect(request.user.local_path)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def reject_membership(request):
|
||||
"""reject an invitation to join a group"""
|
||||
|
||||
group = models.BookwyrmGroup.objects.get(id=request.POST["group"])
|
||||
if not group:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
invite = models.GroupMemberInvitation.objects.get(group=group,user=request.user)
|
||||
if not invite:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
invite.reject()
|
||||
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
return redirect(request.user.local_path)
|
|
@ -1,5 +1,4 @@
|
|||
""" non-interactive pages """
|
||||
from bookwyrm.models.group import GroupMember
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.shortcuts import redirect
|
||||
|
@ -83,7 +82,6 @@ class User(View):
|
|||
data = {
|
||||
"user": user,
|
||||
"is_self": is_self,
|
||||
"has_groups": models.GroupMember.objects.filter(user=user).exists(),
|
||||
"shelves": shelf_preview,
|
||||
"shelf_count": shelves.count(),
|
||||
"activities": paginated.get_page(request.GET.get("page", 1)),
|
||||
|
@ -142,7 +140,7 @@ class Groups(View):
|
|||
user = get_user_from_username(request.user, username)
|
||||
|
||||
paginated = Paginator(
|
||||
GroupMember.objects.filter(user=user)
|
||||
models.BookwyrmGroup.memberships.filter(user=user)
|
||||
)
|
||||
data = {
|
||||
"user": user,
|
||||
|
|
Loading…
Reference in a new issue