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 16ebb2ba..5776106b 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 00000000..7be10a91 --- /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 ca6eb0a5..63194ceb 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 bffd62b4..2774f081 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 00000000..a91c56cb --- /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 00000000..edbba849 --- /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 8ca3bd18..22b8e2ce 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 00000000..d146a922 --- /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 00000000..fb53e60f --- /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 d54347f0..759350cc 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 23063ff7..ceff1b2a 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 d146a922..b469ce00 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 fb53e60f..e640fd26 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 edbba849..3a5b9c76 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 759350cc..bc50bb09 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 5776106b..50f3bf4f 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 7be10a91..f90537b8 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 b469ce00..103ce223 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 e640fd26..38df17a6 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 f90537b8..83cc3b05 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 ceff1b2a..0987924e 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 103ce223..00000000 --- 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 00000000..e7409942 --- /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 00000000..ff6593e5 --- /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 00000000..1c58dc77 --- /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 38df17a6..f764db6f 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 00000000..03a957d0 --- /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 3a5b9c76..39e09bc1 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 a8d65f7f..01a10a39 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 ba5c251a..59d3e8d1 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 01a10a39..c1469415 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 59d3e8d1..fd4e7f2c 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 6f55159d..7969ef7a 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 00000000..9c48a842 --- /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 39e09bc1..912d5ec3 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 00000000..9154a527 --- /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 c1469415..a73d7825 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 00000000..472841e5 --- /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 00000000..c7470b32 --- /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 00000000..51c3179b --- /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 ac7257e3..cdf7493f 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 fd4e7f2c..c344f92b 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 a73d7825..4fea5a84 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 00000000..b10cdd5f --- /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 c7470b32..00000000 --- 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 cdf7493f..e688f813 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 472841e5..40d32f3f 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 a91c56cb..01fdbdd6 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 b10cdd5f..55d7fd74 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 51c3179b..aea0532f 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 e00a8331..a219beb4 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 7dce0891..80dab21c 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 a219beb4..e00a8331 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 aea0532f..f533af6e 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 addf9e47..4214908a 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 2774f081..a4a06eba 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 01fdbdd6..c1aa2d70 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 49fb5375..b73d7708 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 0987924e..1f2221d3 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 4214908a..0ad0bd31 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 69530204..9ef02753 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 4fea5a84..abb24143 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 9a000d3f..0019520e 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 68abafc0..914478ab 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 22b8e2ce..357ad467 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 b73d7708..75f34b9e 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 0019520e..d2f17d63 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 9ef02753..fb224cdf 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 eee5a75f..6c6247ad 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 f000fd08..049de497 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 d2f17d63..a98cae94 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 18789706..4d93c597 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 0ad0bd31..69442760 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 aa174a14..3a2d758b 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 c1aa2d70..6810779c 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 75f34b9e..7ea33a8b 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 e7409942..5e6ce513 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 f764db6f..c47cbbc4 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 03a957d0..f558f169 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 80dab21c..df5f1602 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 9c48a842..5239a365 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 f533af6e..cc394684 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 912d5ec3..1a594072 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 1f2221d3..ffb7581e 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 0c7b1939..a8a0651a 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 1a594072..6b64e4b7 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 69442760..18e13eb5 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 18e13eb5..9319b659 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 c47cbbc4..f684dd01 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 a8a0651a..30ffc868 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 5239a365..f99abc69 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 357ad467..0d07b199 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 9319b659..dfb44a4c 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 63194ceb..d3f52e72 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 35014a7b..ea28bb77 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 fb224cdf..912c3cfd 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 6810779c..4d9d2815 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 abb24143..6c44e3b4 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 6c6247ad..f5405b64 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 3a2d758b..1b4bae1a 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 dfb44a4c..b28aabeb 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 9154a527..99ec67bc 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 6c44e3b4..9617e133 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 df5f1602..a64d840e 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 40d32f3f..91b3784d 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 cc394684..fe1403c4 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 b28aabeb..4d91be67 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 4d9d2815..ea162b2a 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 f99abc69..6b4cef1b 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 6b64e4b7..36736e01 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 0d07b199..a1a5289d 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 f360a30a..676161d8 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 4d91be67..3e1785cc 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 ffb7581e..290e0187 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 a4a06eba..7ac41f1b 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 ea162b2a..103764d2 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 7ea33a8b..49802632 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 a4968f61..3632fa10 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 637baa6e..0e139794 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 30ffc868..05f8ceff 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 4d93c597..fb9e72bc 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 3e1785cc..09bb0dca 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 d3f52e72..562c4933 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 9617e133..1ea8f00d 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 a64d840e..a08e73b9 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 91b3784d..6323ffbe 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 6b4cef1b..f99abc69 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 fe1403c4..7febe2b1 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 36736e01..2735a5b8 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 ae5cd67b..8dba38c6 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 7febe2b1..dd3b93c3 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 00000000..46c4071d --- /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 2735a5b8..9c91fb18 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 a1a5289d..c4ef2d8e 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 fb9e72bc..930fdfcd 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 09bb0dca..60ca8d21 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 3ee27db6..bd91b418 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 00000000..938f48b2 --- /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 1ea8f00d..4d1cdf79 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 ce5eab6d..75dfe491 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 60ca8d21..5ae2cecd 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 bd91b418..52d27f12 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 fdba04ea..dbded115 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 3632fa10..a2ddb874 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 8dba38c6..8c076ccb 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 5ae2cecd..b8c45a4d 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 290e0187..ffb7581e 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 7ac41f1b..c5ea44e0 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 dbded115..2d60ae2d 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 49802632..43f5265d 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 a2ddb874..0cae7790 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 0e139794..637baa6e 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 75dfe491..54ec861d 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 81c4a4b9..eabca2b4 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 3e1d3526..834f95be 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 930fdfcd..4bdfb6ed 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 b8c45a4d..37381165 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 562c4933..f711a779 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 2d60ae2d..3e76a6b7 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 a98cae94..492ccf62 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 ea28bb77..b1246b94 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 912c3cfd..53f39b54 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 1b4bae1a..50119cc1 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 37381165..718aa9ee 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 54ec861d..a719c5fa 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 e8f23632..06ce6db7 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 dd3b93c3..cf9ae15d 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 718aa9ee..17db93ed 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 52d27f12..e005aba6 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 43f5265d..b891a229 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 50119cc1..61652620 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 4d1cdf79..f19e8ee4 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 e005aba6..8c3dac7b 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 17db93ed..cb21842b 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 53f39b54..bed9ee9a 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 492ccf62..9b74655c 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 69240564..19f9e4f5 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 6a855abb..42ae5a12 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 938f48b2..05b17b59 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 049de497..5bf845a4 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 ff6593e5..fd670615 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 99ec67bc..ec890a93 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 f19e8ee4..408f1f94 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 8c3dac7b..f8eefaff 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 a719c5fa..212a1a76 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 3ad67120..5aab79af 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 a30241c5..e6fe72be 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 784c0d00..7ee38b4a 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 cf9ae15d..fd94f14d 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 05b17b59..809d1d1f 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 fc4b6404..b1f4a67a 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 d52c368e..058aa478 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 0cae7790..69ed784b 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 808ca738..dd30b2b4 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 c2dfcb06..8fab4472 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 46d57c2d..8a083b69 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 b1f4a67a..4a6a8095 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 00000000..46e31c5f --- /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 00000000..1c338609 --- /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 8fab4472..f10cb331 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 e6fe72be..9c7a71b6 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 dd30b2b4..bbc2edd4 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 1c338609..ac09c22b 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 46e31c5f..6030c913 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 f10cb331..49a7d754 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 ac09c22b..571194d2 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 bbc2edd4..b5a3f9e1 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 5bf845a4..66fd5a61 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 ec890a93..57d4277c 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 408f1f94..03ccae18 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 f8eefaff..22eb0cb6 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 5aab79af..19acab15 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 c50dba06..63a46260 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 3dbc8159..9c766806 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 571194d2..49d3b63f 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 66fd5a61..0eebc75b 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 49a7d754..febbc91d 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 49d3b63f..c8e6208a 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 febbc91d..f1005d4c 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 914478ab..6e772221 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 f1005d4c..89fb3a0e 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 8a083b69..b0222cef 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 f68994dc..cc27ce42 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 4a6a8095..c66dcdd0 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 89fb3a0e..88320cf9 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 b0222cef..295032f5 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 c66dcdd0..6ef21583 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 1eccf461..2ccd3c06 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 88320cf9..90716869 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 295032f5..202c830c 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 c8e6208a..3b0aa236 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 6ef21583..3e510a4d 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 202c830c..b06cdef8 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 d909f5e8..49091bcf 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 %}