From 3e3c90ec0359c523a061900ee81e913e7a299804 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 07:49:25 +1000 Subject: [PATCH 001/127] add views for groups --- bookwyrm/views/__init__.py | 1 + bookwyrm/views/group.py | 51 ++++++++++++++++++++++++++++++++++++++ bookwyrm/views/user.py | 17 +++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 bookwyrm/views/group.py diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 16ebb2bad..5776106bd 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -32,6 +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 UserGroups from .import_data import Import, ImportStatus from .inbox import Inbox from .interaction import Favorite, Unfavorite, Boost, Unboost diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py new file mode 100644 index 000000000..7be10a917 --- /dev/null +++ b/bookwyrm/views/group.py @@ -0,0 +1,51 @@ +"""group views""" +from typing import Optional +from urllib.parse import urlencode + +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django.core.paginator import Paginator +from django.db import IntegrityError, transaction +from django.db.models import Avg, Count, DecimalField, Q, Max +from django.db.models.functions import Coalesce +from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse +from django.shortcuts import get_object_or_404, redirect +from django.template.response import TemplateResponse +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.http import require_POST + +from bookwyrm import forms, models +from bookwyrm.connectors import connector_manager +from bookwyrm.settings import PAGE_LENGTH +from .helpers import is_api_request, privacy_filter +from .helpers import get_user_from_username + +@method_decorator(login_required, name="dispatch") +class UserGroups(View): + """a user's groups page""" + + def get(self, request, username): + """display a group""" + user = get_user_from_username(request.user, username) + groups = models.GroupMember.objects.filter(user=user) + # groups = privacy_filter(request.user, groups) + paginated = Paginator(groups, 12) + + data = { + "user": user, + "is_self": request.user.id == user.id, + "groups": paginated.get_page(request.GET.get("page")), + "group_form": forms.GroupsForm(), + "path": user.local_path + "/groups", + } + return TemplateResponse(request, "user/groups.html", data) + +# @require_POST +# @login_required +# def save_list(request, group_id): +# """save a group""" +# group = get_object_or_404(models.Group, id=group_id) +# request.user.saved_group.add(group) +# return redirect("user", request.user.id) # TODO: change this to group page \ No newline at end of file diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index ca6eb0a52..63194ceb7 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -1,4 +1,5 @@ """ 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 @@ -132,6 +133,22 @@ class Following(View): } return TemplateResponse(request, "user/relationships/following.html", data) +class Groups(View): + """list of user's groups view""" + + def get(self, request, username): + """list of groups""" + user = get_user_from_username(request.user, username) + + paginated = Paginator( + GroupMember.objects.filter(user=user) + ) + data = { + "user": user, + "is_self": request.user.id == user.id, + "group_list": paginated.get_page(request.GET.get("page")), + } + return TemplateResponse(request, "user/groups.html", data) @require_POST @login_required From b74cd3709629f52c7c0dce1a321888fd7c7b21d1 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 07:49:54 +1000 Subject: [PATCH 002/127] add models for groups --- bookwyrm/models/__init__.py | 2 ++ bookwyrm/models/group.py | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 bookwyrm/models/group.py diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index bffd62b45..2774f081d 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -21,6 +21,8 @@ from .relationship import UserFollows, UserFollowRequest, UserBlocks from .report import Report, ReportComment from .federated_server import FederatedServer +from .group import Group, GroupList, GroupMember + from .import_job import ImportJob, ImportItem from .site import SiteSettings, SiteInvite diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py new file mode 100644 index 000000000..a91c56cb1 --- /dev/null +++ b/bookwyrm/models/group.py @@ -0,0 +1,45 @@ +""" do book related things with other users """ +from django.apps import apps +from django.db import models +from django.utils import timezone + +from bookwyrm.settings import DOMAIN +from .base_model import BookWyrmModel +from . import fields + + +class Group(BookWyrmModel): + """A group of users""" + + name = fields.CharField(max_length=100) + manager = fields.ForeignKey( + "User", on_delete=models.PROTECT) + description = fields.TextField(blank=True, null=True) + privacy = fields.PrivacyField() + + lists = models.ManyToManyField( + "List", + symmetrical=False, + through="GroupList", + through_fields=("group", "book_list"), + ) + + members = models.ManyToManyField( + "User", + symmetrical=False, + through="GroupMember", + through_fields=("group", "user"), + related_name="members" + ) + +class GroupList(BookWyrmModel): + """Lists that group members can edit""" + + group = models.ForeignKey("Group", on_delete=models.CASCADE) + book_list = models.ForeignKey("List", on_delete=models.CASCADE) + +class GroupMember(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) \ No newline at end of file From 71b1c6117ce3ec26236f87ac7848c95e9a83ff93 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 07:50:57 +1000 Subject: [PATCH 003/127] update templates for groups --- bookwyrm/templates/user/groups.html | 42 +++++++++++++++++++++++++++++ bookwyrm/templates/user/layout.html | 6 +++++ 2 files changed, 48 insertions(+) create mode 100644 bookwyrm/templates/user/groups.html diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html new file mode 100644 index 000000000..edbba8496 --- /dev/null +++ b/bookwyrm/templates/user/groups.html @@ -0,0 +1,42 @@ +{% extends 'user/layout.html' %} +{% load i18n %} + +{% block header %} +
+
+

+ {% if is_self %} + {% trans "Your Groups" %} + {% else %} + {% blocktrans with username=user.display_name %}Groups: {{ username }}{% endblocktrans %} + {% endif %} +

+
+ {% if is_self %} +
+ {% trans "Create group" as button_text %} + {% include 'snippets/toggle/open_button.html' with controls_text="create_group" icon_with_text="plus" text=button_text %} +
+ {% endif %} +
+{% endblock %} + + +{% block panel %} +
+ + + +
+
+ +
+{% endblock %} diff --git a/bookwyrm/templates/user/layout.html b/bookwyrm/templates/user/layout.html index 8ca3bd180..22b8e2ce4 100755 --- a/bookwyrm/templates/user/layout.html +++ b/bookwyrm/templates/user/layout.html @@ -75,6 +75,12 @@ {% trans "Lists" %} {% endif %} + {% if is_self or user.groups_set.exists %} + {% url 'user-groups' user|username as url %} + + {% trans "Groups" %} + + {% endif %} {% if user.shelf_set.exists %} {% url 'user-shelves' user|username as url %} From 99b533510a228469563eb4d6cc4e4f6ad78eb2dd Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 07:51:51 +1000 Subject: [PATCH 004/127] add group templates --- bookwyrm/templates/groups/create_form.html | 12 ++++++++ bookwyrm/templates/groups/form.html | 34 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 bookwyrm/templates/groups/create_form.html create mode 100644 bookwyrm/templates/groups/form.html diff --git a/bookwyrm/templates/groups/create_form.html b/bookwyrm/templates/groups/create_form.html new file mode 100644 index 000000000..d146a922e --- /dev/null +++ b/bookwyrm/templates/groups/create_form.html @@ -0,0 +1,12 @@ +{% extends 'components/inline_form.html' %} +{% load i18n %} + +{% block header %} +{% trans "Create Group" %} +{% endblock %} + +{% block form %} +
+ {% include 'group/form.html' %} +
+{% endblock %} diff --git a/bookwyrm/templates/groups/form.html b/bookwyrm/templates/groups/form.html new file mode 100644 index 000000000..fb53e60fd --- /dev/null +++ b/bookwyrm/templates/groups/form.html @@ -0,0 +1,34 @@ +{% load i18n %} +{% csrf_token %} + + +
+
+
+ + {{ group_form.name }} +
+
+ + {{ group_form.description }} +
+
+
+
+
+
+
+ {% include 'snippets/privacy_select.html' with current=group.privacy %} +
+
+ +
+
+
+ {% if group.id %} +
+ {% trans "Delete group" as button_text %} + {% include 'snippets/toggle/toggle_button.html' with class="is-danger" text=button_text icon_with_text="x" controls_text="delete_group" controls_uid=group.id focus="modal_title_delete_group" %} +
+ {% endif %} +
From e07a25e288f33628afe6962f7d65647c56b59d35 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 07:52:40 +1000 Subject: [PATCH 005/127] add groups urls --- bookwyrm/urls.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index d54347f0f..759350cca 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -253,6 +253,9 @@ urlpatterns = [ re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"), # lists re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"), + re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"), + re_path(r"^group/?$", views.UserGroups.as_view(), name="groups"), + # re_path(r"^save-group/(?P\d+)/?$", views.save_group, name="group-save"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"), re_path(r"^list/(?P\d+)(.json)?/?$", views.List.as_view(), name="list"), From 4e93b09067bc50df73c45f179689bd1936296096 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 14:12:36 +1000 Subject: [PATCH 006/127] create group form adds a group creation form to user dashboard --- bookwyrm/forms.py | 4 ++ bookwyrm/templates/groups/create_form.html | 3 +- bookwyrm/templates/groups/form.html | 8 ++-- bookwyrm/templates/user/groups.html | 4 +- bookwyrm/urls.py | 5 ++- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/group.py | 44 ++++++++++++++++------ 7 files changed, 49 insertions(+), 21 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 23063ff7c..ceff1b2a7 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -295,6 +295,10 @@ class ListForm(CustomForm): model = models.List fields = ["user", "name", "description", "curation", "privacy"] +class GroupForm(CustomForm): + class Meta: + model = models.Group + fields = ["name", "description", "privacy"] class ReportForm(CustomForm): class Meta: diff --git a/bookwyrm/templates/groups/create_form.html b/bookwyrm/templates/groups/create_form.html index d146a922e..b469ce00d 100644 --- a/bookwyrm/templates/groups/create_form.html +++ b/bookwyrm/templates/groups/create_form.html @@ -6,7 +6,8 @@ {% endblock %} {% block form %} -
+ {% include 'group/form.html' %}
{% endblock %} + diff --git a/bookwyrm/templates/groups/form.html b/bookwyrm/templates/groups/form.html index fb53e60fd..e640fd26b 100644 --- a/bookwyrm/templates/groups/form.html +++ b/bookwyrm/templates/groups/form.html @@ -5,16 +5,16 @@
- + {{ group_form.name }}
- + {{ group_form.description }}
-
+ diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html index edbba8496..3a5b9c76b 100644 --- a/bookwyrm/templates/user/groups.html +++ b/bookwyrm/templates/user/groups.html @@ -34,9 +34,9 @@ {% include 'groups/form.html' %} - + {% include 'groups/group_items.html' with groups=groups %}
- + {% include 'snippets/pagination.html' with page=user_groups path=path %}
{% endblock %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 759350cca..bc50bb09a 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -254,8 +254,9 @@ urlpatterns = [ # lists re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"), re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"), - re_path(r"^group/?$", views.UserGroups.as_view(), name="groups"), - # re_path(r"^save-group/(?P\d+)/?$", views.save_group, name="group-save"), + re_path(r"^groups/?$", views.UserGroups.as_view(), name="groups"), + # re_path(r"^group/?$", views.Group.as_view(), name="group"), + re_path(r"^create-shelf/?$", views.create_shelf, name="group-create"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"), re_path(r"^list/(?P\d+)(.json)?/?$", views.List.as_view(), name="list"), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 5776106bd..50f3bf4f1 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -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 UserGroups +from .group import Group, UserGroups from .import_data import Import, ImportStatus from .inbox import Inbox from .interaction import Favorite, Unfavorite, Boost, Unboost diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 7be10a917..f90537b85 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -19,9 +19,39 @@ from django.views.decorators.http import require_POST from bookwyrm import forms, models from bookwyrm.connectors import connector_manager from bookwyrm.settings import PAGE_LENGTH -from .helpers import is_api_request, privacy_filter +from .helpers import privacy_filter from .helpers import get_user_from_username +class Group(View): + """group page""" + + def get(self, request): + """display a group""" + + groups = models.Group.objects.query(members__contains=request.user) + groups = privacy_filter( + request.user, groups, privacy_levels=["public", "followers"] + ) + + paginated = Paginator(groups, 12) + data = { + "lists": paginated.get_page(request.GET.get("page")), + "list_form": forms.GroupForm(), + "path": "/group", + } + return TemplateResponse(request, "groups/group.html", data) + + @method_decorator(login_required, name="dispatch") + # pylint: disable=unused-argument + def post(self, request): + """create a book_list""" + form = forms.ListForm(request.POST) + if not form.is_valid(): + return redirect("lists") + book_list = form.save() + + return redirect(book_list.local_path) + @method_decorator(login_required, name="dispatch") class UserGroups(View): """a user's groups page""" @@ -30,22 +60,14 @@ class UserGroups(View): """display a group""" user = get_user_from_username(request.user, username) groups = models.GroupMember.objects.filter(user=user) - # groups = privacy_filter(request.user, groups) + groups = privacy_filter(request.user, groups) paginated = Paginator(groups, 12) data = { "user": user, "is_self": request.user.id == user.id, "groups": paginated.get_page(request.GET.get("page")), - "group_form": forms.GroupsForm(), + "group_form": forms.GroupForm(), "path": user.local_path + "/groups", } return TemplateResponse(request, "user/groups.html", data) - -# @require_POST -# @login_required -# def save_list(request, group_id): -# """save a group""" -# group = get_object_or_404(models.Group, id=group_id) -# request.user.saved_group.add(group) -# return redirect("user", request.user.id) # TODO: change this to group page \ No newline at end of file From f32a2cc4d00d954732ca3a11de7eb735ddd0182d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 15:04:52 +1000 Subject: [PATCH 007/127] group creation form can now be submitted! Whoops --- bookwyrm/templates/groups/create_form.html | 2 +- bookwyrm/templates/groups/form.html | 8 ++++---- bookwyrm/views/group.py | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bookwyrm/templates/groups/create_form.html b/bookwyrm/templates/groups/create_form.html index b469ce00d..103ce223a 100644 --- a/bookwyrm/templates/groups/create_form.html +++ b/bookwyrm/templates/groups/create_form.html @@ -6,7 +6,7 @@ {% endblock %} {% block form %} -
+ {% include 'group/form.html' %}
{% endblock %} diff --git a/bookwyrm/templates/groups/form.html b/bookwyrm/templates/groups/form.html index e640fd26b..38df17a6a 100644 --- a/bookwyrm/templates/groups/form.html +++ b/bookwyrm/templates/groups/form.html @@ -14,12 +14,12 @@
-
@@ -31,4 +31,4 @@ {% include 'snippets/toggle/toggle_button.html' with class="is-danger" text=button_text icon_with_text="x" controls_text="delete_group" controls_uid=group.id focus="modal_title_delete_group" %} {% endif %} - --> + diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index f90537b85..83cc3b05a 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -59,8 +59,9 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - groups = models.GroupMember.objects.filter(user=user) - groups = privacy_filter(request.user, groups) + # groups = models.GroupMember.objects.filter(user=user) + groups = models.Group.objects.filter(members=request.user) + # groups = privacy_filter(request.user, groups) paginated = Paginator(groups, 12) data = { From 9b6d2a9d88b8280527a284a071a6406f7f212532 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 20:34:11 +1000 Subject: [PATCH 008/127] add group page --- bookwyrm/forms.py | 2 +- bookwyrm/templates/groups/create_form.html | 13 ----- bookwyrm/templates/groups/created_text.html | 6 +++ .../templates/groups/delete_group_modal.html | 21 ++++++++ bookwyrm/templates/groups/edit_form.html | 13 +++++ bookwyrm/templates/groups/form.html | 5 +- bookwyrm/templates/groups/layout.html | 32 +++++++++++++ bookwyrm/templates/user/groups.html | 2 +- bookwyrm/urls.py | 7 ++- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/group.py | 48 ++++++++++++------- 11 files changed, 112 insertions(+), 39 deletions(-) delete mode 100644 bookwyrm/templates/groups/create_form.html create mode 100644 bookwyrm/templates/groups/created_text.html create mode 100644 bookwyrm/templates/groups/delete_group_modal.html create mode 100644 bookwyrm/templates/groups/edit_form.html create mode 100644 bookwyrm/templates/groups/layout.html diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index ceff1b2a7..0987924ed 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -298,7 +298,7 @@ class ListForm(CustomForm): class GroupForm(CustomForm): class Meta: model = models.Group - fields = ["name", "description", "privacy"] + fields = ["manager", "privacy", "name", "description"] class ReportForm(CustomForm): class Meta: diff --git a/bookwyrm/templates/groups/create_form.html b/bookwyrm/templates/groups/create_form.html deleted file mode 100644 index 103ce223a..000000000 --- a/bookwyrm/templates/groups/create_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'components/inline_form.html' %} -{% load i18n %} - -{% block header %} -{% trans "Create Group" %} -{% endblock %} - -{% block form %} -
- {% include 'group/form.html' %} -
-{% endblock %} - diff --git a/bookwyrm/templates/groups/created_text.html b/bookwyrm/templates/groups/created_text.html new file mode 100644 index 000000000..e7409942a --- /dev/null +++ b/bookwyrm/templates/groups/created_text.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% spaceless %} + +{% blocktrans with username=group.manager.display_name path=group.manager.local_path %}Managed by {{ username }}{% endblocktrans %} + +{% endspaceless %} diff --git a/bookwyrm/templates/groups/delete_group_modal.html b/bookwyrm/templates/groups/delete_group_modal.html new file mode 100644 index 000000000..ff6593e50 --- /dev/null +++ b/bookwyrm/templates/groups/delete_group_modal.html @@ -0,0 +1,21 @@ +{% extends 'components/modal.html' %} +{% load i18n %} + +{% block modal-title %}{% trans "Delete this group?" %}{% endblock %} + +{% block modal-body %} +{% trans "This action cannot be un-done" %} +{% endblock %} + +{% block modal-footer %} +
+ {% csrf_token %} + + + {% trans "Cancel" as button_text %} + {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="delete_list" controls_uid=list.id %} +
+{% endblock %} + diff --git a/bookwyrm/templates/groups/edit_form.html b/bookwyrm/templates/groups/edit_form.html new file mode 100644 index 000000000..1c58dc77e --- /dev/null +++ b/bookwyrm/templates/groups/edit_form.html @@ -0,0 +1,13 @@ +{% extends 'components/inline_form.html' %} +{% load i18n %} + +{% block header %} +{% trans "Edit Group" %} +{% endblock %} + +{% block form %} +
+ {% include 'groups/form.html' %} +
+{% include "groups/delete_group_modal.html" with controls_text="delete_group" controls_uid=group.id %} +{% endblock %} diff --git a/bookwyrm/templates/groups/form.html b/bookwyrm/templates/groups/form.html index 38df17a6a..f764db6f9 100644 --- a/bookwyrm/templates/groups/form.html +++ b/bookwyrm/templates/groups/form.html @@ -1,9 +1,12 @@ {% load i18n %} {% csrf_token %} - +{{ group_form.non_field_errors }}
+
+ +
{{ group_form.name }} diff --git a/bookwyrm/templates/groups/layout.html b/bookwyrm/templates/groups/layout.html new file mode 100644 index 000000000..03a957d0a --- /dev/null +++ b/bookwyrm/templates/groups/layout.html @@ -0,0 +1,32 @@ +{% extends 'layout.html' %} +{% load i18n %} + +{% block title %}{{ group.name }}{% endblock %} + +{% block content %} +
+
+

{{ group.name }} {% include 'snippets/privacy-icons.html' with item=group %}

+

+ {% include 'groups/created_text.html' with group=group %} +

+
+
+ {% if request.user == group.manager %} + {% trans "Edit group" as button_text %} + {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_group" focus="edit_group_header" %} + {% endif %} +
+
+ +
+ {% include 'snippets/trimmed_text.html' with full=group.description %} +
+ +
+ {% include 'groups/edit_form.html' with controls_text="edit_group" %} +
+ +{% block panel %}{% endblock %} + +{% endblock %} diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html index 3a5b9c76b..39e09bc19 100644 --- a/bookwyrm/templates/user/groups.html +++ b/bookwyrm/templates/user/groups.html @@ -24,7 +24,7 @@ {% block panel %}
-
+ +
+{% endblock %} From 86a60d58e5284df5d7ab5bc20ba8ebd673936467 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 21:24:06 +1000 Subject: [PATCH 010/127] add user cards to group pages --- bookwyrm/templates/groups/group.html | 27 +++++++++++++++++++++------ bookwyrm/views/group.py | 15 ++------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index a8d65f7fc..01a10a39a 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -13,19 +13,34 @@
{% endif %} - {% if not members.object_list.exists %} + {% if not group.members.exists %}

{% trans "This group has no members" %}

{% else %} -
    - {% for member in members %} +

    Group Members

    +
      + {% for member in group.members.all %}
    • +
      + {% include 'directory/user_card.html' %} +
      +
    • + {% endfor %} +
    + {% endif %} + {% if not group.lists.exists %} +

    {% trans "This group has no lists" %}

    + {% else %} +

    Lists

    +
      + {% for list in group.lists.all %} +
    • +
      -

      member.username

      -
      + {{ list.name }}
{% endfor %} - + {% endif %} {% include "snippets/pagination.html" with page=items %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index ba5c251a9..59d3e8d1e 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -41,17 +41,6 @@ class Group(View): } return TemplateResponse(request, "groups/group.html", data) - # @method_decorator(login_required, name="dispatch") - # # pylint: disable=unused-argument - # def post(self, request): - # """create a book_list""" - # form = forms.ListForm(request.POST) - # if not form.is_valid(): - # return redirect("lists") - # book_list = form.save() - - # return redirect(book_list.local_path) - @method_decorator(login_required, name="dispatch") class UserGroups(View): """a user's groups page""" @@ -59,9 +48,7 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - # groups = models.GroupMember.objects.filter(user=user) groups = models.Group.objects.filter(members=user) - # groups = privacy_filter(request.user, groups) paginated = Paginator(groups, 12) data = { @@ -83,4 +70,6 @@ def create_group(request): return redirect(request.headers.get("Referer", "/")) group = form.save() + # TODO: add user as group member + models.GroupMember.objects.create(group=group, user=request.user) return redirect(group.local_path) \ No newline at end of file From d4fcf88cf5357c525ba9c76b962fd9e944f1bae3 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 24 Sep 2021 21:57:01 +1000 Subject: [PATCH 011/127] add list cards to groups page - add list cards to groups page based on lists page - add sort to members on group page --- bookwyrm/templates/groups/group.html | 40 +++++++++++++++++++++++++--- bookwyrm/views/group.py | 2 +- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index 01a10a39a..c14694154 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -34,10 +34,44 @@ diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 59d3e8d1e..fd4e7f2cd 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -48,7 +48,7 @@ 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) + groups = models.Group.objects.filter(members=user).order_by("-updated_date") paginated = Paginator(groups, 12) data = { From 273ad9a4664bfcb4f26281918da9a004bcc36fa8 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 10:55:32 +1000 Subject: [PATCH 012/127] add create_group to __init__.py you probably want this otherwise nothing previously added for group creation will work :-) --- bookwyrm/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 6f55159df..7969ef7ac 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -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, create_group +from .group import Group, UserGroups, FindAndAddUsers, create_group from .import_data import Import, ImportStatus from .inbox import Inbox from .interaction import Favorite, Unfavorite, Boost, Unboost From 8c326ec52fc46bb0a84e392058c520b3ca6b7ad0 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 11:10:06 +1000 Subject: [PATCH 013/127] user groups listing template - creates groups/user_groups template for listing a user's groups on their user page --- bookwyrm/templates/groups/user_groups.html | 35 ++++++++++++++++++++++ bookwyrm/templates/user/groups.html | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/templates/groups/user_groups.html diff --git a/bookwyrm/templates/groups/user_groups.html b/bookwyrm/templates/groups/user_groups.html new file mode 100644 index 000000000..9c48a8429 --- /dev/null +++ b/bookwyrm/templates/groups/user_groups.html @@ -0,0 +1,35 @@ +{% load i18n %} +{% load markdown %} +{% load interaction %} + +
+ {% for group in groups %} +
+
+
+

+ {{ group.name }} {% include 'snippets/privacy-icons.html' with item=group %} +

+ {% if request.user.is_authenticated and request.user|saved:list %} +
+ {% trans "Saved" as text %} + + {{ text }} + +
+ {% endif %} +
+ +
+
+ {% if group.description %} + {{ group.description|to_markdown|safe|truncatechars_html:30 }} + {% else %} +   + {% endif %} +
+
+
+
+ {% endfor %} +
diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html index 39e09bc19..912d5ec3d 100644 --- a/bookwyrm/templates/user/groups.html +++ b/bookwyrm/templates/user/groups.html @@ -34,7 +34,7 @@ {% include 'groups/form.html' %} - {% include 'groups/group_items.html' with groups=groups %} + {% include 'groups/user_groups.html' with groups=groups %}
{% include 'snippets/pagination.html' with page=user_groups path=path %} From cbe172df3d587cb82d772f1a1639095b47c0df79 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 11:11:58 +1000 Subject: [PATCH 014/127] find users for groups - search for users to add to a group - display suggested users on search results screen TODO: actaully enable users to be added! TODO: groups/suggested_users probably could be replaced with some logic in snippets/suggested_users.html --- bookwyrm/templates/groups/find_users.html | 6 +++ bookwyrm/templates/groups/group.html | 23 ++------- .../templates/groups/suggested_users.html | 42 +++++++++++++++++ bookwyrm/templates/groups/users.html | 41 ++++++++++++++++ .../snippets/add_to_group_button.html | 47 +++++++++++++++++++ 5 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 bookwyrm/templates/groups/find_users.html create mode 100644 bookwyrm/templates/groups/suggested_users.html create mode 100644 bookwyrm/templates/groups/users.html create mode 100644 bookwyrm/templates/snippets/add_to_group_button.html diff --git a/bookwyrm/templates/groups/find_users.html b/bookwyrm/templates/groups/find_users.html new file mode 100644 index 000000000..9154a5275 --- /dev/null +++ b/bookwyrm/templates/groups/find_users.html @@ -0,0 +1,6 @@ +{% extends 'groups/group.html' %} + +{% block panel %} +

Add users to {{ group.name }}

+ {% include 'groups/suggested_users.html' with suggested_users=suggested_users query=query %} +{% endblock %} \ No newline at end of file diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index c14694154..a73d78250 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -7,30 +7,13 @@
- {% if request.GET.updated %} -
- {% trans "You successfully added a user to this group!" %} -
- {% endif %} - {% if not group.members.exists %} -

{% trans "This group has no members" %}

- {% else %} -

Group Members

-
    - {% for member in group.members.all %} -
  • -
    - {% include 'directory/user_card.html' %} -
    -
  • - {% endfor %} -
- {% endif %} + {% include "groups/users.html" %} + +

Lists

{% if not group.lists.exists %}

{% trans "This group has no lists" %}

{% else %} -

Lists

    {% for list in group.lists.all %}
  • diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html new file mode 100644 index 000000000..472841e54 --- /dev/null +++ b/bookwyrm/templates/groups/suggested_users.html @@ -0,0 +1,42 @@ +{% load i18n %} +{% load utilities %} +{% load humanize %} +{% if suggested_users %} +
    + {% for user in suggested_users %} +
    +
    + + {% include 'snippets/avatar.html' with user=user large=True %} + {{ user.display_name|truncatechars:10 }} + @{{ user|username|truncatechars:8 }} + + {% include 'snippets/add_to_group_button.html' with user=user minimal=True %} + {% if user.mutuals %} +

    + {% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %} + {{ mutuals }} follower you follow + {% plural %} + {{ mutuals }} followers you follow{% endblocktrans %} +

    + {% elif user.shared_books %} +

    + {% blocktrans trimmed with shared_books=user.shared_books|intcomma count counter=user.shared_books %} + {{ shared_books }} book on your shelves + {% plural %} + {{ shared_books }} books on your shelves + {% endblocktrans %} +

    + {% elif request.user in user.following.all %} +

    + {% trans "Follows you" %} +

    + {% endif %} +
    +
    + {% endfor %} + {% else %} + No users found for {{ query }} +{% endif %} +
    + diff --git a/bookwyrm/templates/groups/users.html b/bookwyrm/templates/groups/users.html new file mode 100644 index 000000000..c7470b328 --- /dev/null +++ b/bookwyrm/templates/groups/users.html @@ -0,0 +1,41 @@ +{% load i18n %} + +{% if request.GET.updated %} +
    + {% trans "You successfully added a user to this group!" %} +
    +{% endif %} + +

    Group Members

    +

    {% trans "Members can add and remove books on your group's book lists" %}

    + +{% block panel %} +
    +
    +
    + + {% if request.GET.query and no_results %} +

    {% blocktrans with query=request.GET.query %}No users found for "{{ query }}"{% endblocktrans %}

    + {% endif %} +
    +
    + +
    +
    + {% include 'snippets/suggested_users.html' with suggested_users=suggested_users %} +
    +{% endblock %} + +
      +{% for member in group.members.all %} +
    • +
      + {% include 'directory/user_card.html' %} +
      +
    • +{% endfor %} +
    \ No newline at end of file diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html new file mode 100644 index 000000000..51c3179be --- /dev/null +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -0,0 +1,47 @@ +{% load i18n %} +{% if request.user == user or not request.user.is_authenticated %} + +{% elif user in request.user.blocks.all %} +{% include 'snippets/block_button.html' with blocks=True %} +{% else %} + +
    +
    + + + + +
    + {% if not minimal %} +
    + {% include 'snippets/user_options.html' with user=user class="is-small" %} +
    + {% endif %} +
    +{% endif %} From 7c0deabcb29ee05bbd76297c7a20ba94682863ca Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 11:14:04 +1000 Subject: [PATCH 015/127] update urls and group view for searching users to add to group --- bookwyrm/urls.py | 6 ++++-- bookwyrm/views/group.py | 44 ++++++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index ac7257e33..cdf7493f1 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -251,11 +251,13 @@ urlpatterns = [ name="user-following", ), re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"), - # lists - re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"), + # groups re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"), re_path(r"^create-group/?$", views.create_group, name="create-group"), re_path(r"^group/(?P\d+)(.json)?/?$", views.Group.as_view(), name="group"), + re_path(r"^group/(?P\d+)/find-users/?$", views.FindAndAddUsers.as_view(), name="group-find-users"), + # lists + re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"), re_path(r"^list/(?P\d+)(.json)?/?$", views.List.as_view(), name="list"), diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index fd4e7f2cd..c344f92bc 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -5,20 +5,17 @@ from urllib.parse import urlencode from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator -from django.db import IntegrityError, transaction -from django.db.models import Avg, Count, DecimalField, Q, Max -from django.db.models.functions import Coalesce from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse -from django.urls import reverse from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.http import require_POST +from django.contrib.postgres.search import TrigramSimilarity +from django.db.models.functions import Greatest from bookwyrm import forms, models -from bookwyrm.connectors import connector_manager -from bookwyrm.settings import PAGE_LENGTH +from bookwyrm.suggested_users import suggested_users from .helpers import privacy_filter from .helpers import get_user_from_username @@ -60,6 +57,39 @@ class UserGroups(View): } return TemplateResponse(request, "user/groups.html", data) +@method_decorator(login_required, name="dispatch") +class FindAndAddUsers(View): + """find friends to add to your group""" + """this is taken from the Get Started friend finder""" + + def get(self, request, group_id): + """basic profile info""" + query = request.GET.get("query") + user_results = ( + models.User.viewer_aware_objects(request.user) + .annotate( + similarity=Greatest( + TrigramSimilarity("username", query), + TrigramSimilarity("localname", query), + ) + ) + .filter( + similarity__gt=0.5, + ) + .order_by("-similarity")[:5] + ) + data = {"no_results": not user_results} + + if user_results.count() < 5: + user_results = list(user_results) + suggested_users.get_suggestions( + request.user + ) + + data["suggested_users"] = user_results + data["group"] = get_object_or_404(models.Group, id=group_id) + data["query"] = query + return TemplateResponse(request, "groups/find_users.html", data) + @login_required @require_POST def create_group(request): @@ -70,6 +100,6 @@ def create_group(request): return redirect(request.headers.get("Referer", "/")) group = form.save() - # TODO: add user as group member + # add the creator as a group member models.GroupMember.objects.create(group=group, user=request.user) return redirect(group.local_path) \ No newline at end of file From 8d17f888ea0e7c2c43180098cca629eff5016dc8 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 11:36:35 +1000 Subject: [PATCH 016/127] improve naming of templates and urls for groups --- bookwyrm/templates/groups/group.html | 2 +- bookwyrm/templates/groups/members.html | 65 ++++++++++++++++++++++++++ bookwyrm/templates/groups/users.html | 41 ---------------- bookwyrm/urls.py | 2 +- 4 files changed, 67 insertions(+), 43 deletions(-) create mode 100644 bookwyrm/templates/groups/members.html delete mode 100644 bookwyrm/templates/groups/users.html diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index a73d78250..4fea5a84e 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -8,7 +8,7 @@
    - {% include "groups/users.html" %} + {% include "groups/members.html" %}

    Lists

    {% if not group.lists.exists %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html new file mode 100644 index 000000000..b10cdd5f4 --- /dev/null +++ b/bookwyrm/templates/groups/members.html @@ -0,0 +1,65 @@ +{% load i18n %} +{% load utilities %} +{% load humanize %} + +{% if request.GET.updated %} +
    + {% trans "You successfully added a user to this group!" %} +
    +{% endif %} + +

    Group Members

    +

    {% trans "Members can add and remove books on your group's book lists" %}

    + +{% block panel %} +
    +
    +
    + +
    +
    + +
    +
    + {% include 'snippets/suggested_users.html' with suggested_users=suggested_users %} +
    +{% endblock %} + +
      +{% for member in group.members.all %} +
      +
      + + {% include 'snippets/avatar.html' with user=user large=True %} + {{ user.display_name|truncatechars:10 }} + @{{ user|username|truncatechars:8 }} + + {% include 'snippets/add_to_group_button.html' with user=user minimal=True %} + {% if user.mutuals %} +

      + {% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %} + {{ mutuals }} follower you follow + {% plural %} + {{ mutuals }} followers you follow{% endblocktrans %} +

      + {% elif user.shared_books %} +

      + {% blocktrans trimmed with shared_books=user.shared_books|intcomma count counter=user.shared_books %} + {{ shared_books }} book on your shelves + {% plural %} + {{ shared_books }} books on your shelves + {% endblocktrans %} +

      + {% elif request.user in user.following.all %} +

      + {% trans "Follows you" %} +

      + {% endif %} +
      +
      +{% endfor %} +
    \ No newline at end of file diff --git a/bookwyrm/templates/groups/users.html b/bookwyrm/templates/groups/users.html deleted file mode 100644 index c7470b328..000000000 --- a/bookwyrm/templates/groups/users.html +++ /dev/null @@ -1,41 +0,0 @@ -{% load i18n %} - -{% if request.GET.updated %} -
    - {% trans "You successfully added a user to this group!" %} -
    -{% endif %} - -

    Group Members

    -

    {% trans "Members can add and remove books on your group's book lists" %}

    - -{% block panel %} -
    -
    -
    - - {% if request.GET.query and no_results %} -

    {% blocktrans with query=request.GET.query %}No users found for "{{ query }}"{% endblocktrans %}

    - {% endif %} -
    -
    - -
    -
    - {% include 'snippets/suggested_users.html' with suggested_users=suggested_users %} -
    -{% endblock %} - -
      -{% for member in group.members.all %} -
    • -
      - {% include 'directory/user_card.html' %} -
      -
    • -{% endfor %} -
    \ No newline at end of file diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index cdf7493f1..e688f813f 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -255,7 +255,7 @@ urlpatterns = [ re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"), re_path(r"^create-group/?$", views.create_group, name="create-group"), re_path(r"^group/(?P\d+)(.json)?/?$", views.Group.as_view(), name="group"), - re_path(r"^group/(?P\d+)/find-users/?$", views.FindAndAddUsers.as_view(), name="group-find-users"), + re_path(r"^group/(?P\d+)/add-users/?$", views.FindAndAddUsers.as_view(), name="group-find-users"), # lists re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"), From e800106be4768ff6d6472b6ebf614604e019647e Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 11:37:08 +1000 Subject: [PATCH 017/127] smaller cards for group members - this will also enable members to be removed easily by managers in a future commit. --- bookwyrm/templates/groups/suggested_users.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html index 472841e54..40d32f3f3 100644 --- a/bookwyrm/templates/groups/suggested_users.html +++ b/bookwyrm/templates/groups/suggested_users.html @@ -1,6 +1,20 @@ {% load i18n %} {% load utilities %} {% load humanize %} +
    +
    +
    + +
    +
    + +
    +
    +
    {% if suggested_users %}
    {% for user in suggested_users %} From b645d753036f6640497f2358581442c5298b2f50 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 17:34:44 +1000 Subject: [PATCH 018/127] add and remove users from groups --- bookwyrm/models/group.py | 9 ++- bookwyrm/templates/groups/members.html | 27 +++---- .../snippets/add_to_group_button.html | 16 ++-- bookwyrm/urls.py | 4 +- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/group.py | 75 ++++++++++++++++--- 6 files changed, 100 insertions(+), 33 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index a91c56cb1..01fdbdd63 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -42,4 +42,11 @@ class GroupMember(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) \ No newline at end of file + user = models.ForeignKey("User", on_delete=models.CASCADE) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["group", "user"], name="unique_member" + ) + ] \ No newline at end of file diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index b10cdd5f4..55d7fd749 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -30,36 +30,37 @@ {% endblock %} \ No newline at end of file diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html index 51c3179be..aea0532f1 100644 --- a/bookwyrm/templates/snippets/add_to_group_button.html +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -7,21 +7,21 @@
    - - - -
    + {% endfor %} +
    \ No newline at end of file diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index e00a8331d..a219beb4f 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -1,6 +1,7 @@ """ template filters """ from django import template from django.db.models import Avg +from django.utils.safestring import mark_safe from bookwyrm import models, views @@ -98,3 +99,15 @@ def mutuals_count(context, user): if not viewer.is_authenticated: return None return user.followers.filter(followers=viewer).count() + +@register.simple_tag(takes_context=True) +def identify_manager(context): + """boolean for whether user is group manager""" + group = context['group'] + member = context['member'] + snippet = mark_safe('') + + if group.manager == member: + snippet = mark_safe('Manager') + + return snippet \ No newline at end of file From 035fc5209d373ef4ba9d23dc1deac94c47ccf816 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 20:23:59 +1000 Subject: [PATCH 020/127] better logic for identifying group manager --- bookwyrm/templates/groups/members.html | 6 +++++- bookwyrm/templatetags/bookwyrm_tags.py | 13 ------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index 7dce08918..80dab21c9 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -38,6 +38,11 @@ {{ member.display_name|truncatechars:10 }} @{{ member|username|truncatechars:8 }} + {% if group.manager == member %} + + Manager + + {% endif %} {% include 'snippets/add_to_group_button.html' with user=member minimal=True %} {% if member.mutuals %}

    @@ -59,7 +64,6 @@ {% trans "Follows you" %}

    {% endif %} - {% identify_manager %}
    {% endfor %}
    \ No newline at end of file diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index a219beb4f..e00a8331d 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -1,7 +1,6 @@ """ template filters """ from django import template from django.db.models import Avg -from django.utils.safestring import mark_safe from bookwyrm import models, views @@ -99,15 +98,3 @@ def mutuals_count(context, user): if not viewer.is_authenticated: return None return user.followers.filter(followers=viewer).count() - -@register.simple_tag(takes_context=True) -def identify_manager(context): - """boolean for whether user is group manager""" - group = context['group'] - member = context['member'] - snippet = mark_safe('') - - if group.manager == member: - snippet = mark_safe('Manager') - - return snippet \ No newline at end of file From ec0720514e9e458f9c933eed63a19744c064f160 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 25 Sep 2021 20:25:30 +1000 Subject: [PATCH 021/127] don't allow non-manager to add and remove group members --- bookwyrm/templates/snippets/add_to_group_button.html | 2 +- bookwyrm/views/group.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html index aea0532f1..f533af6ea 100644 --- a/bookwyrm/templates/snippets/add_to_group_button.html +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -1,5 +1,5 @@ {% load i18n %} -{% if request.user == user or not request.user.is_authenticated %} +{% if request.user == user or not request.user == group.manager or not request.user.is_authenticated %} {% elif user in request.user.blocks.all %} {% include 'snippets/block_button.html' with blocks=True %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index addf9e47c..4214908af 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -47,7 +47,7 @@ class UserGroups(View): data = { "user": user, - "is_self": request.user.id == user.id, + "is_self": request.user.id == user.id, # CHECK is this relevant here? "groups": paginated.get_page(request.GET.get("page")), "group_form": forms.GroupForm(), "path": user.local_path + "/group", @@ -82,9 +82,12 @@ class FindUsers(View): request.user ) + group = get_object_or_404(models.Group, id=group_id) + data["suggested_users"] = user_results - data["group"] = get_object_or_404(models.Group, id=group_id) + data["group"] = group data["query"] = query + data["requestor_is_manager"] = request.user == group.manager return TemplateResponse(request, "groups/find_users.html", data) @login_required @@ -129,7 +132,6 @@ def add_member(request): print("no integrity") pass - # TODO: how do we return and update AJAX data? return redirect(user.local_path) @require_POST @@ -158,5 +160,4 @@ def remove_member(request): print("no integrity") pass - # TODO: how do we return and update AJAX data? return redirect(user.local_path) \ No newline at end of file From 686198472df29ed021ec0c7c77c81d5f9310fbfc Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 26 Sep 2021 15:50:15 +1000 Subject: [PATCH 022/127] update group and list models - remove GroupList model - add a group foreign key value to List model - remove reference to lists in Group model --- bookwyrm/models/__init__.py | 2 +- bookwyrm/models/group.py | 14 -------------- bookwyrm/models/list.py | 7 +++++++ 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 2774f081d..a4a06ebad 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -21,7 +21,7 @@ from .relationship import UserFollows, UserFollowRequest, UserBlocks from .report import Report, ReportComment from .federated_server import FederatedServer -from .group import Group, GroupList, GroupMember +from .group import Group, GroupMember from .import_job import ImportJob, ImportItem diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 01fdbdd63..c1aa2d707 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -16,14 +16,6 @@ class Group(BookWyrmModel): "User", on_delete=models.PROTECT) description = fields.TextField(blank=True, null=True) privacy = fields.PrivacyField() - - lists = models.ManyToManyField( - "List", - symmetrical=False, - through="GroupList", - through_fields=("group", "book_list"), - ) - members = models.ManyToManyField( "User", symmetrical=False, @@ -32,12 +24,6 @@ class Group(BookWyrmModel): related_name="members" ) -class GroupList(BookWyrmModel): - """Lists that group members can edit""" - - group = models.ForeignKey("Group", on_delete=models.CASCADE) - book_list = models.ForeignKey("List", on_delete=models.CASCADE) - class GroupMember(models.Model): """Users who are members of a group""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 49fb53757..b73d77086 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -1,4 +1,5 @@ """ make a list of books!! """ +from dataclasses import field from django.apps import apps from django.db import models from django.utils import timezone @@ -16,6 +17,7 @@ CurationType = models.TextChoices( "closed", "open", "curated", + "group" ], ) @@ -32,6 +34,11 @@ class List(OrderedCollectionMixin, BookWyrmModel): curation = fields.CharField( max_length=255, default="closed", choices=CurationType.choices ) + group = models.ForeignKey( + "Group", + on_delete=models.CASCADE, + null=True + ) books = models.ManyToManyField( "Edition", symmetrical=False, From b921d666cffe8d8c993390525a73158a94444165 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 26 Sep 2021 15:55:16 +1000 Subject: [PATCH 023/127] add group field to ListForm --- bookwyrm/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 0987924ed..1f2221d34 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -293,7 +293,7 @@ class AnnouncementForm(CustomForm): class ListForm(CustomForm): class Meta: model = models.List - fields = ["user", "name", "description", "curation", "privacy"] + fields = ["user", "name", "description", "curation", "privacy", "group"] class GroupForm(CustomForm): class Meta: From f3a3ba5f0105ec7f5ab84b19edc75bb3d3ead7ff Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 26 Sep 2021 15:56:02 +1000 Subject: [PATCH 024/127] pass group value to list views and vice-versa --- bookwyrm/views/group.py | 4 ++-- bookwyrm/views/list.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 4214908af..0ad0bd31d 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -26,10 +26,11 @@ class Group(View): # groups = privacy_filter( # request.user, groups, privacy_levels=["public", "followers"] # ) - + lists = models.List.objects.filter(group=group).order_by("-updated_date") data = { "group": group, + "lists": lists, "list_form": forms.GroupForm(), "path": "/group", } @@ -129,7 +130,6 @@ def add_member(request): ) except IntegrityError: - print("no integrity") pass return redirect(user.local_path) diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 695302041..9ef027538 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -46,9 +46,12 @@ class Lists(View): request.user, lists, privacy_levels=["public", "followers"] ) + user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") + paginated = Paginator(lists, 12) data = { "lists": paginated.get_page(request.GET.get("page")), + "user_groups": user_groups, "list_form": forms.ListForm(), "path": "/list", } @@ -59,6 +62,10 @@ class Lists(View): def post(self, request): """create a book_list""" form = forms.ListForm(request.POST) + # TODO: here we need to take the value of the group (the group.id) + # and fetch the actual group to add to the DB + # but only if curation type is 'group' other wise the value of + # group is None if not form.is_valid(): return redirect("lists") book_list = form.save() @@ -93,12 +100,14 @@ class UserLists(View): user = get_user_from_username(request.user, username) lists = models.List.objects.filter(user=user) lists = privacy_filter(request.user, lists) + user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") paginated = Paginator(lists, 12) data = { "user": user, "is_self": request.user.id == user.id, "lists": paginated.get_page(request.GET.get("page")), + "user_groups": user_groups, "list_form": forms.ListForm(), "path": user.local_path + "/lists", } @@ -171,6 +180,7 @@ class List(View): ).order_by("-updated_date") ][: 5 - len(suggestions)] + user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") page = paginated.get_page(request.GET.get("page")) data = { "list": book_list, @@ -185,6 +195,7 @@ class List(View): "sort_form": forms.SortListForm( {"direction": direction, "sort_by": sort_by} ), + "user_groups": user_groups } return TemplateResponse(request, "lists/list.html", data) From 8bfc71db6e37e4412b0b62191cddd6aa24dce771 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 26 Sep 2021 15:56:52 +1000 Subject: [PATCH 025/127] create group curated lists --- bookwyrm/templates/groups/group.html | 13 +++++------- bookwyrm/templates/lists/form.html | 30 ++++++++++++++++++++++++++++ bookwyrm/templates/lists/layout.html | 2 +- bookwyrm/templates/user/layout.html | 1 + 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index 4fea5a84e..abb241438 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -11,15 +11,13 @@ {% include "groups/members.html" %}

    Lists

    - {% if not group.lists.exists %} + {% if not lists %}

    {% trans "This group has no lists" %}

    {% else %} -
      - {% for list in group.lists.all %} -
    • +
      {% for list in lists %} -
      +

      @@ -55,9 +53,8 @@

      {% endfor %}
      -
    • - {% endfor %} -
    + + {% endif %} {% include "snippets/pagination.html" with page=items %}
diff --git a/bookwyrm/templates/lists/form.html b/bookwyrm/templates/lists/form.html index 9a000d3f5..0019520e8 100644 --- a/bookwyrm/templates/lists/form.html +++ b/bookwyrm/templates/lists/form.html @@ -1,5 +1,6 @@ {% load i18n %} {% csrf_token %} +{% load utilities %}
@@ -31,6 +32,35 @@ {% trans "Open" %}

{% trans "Anyone can add books to this list" %}

+ + + + + + + + {% if user_groups %} + {% csrf_token %} + +
+
+ +
+
+ {% else %} + {% with user|username as username %} + {% url 'user-groups' user|username as url %} +
{% trans "You must create a " %}{% trans "Group" %}{% trans " before you can create Group lists!" %}
+ {% endwith %} + {% endif %}
diff --git a/bookwyrm/templates/lists/layout.html b/bookwyrm/templates/lists/layout.html index 68abafc09..914478abb 100644 --- a/bookwyrm/templates/lists/layout.html +++ b/bookwyrm/templates/lists/layout.html @@ -25,7 +25,7 @@
- {% include 'lists/edit_form.html' with controls_text="edit_list" %} + {% include 'lists/edit_form.html' with controls_text="edit_list" user_groups=user_groups %}
{% block panel %}{% endblock %} diff --git a/bookwyrm/templates/user/layout.html b/bookwyrm/templates/user/layout.html index 22b8e2ce4..357ad4679 100755 --- a/bookwyrm/templates/user/layout.html +++ b/bookwyrm/templates/user/layout.html @@ -75,6 +75,7 @@ {% trans "Lists" %} {% endif %} + {% if is_self or user.groups_set.exists %} {% url 'user-groups' user|username as url %} From 5fccb991a7a15da78ad2ed4d2ca1e753b14aae37 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 26 Sep 2021 18:28:16 +1000 Subject: [PATCH 026/127] remove list from group when changing curation Allows 'group' to be blank when saving a list. Removes the 'group' field when saving a list with curation other than 'group' - this stops the list "sticking" to a group after it is changed from group curation to something else. --- bookwyrm/models/list.py | 3 ++- bookwyrm/templates/lists/form.html | 8 ++------ bookwyrm/views/list.py | 7 +++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index b73d77086..75f34b9e8 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -37,7 +37,8 @@ class List(OrderedCollectionMixin, BookWyrmModel): group = models.ForeignKey( "Group", on_delete=models.CASCADE, - null=True + default=None, + blank=True ) books = models.ManyToManyField( "Edition", diff --git a/bookwyrm/templates/lists/form.html b/bookwyrm/templates/lists/form.html index 0019520e8..d2f17d63b 100644 --- a/bookwyrm/templates/lists/form.html +++ b/bookwyrm/templates/lists/form.html @@ -37,20 +37,16 @@ {% trans "Group" %}

{% trans "Group members can add to and remove from this list" %}

- - - - {% if user_groups %} {% csrf_token %}
diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 9ef027538..fb224cdf5 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -62,10 +62,6 @@ class Lists(View): def post(self, request): """create a book_list""" form = forms.ListForm(request.POST) - # TODO: here we need to take the value of the group (the group.id) - # and fetch the actual group to add to the DB - # but only if curation type is 'group' other wise the value of - # group is None if not form.is_valid(): return redirect("lists") book_list = form.save() @@ -208,6 +204,9 @@ class List(View): if not form.is_valid(): return redirect("list", book_list.id) book_list = form.save() + if not book_list.curation == "group": + book_list.group = None + book_list.save() return redirect(book_list.local_path) From 0e2095bc5e981a6b39e88ef0fb6fa5132d32afbf Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 26 Sep 2021 20:52:44 +1000 Subject: [PATCH 027/127] refer to group in group lists created_text --- bookwyrm/templates/lists/created_text.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/lists/created_text.html b/bookwyrm/templates/lists/created_text.html index eee5a75f2..6c6247adc 100644 --- a/bookwyrm/templates/lists/created_text.html +++ b/bookwyrm/templates/lists/created_text.html @@ -1,7 +1,9 @@ {% load i18n %} {% spaceless %} -{% if list.curation != 'open' %} +{% if list.curation == 'group' %} +{% blocktrans with username=list.user.display_name userpath=list.user.local_path groupname=list.group.name grouppath=list.group.local_path %}Created by {{ username }} and managed by {{ groupname }}{% endblocktrans %} +{% elif list.curation != 'open' %} {% blocktrans with username=list.user.display_name path=list.user.local_path %}Created and curated by {{ username }}{% endblocktrans %} {% else %} {% blocktrans with username=list.user.display_name path=list.user.local_path %}Created by {{ username }}{% endblocktrans %} From 762202c4b0b604c6e2d19b641fc10beb502641d7 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 11:03:41 +1000 Subject: [PATCH 028/127] fix UI for group curated list editing When creating or editing a list, the group selection dropdown will only appear if the user selects "group" as the curation option (or it is already selected). - fix typo in bookwyrm.js comments - add data-hides trigger for hiding elements after they have been unhidden, where simple toggles are not the right approach --- bookwyrm/static/js/bookwyrm.js | 30 ++++++++++++++--- bookwyrm/templates/lists/form.html | 52 +++++++++++++++--------------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index f000fd082..049de497c 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -28,6 +28,12 @@ let BookWyrm = new class { this.revealForm.bind(this)) ); + document.querySelectorAll('[data-hides]') + .forEach(button => button.addEventListener( + 'change', + this.hideForm.bind(this)) + ); + document.querySelectorAll('[data-back]') .forEach(button => button.addEventListener( 'click', @@ -119,7 +125,7 @@ let BookWyrm = new class { } /** - * Toggle form. + * Show form. * * @param {Event} event * @return {undefined} @@ -127,10 +133,26 @@ let BookWyrm = new class { revealForm(event) { let trigger = event.currentTarget; let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0]; - - this.addRemoveClass(hidden, 'is-hidden', !hidden); + // if the form has already been revealed, there is no '.is-hidden' element + // so this doesn't really work as a toggle + if (hidden) { + this.addRemoveClass(hidden, 'is-hidden', !hidden); + } } + /** + * Hide form. + * + * @param {Event} event + * @return {undefined} + */ + hideForm(event) { + let trigger = event.currentTarget; + let targetId = trigger.dataset.hides + let visible = document.getElementById(targetId) + this.addRemoveClass(visible, 'is-hidden', true); + } + /** * Execute actions on targets based on triggers. * @@ -227,7 +249,7 @@ let BookWyrm = new class { } /** - * Check or uncheck a checbox. + * Check or uncheck a checkbox. * * @param {string} checkbox - id of the checkbox * @param {boolean} pressed - Is the trigger pressed? diff --git a/bookwyrm/templates/lists/form.html b/bookwyrm/templates/lists/form.html index d2f17d63b..a98cae94a 100644 --- a/bookwyrm/templates/lists/form.html +++ b/bookwyrm/templates/lists/form.html @@ -18,45 +18,45 @@
{% trans "List curation:" %} -
From 2874e523098107c1b6dea0d7a6391b037aade0df Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 15:34:14 +1000 Subject: [PATCH 029/127] rationalise group creation and prep for group privacy --- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/group.py | 51 ++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 187897066..4d93c5973 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -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, create_group, add_member, remove_member +from .group import Group, UserGroups, FindUsers, add_member, remove_member from .import_data import Import, ImportStatus from .inbox import Inbox from .interaction import Favorite, Unfavorite, Boost, Unboost diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 0ad0bd31d..694427606 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -15,6 +15,7 @@ from bookwyrm import forms, models from bookwyrm.suggested_users import suggested_users from .helpers import privacy_filter # TODO: from .helpers import get_user_from_username +from bookwyrm.settings import DOMAIN class Group(View): """group page""" @@ -23,11 +24,8 @@ class Group(View): """display a group""" group = models.Group.objects.get(id=group_id) - # groups = privacy_filter( - # request.user, groups, privacy_levels=["public", "followers"] - # ) lists = models.List.objects.filter(group=group).order_by("-updated_date") - + lists = privacy_filter(request.user, lists) data = { "group": group, "lists": lists, @@ -36,6 +34,17 @@ class Group(View): } return TemplateResponse(request, "groups/group.html", data) + @method_decorator(login_required, name="dispatch") + # pylint: disable=unused-argument + def post(self, request, group_id): + """edit a group""" + user_group = get_object_or_404(models.Group, id=group_id) + form = forms.GroupForm(request.POST, instance=user_group) + if not form.is_valid(): + return redirect("group", user_group.id) + user_group = form.save() + return redirect("group", user_group.id) + @method_decorator(login_required, name="dispatch") class UserGroups(View): """a user's groups page""" @@ -44,6 +53,7 @@ class UserGroups(View): """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) paginated = Paginator(groups, 12) data = { @@ -55,6 +65,19 @@ class UserGroups(View): } return TemplateResponse(request, "user/groups.html", data) + @method_decorator(login_required, name="dispatch") + # pylint: disable=unused-argument + def post(self, request, username): + """create a user group""" + user = get_user_from_username(request.user, username) + form = forms.GroupForm(request.POST) + if not form.is_valid(): + return redirect("user-groups") + group = form.save() + # add the creator as a group member + models.GroupMember.objects.create(group=group, user=request.user) + return redirect("group", group.id) + @method_decorator(login_required, name="dispatch") class FindUsers(View): """find friends to add to your group""" @@ -88,23 +111,9 @@ class FindUsers(View): data["suggested_users"] = user_results data["group"] = group data["query"] = query - data["requestor_is_manager"] = request.user == group.manager + data["requestor_is_manager"] = request.user == group.user return TemplateResponse(request, "groups/find_users.html", data) -@login_required -@require_POST -def create_group(request): - """user groups""" - form = forms.GroupForm(request.POST) - if not form.is_valid(): - print("invalid!") - return redirect(request.headers.get("Referer", "/")) - - group = form.save() - # add the creator as a group member - models.GroupMember.objects.create(group=group, user=request.user) - return redirect(group.local_path) - @require_POST @login_required def add_member(request): @@ -120,7 +129,7 @@ def add_member(request): if not user: return HttpResponseBadRequest() - if not group.manager == request.user: + if not group.user == request.user: return HttpResponseBadRequest() try: @@ -149,7 +158,7 @@ def remove_member(request): if not user: return HttpResponseBadRequest() - if not group.manager == request.user: + if not group.user == request.user: return HttpResponseBadRequest() try: From f3181690a2121082d9d31d91322e1ab0cfd7849d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 15:36:41 +1000 Subject: [PATCH 030/127] change group owner from 'manager' to 'user' This will allow privacy management to use existing code. Some template updates also are for rationalising how groups are created and edited. --- bookwyrm/models/base_model.py | 4 ++++ bookwyrm/models/group.py | 2 +- bookwyrm/models/list.py | 5 +++-- bookwyrm/templates/groups/created_text.html | 2 +- bookwyrm/templates/groups/form.html | 6 +++--- bookwyrm/templates/groups/layout.html | 2 +- bookwyrm/templates/groups/members.html | 2 +- bookwyrm/templates/groups/user_groups.html | 2 +- bookwyrm/templates/snippets/add_to_group_button.html | 2 +- bookwyrm/templates/user/groups.html | 2 +- 10 files changed, 17 insertions(+), 12 deletions(-) diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index aa174a143..3a2d758b7 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -76,6 +76,10 @@ class BookWyrmModel(models.Model): and self.mention_users.filter(id=viewer.id).first() ): return True + +# TODO: if privacy is direct and the object is a group and viewer is a member of the group +# then return True + return False diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index c1aa2d707..6810779cc 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -12,7 +12,7 @@ class Group(BookWyrmModel): """A group of users""" name = fields.CharField(max_length=100) - manager = fields.ForeignKey( + user = fields.ForeignKey( "User", on_delete=models.PROTECT) description = fields.TextField(blank=True, null=True) privacy = fields.PrivacyField() diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 75f34b9e8..7ea33a8b6 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -36,9 +36,10 @@ class List(OrderedCollectionMixin, BookWyrmModel): ) group = models.ForeignKey( "Group", - on_delete=models.CASCADE, + on_delete=models.PROTECT, default=None, - blank=True + blank=True, + null=True, ) books = models.ManyToManyField( "Edition", diff --git a/bookwyrm/templates/groups/created_text.html b/bookwyrm/templates/groups/created_text.html index e7409942a..5e6ce513d 100644 --- a/bookwyrm/templates/groups/created_text.html +++ b/bookwyrm/templates/groups/created_text.html @@ -1,6 +1,6 @@ {% load i18n %} {% spaceless %} -{% blocktrans with username=group.manager.display_name path=group.manager.local_path %}Managed by {{ username }}{% endblocktrans %} +{% blocktrans with username=group.user.display_name path=group.user.local_path %}Managed by {{ username }}{% endblocktrans %} {% endspaceless %} diff --git a/bookwyrm/templates/groups/form.html b/bookwyrm/templates/groups/form.html index f764db6f9..c47cbbc4d 100644 --- a/bookwyrm/templates/groups/form.html +++ b/bookwyrm/templates/groups/form.html @@ -5,7 +5,7 @@
- +
@@ -20,9 +20,9 @@
- +
diff --git a/bookwyrm/templates/groups/layout.html b/bookwyrm/templates/groups/layout.html index 03a957d0a..f558f169a 100644 --- a/bookwyrm/templates/groups/layout.html +++ b/bookwyrm/templates/groups/layout.html @@ -12,7 +12,7 @@

- {% if request.user == group.manager %} + {% if request.user == group.user %} {% trans "Edit group" as button_text %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_group" focus="edit_group_header" %} {% endif %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index 80dab21c9..df5f1602a 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -38,7 +38,7 @@ {{ member.display_name|truncatechars:10 }} @{{ member|username|truncatechars:8 }} - {% if group.manager == member %} + {% if group.user == member %} Manager diff --git a/bookwyrm/templates/groups/user_groups.html b/bookwyrm/templates/groups/user_groups.html index 9c48a8429..5239a3657 100644 --- a/bookwyrm/templates/groups/user_groups.html +++ b/bookwyrm/templates/groups/user_groups.html @@ -8,7 +8,7 @@

- {{ group.name }} {% include 'snippets/privacy-icons.html' with item=group %} + {{ group.name }} {% include 'snippets/privacy-icons.html' with item=group %}

{% if request.user.is_authenticated and request.user|saved:list %}
diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html index f533af6ea..cc394684c 100644 --- a/bookwyrm/templates/snippets/add_to_group_button.html +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -1,5 +1,5 @@ {% load i18n %} -{% if request.user == user or not request.user == group.manager or not request.user.is_authenticated %} +{% if request.user == user or not request.user == group.user or not request.user.is_authenticated %} {% elif user in request.user.blocks.all %} {% include 'snippets/block_button.html' with blocks=True %} diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html index 912d5ec3d..1a5940728 100644 --- a/bookwyrm/templates/user/groups.html +++ b/bookwyrm/templates/user/groups.html @@ -24,7 +24,7 @@ {% block panel %}
- +

{% trans "Create group" %}

From 0ccd54b05ab40f8bd41734b8651f6fe0080efaec Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 15:38:05 +1000 Subject: [PATCH 031/127] better urls and views for group creation and editing --- bookwyrm/forms.py | 2 +- bookwyrm/urls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 1f2221d34..ffb7581e5 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -298,7 +298,7 @@ class ListForm(CustomForm): class GroupForm(CustomForm): class Meta: model = models.Group - fields = ["manager", "privacy", "name", "description"] + fields = ["user", "privacy", "name", "description"] class ReportForm(CustomForm): class Meta: diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 0c7b19393..a8a0651a7 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -253,7 +253,7 @@ 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"^create-group/?$", views.create_group, name="create-group"), + # re_path(r"^create-group/?$", views.create_group, name="create-group"), re_path(r"^group/(?P\d+)(.json)?/?$", views.Group.as_view(), name="group"), re_path(r"^group/(?P\d+)/add-users/?$", views.FindUsers.as_view(), name="group-find-users"), re_path(r"^add-group-member/?$", views.add_member, name="add-group-member"), From 493ed14f3465eeb2c4495c2f1eb02e1b989131f7 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 16:39:12 +1000 Subject: [PATCH 032/127] better group creation form logic and placement --- bookwyrm/templates/user/groups.html | 2 +- bookwyrm/views/group.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html index 1a5940728..6b64e4b79 100644 --- a/bookwyrm/templates/user/groups.html +++ b/bookwyrm/templates/user/groups.html @@ -24,7 +24,7 @@ {% block panel %}
- +

{% trans "Create group" %}

diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 694427606..18e13eb5d 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -69,10 +69,9 @@ class UserGroups(View): # pylint: disable=unused-argument def post(self, request, username): """create a user group""" - user = get_user_from_username(request.user, username) form = forms.GroupForm(request.POST) if not form.is_valid(): - return redirect("user-groups") + 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) From e38d7b63f36763443fe53a0b5b42c573ed45b2e1 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 16:49:56 +1000 Subject: [PATCH 033/127] make groups actually editable --- bookwyrm/views/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 18e13eb5d..9319b6599 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -29,7 +29,7 @@ class Group(View): data = { "group": group, "lists": lists, - "list_form": forms.GroupForm(), + "group_form": forms.GroupForm(instance=group), "path": "/group", } return TemplateResponse(request, "groups/group.html", data) From e5ca377cd37151f52391ad3587ad5aa7ba0f1398 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 16:50:51 +1000 Subject: [PATCH 034/127] clean up stray code mess --- bookwyrm/templates/groups/form.html | 3 +-- bookwyrm/urls.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bookwyrm/templates/groups/form.html b/bookwyrm/templates/groups/form.html index c47cbbc4d..f684dd010 100644 --- a/bookwyrm/templates/groups/form.html +++ b/bookwyrm/templates/groups/form.html @@ -1,9 +1,8 @@ {% load i18n %} {% csrf_token %} -{{ group_form.non_field_errors }} -
+
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index a8a0651a7..30ffc8689 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -253,7 +253,6 @@ 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"^create-group/?$", views.create_group, name="create-group"), re_path(r"^group/(?P\d+)(.json)?/?$", views.Group.as_view(), name="group"), re_path(r"^group/(?P\d+)/add-users/?$", views.FindUsers.as_view(), name="group-find-users"), re_path(r"^add-group-member/?$", views.add_member, name="add-group-member"), From 277c033fda527a043d2ae219a517041869c975d3 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 17:50:38 +1000 Subject: [PATCH 035/127] show star if this user is the creator/manager of the group --- bookwyrm/templates/groups/user_groups.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/groups/user_groups.html b/bookwyrm/templates/groups/user_groups.html index 5239a3657..f99abc695 100644 --- a/bookwyrm/templates/groups/user_groups.html +++ b/bookwyrm/templates/groups/user_groups.html @@ -10,10 +10,10 @@

{{ group.name }} {% include 'snippets/privacy-icons.html' with item=group %}

- {% if request.user.is_authenticated and request.user|saved:list %} + {% if group.user == user %}
- {% trans "Saved" as text %} - + {% trans "Manager" as text %} + {{ text }}
From 81e5ff5b76e087f301cff63193830b10a064985c Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 17:51:18 +1000 Subject: [PATCH 036/127] show groups on member pages if allowed - display groups on user pages when not the logged in user - restrict visibility of groups on user pages and group pages themselves according to privacy settings --- bookwyrm/templates/user/layout.html | 3 +-- bookwyrm/views/group.py | 10 +++++++++- bookwyrm/views/user.py | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/user/layout.html b/bookwyrm/templates/user/layout.html index 357ad4679..0d07b199b 100755 --- a/bookwyrm/templates/user/layout.html +++ b/bookwyrm/templates/user/layout.html @@ -75,8 +75,7 @@ {% trans "Lists" %} {% endif %} - - {% if is_self or user.groups_set.exists %} + {% if is_self or has_groups %} {% url 'user-groups' user|username as url %} {% trans "Groups" %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 9319b6599..dfb44a4c8 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -23,9 +23,17 @@ class Group(View): def get(self, request, group_id): """display a group""" + # TODO: use get_or_404? + # TODO: what is the difference between privacy filter and visible to user? + # get_object_or_404(models.Group, id=group_id) group = models.Group.objects.get(id=group_id) lists = models.List.objects.filter(group=group).order_by("-updated_date") lists = privacy_filter(request.user, lists) + + # don't show groups to users who shouldn't see them + if not group.visible_to_user(request.user): + return HttpResponseNotFound() + data = { "group": group, "lists": lists, @@ -58,7 +66,7 @@ class UserGroups(View): data = { "user": user, - "is_self": request.user.id == user.id, # CHECK is this relevant here? + "has_groups": models.GroupMember.objects.filter(user=user).exists(), "groups": paginated.get_page(request.GET.get("page")), "group_form": forms.GroupForm(), "path": user.local_path + "/group", diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 63194ceb7..d3f52e729 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -83,6 +83,7 @@ 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)), From c87712c995de998e62b1725f224ba47c37d4537a Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 18:41:29 +1000 Subject: [PATCH 037/127] allow group members to add items to group lists directly NOTE: this will be the case regardless of privacy settings of the list --- bookwyrm/templates/lists/list.html | 4 ++-- bookwyrm/views/list.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index 35014a7b6..ea28bb77f 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -123,7 +123,7 @@ {% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}

- {% if list.curation == 'open' or request.user == list.user %} + {% if list.curation == 'open' or request.user == list.user or is_group_member %} {% trans "Add Books" %} {% else %} {% trans "Suggest Books" %} @@ -176,7 +176,7 @@ {% csrf_token %} - +

diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index fb224cdf5..912c3cfdb 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -177,6 +177,7 @@ class List(View): ][: 5 - len(suggestions)] user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") + is_group_member = book_list.group in user_groups page = paginated.get_page(request.GET.get("page")) data = { "list": book_list, @@ -191,7 +192,8 @@ class List(View): "sort_form": forms.SortListForm( {"direction": direction, "sort_by": sort_by} ), - "user_groups": user_groups + "user_groups": user_groups, + "is_group_member": is_group_member } return TemplateResponse(request, "lists/list.html", data) @@ -292,13 +294,17 @@ def delete_list(request, list_id): def add_book(request): """put a book on a list""" book_list = get_object_or_404(models.List, id=request.POST.get("list")) + is_group_member = False + if book_list.curation == "group": + user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") + is_group_member = book_list.group in user_groups if not book_list.visible_to_user(request.user): return HttpResponseNotFound() book = get_object_or_404(models.Edition, id=request.POST.get("book")) # do you have permission to add to the list? try: - if request.user == book_list.user or book_list.curation == "open": + if request.user == book_list.user or is_group_member or book_list.curation == "open": # add the book at the latest order of approved books, before pending books order_max = ( book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[ From df5a5f94a1e60fc0c2ad9169f0c57504f441afde Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 19:27:39 +1000 Subject: [PATCH 038/127] fix local_path for groups --- bookwyrm/models/group.py | 4 ++ bookwyrm/templates/groups/group.html | 78 +++++++++++----------- bookwyrm/templates/lists/created_text.html | 2 +- 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 6810779cc..4d9d28156 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -24,6 +24,10 @@ class Group(BookWyrmModel): related_name="members" ) + 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): """Users who are members of a group""" diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index abb241438..6c44e3b42 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -15,49 +15,47 @@

{% trans "This group has no lists" %}

{% else %} -
- {% for list in lists %} -
-
-
-

- {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %} -

-
- - {% with list_books=list.listitem_set.all|slice:5 %} - {% if list_books %} - - {% endif %} - {% endwith %} - -
-
- {% if list.description %} - {{ list.description|to_markdown|safe|truncatechars_html:30 }} - {% else %} -   - {% endif %} -
-

- {% include 'lists/created_text.html' with list=list %} -

-
-
-
- {% endfor %} -
- +
+ {% for list in lists %} +
+
+
+

+ {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %} +

+
+ + {% with list_books=list.listitem_set.all|slice:5 %} + {% if list_books %} + + {% endif %} + {% endwith %} + +
+
+ {% if list.description %} + {{ list.description|to_markdown|safe|truncatechars_html:30 }} + {% else %} +   + {% endif %} +
+

+ {% include 'lists/created_text.html' with list=list %} +

+
+
+
+ {% endfor %} +
{% endif %} {% include "snippets/pagination.html" with page=items %}
-
{% endblock %} diff --git a/bookwyrm/templates/lists/created_text.html b/bookwyrm/templates/lists/created_text.html index 6c6247adc..f5405b64a 100644 --- a/bookwyrm/templates/lists/created_text.html +++ b/bookwyrm/templates/lists/created_text.html @@ -2,7 +2,7 @@ {% spaceless %} {% if list.curation == 'group' %} -{% blocktrans with username=list.user.display_name userpath=list.user.local_path groupname=list.group.name grouppath=list.group.local_path %}Created by {{ username }} and managed by {{ groupname }}{% endblocktrans %} +{% blocktrans with username=list.user.display_name userpath=list.user.local_path groupname=list.group.name grouppath=list.group.local_path %}Created by {{ username }} and managed by {{ groupname }}{% endblocktrans %} {% elif list.curation != 'open' %} {% blocktrans with username=list.user.display_name path=list.user.local_path %}Created and curated by {{ username }}{% endblocktrans %} {% else %} From 1a02af145016b79fe2b14c6a93536b88e82d5b7c Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 20:24:25 +1000 Subject: [PATCH 039/127] allow members to see groups and their lists - add additional logic to visible_to_user, for groups and their objects - cleans up some queries in Group view NOTE: I can't work out how to make group lists only visible to users who should be able to see them, on user group listings. They still can't access the actual group, but can see it on user pages. This is potentialy problematic. --- bookwyrm/models/base_model.py | 13 +++++++++++-- bookwyrm/views/group.py | 13 ++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 3a2d758b7..1b4bae1a9 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -77,8 +77,17 @@ class BookWyrmModel(models.Model): ): return True -# TODO: if privacy is direct and the object is a group and viewer is a member of the group -# then return True + # you can see groups of which you are a member + if hasattr(self, "members") and viewer in self.members.all(): + return True + + # you can see objects which have a group of which you are a member + if hasattr(self, "group"): + if ( + hasattr(self.group, "members") + and viewer in self.group.members.all() + ): + return True return False diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index dfb44a4c8..b28aabeb3 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -13,7 +13,7 @@ from django.db.models.functions import Greatest from bookwyrm import forms, models from bookwyrm.suggested_users import suggested_users -from .helpers import privacy_filter # TODO: +from .helpers import privacy_filter from .helpers import get_user_from_username from bookwyrm.settings import DOMAIN @@ -23,10 +23,7 @@ class Group(View): def get(self, request, group_id): """display a group""" - # TODO: use get_or_404? - # TODO: what is the difference between privacy filter and visible to user? - # get_object_or_404(models.Group, id=group_id) - group = models.Group.objects.get(id=group_id) + group = get_object_or_404(models.Group, id=group_id) lists = models.List.objects.filter(group=group).order_by("-updated_date") lists = privacy_filter(request.user, lists) @@ -43,7 +40,6 @@ class Group(View): return TemplateResponse(request, "groups/group.html", data) @method_decorator(login_required, name="dispatch") - # pylint: disable=unused-argument def post(self, request, group_id): """edit a group""" user_group = get_object_or_404(models.Group, id=group_id) @@ -61,7 +57,7 @@ class UserGroups(View): """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 = privacy_filter(request.user, groups) paginated = Paginator(groups, 12) data = { @@ -127,8 +123,7 @@ def add_member(request): """add 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 = models.Group.objects.get(id=request.POST["group"]) + group = get_object_or_404(models.Group, id=request.POST.get("group")) if not group: return HttpResponseBadRequest() From e15eef16c54647dce53bcdc1f4f01fb73587752b Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 21:21:00 +1000 Subject: [PATCH 040/127] improve new group member adding The add-members page now looks almost identical to the group page and is clearer. --- bookwyrm/templates/groups/find_users.html | 6 +++-- bookwyrm/templates/groups/group.html | 24 ++++++++++++++++- bookwyrm/templates/groups/members.html | 26 +------------------ .../templates/groups/suggested_users.html | 15 +---------- 4 files changed, 29 insertions(+), 42 deletions(-) diff --git a/bookwyrm/templates/groups/find_users.html b/bookwyrm/templates/groups/find_users.html index 9154a5275..99ec67bc4 100644 --- a/bookwyrm/templates/groups/find_users.html +++ b/bookwyrm/templates/groups/find_users.html @@ -1,6 +1,8 @@ {% extends 'groups/group.html' %} -{% block panel %} -

Add users to {{ group.name }}

+{% block searchresults %} +

+ Add new members! +

{% include 'groups/suggested_users.html' with suggested_users=suggested_users query=query %} {% endblock %} \ No newline at end of file diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index 6c44e3b42..9617e1332 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -8,9 +8,12 @@
+ {% block searchresults %} + {% endblock %} + {% include "groups/members.html" %} -

Lists

+

Lists

{% if not lists %}

{% trans "This group has no lists" %}

{% else %} @@ -57,5 +60,24 @@ {% endif %} {% include "snippets/pagination.html" with page=items %}
+ +
+
+

Find new members

+
+
+ +
+
+ +
+
+
+
+
{% endblock %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index df5f1602a..a64d840e1 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -2,34 +2,10 @@ {% load utilities %} {% load humanize %} {% load bookwyrm_tags %} - -{% if request.GET.updated %} -
- {% trans "You successfully added a user to this group!" %} -
-{% endif %} -

Group Members

+

Group Members

{% trans "Members can add and remove books on your group's book lists" %}

-{% block panel %} -
-
-
- -
-
- -
-
- {% include 'snippets/suggested_users.html' with suggested_users=suggested_users %} -
-{% endblock %} -
{% for member in group.members.all %}
diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html index 40d32f3f3..91b3784de 100644 --- a/bookwyrm/templates/groups/suggested_users.html +++ b/bookwyrm/templates/groups/suggested_users.html @@ -1,20 +1,7 @@ {% load i18n %} {% load utilities %} {% load humanize %} -
-
-
- -
-
- -
-
-
+ {% if suggested_users %}
{% for user in suggested_users %} From fb823189a01ab7d65a64e6cb9bf20ab8c65e2019 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 27 Sep 2021 21:48:40 +1000 Subject: [PATCH 041/127] don't allow non-local users to join groups (yet) Groups are not compatible with ActivityPub because I don't know what I'm doing. NOTE: this is super hacky, fix ASAP --- bookwyrm/templates/snippets/add_to_group_button.html | 6 +++++- bookwyrm/views/group.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html index cc394684c..fe1403c48 100644 --- a/bookwyrm/templates/snippets/add_to_group_button.html +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -1,6 +1,5 @@ {% load i18n %} {% if request.user == user or not request.user == group.user or not request.user.is_authenticated %} - {% elif user in request.user.blocks.all %} {% include 'snippets/block_button.html' with blocks=True %} {% else %} @@ -11,6 +10,7 @@ {% csrf_token %} + {% if user.local %} + {% else %} + + Remote User + {% endif %}
{% csrf_token %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index b28aabeb3..4d91be67d 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -84,7 +84,7 @@ class UserGroups(View): @method_decorator(login_required, name="dispatch") class FindUsers(View): """find friends to add to your group""" - """this is mostly taken from the Get Started friend finder""" + """this is mostly borrowed from the Get Started friend finder""" def get(self, request, group_id): """basic profile info""" @@ -99,6 +99,7 @@ class FindUsers(View): ) .filter( similarity__gt=0.5, + local=True ) .order_by("-similarity")[:5] ) From 66494e7788aa194153d465c43efd2f6426de014d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 28 Sep 2021 18:53:11 +1000 Subject: [PATCH 042/127] fix reverse reference to user bookwyrm_groups --- bookwyrm/models/group.py | 2 +- bookwyrm/templates/groups/user_groups.html | 2 +- bookwyrm/templates/user/groups.html | 2 +- bookwyrm/templates/user/layout.html | 2 +- bookwyrm/templates/user/user.html | 3 +++ bookwyrm/views/group.py | 24 +++++++++++++++++++--- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 4d9d28156..ea162b2a4 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -21,7 +21,7 @@ class Group(BookWyrmModel): symmetrical=False, through="GroupMember", through_fields=("group", "user"), - related_name="members" + related_name="bookwyrm_groups" ) def get_remote_id(self): diff --git a/bookwyrm/templates/groups/user_groups.html b/bookwyrm/templates/groups/user_groups.html index f99abc695..6b4cef1be 100644 --- a/bookwyrm/templates/groups/user_groups.html +++ b/bookwyrm/templates/groups/user_groups.html @@ -3,7 +3,7 @@ {% load interaction %}
- {% for group in groups %} + {% for group in user.bookwyrm_groups.all %}
diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html index 6b64e4b79..36736e013 100644 --- a/bookwyrm/templates/user/groups.html +++ b/bookwyrm/templates/user/groups.html @@ -34,7 +34,7 @@ {% include 'groups/form.html' %} - {% include 'groups/user_groups.html' with groups=groups %} + {% include 'groups/user_groups.html' %}
{% include 'snippets/pagination.html' with page=user_groups path=path %} diff --git a/bookwyrm/templates/user/layout.html b/bookwyrm/templates/user/layout.html index 0d07b199b..a1a5289d5 100755 --- a/bookwyrm/templates/user/layout.html +++ b/bookwyrm/templates/user/layout.html @@ -75,7 +75,7 @@ {% trans "Lists" %} {% endif %} - {% if is_self or has_groups %} + {% if is_self or user.bookwyrm_groups %} {% url 'user-groups' user|username as url %} {% trans "Groups" %} diff --git a/bookwyrm/templates/user/user.html b/bookwyrm/templates/user/user.html index f360a30af..676161d8e 100755 --- a/bookwyrm/templates/user/user.html +++ b/bookwyrm/templates/user/user.html @@ -22,6 +22,9 @@ {% block panel %} {% if user.bookwyrm_user %} +{% for group in user.bookwyrm_groups.all %} +
{{ group.name }}
+{% endfor %}

{% include 'user/shelf/books_header.html' %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 4d91be67d..3e1785cce 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -1,4 +1,5 @@ """group views""" +from django.apps import apps from django.contrib.auth.decorators import login_required from django.db import IntegrityError from django.core.paginator import Paginator @@ -62,8 +63,6 @@ class UserGroups(View): data = { "user": user, - "has_groups": models.GroupMember.objects.filter(user=user).exists(), - "groups": paginated.get_page(request.GET.get("page")), "group_form": forms.GroupForm(), "path": user.local_path + "/group", } @@ -144,6 +143,21 @@ def add_member(request): except IntegrityError: pass +# TODO: actually this needs to be associated with the user ACCEPTING AN INVITE!!! DOH! + + """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", + ) + return redirect(user.local_path) @require_POST @@ -151,8 +165,12 @@ def add_member(request): def remove_member(request): """remove a member from the group""" + # 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 + # 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.Group, id=request.POST.get("group")) # NOTE: does this not work? group = models.Group.objects.get(id=request.POST["group"]) if not group: return HttpResponseBadRequest() From 2f42161dda41dd647ef52c3cbc58044131a24d7a Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 10:10:37 +1000 Subject: [PATCH 043/127] disambiguate groups and prep for group invitations - rename Group to BookwyrmGroup - create group memberships and invitations - adjust all model name references accordingly --- bookwyrm/forms.py | 2 +- bookwyrm/models/__init__.py | 2 +- bookwyrm/models/group.py | 155 +++++++++++++++++++++++++++----- bookwyrm/models/list.py | 5 +- bookwyrm/models/notification.py | 10 ++- bookwyrm/models/user.py | 5 ++ bookwyrm/urls.py | 9 +- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/group.py | 130 +++++++++++++++++++-------- bookwyrm/views/user.py | 4 +- 10 files changed, 255 insertions(+), 69 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index ffb7581e5..290e01877 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -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): diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index a4a06ebad..7ac41f1b9 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -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 diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index ea162b2a4..103764d2d 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -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" ) - ] \ No newline at end of file + ] + + 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 \ No newline at end of file diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 7ea33a8b6..498026326 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -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""" diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index a4968f61f..3632fa10f 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -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, diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 637baa6ee..0e1397949 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -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""" diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 30ffc8689..05f8ceffe 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -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\d+)(.json)?/?$", views.Group.as_view(), name="group"), + re_path(r"^group/(?P\d+)(.json)?/?$", views.BookwyrmGroup.as_view(), name="group"), re_path(r"^group/(?P\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"), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 4d93c5973..fb9e72bc2 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -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 diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 3e1785cce..09bb0dcad 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -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) \ No newline at end of file + 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) \ No newline at end of file diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index d3f52e729..562c49335 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -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, From 0f3be40957e65e956b16dedb5c30c13942badb78 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 10:47:42 +1000 Subject: [PATCH 044/127] fix group references in templates Let's do this the sensible way huh, by using backwards references to memberships etc Also adds filters for is_member and is_invited so we don't have to do weird things in group Views --- bookwyrm/templates/groups/group.html | 2 +- bookwyrm/templates/groups/members.html | 6 ++++-- .../templates/groups/suggested_users.html | 2 +- bookwyrm/templates/groups/user_groups.html | 2 +- .../snippets/add_to_group_button.html | 20 ++++++++----------- bookwyrm/templates/user/groups.html | 2 +- bookwyrm/templates/user/user.html | 3 --- bookwyrm/templatetags/bookwyrm_group_tags.py | 19 ++++++++++++++++++ 8 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 bookwyrm/templatetags/bookwyrm_group_tags.py diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index 9617e1332..1ea8f00dd 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -11,7 +11,7 @@ {% block searchresults %} {% endblock %} - {% include "groups/members.html" %} + {% include "groups/members.html" with group=group %}

Lists

{% if not lists %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index a64d840e1..a08e73b92 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -7,7 +7,8 @@

{% trans "Members can add and remove books on your group's book lists" %}

\ No newline at end of file diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html index 91b3784de..6323ffbe1 100644 --- a/bookwyrm/templates/groups/suggested_users.html +++ b/bookwyrm/templates/groups/suggested_users.html @@ -12,7 +12,7 @@ {{ user.display_name|truncatechars:10 }} @{{ user|username|truncatechars:8 }} - {% include 'snippets/add_to_group_button.html' with user=user minimal=True %} + {% include 'snippets/add_to_group_button.html' with user=user group=group minimal=True %} {% if user.mutuals %}

{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %} diff --git a/bookwyrm/templates/groups/user_groups.html b/bookwyrm/templates/groups/user_groups.html index 6b4cef1be..f99abc695 100644 --- a/bookwyrm/templates/groups/user_groups.html +++ b/bookwyrm/templates/groups/user_groups.html @@ -3,7 +3,7 @@ {% load interaction %}

- {% for group in user.bookwyrm_groups.all %} + {% for group in groups %}
diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html index fe1403c48..7febe2b17 100644 --- a/bookwyrm/templates/snippets/add_to_group_button.html +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -1,4 +1,5 @@ {% load i18n %} +{% load bookwyrm_group_tags %} {% if request.user == user or not request.user == group.user or not request.user.is_authenticated %} {% elif user in request.user.blocks.all %} {% include 'snippets/block_button.html' with blocks=True %} @@ -6,30 +7,30 @@
-
+ {% csrf_token %} {% if user.local %} {% else %} - + Remote User {% endif %}
-
+ {% csrf_token %} - {% if user.manually_approves_followers and request.user not in user.followers.all %} + {% if group|is_invited:user %} {% else %}
- {% if not minimal %} -
- {% include 'snippets/user_options.html' with user=user class="is-small" %} -
- {% endif %}
{% endif %} diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html index 36736e013..2735a5b8e 100644 --- a/bookwyrm/templates/user/groups.html +++ b/bookwyrm/templates/user/groups.html @@ -24,7 +24,7 @@ {% block panel %}
-
diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index ae5cd67b1..8dba38c6e 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -42,7 +42,7 @@ {% elif notification.notification_type == 'REPLY' %} - {% elif notification.notification_type == 'FOLLOW' or notification.notification_type == 'FOLLOW_REQUEST' %} + {% elif notification.notification_type == 'FOLLOW' or notification.notification_type == 'FOLLOW_REQUEST' or notification.notification_type == 'INVITE' or notification.notification_type == 'ACCEPT' %} {% elif notification.notification_type == 'BOOST' %} @@ -122,6 +122,17 @@ {% else %} {% blocktrans with book_path=notification.related_list_item.book.local_path book_title=notification.related_list_item.book.title list_path=notification.related_list_item.book_list.local_path list_name=notification.related_list_item.book_list.name %} suggested adding {{ book_title }} to your list "{{ list_name }}"{% endblocktrans %} {% endif %} + {% elif notification.notification_type == 'INVITE' %} + {% if notification.related_group %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} invited you to join the group {{ group_name }} {% endblocktrans %} +
+ {% include 'snippets/join_invitation_buttons.html' with group=notification.related_group %} +
+ {% endif %} + {% elif notification.notification_type == 'ACCEPT' %} + {% if notification.related_group %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} accepted an invitation to join your group "{{ group_name }}"{% endblocktrans %} + {% endif %} {% endif %} {% elif notification.related_import %} {% url 'import-status' notification.related_import.id as url %} diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html index 7febe2b17..dd3b93c3c 100644 --- a/bookwyrm/templates/snippets/add_to_group_button.html +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -24,22 +24,22 @@ Remote User {% endif %} -
+ {% csrf_token %} - {% if group|is_invited:user %} + {% if not group|is_member:user %} {% else %} + {% if show_username %} + {% blocktrans with username=user.localname %}Remove @{{ username }}{% endblocktrans %} + {% else %} + {% trans "Remove" %} + {% endif %} + {% endif %}
diff --git a/bookwyrm/templates/snippets/join_invitation_buttons.html b/bookwyrm/templates/snippets/join_invitation_buttons.html new file mode 100644 index 000000000..46c4071d4 --- /dev/null +++ b/bookwyrm/templates/snippets/join_invitation_buttons.html @@ -0,0 +1,16 @@ +{% load i18n %} +{% load bookwyrm_group_tags %} +{% if group|is_invited:request.user %} +
+
+ {% csrf_token %} + + +
+
+ {% csrf_token %} + + +
+
+{% endif %} diff --git a/bookwyrm/templates/user/groups.html b/bookwyrm/templates/user/groups.html index 2735a5b8e..9c91fb186 100644 --- a/bookwyrm/templates/user/groups.html +++ b/bookwyrm/templates/user/groups.html @@ -24,7 +24,7 @@ {% block panel %}
-
- {% include 'snippets/pagination.html' with page=user_groups path=path %} + {% include 'snippets/pagination.html' with page=user.memberships path=path %}
{% endblock %} diff --git a/bookwyrm/templates/user/layout.html b/bookwyrm/templates/user/layout.html index a1a5289d5..c4ef2d8e5 100755 --- a/bookwyrm/templates/user/layout.html +++ b/bookwyrm/templates/user/layout.html @@ -4,6 +4,7 @@ {% load utilities %} {% load markdown %} {% load layout %} +{% load bookwyrm_group_tags %} {% block title %}{{ user.display_name }}{% endblock %} @@ -75,7 +76,7 @@ {% trans "Lists" %} {% endif %} - {% if is_self or user.bookwyrm_groups %} + {% if is_self or user|has_groups %} {% url 'user-groups' user|username as url %} {% trans "Groups" %} diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index fb9e72bc2..930fdfcd9 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -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 BookwyrmGroup, UserGroups, FindUsers, invite_member, remove_member, uninvite_member, accept_membership, reject_membership +from .group import BookwyrmGroup, UserGroups, FindUsers, invite_member, remove_member, accept_membership, reject_membership from .import_data import Import, ImportStatus from .inbox import Inbox from .interaction import Favorite, Unfavorite, Boost, Unboost diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 09bb0dcad..60ca8d21f 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -57,11 +57,11 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - groups = user.bookwyrmgroup_set.all() # follow the relationship backwards, nice - paginated = Paginator(groups, 12) + memberships = models.BookwyrmGroupMember.objects.filter(user=user).all() + paginated = Paginator(memberships, 12) data = { - "groups": paginated.get_page(request.GET.get("page")), + "memberships": paginated.get_page(request.GET.get("page")), "is_self": request.user.id == user.id, "user": user, "group_form": forms.GroupForm(), @@ -89,8 +89,10 @@ class FindUsers(View): def get(self, request, group_id): """basic profile info""" query = request.GET.get("query") + group = models.BookwyrmGroup.objects.get(id=group_id) user_results = ( models.User.viewer_aware_objects(request.user) + .exclude(memberships__in=group.memberships.all()) # don't suggest users who are already members .annotate( similarity=Greatest( TrigramSimilarity("username", query), @@ -149,7 +151,7 @@ def invite_member(request): @require_POST @login_required -def uninvite_member(request): +def remove_member(request): """invite a member to the group""" group = get_object_or_404(models.BookwyrmGroup, id=request.POST.get("group")) @@ -160,51 +162,31 @@ def uninvite_member(request): if not user: return HttpResponseBadRequest() - if not group.user == request.user: - return HttpResponseBadRequest() + is_member = models.BookwyrmGroupMember.objects.filter(group=group,user=user).exists() + is_invited = models.GroupMemberInvitation.objects.filter(group=group,user=user).exists() - try: - invitation = models.GroupMemberInvitation.objects.get( - user=user, - group=group - ) + if is_invited: + try: + invitation = models.GroupMemberInvitation.objects.get( + user=user, + group=group + ) - invitation.reject() + invitation.reject() - except IntegrityError: - pass + except IntegrityError: + pass - return redirect(user.local_path) + if is_member: -@require_POST -@login_required -def remove_member(request): - """remove a member from the group""" + try: + membership = models.BookwyrmGroupMember.objects.get(group=group,user=user) + membership.delete() - # 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 + except IntegrityError: + pass - # 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.BookwyrmGroup.objects.get(id=request.POST["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: - membership = models.BookwyrmGroupMember.objects.get(group=group,user=user) # BUG: wrong - membership.delete() - - except IntegrityError: - pass + # TODO: should send notification to all members including the now ex-member that they have been removed. return redirect(user.local_path) @@ -227,7 +209,7 @@ def accept_membership(request): except IntegrityError: pass - return redirect(request.user.local_path) + return redirect(group.local_path) @require_POST @login_required From 5237e88abab9cce0f0120cfb3d67ebbb29c5ff30 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 13:48:53 +1000 Subject: [PATCH 049/127] remove user button for groups --- bookwyrm/templates/groups/members.html | 20 ++-------------- .../snippets/remove_from_group_button.html | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 18 deletions(-) create mode 100644 bookwyrm/templates/snippets/remove_from_group_button.html diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index 3ee27db61..bd91b418c 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -20,24 +20,8 @@ Manager {% endif %} - - {% include 'snippets/add_to_group_button.html' with user=member group=group minimal=True %} - {% if member.mutuals %} -

- {% blocktrans trimmed with mutuals=member.mutuals|intcomma count counter=member.mutuals %} - {{ mutuals }} follower you follow - {% plural %} - {{ mutuals }} followers you follow{% endblocktrans %} -

- {% elif member.shared_books %} -

- {% blocktrans trimmed with shared_books=member.shared_books|intcomma count counter=member.shared_books %} - {{ shared_books }} book on your shelves - {% plural %} - {{ shared_books }} books on your shelves - {% endblocktrans %} -

- {% elif request.user in member.following.all %} + {% include 'snippets/remove_from_group_button.html' with user=member group=group minimal=True %} + {% if request.user in member.following.all %}

{% trans "Follows you" %}

diff --git a/bookwyrm/templates/snippets/remove_from_group_button.html b/bookwyrm/templates/snippets/remove_from_group_button.html new file mode 100644 index 000000000..938f48b25 --- /dev/null +++ b/bookwyrm/templates/snippets/remove_from_group_button.html @@ -0,0 +1,23 @@ +{% load i18n %} +{% load bookwyrm_group_tags %} +{% if request.user == user or not request.user == group.user or not request.user.is_authenticated %} +{% elif user in request.user.blocks.all %} +{% include 'snippets/block_button.html' with blocks=True %} +{% else %} +
+
+
+ {% csrf_token %} + + + +
+
+
+{% endif %} From 70e0128052944f03fdd5d718ef0d39dc71fbab11 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 14:41:23 +1000 Subject: [PATCH 050/127] non-owners can't add users to groups - hide add-user pages from non-owners - hide user searchbox from non-owners - fix find-user searchbox being in wrong place where no results --- bookwyrm/templates/groups/group.html | 2 ++ .../templates/groups/suggested_users.html | 7 +++---- bookwyrm/views/group.py | 19 ++++++++++++++++++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index 1ea8f00dd..4d1cdf79c 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -61,6 +61,7 @@ {% include "snippets/pagination.html" with page=items %} + {% if group.user == request.user %}

Find new members

@@ -78,6 +79,7 @@
+ {% endif %}
{% endblock %} diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html index ce5eab6d8..75dfe491c 100644 --- a/bookwyrm/templates/groups/suggested_users.html +++ b/bookwyrm/templates/groups/suggested_users.html @@ -3,7 +3,6 @@ {% load humanize %} {% if suggested_users %} -
{% for user in suggested_users %}
@@ -37,7 +36,7 @@
{% endfor %} {% else %} - No potential members found for "{{ query }}" +
+ No potential members found for "{{ query }}" +
{% endif %} -
- diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 60ca8d21f..5ae2cecdb 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -114,6 +114,12 @@ class FindUsers(View): group = get_object_or_404(models.BookwyrmGroup, id=group_id) + if not group: + return HttpResponseBadRequest() + + if not group.user == request.user: + return HttpResponseBadRequest() + data = { "suggested_users": user_results, "group": group, @@ -186,7 +192,18 @@ def remove_member(request): except IntegrityError: pass - # TODO: should send notification to all members including the now ex-member that they have been removed. + # let the other members know about it + model = apps.get_model("bookwyrm.Notification", require_ready=True) + memberships = models.BookwyrmGroupMember.objects.get(group=group) + for membership in memberships: + member = membership.user + if member != request.user: + model.objects.create( + user=member, + related_user=request.user, + related_group=request.group, + notification_type="REMOVE", + ) return redirect(user.local_path) From f82af6382fb998f64ad319da90afd3db8e4d4be2 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 15:48:34 +1000 Subject: [PATCH 051/127] make message about group members more generic --- bookwyrm/templates/groups/members.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index bd91b418c..52d27f120 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -4,7 +4,7 @@ {% load bookwyrm_tags %}

Group Members

-

{% trans "Members can add and remove books on your group's book lists" %}

+

{% trans "Members can add and remove books on a group's book lists" %}

{% for membership in group.memberships.all %} From 21e6ed7388db16113b49807ab41bd04df913fe1f Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 15:48:55 +1000 Subject: [PATCH 052/127] complete group notifications - notify group members when a new member accepts an invitation - notify all group members when a member leaves or is removed - notify ex-member when they are removed --- bookwyrm/models/group.py | 16 +++++++++++----- bookwyrm/models/notification.py | 2 +- bookwyrm/templates/notifications.html | 20 ++++++++++++++++++-- bookwyrm/views/group.py | 21 +++++++++++++++------ 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index fdba04ea5..dbded1154 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -133,21 +133,27 @@ class GroupMemberInvitation(models.Model): with transaction.atomic(): BookwyrmGroupMember.from_request(self) - # let the other members know about it 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 != self.user: + if member != self.user and member != self.group.user: model.objects.create( user=member, related_user=self.user, related_group=self.group, - notification_type="ACCEPT", + notification_type="JOIN", ) def reject(self): """generate a Reject for this membership request""" self.delete() - - # TODO: send notification \ No newline at end of file diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 3632fa10f..a2ddb8747 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -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 INVITE ACCEPT", + "FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT INVITE ACCEPT JOIN LEAVE REMOVE", ) diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index 8dba38c6e..8c076ccbd 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -42,7 +42,7 @@ {% elif notification.notification_type == 'REPLY' %} - {% elif notification.notification_type == 'FOLLOW' or notification.notification_type == 'FOLLOW_REQUEST' or notification.notification_type == 'INVITE' or notification.notification_type == 'ACCEPT' %} + {% elif notification.notification_type == 'FOLLOW' or notification.notification_type == 'FOLLOW_REQUEST' or notification.notification_type == 'INVITE' or notification.notification_type == 'ACCEPT' or notification.notification_type == 'JOIN' or notification.notification_type == 'LEAVE' or notification.notification_type == 'REMOVE'%} {% elif notification.notification_type == 'BOOST' %} @@ -131,9 +131,25 @@ {% endif %} {% elif notification.notification_type == 'ACCEPT' %} {% if notification.related_group %} - {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} accepted an invitation to join your group "{{ group_name }}"{% endblocktrans %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} accepted your invitation to join group "{{ group_name }}"{% endblocktrans %} + {% endif %} + {% elif notification.notification_type == 'JOIN' %} + {% if notification.related_group %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has joined your group "{{ group_name }}"{% endblocktrans %} + {% endif %} + {% elif notification.notification_type == 'LEAVE' %} + {% if notification.related_group %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has left your group "{{ group_name }}"{% endblocktrans %} + {% endif %} + {% elif notification.notification_type == 'REMOVE' %} + {% if notification.related_group %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has been removed from your group "{{ group_name }}"{% endblocktrans %} {% endif %} {% endif %} + {% elif notification.notification_type == 'REMOVE' and notification.related_group %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + You have been removed from the "{{ group_name }} group" + {% endblocktrans %} {% elif notification.related_import %} {% url 'import-status' notification.related_import.id as url %} {% blocktrans %}Your import completed.{% endblocktrans %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 5ae2cecdb..b8c45a4da 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -158,7 +158,7 @@ def invite_member(request): @require_POST @login_required def remove_member(request): - """invite a member to the group""" + """remove a member from the group""" group = get_object_or_404(models.BookwyrmGroup, id=request.POST.get("group")) if not group: @@ -192,19 +192,28 @@ def remove_member(request): except IntegrityError: pass - # let the other members know about it + memberships = models.BookwyrmGroupMember.objects.filter(group=group) model = apps.get_model("bookwyrm.Notification", require_ready=True) - memberships = models.BookwyrmGroupMember.objects.get(group=group) + notification_type = "LEAVE" if "self_removal" in request.POST and request.POST["self_removal"] else "REMOVE" + # let the other members know about it for membership in memberships: member = membership.user if member != request.user: model.objects.create( user=member, - related_user=request.user, - related_group=request.group, - notification_type="REMOVE", + related_user=user, + related_group=group, + notification_type=notification_type, ) + # let the user (now ex-member) know as well, if they were removed + if notification_type == "REMOVE": + model.objects.create( + user=user, + related_group=group, + notification_type=notification_type, + ) + return redirect(user.local_path) @require_POST From 52a083a9070142e980bd8cbd48d7e4370d9a002e Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 16:52:34 +1000 Subject: [PATCH 053/127] revert name change for Group, GroupMember these were named as BookwyrmGroup and BookwyrmGroupMember due to a misunderstanding about related_name and a dodgy development environment. This naming makes more sense. --- bookwyrm/forms.py | 2 +- bookwyrm/models/__init__.py | 2 +- bookwyrm/models/group.py | 14 +++++----- bookwyrm/models/list.py | 2 +- bookwyrm/models/notification.py | 2 +- bookwyrm/models/user.py | 5 ---- .../templates/groups/suggested_users.html | 2 +- bookwyrm/templatetags/bookwyrm_group_tags.py | 4 +-- bookwyrm/urls.py | 2 +- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/group.py | 28 +++++++++---------- bookwyrm/views/user.py | 2 +- 12 files changed, 31 insertions(+), 36 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 290e01877..ffb7581e5 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -297,7 +297,7 @@ class ListForm(CustomForm): class GroupForm(CustomForm): class Meta: - model = models.BookwyrmGroup + model = models.Group fields = ["user", "privacy", "name", "description"] class ReportForm(CustomForm): diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 7ac41f1b9..c5ea44e0a 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -21,7 +21,7 @@ from .relationship import UserFollows, UserFollowRequest, UserBlocks from .report import Report, ReportComment from .federated_server import FederatedServer -from .group import BookwyrmGroup, BookwyrmGroupMember, GroupMemberInvitation +from .group import Group, GroupMember, GroupMemberInvitation from .import_job import ImportJob, ImportItem diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index dbded1154..2d60ae2de 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -8,7 +8,7 @@ from . import fields from .relationship import UserBlocks # from .user import User -class BookwyrmGroup(BookWyrmModel): +class Group(BookWyrmModel): """A group of users""" name = fields.CharField(max_length=100) @@ -17,12 +17,12 @@ class BookwyrmGroup(BookWyrmModel): description = fields.TextField(blank=True, null=True) privacy = fields.PrivacyField() -class BookwyrmGroupMember(models.Model): +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( - "BookwyrmGroup", + "Group", on_delete=models.CASCADE, related_name="memberships" ) @@ -53,7 +53,7 @@ class BookwyrmGroupMember(models.Model): ) ).exists(): raise IntegrityError() - # accepts and requests are handled by the BookwyrmGroupInvitation model + # accepts and requests are handled by the GroupInvitation model super().save(*args, **kwargs) @classmethod @@ -74,7 +74,7 @@ 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", + "Group", on_delete=models.CASCADE, related_name="user_invitations" ) @@ -94,7 +94,7 @@ class GroupMemberInvitation(models.Model): """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( + if GroupMember.objects.filter( user=self.user, group=self.group ).exists(): @@ -131,7 +131,7 @@ class GroupMemberInvitation(models.Model): """turn this request into the real deal""" with transaction.atomic(): - BookwyrmGroupMember.from_request(self) + GroupMember.from_request(self) model = apps.get_model("bookwyrm.Notification", require_ready=True) # tell the group owner diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 498026326..43f5265d0 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -35,7 +35,7 @@ class List(OrderedCollectionMixin, BookWyrmModel): max_length=255, default="closed", choices=CurationType.choices ) group = models.ForeignKey( - "BookwyrmGroup", + "Group", on_delete=models.PROTECT, default=None, blank=True, diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index a2ddb8747..0cae7790c 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -23,7 +23,7 @@ class Notification(BookWyrmModel): "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" + "Group", 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) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 0e1397949..637baa6ee 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -143,11 +143,6 @@ 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""" diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html index 75dfe491c..54ec861db 100644 --- a/bookwyrm/templates/groups/suggested_users.html +++ b/bookwyrm/templates/groups/suggested_users.html @@ -37,6 +37,6 @@ {% endfor %} {% else %}
- No potential members found for "{{ query }}" +

No potential members found for "{{ query }}"

{% endif %} diff --git a/bookwyrm/templatetags/bookwyrm_group_tags.py b/bookwyrm/templatetags/bookwyrm_group_tags.py index 81c4a4b94..eabca2b41 100644 --- a/bookwyrm/templatetags/bookwyrm_group_tags.py +++ b/bookwyrm/templatetags/bookwyrm_group_tags.py @@ -10,13 +10,13 @@ register = template.Library() def has_groups(user): """whether or not the user has a pending invitation to join this group""" - return models.BookwyrmGroupMember.objects.filter(user=user).exists() + return models.GroupMember.objects.filter(user=user).exists() @register.filter(name="is_member") def is_member(group, user): """whether or not the user is a member of this group""" - return models.BookwyrmGroupMember.objects.filter(group=group,user=user).exists() + return models.GroupMember.objects.filter(group=group,user=user).exists() @register.filter(name="is_invited") def is_invited(group, user): diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 3e1d35266..834f95be9 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -253,7 +253,7 @@ 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\d+)(.json)?/?$", views.BookwyrmGroup.as_view(), name="group"), + re_path(r"^group/(?P\d+)(.json)?/?$", views.Group.as_view(), name="group"), re_path(r"^group/(?P\d+)/add-users/?$", views.FindUsers.as_view(), name="group-find-users"), re_path(r"^add-group-member/?$", views.invite_member, name="invite-group-member"), re_path(r"^remove-group-member/?$", views.remove_member, name="remove-group-member"), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 930fdfcd9..4bdfb6ed6 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -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 BookwyrmGroup, UserGroups, FindUsers, invite_member, remove_member, accept_membership, reject_membership +from .group import Group, UserGroups, FindUsers, invite_member, remove_member, accept_membership, reject_membership from .import_data import Import, ImportStatus from .inbox import Inbox from .interaction import Favorite, Unfavorite, Boost, Unboost diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index b8c45a4da..373811655 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -18,13 +18,13 @@ from .helpers import privacy_filter from .helpers import get_user_from_username from bookwyrm.settings import DOMAIN -class BookwyrmGroup(View): +class Group(View): """group page""" def get(self, request, group_id): """display a group""" - group = get_object_or_404(models.BookwyrmGroup, id=group_id) + group = get_object_or_404(models.Group, 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 BookwyrmGroup(View): @method_decorator(login_required, name="dispatch") def post(self, request, group_id): """edit a group""" - user_group = get_object_or_404(models.BookwyrmGroup, id=group_id) + user_group = get_object_or_404(models.Group, id=group_id) form = forms.GroupForm(request.POST, instance=user_group) if not form.is_valid(): return redirect("group", user_group.id) @@ -57,7 +57,7 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - memberships = models.BookwyrmGroupMember.objects.filter(user=user).all() + memberships = models.GroupMember.objects.filter(user=user).all() paginated = Paginator(memberships, 12) data = { @@ -78,7 +78,7 @@ class UserGroups(View): return redirect(request.user.local_path + "groups") group = form.save() # add the creator as a group member - models.BookwyrmGroupMember.objects.create(group=group, user=request.user) + models.GroupMember.objects.create(group=group, user=request.user) return redirect("group", group.id) @method_decorator(login_required, name="dispatch") @@ -89,7 +89,7 @@ class FindUsers(View): def get(self, request, group_id): """basic profile info""" query = request.GET.get("query") - group = models.BookwyrmGroup.objects.get(id=group_id) + group = models.Group.objects.get(id=group_id) user_results = ( models.User.viewer_aware_objects(request.user) .exclude(memberships__in=group.memberships.all()) # don't suggest users who are already members @@ -112,7 +112,7 @@ class FindUsers(View): request.user ) - group = get_object_or_404(models.BookwyrmGroup, id=group_id) + group = get_object_or_404(models.Group, id=group_id) if not group: return HttpResponseBadRequest() @@ -133,7 +133,7 @@ class FindUsers(View): def invite_member(request): """invite a member to the group""" - group = get_object_or_404(models.BookwyrmGroup, id=request.POST.get("group")) + group = get_object_or_404(models.Group, id=request.POST.get("group")) if not group: return HttpResponseBadRequest() @@ -160,7 +160,7 @@ def invite_member(request): def remove_member(request): """remove a member from the group""" - group = get_object_or_404(models.BookwyrmGroup, id=request.POST.get("group")) + group = get_object_or_404(models.Group, id=request.POST.get("group")) if not group: return HttpResponseBadRequest() @@ -168,7 +168,7 @@ def remove_member(request): if not user: return HttpResponseBadRequest() - is_member = models.BookwyrmGroupMember.objects.filter(group=group,user=user).exists() + is_member = models.GroupMember.objects.filter(group=group,user=user).exists() is_invited = models.GroupMemberInvitation.objects.filter(group=group,user=user).exists() if is_invited: @@ -186,13 +186,13 @@ def remove_member(request): if is_member: try: - membership = models.BookwyrmGroupMember.objects.get(group=group,user=user) + membership = models.GroupMember.objects.get(group=group,user=user) membership.delete() except IntegrityError: pass - memberships = models.BookwyrmGroupMember.objects.filter(group=group) + memberships = models.GroupMember.objects.filter(group=group) model = apps.get_model("bookwyrm.Notification", require_ready=True) notification_type = "LEAVE" if "self_removal" in request.POST and request.POST["self_removal"] else "REMOVE" # let the other members know about it @@ -221,7 +221,7 @@ def remove_member(request): def accept_membership(request): """accept an invitation to join a group""" - group = models.BookwyrmGroup.objects.get(id=request.POST["group"]) + group = models.Group.objects.get(id=request.POST["group"]) if not group: return HttpResponseBadRequest() @@ -242,7 +242,7 @@ def accept_membership(request): def reject_membership(request): """reject an invitation to join a group""" - group = models.BookwyrmGroup.objects.get(id=request.POST["group"]) + group = models.Group.objects.get(id=request.POST["group"]) if not group: return HttpResponseBadRequest() diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 562c49335..f711a7798 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -140,7 +140,7 @@ class Groups(View): user = get_user_from_username(request.user, username) paginated = Paginator( - models.BookwyrmGroup.memberships.filter(user=user) + models.Group.memberships.filter(user=user) ) data = { "user": user, From 832a9b9890a69095a7e521d69159c9ab7a4b6224 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 16:54:44 +1000 Subject: [PATCH 054/127] fix group local_path as per Lists, we need to override get_remote_id to remove the user from the URL --- bookwyrm/models/group.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 2d60ae2de..3e76a6b7b 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -17,6 +17,10 @@ class Group(BookWyrmModel): 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}" + class GroupMember(models.Model): """Users who are members of a group""" created_date = models.DateTimeField(auto_now_add=True) From 8496f2403258eb13d10894ecfd1d94aa461d3aee Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 18:09:15 +1000 Subject: [PATCH 055/127] fix filters for group members to see and edit group lists --- bookwyrm/templates/lists/form.html | 6 +++--- bookwyrm/templates/lists/list.html | 9 +++++---- bookwyrm/views/list.py | 19 ++++--------------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/bookwyrm/templates/lists/form.html b/bookwyrm/templates/lists/form.html index a98cae94a..492ccf62a 100644 --- a/bookwyrm/templates/lists/form.html +++ b/bookwyrm/templates/lists/form.html @@ -37,14 +37,14 @@ {% trans "Group" %}

{% trans "Group members can add to and remove from this list" %}

- {% if user_groups %} + {% if user.memberships %}
diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index ea28bb77f..b1246b943 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -1,6 +1,7 @@ {% extends 'lists/layout.html' %} {% load i18n %} {% load bookwyrm_tags %} +{% load bookwyrm_group_tags %} {% load markdown %} {% block panel %} @@ -16,7 +17,7 @@
{% if request.GET.updated %}
- {% if list.curation != "open" and request.user != list.user %} + {% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %} {% trans "You successfully suggested a book for this list!" %} {% else %} {% trans "You successfully added a book to this list!" %} @@ -66,7 +67,7 @@

{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by {{ username }}{% endblocktrans %}

- {% if list.user == request.user or list.curation == 'open' and item.user == request.user %} + {% if list.user == request.user or list.curation == 'open' and item.user == request.user or list.group|is_member:request.user %} diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 912c3cfdb..53f39b549 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -45,13 +45,9 @@ class Lists(View): lists = privacy_filter( request.user, lists, privacy_levels=["public", "followers"] ) - - user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") - paginated = Paginator(lists, 12) data = { "lists": paginated.get_page(request.GET.get("page")), - "user_groups": user_groups, "list_form": forms.ListForm(), "path": "/list", } @@ -96,14 +92,12 @@ class UserLists(View): user = get_user_from_username(request.user, username) lists = models.List.objects.filter(user=user) lists = privacy_filter(request.user, lists) - user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") paginated = Paginator(lists, 12) data = { "user": user, "is_self": request.user.id == user.id, "lists": paginated.get_page(request.GET.get("page")), - "user_groups": user_groups, "list_form": forms.ListForm(), "path": user.local_path + "/lists", } @@ -176,8 +170,6 @@ class List(View): ).order_by("-updated_date") ][: 5 - len(suggestions)] - user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") - is_group_member = book_list.group in user_groups page = paginated.get_page(request.GET.get("page")) data = { "list": book_list, @@ -191,9 +183,7 @@ class List(View): "query": query or "", "sort_form": forms.SortListForm( {"direction": direction, "sort_by": sort_by} - ), - "user_groups": user_groups, - "is_group_member": is_group_member + ) } return TemplateResponse(request, "lists/list.html", data) @@ -296,8 +286,7 @@ def add_book(request): book_list = get_object_or_404(models.List, id=request.POST.get("list")) is_group_member = False if book_list.curation == "group": - user_groups = models.Group.objects.filter(members=request.user).order_by("-updated_date") - is_group_member = book_list.group in user_groups + is_group_member = models.GroupMember.objects.filter(group=book_list.group, user=request.user).exists() if not book_list.visible_to_user(request.user): return HttpResponseNotFound() @@ -350,8 +339,8 @@ def remove_book(request, list_id): with transaction.atomic(): book_list = get_object_or_404(models.List, id=list_id) item = get_object_or_404(models.ListItem, id=request.POST.get("item")) - - if not book_list.user == request.user and not item.user == request.user: + is_group_member = models.GroupMember.objects.filter(group=book_list.group, user=request.user).exists() + if not book_list.user == request.user and not item.user == request.user and not is_group_member: return HttpResponseNotFound() deleted_order = item.order From 8708d71f4bd76454f8358902f0d024126ec4618d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 18:31:56 +1000 Subject: [PATCH 056/127] group members can see lists - fix visible_to_user for group objects (like lists) - temporarily disable privacy_filter on group lists --- bookwyrm/models/base_model.py | 4 ++-- bookwyrm/views/group.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 1b4bae1a9..50119cc11 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -84,8 +84,8 @@ class BookWyrmModel(models.Model): # you can see objects which have a group of which you are a member if hasattr(self, "group"): if ( - hasattr(self.group, "members") - and viewer in self.group.members.all() + hasattr(self.group, "memberships") + and self.group.memberships.filter(user=viewer).exists() ): return True diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 373811655..718aa9eeb 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -26,7 +26,7 @@ class Group(View): group = get_object_or_404(models.Group, id=group_id) lists = models.List.objects.filter(group=group).order_by("-updated_date") - lists = privacy_filter(request.user, lists) + # lists = privacy_filter(request.user, lists) # don't show groups to users who shouldn't see them if not group.visible_to_user(request.user): From 2c399fe1aa467c6b46a5c0d143ef317386fdc05e Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 19:35:08 +1000 Subject: [PATCH 057/127] fix suggested members all appearing in a column --- bookwyrm/templates/groups/suggested_users.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html index 54ec861db..a719c5fad 100644 --- a/bookwyrm/templates/groups/suggested_users.html +++ b/bookwyrm/templates/groups/suggested_users.html @@ -3,8 +3,8 @@ {% load humanize %} {% if suggested_users %} - {% for user in suggested_users %} - {% endfor %} {% else %} -
-

No potential members found for "{{ query }}"

-
+

No potential members found for "{{ query }}"


+ {% endif %} From 29f18ee123c79997834fbf9ada7b6021009b5ce8 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 19:35:57 +1000 Subject: [PATCH 058/127] only suggest local users as potential group members --- bookwyrm/suggested_users.py | 22 +++++++++++++++++++ .../snippets/add_to_group_button.html | 5 ----- bookwyrm/views/group.py | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/bookwyrm/suggested_users.py b/bookwyrm/suggested_users.py index e8f236324..06ce6db7b 100644 --- a/bookwyrm/suggested_users.py +++ b/bookwyrm/suggested_users.py @@ -103,6 +103,28 @@ class SuggestedUsers(RedisStore): break return results + def get_group_suggestions(self, user): + """get suggestions for new group members""" + values = self.get_store(self.store_id(user), withscores=True) + results = [] + # annotate users with mutuals and shared book counts + for user_id, rank in values: + counts = self.get_counts_from_rank(rank) + try: + user = models.User.objects.get( + id=user_id, is_active=True, bookwyrm_user=True + ) + except models.User.DoesNotExist as err: + # if this happens, the suggestions are janked way up + logger.exception(err) + continue + user.mutuals = counts["mutuals"] + # only suggest local users until Groups are ActivityPub compliant + if user.local: + results.append(user) + if len(results) >= 5: + break + return results def get_annotated_users(viewer, *args, **kwargs): """Users, annotated with things they have in common""" diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html index dd3b93c3c..cf9ae15df 100644 --- a/bookwyrm/templates/snippets/add_to_group_button.html +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -11,7 +11,6 @@ {% csrf_token %} - {% if user.local %} - {% else %} - - Remote User - {% endif %}
{% csrf_token %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 718aa9eeb..17db93eda 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -108,7 +108,7 @@ class FindUsers(View): data = {"no_results": not user_results} if user_results.count() < 5: - user_results = list(user_results) + suggested_users.get_suggestions( + user_results = list(user_results) + suggested_users.get_group_suggestions( request.user ) From 3a954ca6ae6d7519f4fe3b6e57741ed722a2e482 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 20:05:19 +1000 Subject: [PATCH 059/127] improve responsive layout for groups --- bookwyrm/templates/groups/members.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index 52d27f120..e005aba65 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -6,10 +6,10 @@

Group Members

{% trans "Members can add and remove books on a group's book lists" %}

-
+
{% for membership in group.memberships.all %} {% with member=membership.user %} -
+
{% include 'snippets/avatar.html' with user=member large=True %} {{ member.display_name|truncatechars:10 }} From 72e00f75c9cc53419f9c1c3c06877f4705da78e7 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 20:14:53 +1000 Subject: [PATCH 060/127] send notification when other group members add books to group lists --- bookwyrm/models/list.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 43f5265d0..b891a229d 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -91,9 +91,9 @@ class ListItem(CollectionItemMixin, BookWyrmModel): self.book_list.save(broadcast=False) list_owner = self.book_list.user + model = apps.get_model("bookwyrm.Notification", require_ready=True) # create a notification if somoene ELSE added to a local user's list if created and list_owner.local and list_owner != self.user: - model = apps.get_model("bookwyrm.Notification", require_ready=True) model.objects.create( user=list_owner, related_user=self.user, @@ -101,9 +101,15 @@ 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 - + if self.book_list.group: + for membership in self.book_list.group.memberships.all(): + if membership.user != self.user: + model.objects.create( + user=membership.user, + related_user=self.user, + related_list_item=self, + notification_type="ADD" + ) class Meta: """A book may only be placed into a list once, and each order in the list may be used only once""" From eed9d44cfdbbab0b343b4eb8c1f68c1c56d6bf8c Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 20:52:19 +1000 Subject: [PATCH 061/127] fix visible_to_user for groups user is a member of --- bookwyrm/models/base_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 50119cc11..61652620e 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -78,7 +78,7 @@ class BookWyrmModel(models.Model): return True # you can see groups of which you are a member - if hasattr(self, "members") and viewer in self.members.all(): + if hasattr(self, "memberships") and self.memberships.filter(user=viewer).exists(): return True # you can see objects which have a group of which you are a member From 680e547c8b2285318cb3875337025741df1ee2e4 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 21:24:26 +1000 Subject: [PATCH 062/127] add button for non-owner members to leave group --- bookwyrm/templates/groups/group.html | 5 +++-- bookwyrm/templates/groups/members.html | 18 +++++++++++++++++- bookwyrm/views/group.py | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index 4d1cdf79c..f19e8ee4f 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -10,8 +10,9 @@ {% block searchresults %} {% endblock %} - - {% include "groups/members.html" with group=group %} +
+ {% include "groups/members.html" with group=group %} +

Lists

{% if not lists %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index e005aba65..8c3dac7b4 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -2,6 +2,7 @@ {% load utilities %} {% load humanize %} {% load bookwyrm_tags %} +{% load bookwyrm_group_tags %}

Group Members

{% trans "Members can add and remove books on a group's book lists" %}

@@ -29,4 +30,19 @@
{% endwith %} {% endfor %} -
\ No newline at end of file +
+ +{% if group.user != request.user and group|is_member:request.user %} + + {% csrf_token %} + + + + +{% endif %} \ No newline at end of file diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 17db93eda..cb21842b7 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -194,7 +194,7 @@ def remove_member(request): memberships = models.GroupMember.objects.filter(group=group) model = apps.get_model("bookwyrm.Notification", require_ready=True) - notification_type = "LEAVE" if "self_removal" in request.POST and request.POST["self_removal"] else "REMOVE" + notification_type = "LEAVE" if user == request.user else "REMOVE" # let the other members know about it for membership in memberships: member = membership.user From 4ea99d17639e6084e03ecca66c359b1b15427427 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 3 Oct 2021 09:06:06 +1100 Subject: [PATCH 063/127] don't assign a group when creating non-group curated lists same as updating a list but for if a user changes their mind about curation when initially creating a list. --- bookwyrm/views/list.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 53f39b549..bed9ee9a4 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -61,6 +61,10 @@ class Lists(View): if not form.is_valid(): return redirect("lists") book_list = form.save() + # list should not have a group if it is not group curated + if not book_list.curation == "group": + book_list.group = None + book_list.save() return redirect(book_list.local_path) From a179de33bc135c657e29ae47af9a95a3ff12f31b Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 3 Oct 2021 09:07:42 +1100 Subject: [PATCH 064/127] fix incorrect wording on group selection select a group, not a list! --- bookwyrm/templates/lists/form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/lists/form.html b/bookwyrm/templates/lists/form.html index 492ccf62a..9b74655cd 100644 --- a/bookwyrm/templates/lists/form.html +++ b/bookwyrm/templates/lists/form.html @@ -42,7 +42,7 @@
From c04659984f0803ad39ae0857d059b07a2ac70919 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 3 Oct 2021 13:45:19 +1100 Subject: [PATCH 072/127] fix raise_not_editable for group lists --- bookwyrm/models/list.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 692405647..19f9e4f56 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -64,6 +64,16 @@ class List(OrderedCollectionMixin, BookWyrmModel): ordering = ("-updated_date",) + def raise_not_editable(self, viewer): + """the associated user OR the list owner can edit""" + print("raising not editable") + if self.user == viewer: + return + # group members can edit items in group lists + is_group_member = GroupMember.objects.filter(group=self.group, user=viewer).exists() + if is_group_member: + return + super().raise_not_editable(viewer) class ListItem(CollectionItemMixin, BookWyrmModel): """ok""" From 9d8e9786864df23a8ee4f763cad01f987fb58526 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 3 Oct 2021 13:45:41 +1100 Subject: [PATCH 073/127] sort group members in UserGroups view --- bookwyrm/views/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 6a855abb7..42ae5a128 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -56,7 +56,7 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - memberships = models.GroupMember.objects.filter(user=user).all() + memberships = models.GroupMember.objects.filter(user=user).all().order_by("-updated_date") paginated = Paginator(memberships, 12) data = { From 0d5c20bcde2ea84e5fbff2b52d0dc20a320015df Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 3 Oct 2021 13:57:21 +1100 Subject: [PATCH 074/127] remove_from_group button updates - enable blocked users to be removed - make "remove" button more subtle --- bookwyrm/templates/snippets/remove_from_group_button.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/snippets/remove_from_group_button.html b/bookwyrm/templates/snippets/remove_from_group_button.html index 938f48b25..05b17b594 100644 --- a/bookwyrm/templates/snippets/remove_from_group_button.html +++ b/bookwyrm/templates/snippets/remove_from_group_button.html @@ -1,16 +1,18 @@ {% load i18n %} {% load bookwyrm_group_tags %} {% if request.user == user or not request.user == group.user or not request.user.is_authenticated %} -{% elif user in request.user.blocks.all %} -{% include 'snippets/block_button.html' with blocks=True %} {% else %} +{% if user in request.user.blocks.all %} +{% include 'snippets/block_button.html' with blocks=True %} +
+{% endif %}
{% csrf_token %} -
From 3a9031112978b7e79eef228deb16d750d2b0ab2f Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 4 Oct 2021 22:20:02 +1100 Subject: [PATCH 079/127] update indenting for linter --- bookwyrm/static/js/bookwyrm.js | 3 + .../templates/groups/delete_group_modal.html | 18 ++--- bookwyrm/templates/groups/find_users.html | 8 +- bookwyrm/templates/groups/group.html | 74 +++++++++---------- bookwyrm/templates/groups/members.html | 52 ++++++------- .../templates/groups/suggested_users.html | 3 +- .../templates/notifications/items/accept.html | 10 +-- .../templates/notifications/items/leave.html | 10 +-- .../templates/notifications/items/remove.html | 10 +-- .../snippets/add_to_group_button.html | 14 ++-- .../snippets/remove_from_group_button.html | 12 +-- 11 files changed, 109 insertions(+), 105 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 049de497c..5bf845a4e 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -133,8 +133,10 @@ let BookWyrm = new class { revealForm(event) { let trigger = event.currentTarget; let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0]; + // if the form has already been revealed, there is no '.is-hidden' element // so this doesn't really work as a toggle + if (hidden) { this.addRemoveClass(hidden, 'is-hidden', !hidden); } @@ -150,6 +152,7 @@ let BookWyrm = new class { let trigger = event.currentTarget; let targetId = trigger.dataset.hides let visible = document.getElementById(targetId) + this.addRemoveClass(visible, 'is-hidden', true); } diff --git a/bookwyrm/templates/groups/delete_group_modal.html b/bookwyrm/templates/groups/delete_group_modal.html index ff6593e50..fd6706157 100644 --- a/bookwyrm/templates/groups/delete_group_modal.html +++ b/bookwyrm/templates/groups/delete_group_modal.html @@ -8,14 +8,14 @@ {% endblock %} {% block modal-footer %} -
- {% csrf_token %} - - - {% trans "Cancel" as button_text %} - {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="delete_list" controls_uid=list.id %} -
+
+ {% csrf_token %} + + + {% trans "Cancel" as button_text %} + {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="delete_list" controls_uid=list.id %} +
{% endblock %} diff --git a/bookwyrm/templates/groups/find_users.html b/bookwyrm/templates/groups/find_users.html index 99ec67bc4..ec890a93d 100644 --- a/bookwyrm/templates/groups/find_users.html +++ b/bookwyrm/templates/groups/find_users.html @@ -1,8 +1,8 @@ {% extends 'groups/group.html' %} {% block searchresults %} -

- Add new members! -

- {% include 'groups/suggested_users.html' with suggested_users=suggested_users query=query %} +

+ Add new members! +

+ {% include 'groups/suggested_users.html' with suggested_users=suggested_users query=query %} {% endblock %} \ No newline at end of file diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index f19e8ee4f..408f1f945 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -11,7 +11,7 @@ {% block searchresults %} {% endblock %}
- {% include "groups/members.html" with group=group %} + {% include "groups/members.html" with group=group %}

Lists

@@ -20,42 +20,42 @@ {% else %}
- {% for list in lists %} -
-
-
-

- {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %} -

-
- - {% with list_books=list.listitem_set.all|slice:5 %} - {% if list_books %} - - {% endif %} - {% endwith %} - -
-
- {% if list.description %} - {{ list.description|to_markdown|safe|truncatechars_html:30 }} - {% else %} -   - {% endif %} -
-

- {% include 'lists/created_text.html' with list=list %} -

-
-
-
- {% endfor %} + {% for list in lists %} +
+
+
+

+ {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %} +

+
+ + {% with list_books=list.listitem_set.all|slice:5 %} + {% if list_books %} + + {% endif %} + {% endwith %} + +
+
+ {% if list.description %} + {{ list.description|to_markdown|safe|truncatechars_html:30 }} + {% else %} +   + {% endif %} +
+

+ {% include 'lists/created_text.html' with list=list %} +

+
+
+
+ {% endfor %}
{% endif %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index 8c3dac7b4..f8eefaff4 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -8,29 +8,29 @@

{% trans "Members can add and remove books on a group's book lists" %}

- {% for membership in group.memberships.all %} - {% with member=membership.user %} -
- - {% include 'snippets/avatar.html' with user=member large=True %} - {{ member.display_name|truncatechars:10 }} - @{{ member|username|truncatechars:8 }} - - {% if group.user == member %} - - Manager - - {% endif %} - {% include 'snippets/remove_from_group_button.html' with user=member group=group minimal=True %} - {% if request.user in member.following.all %} -

- {% trans "Follows you" %} -

- {% endif %} + {% for membership in group.memberships.all %} + {% with member=membership.user %} +
+ + {% include 'snippets/avatar.html' with user=member large=True %} + {{ member.display_name|truncatechars:10 }} + @{{ member|username|truncatechars:8 }} + + {% if group.user == member %} + + Manager + + {% endif %} + {% include 'snippets/remove_from_group_button.html' with user=member group=group minimal=True %} + {% if request.user in member.following.all %} +

+ {% trans "Follows you" %} +

+ {% endif %}
{% endwith %} {% endfor %} -
+
{% if group.user != request.user and group|is_member:request.user %} @@ -38,11 +38,11 @@ + {% if show_username %} + {% blocktrans with username=user.localname %}Remove @{{ username }}{% endblocktrans %} + {% else %} + {% trans "Remove self from group" %} + {% endif %} + {% endif %} \ No newline at end of file diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html index a719c5fad..212a1a76e 100644 --- a/bookwyrm/templates/groups/suggested_users.html +++ b/bookwyrm/templates/groups/suggested_users.html @@ -36,6 +36,7 @@ {% endfor %}
{% else %} -

No potential members found for "{{ query }}"


+

No potential members found for "{{ query }}"

+
{% endif %} diff --git a/bookwyrm/templates/notifications/items/accept.html b/bookwyrm/templates/notifications/items/accept.html index 3ad671201..5aab79af0 100644 --- a/bookwyrm/templates/notifications/items/accept.html +++ b/bookwyrm/templates/notifications/items/accept.html @@ -4,17 +4,17 @@ {% load utilities %} {% block primary_link %}{% spaceless %} - {{ notification.related_group.local_path }} + {{ notification.related_group.local_path }} {% endspaceless %}{% endblock %} {% block icon %} - + {% endblock %} {% block description %} -{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} -accepted your invitation to join group "{{ group_name }}" -{% endblocktrans %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + accepted your invitation to join group "{{ group_name }}" + {% endblocktrans %} {% endblock %} \ No newline at end of file diff --git a/bookwyrm/templates/notifications/items/leave.html b/bookwyrm/templates/notifications/items/leave.html index a30241c52..e6fe72be9 100644 --- a/bookwyrm/templates/notifications/items/leave.html +++ b/bookwyrm/templates/notifications/items/leave.html @@ -4,17 +4,17 @@ {% load utilities %} {% block primary_link %}{% spaceless %} - {{ notification.related_group.local_path }} + {{ notification.related_group.local_path }} {% endspaceless %}{% endblock %} {% block icon %} - + {% endblock %} {% block description %} -{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} -has left your group "{{ group_name }}" -{% endblocktrans %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + has left your group "{{ group_name }}" + {% endblocktrans %} {% endblock %} \ No newline at end of file diff --git a/bookwyrm/templates/notifications/items/remove.html b/bookwyrm/templates/notifications/items/remove.html index 784c0d00c..7ee38b4a7 100644 --- a/bookwyrm/templates/notifications/items/remove.html +++ b/bookwyrm/templates/notifications/items/remove.html @@ -4,11 +4,11 @@ {% load utilities %} {% block primary_link %}{% spaceless %} - {{ notification.related_group.local_path }} + {{ notification.related_group.local_path }} {% endspaceless %}{% endblock %} {% block icon %} - + {% endblock %} {% block description %} @@ -20,9 +20,9 @@ has been removed from your group "{{ group_name }}{{ group_name }} group" -{% endblocktrans %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + You have been removed from the "{{ group_name }} group" + {% endblocktrans %} {% endif %} {% endblock %} diff --git a/bookwyrm/templates/snippets/add_to_group_button.html b/bookwyrm/templates/snippets/add_to_group_button.html index cf9ae15df..fd94f14d1 100644 --- a/bookwyrm/templates/snippets/add_to_group_button.html +++ b/bookwyrm/templates/snippets/add_to_group_button.html @@ -29,13 +29,13 @@ {% else %} - {% endif %} + {% if show_username %} + {% blocktrans with username=user.localname %}Remove @{{ username }}{% endblocktrans %} + {% else %} + {% trans "Remove" %} + {% endif %} + + {% endif %}

diff --git a/bookwyrm/templates/snippets/remove_from_group_button.html b/bookwyrm/templates/snippets/remove_from_group_button.html index 05b17b594..809d1d1fe 100644 --- a/bookwyrm/templates/snippets/remove_from_group_button.html +++ b/bookwyrm/templates/snippets/remove_from_group_button.html @@ -13,12 +13,12 @@ + {% if show_username %} + {% blocktrans with username=user.localname %}Remove @{{ username }}{% endblocktrans %} + {% else %} + {% trans "Remove" %} + {% endif %} +
From da53bad0f50305ec982c6272b6eedb555247ed71 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Mon, 4 Oct 2021 22:22:00 +1100 Subject: [PATCH 080/127] make Black happy --- bookwyrm/views/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index fc4b64041..b1f4a67ae 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -89,7 +89,7 @@ class UserGroups(View): class FindUsers(View): """find friends to add to your group""" - #this is mostly borrowed from the Get Started friend finder + # this is mostly borrowed from the Get Started friend finder def get(self, request, group_id): """basic profile info""" From 78f50034079eb691c0122a8b0359ed72fea3d139 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 08:09:24 +1100 Subject: [PATCH 081/127] lint raise_visible_to_user Don't return True unnecessarily --- bookwyrm/models/base_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index d52c368ec..058aa4788 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -79,14 +79,14 @@ class BookWyrmModel(models.Model): and self.mention_users.filter(id=viewer.id).first() ): - return True + return # you can see groups of which you are a member if ( hasattr(self, "memberships") and self.memberships.filter(user=viewer).exists() ): - return True + return # you can see objects which have a group of which you are a member if hasattr(self, "group"): @@ -94,7 +94,7 @@ class BookWyrmModel(models.Model): hasattr(self.group, "memberships") and self.group.memberships.filter(user=viewer).exists() ): - return True + return raise Http404() From 90d92edd7523e83e4611563ce51f0121c8042e68 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 08:10:23 +1100 Subject: [PATCH 082/127] disable pylint on NotificationType now being "too long" --- bookwyrm/models/notification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 0cae7790c..69ed784b9 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -4,7 +4,7 @@ from django.dispatch import receiver from .base_model import BookWyrmModel from . import Boost, Favorite, ImportJob, Report, Status, User - +# pylint: disable=line-too-long NotificationType = models.TextChoices( "NotificationType", "FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT INVITE ACCEPT JOIN LEAVE REMOVE", From 484e9ed959d9f5333e581e24133aec1c0791e74a Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 08:14:52 +1100 Subject: [PATCH 083/127] fix user Groups view pagination function --- bookwyrm/views/user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 808ca7385..dd30b2b46 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -145,7 +145,9 @@ class Groups(View): """list of groups""" user = get_user_from_username(request.user, username) - paginated = Paginator(models.Group.memberships.filter(user=user)) + paginated = Paginator( + models.Group.memberships.filter(user=user).order_by("-created_date"), PAGE_LENGTH + ) data = { "user": user, "is_self": request.user.id == user.id, From cc8db1c3533e16eddfc2124e788e98fde634ec24 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 09:05:20 +1100 Subject: [PATCH 084/127] linting fixes - remove unused imports - add class docstrings --- bookwyrm/models/group.py | 4 +++- bookwyrm/models/list.py | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index c2dfcb062..8fab44726 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -1,6 +1,6 @@ """ do book related things with other users """ from django.apps import apps -from django.db import models, IntegrityError, models, transaction +from django.db import models, IntegrityError, transaction from django.db.models import Q from bookwyrm.settings import DOMAIN from .base_model import BookWyrmModel @@ -36,6 +36,7 @@ class GroupMember(models.Model): ) class Meta: + """Users can only have one membership per group""" constraints = [ models.UniqueConstraint(fields=["group", "user"], name="unique_membership") ] @@ -83,6 +84,7 @@ class GroupMemberInvitation(models.Model): ) class Meta: + """Users can only have one outstanding invitation per group""" constraints = [ models.UniqueConstraint(fields=["group", "user"], name="unique_invitation") ] diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 46d57c2d0..8a083b690 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -1,16 +1,15 @@ """ make a list of books!! """ -from bookwyrm.models.group import GroupMember -from dataclasses import field from django.apps import apps from django.db import models from django.utils import timezone from bookwyrm import activitypub from bookwyrm.settings import DOMAIN + from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin from .base_model import BookWyrmModel -from . import fields from .group import GroupMember +from . import fields CurationType = models.TextChoices( "Curation", From b1bb43d1432de50c79f5f41d19b37e9bd21901c0 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 18:04:47 +1100 Subject: [PATCH 085/127] lint Group views file --- bookwyrm/views/group.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index b1f4a67ae..4a6a80954 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -3,7 +3,7 @@ from django.apps import apps from django.contrib.auth.decorators import login_required from django.db import IntegrityError from django.core.paginator import Paginator -from django.http import HttpResponseNotFound, HttpResponseBadRequest +from django.http import HttpResponseBadRequest from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator @@ -14,11 +14,9 @@ from django.db.models.functions import Greatest from bookwyrm import forms, models from bookwyrm.suggested_users import suggested_users -from .helpers import privacy_filter from .helpers import get_user_from_username -from bookwyrm.settings import DOMAIN - +# pylint: disable=no-self-use class Group(View): """group page""" @@ -94,7 +92,14 @@ class FindUsers(View): def get(self, request, group_id): """basic profile info""" query = request.GET.get("query") - group = models.Group.objects.get(id=group_id) + group = get_object_or_404(models.Group, id=group_id) + + if not group: + return HttpResponseBadRequest() + + if not group.user == request.user: + return HttpResponseBadRequest() + user_results = ( models.User.viewer_aware_objects(request.user) .exclude( @@ -116,14 +121,6 @@ class FindUsers(View): request.user, local=True ) - group = get_object_or_404(models.Group, id=group_id) - - if not group: - return HttpResponseBadRequest() - - if not group.user == request.user: - return HttpResponseBadRequest() - data = { "suggested_users": user_results, "group": group, From fe87e815e6d1953a5105187fb5d1dba45f4de8ff Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 20:41:48 +1100 Subject: [PATCH 086/127] database migrations for Groups --- .../migrations/0106_auto_20211005_0935.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 bookwyrm/migrations/0106_auto_20211005_0935.py diff --git a/bookwyrm/migrations/0106_auto_20211005_0935.py b/bookwyrm/migrations/0106_auto_20211005_0935.py new file mode 100644 index 000000000..46e31c5f9 --- /dev/null +++ b/bookwyrm/migrations/0106_auto_20211005_0935.py @@ -0,0 +1,117 @@ +# Generated by Django 3.2.5 on 2021-10-05 09:35 + +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0105_alter_connector_connector_file'), + ] + + operations = [ + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])), + ('name', bookwyrm.models.fields.CharField(max_length=100)), + ('description', bookwyrm.models.fields.TextField(blank=True, null=True)), + ('privacy', bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='GroupMember', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='GroupMemberInvitation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.RemoveConstraint( + model_name='notification', + name='notification_type_valid', + ), + migrations.AddField( + model_name='notification', + name='related_group_member', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_group_member', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='list', + name='curation', + field=bookwyrm.models.fields.CharField(choices=[('closed', 'Closed'), ('open', 'Open'), ('curated', 'Curated'), ('group', 'Group')], default='closed', max_length=255), + ), + migrations.AlterField( + model_name='notification', + name='notification_type', + field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import'), ('ADD', 'Add'), ('REPORT', 'Report'), ('INVITE', 'Invite'), ('ACCEPT', 'Accept'), ('JOIN', 'Join'), ('LEAVE', 'Leave'), ('REMOVE', 'Remove')], max_length=255), + ), + migrations.AlterField( + model_name='user', + name='preferred_timezone', + field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fort_Wayne', 'America/Fort_Wayne'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Knox_IN', 'America/Knox_IN'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Rosario', 'America/Rosario'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/South_Pole', 'Antarctica/South_Pole'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Ashkhabad', 'Asia/Ashkhabad'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Chungking', 'Asia/Chungking'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Dacca', 'Asia/Dacca'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macao', 'Asia/Macao'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimbu', 'Asia/Thimbu'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ujung_Pandang', 'Asia/Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Ulan_Bator', 'Asia/Ulan_Bator'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/ACT', 'Australia/ACT'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/LHI', 'Australia/LHI'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/NSW', 'Australia/NSW'), ('Australia/North', 'Australia/North'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Queensland', 'Australia/Queensland'), ('Australia/South', 'Australia/South'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Tasmania', 'Australia/Tasmania'), ('Australia/Victoria', 'Australia/Victoria'), ('Australia/West', 'Australia/West'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Brazil/Acre', 'Brazil/Acre'), ('Brazil/DeNoronha', 'Brazil/DeNoronha'), ('Brazil/East', 'Brazil/East'), ('Brazil/West', 'Brazil/West'), ('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Canada/Saskatchewan', 'Canada/Saskatchewan'), ('Canada/Yukon', 'Canada/Yukon'), ('Chile/Continental', 'Chile/Continental'), ('Chile/EasterIsland', 'Chile/EasterIsland'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('GMT', 'GMT'), ('GMT+0', 'GMT+0'), ('GMT-0', 'GMT-0'), ('GMT0', 'GMT0'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('Mexico/BajaNorte', 'Mexico/BajaNorte'), ('Mexico/BajaSur', 'Mexico/BajaSur'), ('Mexico/General', 'Mexico/General'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('US/Alaska', 'US/Alaska'), ('US/Aleutian', 'US/Aleutian'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/East-Indiana', 'US/East-Indiana'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Indiana-Starke', 'US/Indiana-Starke'), ('US/Michigan', 'US/Michigan'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('US/Samoa', 'US/Samoa'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')], default='UTC', max_length=255), + ), + migrations.AddConstraint( + model_name='notification', + constraint=models.CheckConstraint(check=models.Q(('notification_type__in', ['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT', 'ADD', 'REPORT', 'INVITE', 'ACCEPT', 'JOIN', 'LEAVE', 'REMOVE'])), name='notification_type_valid'), + ), + migrations.AddField( + model_name='groupmemberinvitation', + name='group', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_invitations', to='bookwyrm.group'), + ), + migrations.AddField( + model_name='groupmemberinvitation', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_invitations', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='groupmember', + name='group', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='bookwyrm.group'), + ), + migrations.AddField( + model_name='groupmember', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='group', + name='user', + field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='list', + name='group', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.group'), + ), + migrations.AddField( + model_name='notification', + name='related_group', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='bookwyrm.group'), + ), + migrations.AddConstraint( + model_name='groupmemberinvitation', + constraint=models.UniqueConstraint(fields=('group', 'user'), name='unique_invitation'), + ), + migrations.AddConstraint( + model_name='groupmember', + constraint=models.UniqueConstraint(fields=('group', 'user'), name='unique_membership'), + ), + ] From cdf7775e058402f0d1fddd1a53a549740ecd9b8b Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 21:06:09 +1100 Subject: [PATCH 087/127] add test for Group views --- bookwyrm/tests/views/test_group.py | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 bookwyrm/tests/views/test_group.py diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py new file mode 100644 index 000000000..1c3386094 --- /dev/null +++ b/bookwyrm/tests/views/test_group.py @@ -0,0 +1,94 @@ +""" test for app action functionality """ +from unittest.mock import patch + +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models, views +from bookwyrm.tests.validate_html import validate_html + + +@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") +class GroupViews(TestCase): + """view group and edit details""" + + def setUp(self): + """we need basic test data and mocks""" + self.factory = RequestFactory() + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ): + self.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + + with patch(): + self.testgroup = models.Group.objects.create( + id=999, + name="Test Group", + user=self.local_user, + privacy="public" + ) + self.membership = models.GroupMember.objects.create( + group=self.testgroup, user=self.local_user + ) + + models.SiteSettings.objects.create() + + def test_group_get(self, _): + """there are so many views, this just makes sure it LOADS""" + view = views.Group.as_view() + request = self.factory.get("") + request.user = self.local_user + result = view(request) + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) + + def test_usergroups_get(self, _): + """there are so many views, this just makes sure it LOADS""" + view = views.UserGroups.as_view() + request = self.factory.get("") + request.user = self.local_user + result = view(request) + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) + + def test_findusers_get(self, _): + """there are so many views, this just makes sure it LOADS""" + view = views.FindUsers.as_view() + request = self.factory.get("") + request.user = self.local_user + result = view(request) + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) + + def test_group_post(self, _): + """edit a "group" database entry""" + view = views.Group.as_view() + self.factory.post( + group_id=999, + name="Test Group", + user=self.local_user, + privacy="public", + description="Test description", + ) + + self.assertEqual("Test description", self.testgroup.description) \ No newline at end of file From 6fde19e9b195f3b30ae7938d45fea3836e620bac Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 21:29:33 +1100 Subject: [PATCH 088/127] lint fixes --- bookwyrm/models/group.py | 2 +- bookwyrm/templates/notifications/items/leave.html | 2 +- bookwyrm/views/user.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 8fab44726..f10cb3312 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -141,7 +141,7 @@ class GroupMemberInvitation(models.Model): # let the other members know about it for membership in self.group.memberships.all(): member = membership.user - if member != self.user and member != self.group.user: + if member not in (self.user, self.group.user): model.objects.create( user=member, related_user=self.user, diff --git a/bookwyrm/templates/notifications/items/leave.html b/bookwyrm/templates/notifications/items/leave.html index e6fe72be9..9c7a71b61 100644 --- a/bookwyrm/templates/notifications/items/leave.html +++ b/bookwyrm/templates/notifications/items/leave.html @@ -17,4 +17,4 @@ has left your group "{{ group_name }}" {% endblocktrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index dd30b2b46..bbc2edd40 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -146,7 +146,7 @@ class Groups(View): user = get_user_from_username(request.user, username) paginated = Paginator( - models.Group.memberships.filter(user=user).order_by("-created_date"), PAGE_LENGTH + models.Group.memberships.filter(user=user).order_by("-created_date"), PAGE_LENGTH ) data = { "user": user, From b3dc81dea0260acaba5d4d50bcf3013e4431664f Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 21:29:46 +1100 Subject: [PATCH 089/127] update tests --- bookwyrm/tests/views/test_group.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py index 1c3386094..ac09c22b3 100644 --- a/bookwyrm/tests/views/test_group.py +++ b/bookwyrm/tests/views/test_group.py @@ -37,7 +37,6 @@ class GroupViews(TestCase): outbox="https://example.com/users/rat/outbox", ) - with patch(): self.testgroup = models.Group.objects.create( id=999, name="Test Group", @@ -83,7 +82,7 @@ class GroupViews(TestCase): def test_group_post(self, _): """edit a "group" database entry""" view = views.Group.as_view() - self.factory.post( + view.post( group_id=999, name="Test Group", user=self.local_user, From f8e0de1ea98fc70a52dc5caa5f39349eb1ea8378 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 21:32:48 +1100 Subject: [PATCH 090/127] run black for clean code Godammit Hugh remember to do this before pushing new code. --- .../migrations/0106_auto_20211005_0935.py | 863 ++++++++++++++++-- bookwyrm/models/group.py | 2 + bookwyrm/tests/views/test_group.py | 17 +- bookwyrm/views/user.py | 3 +- 4 files changed, 816 insertions(+), 69 deletions(-) diff --git a/bookwyrm/migrations/0106_auto_20211005_0935.py b/bookwyrm/migrations/0106_auto_20211005_0935.py index 46e31c5f9..6030c9130 100644 --- a/bookwyrm/migrations/0106_auto_20211005_0935.py +++ b/bookwyrm/migrations/0106_auto_20211005_0935.py @@ -9,109 +9,856 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0105_alter_connector_connector_file'), + ("bookwyrm", "0105_alter_connector_connector_file"), ] operations = [ migrations.CreateModel( - name='Group', + name="Group", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])), - ('name', bookwyrm.models.fields.CharField(max_length=100)), - ('description', bookwyrm.models.fields.TextField(blank=True, null=True)), - ('privacy', bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "remote_id", + bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + ("name", bookwyrm.models.fields.CharField(max_length=100)), + ( + "description", + bookwyrm.models.fields.TextField(blank=True, null=True), + ), + ( + "privacy", + bookwyrm.models.fields.PrivacyField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='GroupMember', + name="GroupMember", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), ], ), migrations.CreateModel( - name='GroupMemberInvitation', + name="GroupMemberInvitation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), ], ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AddField( - model_name='notification', - name='related_group_member', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_group_member', to=settings.AUTH_USER_MODEL), + model_name="notification", + name="related_group_member", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="related_group_member", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='list', - name='curation', - field=bookwyrm.models.fields.CharField(choices=[('closed', 'Closed'), ('open', 'Open'), ('curated', 'Curated'), ('group', 'Group')], default='closed', max_length=255), + model_name="list", + name="curation", + field=bookwyrm.models.fields.CharField( + choices=[ + ("closed", "Closed"), + ("open", "Open"), + ("curated", "Curated"), + ("group", "Group"), + ], + default="closed", + max_length=255, + ), ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import'), ('ADD', 'Add'), ('REPORT', 'Report'), ('INVITE', 'Invite'), ('ACCEPT', 'Accept'), ('JOIN', 'Join'), ('LEAVE', 'Leave'), ('REMOVE', 'Remove')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ("ADD", "Add"), + ("REPORT", "Report"), + ("INVITE", "Invite"), + ("ACCEPT", "Accept"), + ("JOIN", "Join"), + ("LEAVE", "Leave"), + ("REMOVE", "Remove"), + ], + max_length=255, + ), ), migrations.AlterField( - model_name='user', - name='preferred_timezone', - field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fort_Wayne', 'America/Fort_Wayne'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Knox_IN', 'America/Knox_IN'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Rosario', 'America/Rosario'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/South_Pole', 'Antarctica/South_Pole'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Ashkhabad', 'Asia/Ashkhabad'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Chungking', 'Asia/Chungking'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Dacca', 'Asia/Dacca'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macao', 'Asia/Macao'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimbu', 'Asia/Thimbu'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ujung_Pandang', 'Asia/Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Ulan_Bator', 'Asia/Ulan_Bator'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/ACT', 'Australia/ACT'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/LHI', 'Australia/LHI'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/NSW', 'Australia/NSW'), ('Australia/North', 'Australia/North'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Queensland', 'Australia/Queensland'), ('Australia/South', 'Australia/South'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Tasmania', 'Australia/Tasmania'), ('Australia/Victoria', 'Australia/Victoria'), ('Australia/West', 'Australia/West'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Brazil/Acre', 'Brazil/Acre'), ('Brazil/DeNoronha', 'Brazil/DeNoronha'), ('Brazil/East', 'Brazil/East'), ('Brazil/West', 'Brazil/West'), ('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Canada/Saskatchewan', 'Canada/Saskatchewan'), ('Canada/Yukon', 'Canada/Yukon'), ('Chile/Continental', 'Chile/Continental'), ('Chile/EasterIsland', 'Chile/EasterIsland'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('GMT', 'GMT'), ('GMT+0', 'GMT+0'), ('GMT-0', 'GMT-0'), ('GMT0', 'GMT0'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('Mexico/BajaNorte', 'Mexico/BajaNorte'), ('Mexico/BajaSur', 'Mexico/BajaSur'), ('Mexico/General', 'Mexico/General'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('US/Alaska', 'US/Alaska'), ('US/Aleutian', 'US/Aleutian'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/East-Indiana', 'US/East-Indiana'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Indiana-Starke', 'US/Indiana-Starke'), ('US/Michigan', 'US/Michigan'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('US/Samoa', 'US/Samoa'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')], default='UTC', max_length=255), + model_name="user", + name="preferred_timezone", + field=models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "America/Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ("America/Kentucky/Louisville", "America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "America/Kentucky/Monticello"), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "America/North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="UTC", + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(('notification_type__in', ['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT', 'ADD', 'REPORT', 'INVITE', 'ACCEPT', 'JOIN', 'LEAVE', 'REMOVE'])), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + ( + "notification_type__in", + [ + "FAVORITE", + "REPLY", + "MENTION", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT", + "ADD", + "REPORT", + "INVITE", + "ACCEPT", + "JOIN", + "LEAVE", + "REMOVE", + ], + ) + ), + name="notification_type_valid", + ), ), migrations.AddField( - model_name='groupmemberinvitation', - name='group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_invitations', to='bookwyrm.group'), + model_name="groupmemberinvitation", + name="group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_invitations", + to="bookwyrm.group", + ), ), migrations.AddField( - model_name='groupmemberinvitation', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_invitations', to=settings.AUTH_USER_MODEL), + model_name="groupmemberinvitation", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="group_invitations", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='groupmember', - name='group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='bookwyrm.group'), + model_name="groupmember", + name="group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="memberships", + to="bookwyrm.group", + ), ), migrations.AddField( - model_name='groupmember', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to=settings.AUTH_USER_MODEL), + model_name="groupmember", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="memberships", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='group', - name='user', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="group", + name="user", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='list', - name='group', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.group'), + model_name="list", + name="group", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.group", + ), ), migrations.AddField( - model_name='notification', - name='related_group', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='bookwyrm.group'), + model_name="notification", + name="related_group", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="notifications", + to="bookwyrm.group", + ), ), migrations.AddConstraint( - model_name='groupmemberinvitation', - constraint=models.UniqueConstraint(fields=('group', 'user'), name='unique_invitation'), + model_name="groupmemberinvitation", + constraint=models.UniqueConstraint( + fields=("group", "user"), name="unique_invitation" + ), ), migrations.AddConstraint( - model_name='groupmember', - constraint=models.UniqueConstraint(fields=('group', 'user'), name='unique_membership'), + model_name="groupmember", + constraint=models.UniqueConstraint( + fields=("group", "user"), name="unique_membership" + ), ), ] diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index f10cb3312..49a7d754a 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -37,6 +37,7 @@ class GroupMember(models.Model): class Meta: """Users can only have one membership per group""" + constraints = [ models.UniqueConstraint(fields=["group", "user"], name="unique_membership") ] @@ -85,6 +86,7 @@ class GroupMemberInvitation(models.Model): class Meta: """Users can only have one outstanding invitation per group""" + constraints = [ models.UniqueConstraint(fields=["group", "user"], name="unique_invitation") ] diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py index ac09c22b3..571194d29 100644 --- a/bookwyrm/tests/views/test_group.py +++ b/bookwyrm/tests/views/test_group.py @@ -38,10 +38,7 @@ class GroupViews(TestCase): ) self.testgroup = models.Group.objects.create( - id=999, - name="Test Group", - user=self.local_user, - privacy="public" + id=999, name="Test Group", user=self.local_user, privacy="public" ) self.membership = models.GroupMember.objects.create( group=self.testgroup, user=self.local_user @@ -83,11 +80,11 @@ class GroupViews(TestCase): """edit a "group" database entry""" view = views.Group.as_view() view.post( - group_id=999, - name="Test Group", - user=self.local_user, - privacy="public", - description="Test description", + group_id=999, + name="Test Group", + user=self.local_user, + privacy="public", + description="Test description", ) - self.assertEqual("Test description", self.testgroup.description) \ No newline at end of file + self.assertEqual("Test description", self.testgroup.description) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index bbc2edd40..b5a3f9e13 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -146,7 +146,8 @@ class Groups(View): user = get_user_from_username(request.user, username) paginated = Paginator( - models.Group.memberships.filter(user=user).order_by("-created_date"), PAGE_LENGTH + models.Group.memberships.filter(user=user).order_by("-created_date"), + PAGE_LENGTH, ) data = { "user": user, From ec7d0db8430bf447f714ef88ec679360721b89db Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Tue, 5 Oct 2021 21:48:59 +1100 Subject: [PATCH 091/127] linting fixes --- bookwyrm/static/js/bookwyrm.js | 6 ++-- bookwyrm/templates/groups/find_users.html | 2 +- bookwyrm/templates/groups/group.html | 30 +++++++++---------- bookwyrm/templates/groups/members.html | 2 +- .../templates/notifications/items/accept.html | 2 +- .../templates/notifications/items/invite.html | 2 +- .../templates/notifications/items/join.html | 2 +- 7 files changed, 24 insertions(+), 22 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 5bf845a4e..66fd5a615 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -134,8 +134,10 @@ let BookWyrm = new class { let trigger = event.currentTarget; let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0]; - // if the form has already been revealed, there is no '.is-hidden' element - // so this doesn't really work as a toggle + /** + * if the form has already been revealed, there is no '.is-hidden' element + * so this doesn't really work as a toggle + */ if (hidden) { this.addRemoveClass(hidden, 'is-hidden', !hidden); diff --git a/bookwyrm/templates/groups/find_users.html b/bookwyrm/templates/groups/find_users.html index ec890a93d..57d4277c1 100644 --- a/bookwyrm/templates/groups/find_users.html +++ b/bookwyrm/templates/groups/find_users.html @@ -5,4 +5,4 @@ Add new members! {% include 'groups/suggested_users.html' with suggested_users=suggested_users query=query %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index 408f1f945..03ccae18f 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -64,21 +64,21 @@ {% if group.user == request.user %}
-
-

Find new members

-
-
- -
-
- -
-
-
+
+

Find new members

+
+
+ +
+
+ +
+
+
{% endif %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index f8eefaff4..22eb0cb6a 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -45,4 +45,4 @@ {% endif %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/bookwyrm/templates/notifications/items/accept.html b/bookwyrm/templates/notifications/items/accept.html index 5aab79af0..19acab15e 100644 --- a/bookwyrm/templates/notifications/items/accept.html +++ b/bookwyrm/templates/notifications/items/accept.html @@ -17,4 +17,4 @@ accepted your invitation to join group "{{ group_name }}" {% endblocktrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/bookwyrm/templates/notifications/items/invite.html b/bookwyrm/templates/notifications/items/invite.html index c50dba06d..63a462608 100644 --- a/bookwyrm/templates/notifications/items/invite.html +++ b/bookwyrm/templates/notifications/items/invite.html @@ -4,7 +4,7 @@ {% load utilities %} {% block primary_link %}{% spaceless %} - {{ notification.related_group.local_path }} + {{ notification.related_group.local_path }} {% endspaceless %}{% endblock %} {% block icon %} diff --git a/bookwyrm/templates/notifications/items/join.html b/bookwyrm/templates/notifications/items/join.html index 3dbc8159c..9c766806f 100644 --- a/bookwyrm/templates/notifications/items/join.html +++ b/bookwyrm/templates/notifications/items/join.html @@ -4,7 +4,7 @@ {% load utilities %} {% block primary_link %}{% spaceless %} - {{ notification.related_group.local_path }} + {{ notification.related_group.local_path }} {% endspaceless %}{% endblock %} {% block icon %} From 3003b103e4797d29b4991768f69a8c6ac276c79a Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 08:38:00 +1100 Subject: [PATCH 092/127] add group views tests TODO: the POST test needs to test that the group was actually updated. --- bookwyrm/tests/views/test_group.py | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py index 571194d29..49d3b63f3 100644 --- a/bookwyrm/tests/views/test_group.py +++ b/bookwyrm/tests/views/test_group.py @@ -1,11 +1,12 @@ """ test for app action functionality """ from unittest.mock import patch +from django.contrib.auth import decorators from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import models, views +from bookwyrm import models, views, forms from bookwyrm.tests.validate_html import validate_html @@ -51,7 +52,7 @@ class GroupViews(TestCase): view = views.Group.as_view() request = self.factory.get("") request.user = self.local_user - result = view(request) + result = view(request, group_id=999) self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) @@ -61,7 +62,7 @@ class GroupViews(TestCase): view = views.UserGroups.as_view() request = self.factory.get("") request.user = self.local_user - result = view(request) + result = view(request, username="mouse@local.com") self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) @@ -71,20 +72,25 @@ class GroupViews(TestCase): view = views.FindUsers.as_view() request = self.factory.get("") request.user = self.local_user - result = view(request) + result = view(request,group_id=999) self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) def test_group_post(self, _): - """edit a "group" database entry""" - view = views.Group.as_view() - view.post( - group_id=999, - name="Test Group", - user=self.local_user, - privacy="public", - description="Test description", - ) + """test editing a "group" database entry""" - self.assertEqual("Test description", self.testgroup.description) + view = views.Group.as_view() + group_fields = { + "name": "Updated Group", + "privacy": "private", + "description": "Test description", + "user": self.local_user + } + request = self.factory.post("", group_fields) + request.user = self.local_user + + result = view(request, group_id=999) + self.assertEqual(result.status_code, 302) + + # TODO: test group was updated. From 48fc85c7613b06bb64abddec317b0892282c8d50 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 18:45:28 +1100 Subject: [PATCH 093/127] adjust commenting on js file --- bookwyrm/static/js/bookwyrm.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 66fd5a615..0eebc75b2 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -126,19 +126,14 @@ let BookWyrm = new class { /** * Show form. - * + * If the form has already been revealed, there is no '.is-hidden' element + * so this doesn't work as a toggle - use hideForm to hide it again * @param {Event} event * @return {undefined} */ revealForm(event) { let trigger = event.currentTarget; let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0]; - - /** - * if the form has already been revealed, there is no '.is-hidden' element - * so this doesn't really work as a toggle - */ - if (hidden) { this.addRemoveClass(hidden, 'is-hidden', !hidden); } From 05bde27944e5b273421e38ca6bf336880723045d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 18:46:30 +1100 Subject: [PATCH 094/127] remove commented out code --- bookwyrm/models/group.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 49a7d754a..febbc91d0 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -7,9 +7,6 @@ from .base_model import BookWyrmModel from . import fields from .relationship import UserBlocks -# from .user import User - - class Group(BookWyrmModel): """A group of users""" From 5a4026cda3bd7a73b99851cb8738770801fc1dec Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 18:47:03 +1100 Subject: [PATCH 095/127] group views tests --- bookwyrm/tests/views/test_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py index 49d3b63f3..c8e6208a1 100644 --- a/bookwyrm/tests/views/test_group.py +++ b/bookwyrm/tests/views/test_group.py @@ -93,4 +93,4 @@ class GroupViews(TestCase): result = view(request, group_id=999) self.assertEqual(result.status_code, 302) - # TODO: test group was updated. + # TODO: test that group was updated. From 056150d583a954c66e3c2c4d0978538dba779382 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 21:21:19 +1100 Subject: [PATCH 096/127] CASCADE group.user Delete groups when group.user is deleted. --- bookwyrm/models/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index febbc91d0..f1005d4cf 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -11,7 +11,7 @@ class Group(BookWyrmModel): """A group of users""" name = fields.CharField(max_length=100) - user = fields.ForeignKey("User", on_delete=models.PROTECT) + user = fields.ForeignKey("User", on_delete=models.CASCADE) description = fields.TextField(blank=True, null=True) privacy = fields.PrivacyField() From 714a36924641b746402374371bb5072cd249c9a3 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 16:10:00 +1100 Subject: [PATCH 097/127] only show list edit form to list.user --- bookwyrm/templates/lists/layout.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/lists/layout.html b/bookwyrm/templates/lists/layout.html index 914478abb..6e772221a 100644 --- a/bookwyrm/templates/lists/layout.html +++ b/bookwyrm/templates/lists/layout.html @@ -25,7 +25,9 @@
- {% include 'lists/edit_form.html' with controls_text="edit_list" user_groups=user_groups %} + {% if request.user == list.user %} + {% include 'lists/edit_form.html' with controls_text="edit_list" user_groups=user_groups %} + {% endif %}
{% block panel %}{% endblock %} From 1bf5758e01b20c90baf1ba2b4f9085a3e61c8a9d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 16:11:11 +1100 Subject: [PATCH 098/127] overide filters for groups and group lists - use more sensible query for displaying groups on user page - privacy_filter now allows group members to see followers_only and private lists and groups they would otherwise not see --- bookwyrm/models/group.py | 16 ++++++++++++++++ bookwyrm/models/list.py | 17 +++++++++++++++++ bookwyrm/templates/groups/user_groups.html | 4 +--- bookwyrm/views/group.py | 13 ++++--------- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index f1005d4cf..89fb3a0e5 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -19,6 +19,22 @@ class Group(BookWyrmModel): """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""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 8a083b690..b0222cefa 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -1,6 +1,7 @@ """ make a list of books!! """ from django.apps import apps from django.db import models +from django.db.models import Q from django.utils import timezone from bookwyrm import activitypub @@ -71,6 +72,22 @@ class List(OrderedCollectionMixin, BookWyrmModel): return super().raise_not_editable(viewer) + @classmethod + def followers_filter(cls, queryset, viewer): + """Override filter for "followers" privacy level to allow non-following group members to see the existence of 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(group__memberships__user=viewer) + ), + privacy="followers", # and the status (of the list) 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 group lists""" + + return queryset.exclude(~Q(group__memberships__user=viewer), privacy="direct") class ListItem(CollectionItemMixin, BookWyrmModel): """ok""" diff --git a/bookwyrm/templates/groups/user_groups.html b/bookwyrm/templates/groups/user_groups.html index f68994dc1..cc27ce42d 100644 --- a/bookwyrm/templates/groups/user_groups.html +++ b/bookwyrm/templates/groups/user_groups.html @@ -3,8 +3,7 @@ {% load interaction %}
- {% for membership in memberships %} - {% with group=membership.group %} + {% for group in groups %}
@@ -32,6 +31,5 @@
- {% endwith %} {% endfor %}
diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 4a6a80954..c66dcdd05 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -24,11 +24,8 @@ class Group(View): """display a group""" group = get_object_or_404(models.Group, id=group_id) - lists = models.List.objects.filter(group=group).order_by("-updated_date") - # lists = privacy_filter(request.user, lists) - - # don't show groups to users who shouldn't see them group.raise_visible_to_user(request.user) + lists = models.List.privacy_filter(request.user).filter(group=group).order_by("-updated_date") data = { "group": group, @@ -56,13 +53,11 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - memberships = ( - models.GroupMember.objects.filter(user=user).all().order_by("-updated_date") - ) - paginated = Paginator(memberships, 12) + groups = models.Group.privacy_filter(request.user).filter(memberships__user=user).order_by("-updated_date") + paginated = Paginator(groups, 12) data = { - "memberships": paginated.get_page(request.GET.get("page")), + "groups": paginated.get_page(request.GET.get("page")), "is_self": request.user.id == user.id, "user": user, "group_form": forms.GroupForm(), From 9940abfd81232dd4da3cd79eb72e45de11d43155 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 22:11:46 +1100 Subject: [PATCH 099/127] refactor removing user from group 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 --- bookwyrm/models/group.py | 9 +++++++++ bookwyrm/models/list.py | 20 +++++++++++++++++++- bookwyrm/views/group.py | 13 ++++++------- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 89fb3a0e5..88320cf90 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -85,6 +85,15 @@ class GroupMember(models.Model): 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""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index b0222cefa..295032f58 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -2,6 +2,7 @@ from django.apps import apps from django.db import models from django.db.models import Q +from django.db.models.fields import NullBooleanField from django.utils import timezone from bookwyrm import activitypub @@ -87,7 +88,24 @@ class List(OrderedCollectionMixin, BookWyrmModel): def direct_filter(cls, queryset, viewer): """Override filter for "direct" privacy level to allow group members to see the existence of group lists""" - return queryset.exclude(~Q(group__memberships__user=viewer), privacy="direct") + return queryset.exclude( + ~Q( # user not self and not in the group if this is a group list + Q(user=viewer) | Q(group__memberships__user=viewer) + ), + privacy="direct" + ) + + @classmethod + def remove_from_group(cls, owner, user): + """remove a list from a group""" + + memberships = GroupMember.objects.filter(group__user=owner, user=user).all() + for m in memberships: + # remove this user's group-curated lists from the group + cls.objects.filter(group=m.group, user=m.user).update( + group=None, curation="closed" + ) + class ListItem(CollectionItemMixin, BookWyrmModel): """ok""" diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index c66dcdd05..6ef215830 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -163,6 +163,10 @@ def remove_member(request): if not user: return HttpResponseBadRequest() + # you can't be removed from your own group + if request.POST["user"]== group.user: + return HttpResponseBadRequest() + is_member = models.GroupMember.objects.filter(group=group, user=user).exists() is_invited = models.GroupMemberInvitation.objects.filter( group=group, user=user @@ -182,13 +186,8 @@ def remove_member(request): if is_member: try: - membership = models.GroupMember.objects.get(group=group, user=user) - membership.delete() - - # remove this user's group-curated lists from the group - models.List.objects.filter(group=group, user=user).update( - group=None, curation="closed" - ) + models.List.remove_from_group(group.user, user) + models.GroupMember.remove(group.user, user) except IntegrityError: pass From b3cc9e5b75917e7d514a8b7f9b911ace1f5e397d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 22:13:12 +1100 Subject: [PATCH 100/127] remove user and their lists from group when group.user blocks them Lists are changed to closed curation with no group. --- bookwyrm/views/preferences/block.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bookwyrm/views/preferences/block.py b/bookwyrm/views/preferences/block.py index 1eccf4612..2ccd3c065 100644 --- a/bookwyrm/views/preferences/block.py +++ b/bookwyrm/views/preferences/block.py @@ -23,6 +23,10 @@ class Block(View): models.UserBlocks.objects.create( user_subject=request.user, user_object=to_block ) + # remove the blocked users's lists from the groups + models.List.remove_from_group(request.user, to_block) + # remove the blocked user from all blocker's owned groups + models.GroupMember.remove(request.user, to_block) return redirect("prefs-block") From 252ff0d689ed37e5bf744079e785c1ab0228f0be Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 22:15:24 +1100 Subject: [PATCH 101/127] emblacken files Wouldn't it be great if I just remembered to run Black before every commit? --- bookwyrm/models/group.py | 2 ++ bookwyrm/models/list.py | 12 +++++++----- bookwyrm/tests/views/test_group.py | 4 ++-- bookwyrm/views/group.py | 14 +++++++++++--- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 88320cf90..907168699 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -7,6 +7,7 @@ from .base_model import BookWyrmModel from . import fields from .relationship import UserBlocks + class Group(BookWyrmModel): """A group of users""" @@ -36,6 +37,7 @@ class Group(BookWyrmModel): return queryset.exclude(~Q(memberships__user=viewer), privacy="direct") + class GroupMember(models.Model): """Users who are members of a group""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 295032f58..202c830c1 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -79,7 +79,9 @@ class List(OrderedCollectionMixin, BookWyrmModel): 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(group__memberships__user=viewer) + Q(user__followers=viewer) + | Q(user=viewer) + | Q(group__memberships__user=viewer) ), privacy="followers", # and the status (of the list) is followers only ) @@ -89,10 +91,10 @@ class List(OrderedCollectionMixin, BookWyrmModel): """Override filter for "direct" privacy level to allow group members to see the existence of group lists""" return queryset.exclude( - ~Q( # user not self and not in the group if this is a group list - Q(user=viewer) | Q(group__memberships__user=viewer) - ), - privacy="direct" + ~Q( # user not self and not in the group if this is a group list + Q(user=viewer) | Q(group__memberships__user=viewer) + ), + privacy="direct", ) @classmethod diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py index c8e6208a1..3b0aa236c 100644 --- a/bookwyrm/tests/views/test_group.py +++ b/bookwyrm/tests/views/test_group.py @@ -72,7 +72,7 @@ class GroupViews(TestCase): view = views.FindUsers.as_view() request = self.factory.get("") request.user = self.local_user - result = view(request,group_id=999) + result = view(request, group_id=999) self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) @@ -85,7 +85,7 @@ class GroupViews(TestCase): "name": "Updated Group", "privacy": "private", "description": "Test description", - "user": self.local_user + "user": self.local_user, } request = self.factory.post("", group_fields) request.user = self.local_user diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 6ef215830..3e510a4d4 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -25,7 +25,11 @@ class Group(View): group = get_object_or_404(models.Group, id=group_id) group.raise_visible_to_user(request.user) - lists = models.List.privacy_filter(request.user).filter(group=group).order_by("-updated_date") + lists = ( + models.List.privacy_filter(request.user) + .filter(group=group) + .order_by("-updated_date") + ) data = { "group": group, @@ -53,7 +57,11 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - groups = models.Group.privacy_filter(request.user).filter(memberships__user=user).order_by("-updated_date") + groups = ( + models.Group.privacy_filter(request.user) + .filter(memberships__user=user) + .order_by("-updated_date") + ) paginated = Paginator(groups, 12) data = { @@ -164,7 +172,7 @@ def remove_member(request): return HttpResponseBadRequest() # you can't be removed from your own group - if request.POST["user"]== group.user: + if request.POST["user"] == group.user: return HttpResponseBadRequest() is_member = models.GroupMember.objects.filter(group=group, user=user).exists() From 83f46b6cdacb26aee0287f831685aaea73731d5b Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 10 Oct 2021 12:01:21 +1100 Subject: [PATCH 102/127] remove print() statement Whoops accidentally left this behind from manual troubleshooting --- bookwyrm/models/list.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 202c830c1..b06cdef8c 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -62,7 +62,6 @@ class List(OrderedCollectionMixin, BookWyrmModel): def raise_not_editable(self, viewer): """the associated user OR the list owner can edit""" - print("raising not editable") if self.user == viewer: return # group members can edit items in group lists From d6a5794ac3039e37b4d0ae044f387cc3e921197e Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 10 Oct 2021 12:02:27 +1100 Subject: [PATCH 103/127] do not load list edit form if viewer not authenticated --- bookwyrm/templates/lists/lists.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/lists/lists.html b/bookwyrm/templates/lists/lists.html index d909f5e81..49091bcf0 100644 --- a/bookwyrm/templates/lists/lists.html +++ b/bookwyrm/templates/lists/lists.html @@ -22,10 +22,11 @@
{% endif %} - +{% if request.user.is_authenticated %}
{% include 'lists/create_form.html' with controls_text="create_list" %}
+{% endif %} {% if request.user.is_authenticated %}