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

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

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

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

+

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

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

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

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

    Group Members

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

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

    + {% else %} +

    Lists

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

      member.username

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

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

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

Add users to {{ group.name }}

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

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

- {% else %} -

Group Members

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

Lists

{% if not group.lists.exists %}

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

{% else %} -

Lists

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

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

    + {% elif user.shared_books %} +

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

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

    + {% trans "Follows you" %} +

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

    Group Members

    +

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

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

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

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

    Lists

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

    Group Members

    +

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

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

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

      + {% elif user.shared_books %} +

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

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

      + {% trans "Follows you" %} +

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

    Group Members

    -

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

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

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

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

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

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

    Lists

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

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

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

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

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

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

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

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

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

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

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

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

{% trans "Create group" %}

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

{% trans "Create group" %}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Add users to {{ group.name }}

+{% block searchresults %} +

+ Add new members! +

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

Lists

+

Lists

{% if not lists %}

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

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

Find new members

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

Group Members

+

Group Members

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

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

{% include 'user/shelf/books_header.html' %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 4d91be67d..3e1785cce 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -1,4 +1,5 @@ """group views""" +from django.apps import apps from django.contrib.auth.decorators import login_required from django.db import IntegrityError from django.core.paginator import Paginator @@ -62,8 +63,6 @@ class UserGroups(View): data = { "user": user, - "has_groups": models.GroupMember.objects.filter(user=user).exists(), - "groups": paginated.get_page(request.GET.get("page")), "group_form": forms.GroupForm(), "path": user.local_path + "/group", } @@ -144,6 +143,21 @@ def add_member(request): except IntegrityError: pass +# TODO: actually this needs to be associated with the user ACCEPTING AN INVITE!!! DOH! + + """create a notification too""" + # notify all team members when a user is added to the group + model = apps.get_model("bookwyrm.Notification", require_ready=True) + for team_member in group.members.all(): + if team_member.local and team_member != request.user: + model.objects.create( + user=team_member, + related_user=request.user, + related_group_member=user, + related_group=group, + notification_type="ADD", + ) + return redirect(user.local_path) @require_POST @@ -151,8 +165,12 @@ def add_member(request): def remove_member(request): """remove a member from the group""" + # TODO: send notification to user telling them they have been removed + # TODO: remove yourself from a group!!!! (except owner) + # FUTURE TODO: transfer ownership of group + # TODO: if groups become AP values we need something like get_group_from_group_fullname - # group = get_object_or_404(models.Group, id=request.POST.get("group")) + # group = get_object_or_404(models.Group, id=request.POST.get("group")) # NOTE: does this not work? group = models.Group.objects.get(id=request.POST["group"]) if not group: return HttpResponseBadRequest() From 2f42161dda41dd647ef52c3cbc58044131a24d7a Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 10:10:37 +1000 Subject: [PATCH 043/193] disambiguate groups and prep for group invitations - rename Group to BookwyrmGroup - create group memberships and invitations - adjust all model name references accordingly --- bookwyrm/forms.py | 2 +- bookwyrm/models/__init__.py | 2 +- bookwyrm/models/group.py | 155 +++++++++++++++++++++++++++----- bookwyrm/models/list.py | 5 +- bookwyrm/models/notification.py | 10 ++- bookwyrm/models/user.py | 5 ++ bookwyrm/urls.py | 9 +- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/group.py | 130 +++++++++++++++++++-------- bookwyrm/views/user.py | 4 +- 10 files changed, 255 insertions(+), 69 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index ffb7581e5..290e01877 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -297,7 +297,7 @@ class ListForm(CustomForm): class GroupForm(CustomForm): class Meta: - model = models.Group + model = models.BookwyrmGroup fields = ["user", "privacy", "name", "description"] class ReportForm(CustomForm): diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index a4a06ebad..7ac41f1b9 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -21,7 +21,7 @@ from .relationship import UserFollows, UserFollowRequest, UserBlocks from .report import Report, ReportComment from .federated_server import FederatedServer -from .group import Group, GroupMember +from .group import BookwyrmGroup, BookwyrmGroupMember, GroupMemberInvitation from .import_job import ImportJob, ImportItem diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index ea162b2a4..103764d2d 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -1,14 +1,14 @@ """ do book related things with other users """ from django.apps import apps -from django.db import models -from django.utils import timezone - +from django.db import models, IntegrityError, models, transaction +from django.db.models import Q from bookwyrm.settings import DOMAIN from .base_model import BookWyrmModel from . import fields +from .relationship import UserBlocks +# from .user import User - -class Group(BookWyrmModel): +class BookwyrmGroup(BookWyrmModel): """A group of users""" name = fields.CharField(max_length=100) @@ -16,27 +16,138 @@ class Group(BookWyrmModel): "User", on_delete=models.PROTECT) description = fields.TextField(blank=True, null=True) privacy = fields.PrivacyField() - members = models.ManyToManyField( - "User", - symmetrical=False, - through="GroupMember", - through_fields=("group", "user"), - related_name="bookwyrm_groups" - ) - def get_remote_id(self): - """don't want the user to be in there in this case""" - return f"https://{DOMAIN}/group/{self.id}" - -class GroupMember(models.Model): +class BookwyrmGroupMember(models.Model): """Users who are members of a group""" - - group = models.ForeignKey("Group", on_delete=models.CASCADE) - user = models.ForeignKey("User", on_delete=models.CASCADE) + created_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) + group = models.ForeignKey( + "BookwyrmGroup", + on_delete=models.CASCADE, + related_name="memberships" + ) + user = models.ForeignKey( + "User", + on_delete=models.CASCADE, + related_name="memberships" + ) class Meta: constraints = [ models.UniqueConstraint( - fields=["group", "user"], name="unique_member" + fields=["group", "user"], name="unique_membership" ) - ] \ No newline at end of file + ] + + def save(self, *args, **kwargs): + """don't let a user invite someone who blocked them""" + # blocking in either direction is a no-go + if UserBlocks.objects.filter( + Q( + user_subject=self.group.user, + user_object=self.user, + ) + | Q( + user_subject=self.user, + user_object=self.group.user, + ) + ).exists(): + raise IntegrityError() + # accepts and requests are handled by the BookwyrmGroupInvitation model + super().save(*args, **kwargs) + + @classmethod + def from_request(cls, join_request): + """converts a join request into a member relationship""" + + # remove the invite + join_request.delete() + + # make a group member + return cls.objects.create( + user=join_request.user, + group=join_request.group, + ) + + +class GroupMemberInvitation(models.Model): + """adding a user to a group requires manual confirmation""" + created_date = models.DateTimeField(auto_now_add=True) + group = models.ForeignKey( + "BookwyrmGroup", + on_delete=models.CASCADE, + related_name="user_invitations" + ) + user = models.ForeignKey( + "User", + on_delete=models.CASCADE, + related_name="group_invitations" + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["group", "user"], name="unique_invitation" + ) + ] + def save(self, *args, **kwargs): # pylint: disable=arguments-differ + """make sure the membership doesn't already exist""" + # if there's an invitation for a membership that already exists, accept it + # without changing the local database state + if BookwyrmGroupMember.objects.filter( + user=self.user, + group=self.group + ).exists(): + self.accept() + return + + # blocking in either direction is a no-go + if UserBlocks.objects.filter( + Q( + user_subject=self.group.user, + user_object=self.user, + ) + | Q( + user_subject=self.user, + user_object=self.group.user, + ) + ).exists(): + raise IntegrityError() + + # make an invitation + super().save(*args, **kwargs) + + # now send the invite + model = apps.get_model("bookwyrm.Notification", require_ready=True) + notification_type = "INVITE" + model.objects.create( + user=self.user, + related_user=self.group.user, + related_group=self.group, + notification_type=notification_type, + ) + + def accept(self): + """turn this request into the real deal""" + + with transaction.atomic(): + BookwyrmGroupMember.from_request(self) + self.delete() + + # let the other members know about it + model = apps.get_model("bookwyrm.Notification", require_ready=True) + for member in self.group.members.all: + if member != self.user: + model.objects.create( + user=member, + related_user=self.user, + related_group=self.group, + notification_type="ACCEPT", + ) + + def reject(self): + """generate a Reject for this membership request""" + + self.delete() + + # TODO: send notification \ No newline at end of file diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 7ea33a8b6..498026326 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -35,7 +35,7 @@ class List(OrderedCollectionMixin, BookWyrmModel): max_length=255, default="closed", choices=CurationType.choices ) group = models.ForeignKey( - "Group", + "BookwyrmGroup", on_delete=models.PROTECT, default=None, blank=True, @@ -101,6 +101,9 @@ class ListItem(CollectionItemMixin, BookWyrmModel): notification_type="ADD", ) + # TODO: send a notification to all team members except the one who added the book + # for team curated lists + class Meta: """A book may only be placed into a list once, and each order in the list may be used only once""" diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index a4968f61f..3632fa10f 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -7,7 +7,7 @@ from . import Boost, Favorite, ImportJob, Report, Status, User NotificationType = models.TextChoices( "NotificationType", - "FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT", + "FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT INVITE ACCEPT", ) @@ -19,6 +19,12 @@ class Notification(BookWyrmModel): related_user = models.ForeignKey( "User", on_delete=models.CASCADE, null=True, related_name="related_user" ) + related_group_member = models.ForeignKey( + "User", on_delete=models.CASCADE, null=True, related_name="related_group_member" + ) + related_group = models.ForeignKey( + "BookwyrmGroup", on_delete=models.CASCADE, null=True, related_name="notifications" + ) related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True) related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True) related_list_item = models.ForeignKey( @@ -37,6 +43,8 @@ class Notification(BookWyrmModel): user=self.user, related_book=self.related_book, related_user=self.related_user, + related_group_member=self.related_group_member, + related_group=self.related_group, related_status=self.related_status, related_import=self.related_import, related_list_item=self.related_list_item, diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 637baa6ee..0e1397949 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -143,6 +143,11 @@ class User(OrderedCollectionPageMixin, AbstractUser): property_fields = [("following_link", "following")] field_tracker = FieldTracker(fields=["name", "avatar"]) + # @property + # def bookwyrm_groups(self): + # group_ids = bookwyrm_group_membership.values_list("user", flat=True) + # return BookwyrmGroup.objects.in_bulk(group_ids).values() + @property def confirmation_link(self): """helper for generating confirmation links""" diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 30ffc8689..05f8ceffe 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -253,10 +253,13 @@ urlpatterns = [ re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"), # groups re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"), - re_path(r"^group/(?P\d+)(.json)?/?$", views.Group.as_view(), name="group"), + re_path(r"^group/(?P\d+)(.json)?/?$", views.BookwyrmGroup.as_view(), name="group"), re_path(r"^group/(?P\d+)/add-users/?$", views.FindUsers.as_view(), name="group-find-users"), - re_path(r"^add-group-member/?$", views.add_member, name="add-group-member"), - re_path(r"^remove-group-member/?$", views.remove_member, name="remove-group-member"), + re_path(r"^add-group-member/?$", views.invite_member, name="invite-group-member"), + re_path(r"^add-group-member/?$", views.uninvite_member, name="uninvite-group-member"), + re_path(r"^add-group-member/?$", views.uninvite_member, name="uninvite-group-member"), + re_path(r"^accept-group-invitation/?$", views.accept_membership, name="accept-group-invitation"), + re_path(r"^reject-group-invitation/?$", views.reject_membership, name="reject-group-invitation"), # lists re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 4d93c5973..fb9e72bc2 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -32,7 +32,7 @@ from .follow import follow, unfollow from .follow import accept_follow_request, delete_follow_request from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers from .goal import Goal, hide_goal -from .group import Group, UserGroups, FindUsers, add_member, remove_member +from .group import BookwyrmGroup, UserGroups, FindUsers, invite_member, remove_member, uninvite_member, accept_membership, reject_membership from .import_data import Import, ImportStatus from .inbox import Inbox from .interaction import Favorite, Unfavorite, Boost, Unboost diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 3e1785cce..09bb0dcad 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -18,13 +18,13 @@ from .helpers import privacy_filter from .helpers import get_user_from_username from bookwyrm.settings import DOMAIN -class Group(View): +class BookwyrmGroup(View): """group page""" def get(self, request, group_id): """display a group""" - group = get_object_or_404(models.Group, id=group_id) + group = get_object_or_404(models.BookwyrmGroup, id=group_id) lists = models.List.objects.filter(group=group).order_by("-updated_date") lists = privacy_filter(request.user, lists) @@ -43,7 +43,7 @@ class Group(View): @method_decorator(login_required, name="dispatch") def post(self, request, group_id): """edit a group""" - user_group = get_object_or_404(models.Group, id=group_id) + user_group = get_object_or_404(models.BookwyrmGroup, id=group_id) form = forms.GroupForm(request.POST, instance=user_group) if not form.is_valid(): return redirect("group", user_group.id) @@ -57,11 +57,12 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - groups = models.Group.objects.filter(members=user).order_by("-updated_date") - # groups = privacy_filter(request.user, groups) + groups = user.bookwyrmgroup_set.all() # follow the relationship backwards, nice paginated = Paginator(groups, 12) data = { + "groups": paginated.get_page(request.GET.get("page")), + "is_self": request.user.id == user.id, "user": user, "group_form": forms.GroupForm(), "path": user.local_path + "/group", @@ -77,7 +78,7 @@ class UserGroups(View): return redirect(request.user.local_path + "groups") group = form.save() # add the creator as a group member - models.GroupMember.objects.create(group=group, user=request.user) + models.BookwyrmGroupMember.objects.create(group=group, user=request.user) return redirect("group", group.id) @method_decorator(login_required, name="dispatch") @@ -109,21 +110,22 @@ class FindUsers(View): request.user ) - group = get_object_or_404(models.Group, id=group_id) + group = get_object_or_404(models.BookwyrmGroup, id=group_id) - data["suggested_users"] = user_results - data["group"] = group - data["query"] = query - data["requestor_is_manager"] = request.user == group.user + data = { + "suggested_users": user_results, + "group": group, + "query": query, + "requestor_is_manager": request.user == group.user + } return TemplateResponse(request, "groups/find_users.html", data) @require_POST @login_required -def add_member(request): - """add a member to the group""" +def invite_member(request): + """invite a member to the group""" - # TODO: if groups become AP values we need something like get_group_from_group_fullname - group = get_object_or_404(models.Group, id=request.POST.get("group")) + group = get_object_or_404(models.BookwyrmGroup, id=request.POST.get("group")) if not group: return HttpResponseBadRequest() @@ -135,28 +137,42 @@ def add_member(request): return HttpResponseBadRequest() try: - models.GroupMember.objects.create( - group=group, - user=user - ) + models.GroupMemberInvitation.objects.create( + user=user, + group=group + ) except IntegrityError: pass -# TODO: actually this needs to be associated with the user ACCEPTING AN INVITE!!! DOH! + return redirect(user.local_path) - """create a notification too""" - # notify all team members when a user is added to the group - model = apps.get_model("bookwyrm.Notification", require_ready=True) - for team_member in group.members.all(): - if team_member.local and team_member != request.user: - model.objects.create( - user=team_member, - related_user=request.user, - related_group_member=user, - related_group=group, - notification_type="ADD", - ) +@require_POST +@login_required +def uninvite_member(request): + """invite a member to the group""" + + group = get_object_or_404(models.BookwyrmGroup, id=request.POST.get("group")) + if not group: + return HttpResponseBadRequest() + + user = get_user_from_username(request.user, request.POST["user"]) + if not user: + return HttpResponseBadRequest() + + if not group.user == request.user: + return HttpResponseBadRequest() + + try: + invitation = models.GroupMemberInvitation.objects.get( + user=user, + group=group + ) + + invitation.reject() + + except IntegrityError: + pass return redirect(user.local_path) @@ -168,10 +184,11 @@ def remove_member(request): # TODO: send notification to user telling them they have been removed # TODO: remove yourself from a group!!!! (except owner) # FUTURE TODO: transfer ownership of group + # THIS LOGIC SHOULD BE IN MODEL # TODO: if groups become AP values we need something like get_group_from_group_fullname # group = get_object_or_404(models.Group, id=request.POST.get("group")) # NOTE: does this not work? - group = models.Group.objects.get(id=request.POST["group"]) + group = models.BookwyrmGroup.objects.get(id=request.POST["group"]) if not group: return HttpResponseBadRequest() @@ -183,11 +200,52 @@ def remove_member(request): return HttpResponseBadRequest() try: - membership = models.GroupMember.objects.get(group=group,user=user) + membership = models.BookwyrmGroupMember.objects.get(group=group,user=user) # BUG: wrong membership.delete() except IntegrityError: - print("no integrity") pass - return redirect(user.local_path) \ No newline at end of file + return redirect(user.local_path) + +@require_POST +@login_required +def accept_membership(request): + """accept an invitation to join a group""" + + group = models.BookwyrmGroup.objects.get(id=request.POST["group"]) + if not group: + return HttpResponseBadRequest() + + invite = models.GroupMemberInvitation.objects.get(group=group,user=request.user) + if not invite: + return HttpResponseBadRequest() + + try: + invite.accept() + + except IntegrityError: + pass + + return redirect(request.user.local_path) + +@require_POST +@login_required +def reject_membership(request): + """reject an invitation to join a group""" + + group = models.BookwyrmGroup.objects.get(id=request.POST["group"]) + if not group: + return HttpResponseBadRequest() + + invite = models.GroupMemberInvitation.objects.get(group=group,user=request.user) + if not invite: + return HttpResponseBadRequest() + + try: + invite.reject() + + except IntegrityError: + pass + + return redirect(request.user.local_path) \ No newline at end of file diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index d3f52e729..562c49335 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -1,5 +1,4 @@ """ non-interactive pages """ -from bookwyrm.models.group import GroupMember from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.shortcuts import redirect @@ -83,7 +82,6 @@ class User(View): data = { "user": user, "is_self": is_self, - "has_groups": models.GroupMember.objects.filter(user=user).exists(), "shelves": shelf_preview, "shelf_count": shelves.count(), "activities": paginated.get_page(request.GET.get("page", 1)), @@ -142,7 +140,7 @@ class Groups(View): user = get_user_from_username(request.user, username) paginated = Paginator( - GroupMember.objects.filter(user=user) + models.BookwyrmGroup.memberships.filter(user=user) ) data = { "user": user, From 0f3be40957e65e956b16dedb5c30c13942badb78 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 2 Oct 2021 10:47:42 +1000 Subject: [PATCH 044/193] fix group references in templates Let's do this the sensible way huh, by using backwards references to memberships etc Also adds filters for is_member and is_invited so we don't have to do weird things in group Views --- bookwyrm/templates/groups/group.html | 2 +- bookwyrm/templates/groups/members.html | 6 ++++-- .../templates/groups/suggested_users.html | 2 +- bookwyrm/templates/groups/user_groups.html | 2 +- .../snippets/add_to_group_button.html | 20 ++++++++----------- bookwyrm/templates/user/groups.html | 2 +- bookwyrm/templates/user/user.html | 3 --- bookwyrm/templatetags/bookwyrm_group_tags.py | 19 ++++++++++++++++++ 8 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 bookwyrm/templatetags/bookwyrm_group_tags.py diff --git a/bookwyrm/templates/groups/group.html b/bookwyrm/templates/groups/group.html index 9617e1332..1ea8f00dd 100644 --- a/bookwyrm/templates/groups/group.html +++ b/bookwyrm/templates/groups/group.html @@ -11,7 +11,7 @@ {% block searchresults %} {% endblock %} - {% include "groups/members.html" %} + {% include "groups/members.html" with group=group %}

Lists

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

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

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

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

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

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

- {% elif member.shared_books %} -

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

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

{% trans "Follows you" %}

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

Find new members

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

Group Members

-

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

+

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

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

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

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

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

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

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

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

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

-
+

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


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

Group Members

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

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

Lists

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

Group Members

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

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

- Add new members! -

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

+ Add new members! +

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

Lists

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

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

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

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

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

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

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

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

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

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

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

- {% trans "Follows you" %} -

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

+ {% trans "Follows you" %} +

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

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


+

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

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

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

Find new members

-
-
- -
-
- -
-
-
+
+

Find new members

+
+
+ +
+
+ +
+
+
{% endif %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index f8eefaff4..22eb0cb6a 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -45,4 +45,4 @@ {% endif %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/bookwyrm/templates/notifications/items/accept.html b/bookwyrm/templates/notifications/items/accept.html index 5aab79af0..19acab15e 100644 --- a/bookwyrm/templates/notifications/items/accept.html +++ b/bookwyrm/templates/notifications/items/accept.html @@ -17,4 +17,4 @@ accepted your invitation to join group "{{ group_name }}" {% endblocktrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/bookwyrm/templates/notifications/items/invite.html b/bookwyrm/templates/notifications/items/invite.html index c50dba06d..63a462608 100644 --- a/bookwyrm/templates/notifications/items/invite.html +++ b/bookwyrm/templates/notifications/items/invite.html @@ -4,7 +4,7 @@ {% load utilities %} {% block primary_link %}{% spaceless %} - {{ notification.related_group.local_path }} + {{ notification.related_group.local_path }} {% endspaceless %}{% endblock %} {% block icon %} diff --git a/bookwyrm/templates/notifications/items/join.html b/bookwyrm/templates/notifications/items/join.html index 3dbc8159c..9c766806f 100644 --- a/bookwyrm/templates/notifications/items/join.html +++ b/bookwyrm/templates/notifications/items/join.html @@ -4,7 +4,7 @@ {% load utilities %} {% block primary_link %}{% spaceless %} - {{ notification.related_group.local_path }} + {{ notification.related_group.local_path }} {% endspaceless %}{% endblock %} {% block icon %} From 3003b103e4797d29b4991768f69a8c6ac276c79a Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 08:38:00 +1100 Subject: [PATCH 092/193] add group views tests TODO: the POST test needs to test that the group was actually updated. --- bookwyrm/tests/views/test_group.py | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py index 571194d29..49d3b63f3 100644 --- a/bookwyrm/tests/views/test_group.py +++ b/bookwyrm/tests/views/test_group.py @@ -1,11 +1,12 @@ """ test for app action functionality """ from unittest.mock import patch +from django.contrib.auth import decorators from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import models, views +from bookwyrm import models, views, forms from bookwyrm.tests.validate_html import validate_html @@ -51,7 +52,7 @@ class GroupViews(TestCase): view = views.Group.as_view() request = self.factory.get("") request.user = self.local_user - result = view(request) + result = view(request, group_id=999) self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) @@ -61,7 +62,7 @@ class GroupViews(TestCase): view = views.UserGroups.as_view() request = self.factory.get("") request.user = self.local_user - result = view(request) + result = view(request, username="mouse@local.com") self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) @@ -71,20 +72,25 @@ class GroupViews(TestCase): view = views.FindUsers.as_view() request = self.factory.get("") request.user = self.local_user - result = view(request) + result = view(request,group_id=999) self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) def test_group_post(self, _): - """edit a "group" database entry""" - view = views.Group.as_view() - view.post( - group_id=999, - name="Test Group", - user=self.local_user, - privacy="public", - description="Test description", - ) + """test editing a "group" database entry""" - self.assertEqual("Test description", self.testgroup.description) + view = views.Group.as_view() + group_fields = { + "name": "Updated Group", + "privacy": "private", + "description": "Test description", + "user": self.local_user + } + request = self.factory.post("", group_fields) + request.user = self.local_user + + result = view(request, group_id=999) + self.assertEqual(result.status_code, 302) + + # TODO: test group was updated. From 48fc85c7613b06bb64abddec317b0892282c8d50 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 18:45:28 +1100 Subject: [PATCH 093/193] adjust commenting on js file --- bookwyrm/static/js/bookwyrm.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 66fd5a615..0eebc75b2 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -126,19 +126,14 @@ let BookWyrm = new class { /** * Show form. - * + * If the form has already been revealed, there is no '.is-hidden' element + * so this doesn't work as a toggle - use hideForm to hide it again * @param {Event} event * @return {undefined} */ revealForm(event) { let trigger = event.currentTarget; let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0]; - - /** - * if the form has already been revealed, there is no '.is-hidden' element - * so this doesn't really work as a toggle - */ - if (hidden) { this.addRemoveClass(hidden, 'is-hidden', !hidden); } From 05bde27944e5b273421e38ca6bf336880723045d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 18:46:30 +1100 Subject: [PATCH 094/193] remove commented out code --- bookwyrm/models/group.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 49a7d754a..febbc91d0 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -7,9 +7,6 @@ from .base_model import BookWyrmModel from . import fields from .relationship import UserBlocks -# from .user import User - - class Group(BookWyrmModel): """A group of users""" From 5a4026cda3bd7a73b99851cb8738770801fc1dec Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 18:47:03 +1100 Subject: [PATCH 095/193] group views tests --- bookwyrm/tests/views/test_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py index 49d3b63f3..c8e6208a1 100644 --- a/bookwyrm/tests/views/test_group.py +++ b/bookwyrm/tests/views/test_group.py @@ -93,4 +93,4 @@ class GroupViews(TestCase): result = view(request, group_id=999) self.assertEqual(result.status_code, 302) - # TODO: test group was updated. + # TODO: test that group was updated. From 056150d583a954c66e3c2c4d0978538dba779382 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 8 Oct 2021 21:21:19 +1100 Subject: [PATCH 096/193] CASCADE group.user Delete groups when group.user is deleted. --- bookwyrm/models/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index febbc91d0..f1005d4cf 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -11,7 +11,7 @@ class Group(BookWyrmModel): """A group of users""" name = fields.CharField(max_length=100) - user = fields.ForeignKey("User", on_delete=models.PROTECT) + user = fields.ForeignKey("User", on_delete=models.CASCADE) description = fields.TextField(blank=True, null=True) privacy = fields.PrivacyField() From 714a36924641b746402374371bb5072cd249c9a3 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 16:10:00 +1100 Subject: [PATCH 097/193] only show list edit form to list.user --- bookwyrm/templates/lists/layout.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/lists/layout.html b/bookwyrm/templates/lists/layout.html index 914478abb..6e772221a 100644 --- a/bookwyrm/templates/lists/layout.html +++ b/bookwyrm/templates/lists/layout.html @@ -25,7 +25,9 @@
- {% include 'lists/edit_form.html' with controls_text="edit_list" user_groups=user_groups %} + {% if request.user == list.user %} + {% include 'lists/edit_form.html' with controls_text="edit_list" user_groups=user_groups %} + {% endif %}
{% block panel %}{% endblock %} From 1bf5758e01b20c90baf1ba2b4f9085a3e61c8a9d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 16:11:11 +1100 Subject: [PATCH 098/193] overide filters for groups and group lists - use more sensible query for displaying groups on user page - privacy_filter now allows group members to see followers_only and private lists and groups they would otherwise not see --- bookwyrm/models/group.py | 16 ++++++++++++++++ bookwyrm/models/list.py | 17 +++++++++++++++++ bookwyrm/templates/groups/user_groups.html | 4 +--- bookwyrm/views/group.py | 13 ++++--------- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index f1005d4cf..89fb3a0e5 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -19,6 +19,22 @@ class Group(BookWyrmModel): """don't want the user to be in there in this case""" return f"https://{DOMAIN}/group/{self.id}" + @classmethod + def followers_filter(cls, queryset, viewer): + """Override filter for "followers" privacy level to allow non-following group members to see the existence of groups and group lists""" + + return queryset.exclude( + ~Q( # user isn't following and it isn't their own status and they are not a group member + Q(user__followers=viewer) | Q(user=viewer) | Q(memberships__user=viewer) + ), + privacy="followers", # and the status of the group is followers only + ) + + @classmethod + def direct_filter(cls, queryset, viewer): + """Override filter for "direct" privacy level to allow group members to see the existence of groups and group lists""" + + return queryset.exclude(~Q(memberships__user=viewer), privacy="direct") class GroupMember(models.Model): """Users who are members of a group""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 8a083b690..b0222cefa 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -1,6 +1,7 @@ """ make a list of books!! """ from django.apps import apps from django.db import models +from django.db.models import Q from django.utils import timezone from bookwyrm import activitypub @@ -71,6 +72,22 @@ class List(OrderedCollectionMixin, BookWyrmModel): return super().raise_not_editable(viewer) + @classmethod + def followers_filter(cls, queryset, viewer): + """Override filter for "followers" privacy level to allow non-following group members to see the existence of group lists""" + + return queryset.exclude( + ~Q( # user isn't following and it isn't their own status and they are not a group member + Q(user__followers=viewer) | Q(user=viewer) | Q(group__memberships__user=viewer) + ), + privacy="followers", # and the status (of the list) is followers only + ) + + @classmethod + def direct_filter(cls, queryset, viewer): + """Override filter for "direct" privacy level to allow group members to see the existence of group lists""" + + return queryset.exclude(~Q(group__memberships__user=viewer), privacy="direct") class ListItem(CollectionItemMixin, BookWyrmModel): """ok""" diff --git a/bookwyrm/templates/groups/user_groups.html b/bookwyrm/templates/groups/user_groups.html index f68994dc1..cc27ce42d 100644 --- a/bookwyrm/templates/groups/user_groups.html +++ b/bookwyrm/templates/groups/user_groups.html @@ -3,8 +3,7 @@ {% load interaction %}
- {% for membership in memberships %} - {% with group=membership.group %} + {% for group in groups %}
@@ -32,6 +31,5 @@
- {% endwith %} {% endfor %}
diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 4a6a80954..c66dcdd05 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -24,11 +24,8 @@ class Group(View): """display a group""" group = get_object_or_404(models.Group, id=group_id) - lists = models.List.objects.filter(group=group).order_by("-updated_date") - # lists = privacy_filter(request.user, lists) - - # don't show groups to users who shouldn't see them group.raise_visible_to_user(request.user) + lists = models.List.privacy_filter(request.user).filter(group=group).order_by("-updated_date") data = { "group": group, @@ -56,13 +53,11 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - memberships = ( - models.GroupMember.objects.filter(user=user).all().order_by("-updated_date") - ) - paginated = Paginator(memberships, 12) + groups = models.Group.privacy_filter(request.user).filter(memberships__user=user).order_by("-updated_date") + paginated = Paginator(groups, 12) data = { - "memberships": paginated.get_page(request.GET.get("page")), + "groups": paginated.get_page(request.GET.get("page")), "is_self": request.user.id == user.id, "user": user, "group_form": forms.GroupForm(), From 9940abfd81232dd4da3cd79eb72e45de11d43155 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 22:11:46 +1100 Subject: [PATCH 099/193] refactor removing user from group This is in preparation for removing a user and their lists when the group owner blocks them. Remove the user via models.group Remove the lists via models.list --- bookwyrm/models/group.py | 9 +++++++++ bookwyrm/models/list.py | 20 +++++++++++++++++++- bookwyrm/views/group.py | 13 ++++++------- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 89fb3a0e5..88320cf90 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -85,6 +85,15 @@ class GroupMember(models.Model): group=join_request.group, ) + @classmethod + def remove(cls, owner, user): + """remove a user from a group""" + + memberships = cls.objects.filter(group__user=owner, user=user).all() + for m in memberships: + # remove this user + m.delete() + class GroupMemberInvitation(models.Model): """adding a user to a group requires manual confirmation""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index b0222cefa..295032f58 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -2,6 +2,7 @@ from django.apps import apps from django.db import models from django.db.models import Q +from django.db.models.fields import NullBooleanField from django.utils import timezone from bookwyrm import activitypub @@ -87,7 +88,24 @@ class List(OrderedCollectionMixin, BookWyrmModel): def direct_filter(cls, queryset, viewer): """Override filter for "direct" privacy level to allow group members to see the existence of group lists""" - return queryset.exclude(~Q(group__memberships__user=viewer), privacy="direct") + return queryset.exclude( + ~Q( # user not self and not in the group if this is a group list + Q(user=viewer) | Q(group__memberships__user=viewer) + ), + privacy="direct" + ) + + @classmethod + def remove_from_group(cls, owner, user): + """remove a list from a group""" + + memberships = GroupMember.objects.filter(group__user=owner, user=user).all() + for m in memberships: + # remove this user's group-curated lists from the group + cls.objects.filter(group=m.group, user=m.user).update( + group=None, curation="closed" + ) + class ListItem(CollectionItemMixin, BookWyrmModel): """ok""" diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index c66dcdd05..6ef215830 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -163,6 +163,10 @@ def remove_member(request): if not user: return HttpResponseBadRequest() + # you can't be removed from your own group + if request.POST["user"]== group.user: + return HttpResponseBadRequest() + is_member = models.GroupMember.objects.filter(group=group, user=user).exists() is_invited = models.GroupMemberInvitation.objects.filter( group=group, user=user @@ -182,13 +186,8 @@ def remove_member(request): if is_member: try: - membership = models.GroupMember.objects.get(group=group, user=user) - membership.delete() - - # remove this user's group-curated lists from the group - models.List.objects.filter(group=group, user=user).update( - group=None, curation="closed" - ) + models.List.remove_from_group(group.user, user) + models.GroupMember.remove(group.user, user) except IntegrityError: pass From b3cc9e5b75917e7d514a8b7f9b911ace1f5e397d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 22:13:12 +1100 Subject: [PATCH 100/193] remove user and their lists from group when group.user blocks them Lists are changed to closed curation with no group. --- bookwyrm/views/preferences/block.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bookwyrm/views/preferences/block.py b/bookwyrm/views/preferences/block.py index 1eccf4612..2ccd3c065 100644 --- a/bookwyrm/views/preferences/block.py +++ b/bookwyrm/views/preferences/block.py @@ -23,6 +23,10 @@ class Block(View): models.UserBlocks.objects.create( user_subject=request.user, user_object=to_block ) + # remove the blocked users's lists from the groups + models.List.remove_from_group(request.user, to_block) + # remove the blocked user from all blocker's owned groups + models.GroupMember.remove(request.user, to_block) return redirect("prefs-block") From 252ff0d689ed37e5bf744079e785c1ab0228f0be Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sat, 9 Oct 2021 22:15:24 +1100 Subject: [PATCH 101/193] emblacken files Wouldn't it be great if I just remembered to run Black before every commit? --- bookwyrm/models/group.py | 2 ++ bookwyrm/models/list.py | 12 +++++++----- bookwyrm/tests/views/test_group.py | 4 ++-- bookwyrm/views/group.py | 14 +++++++++++--- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 88320cf90..907168699 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -7,6 +7,7 @@ from .base_model import BookWyrmModel from . import fields from .relationship import UserBlocks + class Group(BookWyrmModel): """A group of users""" @@ -36,6 +37,7 @@ class Group(BookWyrmModel): return queryset.exclude(~Q(memberships__user=viewer), privacy="direct") + class GroupMember(models.Model): """Users who are members of a group""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 295032f58..202c830c1 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -79,7 +79,9 @@ class List(OrderedCollectionMixin, BookWyrmModel): return queryset.exclude( ~Q( # user isn't following and it isn't their own status and they are not a group member - Q(user__followers=viewer) | Q(user=viewer) | Q(group__memberships__user=viewer) + Q(user__followers=viewer) + | Q(user=viewer) + | Q(group__memberships__user=viewer) ), privacy="followers", # and the status (of the list) is followers only ) @@ -89,10 +91,10 @@ class List(OrderedCollectionMixin, BookWyrmModel): """Override filter for "direct" privacy level to allow group members to see the existence of group lists""" return queryset.exclude( - ~Q( # user not self and not in the group if this is a group list - Q(user=viewer) | Q(group__memberships__user=viewer) - ), - privacy="direct" + ~Q( # user not self and not in the group if this is a group list + Q(user=viewer) | Q(group__memberships__user=viewer) + ), + privacy="direct", ) @classmethod diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py index c8e6208a1..3b0aa236c 100644 --- a/bookwyrm/tests/views/test_group.py +++ b/bookwyrm/tests/views/test_group.py @@ -72,7 +72,7 @@ class GroupViews(TestCase): view = views.FindUsers.as_view() request = self.factory.get("") request.user = self.local_user - result = view(request,group_id=999) + result = view(request, group_id=999) self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) @@ -85,7 +85,7 @@ class GroupViews(TestCase): "name": "Updated Group", "privacy": "private", "description": "Test description", - "user": self.local_user + "user": self.local_user, } request = self.factory.post("", group_fields) request.user = self.local_user diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 6ef215830..3e510a4d4 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -25,7 +25,11 @@ class Group(View): group = get_object_or_404(models.Group, id=group_id) group.raise_visible_to_user(request.user) - lists = models.List.privacy_filter(request.user).filter(group=group).order_by("-updated_date") + lists = ( + models.List.privacy_filter(request.user) + .filter(group=group) + .order_by("-updated_date") + ) data = { "group": group, @@ -53,7 +57,11 @@ class UserGroups(View): def get(self, request, username): """display a group""" user = get_user_from_username(request.user, username) - groups = models.Group.privacy_filter(request.user).filter(memberships__user=user).order_by("-updated_date") + groups = ( + models.Group.privacy_filter(request.user) + .filter(memberships__user=user) + .order_by("-updated_date") + ) paginated = Paginator(groups, 12) data = { @@ -164,7 +172,7 @@ def remove_member(request): return HttpResponseBadRequest() # you can't be removed from your own group - if request.POST["user"]== group.user: + if request.POST["user"] == group.user: return HttpResponseBadRequest() is_member = models.GroupMember.objects.filter(group=group, user=user).exists() From 83f46b6cdacb26aee0287f831685aaea73731d5b Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 10 Oct 2021 12:01:21 +1100 Subject: [PATCH 102/193] remove print() statement Whoops accidentally left this behind from manual troubleshooting --- bookwyrm/models/list.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 202c830c1..b06cdef8c 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -62,7 +62,6 @@ class List(OrderedCollectionMixin, BookWyrmModel): def raise_not_editable(self, viewer): """the associated user OR the list owner can edit""" - print("raising not editable") if self.user == viewer: return # group members can edit items in group lists From d6a5794ac3039e37b4d0ae044f387cc3e921197e Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 10 Oct 2021 12:02:27 +1100 Subject: [PATCH 103/193] do not load list edit form if viewer not authenticated --- bookwyrm/templates/lists/lists.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/lists/lists.html b/bookwyrm/templates/lists/lists.html index d909f5e81..49091bcf0 100644 --- a/bookwyrm/templates/lists/lists.html +++ b/bookwyrm/templates/lists/lists.html @@ -22,10 +22,11 @@
{% endif %} - +{% if request.user.is_authenticated %}
{% include 'lists/create_form.html' with controls_text="create_list" %}
+{% endif %} {% if request.user.is_authenticated %}
{% include 'shelf/create_shelf_form.html' with controls_text='create_shelf_form' %} From b64a616ff94859ae9c5f2071fb4de068c639bb35 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 13:56:55 -0700 Subject: [PATCH 166/193] Fixes mock in test --- bookwyrm/tests/views/test_shelf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index b78e241cc..35cc63f21 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -52,7 +52,7 @@ class ShelfViews(TestCase): shelf = self.local_user.shelf_set.first() request = self.factory.get("") request.user = self.local_user - with patch("bookwyrm.views.shelf.is_api_request") as is_api: + with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api: is_api.return_value = False result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, TemplateResponse) From 1bb23a8edf1d916a7f315cf127f8566678df8bd9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 14:15:05 -0700 Subject: [PATCH 167/193] Adds more tests of shelf views --- bookwyrm/tests/views/test_shelf.py | 45 ++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index 35cc63f21..914f73973 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -2,6 +2,7 @@ import json from unittest.mock import patch +from django.contrib.auth.models import AnonymousUser from django.core.exceptions import PermissionDenied from django.template.response import TemplateResponse from django.test import TestCase @@ -46,6 +47,46 @@ class ShelfViews(TestCase): ) models.SiteSettings.objects.create() + self.anonymous_user = AnonymousUser + self.anonymous_user.is_authenticated = False + + def test_shelf_page_all_books(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Shelf.as_view() + request = self.factory.get("") + request.user = self.local_user + with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.local_user.username) + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) + + def test_shelf_page_all_books_anonymous(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Shelf.as_view() + request = self.factory.get("") + request.user = self.anonymous_user + with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.local_user.username) + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) + + def test_shelf_page_sorted(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Shelf.as_view() + shelf = self.local_user.shelf_set.first() + request = self.factory.get("", {"sort": "author"}) + request.user = self.local_user + with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.local_user.username, shelf.identifier) + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) + def test_shelf_page(self, *_): """there are so many views, this just makes sure it LOADS""" view = views.Shelf.as_view() @@ -59,7 +100,7 @@ class ShelfViews(TestCase): validate_html(result.render()) self.assertEqual(result.status_code, 200) - with patch("bookwyrm.views.shelf.is_api_request") as is_api: + with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api: is_api.return_value = True result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, ActivitypubResponse) @@ -67,7 +108,7 @@ class ShelfViews(TestCase): request = self.factory.get("/?page=1") request.user = self.local_user - with patch("bookwyrm.views.shelf.is_api_request") as is_api: + with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api: is_api.return_value = True result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, ActivitypubResponse) From 3d92afdf280231a4d07a57bf6770f7c532ca929d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 14:16:13 -0700 Subject: [PATCH 168/193] Moves shelf tests into subdirectory --- bookwyrm/tests/views/shelf/__init__.py | 1 + bookwyrm/tests/views/{ => shelf}/test_shelf.py | 0 2 files changed, 1 insertion(+) create mode 100644 bookwyrm/tests/views/shelf/__init__.py rename bookwyrm/tests/views/{ => shelf}/test_shelf.py (100%) diff --git a/bookwyrm/tests/views/shelf/__init__.py b/bookwyrm/tests/views/shelf/__init__.py new file mode 100644 index 000000000..b6e690fd5 --- /dev/null +++ b/bookwyrm/tests/views/shelf/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/shelf/test_shelf.py similarity index 100% rename from bookwyrm/tests/views/test_shelf.py rename to bookwyrm/tests/views/shelf/test_shelf.py From 5c2d6e651029ca597f4b3a5de9fc3801b9cf46b5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 14:30:11 -0700 Subject: [PATCH 169/193] Separate out test files and add more tests --- bookwyrm/tests/views/shelf/test_shelf.py | 153 +------------ .../tests/views/shelf/test_shelf_actions.py | 216 ++++++++++++++++++ 2 files changed, 217 insertions(+), 152 deletions(-) create mode 100644 bookwyrm/tests/views/shelf/test_shelf_actions.py diff --git a/bookwyrm/tests/views/shelf/test_shelf.py b/bookwyrm/tests/views/shelf/test_shelf.py index 914f73973..71df3631f 100644 --- a/bookwyrm/tests/views/shelf/test_shelf.py +++ b/bookwyrm/tests/views/shelf/test_shelf.py @@ -1,14 +1,12 @@ """ test for app action functionality """ -import json from unittest.mock import patch from django.contrib.auth.models import AnonymousUser -from django.core.exceptions import PermissionDenied from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import forms, models, views +from bookwyrm import models, views from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.tests.validate_html import validate_html @@ -165,152 +163,3 @@ class ShelfViews(TestCase): view(request, request.user.username, shelf.identifier) self.assertEqual(shelf.name, "To Read") - - def test_shelve(self, *_): - """shelve a book""" - request = self.factory.post( - "", {"book": self.book.id, "shelf": self.shelf.identifier} - ) - request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: - views.shelve(request) - - self.assertEqual(mock.call_count, 1) - activity = json.loads(mock.call_args[0][1]) - self.assertEqual(activity["type"], "Add") - - item = models.ShelfBook.objects.get() - self.assertEqual(activity["object"]["id"], item.remote_id) - # make sure the book is on the shelf - self.assertEqual(self.shelf.books.get(), self.book) - - def test_shelve_to_read(self, *_): - """special behavior for the to-read shelf""" - shelf = models.Shelf.objects.get(identifier="to-read") - request = self.factory.post( - "", {"book": self.book.id, "shelf": shelf.identifier} - ) - request.user = self.local_user - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.shelve(request) - # make sure the book is on the shelf - self.assertEqual(shelf.books.get(), self.book) - - def test_shelve_reading(self, *_): - """special behavior for the reading shelf""" - shelf = models.Shelf.objects.get(identifier="reading") - request = self.factory.post( - "", {"book": self.book.id, "shelf": shelf.identifier} - ) - request.user = self.local_user - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.shelve(request) - # make sure the book is on the shelf - self.assertEqual(shelf.books.get(), self.book) - - def test_shelve_read(self, *_): - """special behavior for the read shelf""" - shelf = models.Shelf.objects.get(identifier="read") - request = self.factory.post( - "", {"book": self.book.id, "shelf": shelf.identifier} - ) - request.user = self.local_user - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.shelve(request) - # make sure the book is on the shelf - self.assertEqual(shelf.books.get(), self.book) - - def test_unshelve(self, *_): - """remove a book from a shelf""" - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - models.ShelfBook.objects.create( - book=self.book, user=self.local_user, shelf=self.shelf - ) - item = models.ShelfBook.objects.get() - - self.shelf.save() - self.assertEqual(self.shelf.books.count(), 1) - request = self.factory.post("", {"book": self.book.id, "shelf": self.shelf.id}) - request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: - views.unshelve(request) - activity = json.loads(mock.call_args[0][1]) - self.assertEqual(activity["type"], "Remove") - self.assertEqual(activity["object"]["id"], item.remote_id) - self.assertEqual(self.shelf.books.count(), 0) - - def test_create_shelf(self, *_): - """a brand new custom shelf""" - form = forms.ShelfForm() - form.data["user"] = self.local_user.id - form.data["name"] = "new shelf name" - form.data["description"] = "desc" - form.data["privacy"] = "unlisted" - request = self.factory.post("", form.data) - request.user = self.local_user - - views.create_shelf(request) - - shelf = models.Shelf.objects.get(name="new shelf name") - self.assertEqual(shelf.privacy, "unlisted") - self.assertEqual(shelf.description, "desc") - self.assertEqual(shelf.user, self.local_user) - - def test_delete_shelf(self, *_): - """delete a brand new custom shelf""" - request = self.factory.post("") - request.user = self.local_user - shelf_id = self.shelf.id - - views.delete_shelf(request, shelf_id) - - self.assertFalse(models.Shelf.objects.filter(id=shelf_id).exists()) - - def test_delete_shelf_unauthorized(self, *_): - """delete a brand new custom shelf""" - with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( - "bookwyrm.activitystreams.populate_stream_task.delay" - ): - rat = models.User.objects.create_user( - "rat@local.com", - "rat@mouse.mouse", - "password", - local=True, - localname="rat", - ) - request = self.factory.post("") - request.user = rat - - with self.assertRaises(PermissionDenied): - views.delete_shelf(request, self.shelf.id) - - self.assertTrue(models.Shelf.objects.filter(id=self.shelf.id).exists()) - - def test_delete_shelf_has_book(self, *_): - """delete a brand new custom shelf""" - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - models.ShelfBook.objects.create( - book=self.book, user=self.local_user, shelf=self.shelf - ) - request = self.factory.post("") - request.user = self.local_user - - with self.assertRaises(PermissionDenied): - views.delete_shelf(request, self.shelf.id) - - self.assertTrue(models.Shelf.objects.filter(id=self.shelf.id).exists()) - - def test_delete_shelf_not_editable(self, *_): - """delete a brand new custom shelf""" - shelf = self.local_user.shelf_set.first() - self.assertFalse(shelf.editable) - request = self.factory.post("") - request.user = self.local_user - - with self.assertRaises(PermissionDenied): - views.delete_shelf(request, shelf.id) - - self.assertTrue(models.Shelf.objects.filter(id=shelf.id).exists()) diff --git a/bookwyrm/tests/views/shelf/test_shelf_actions.py b/bookwyrm/tests/views/shelf/test_shelf_actions.py new file mode 100644 index 000000000..50b319d0e --- /dev/null +++ b/bookwyrm/tests/views/shelf/test_shelf_actions.py @@ -0,0 +1,216 @@ +""" test for app action functionality """ +import json +from unittest.mock import patch + +from django.core.exceptions import PermissionDenied +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import forms, models, views + + +@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") +@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") +@patch("bookwyrm.activitystreams.populate_stream_task.delay") +@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") +@patch("bookwyrm.activitystreams.remove_book_statuses_task.delay") +class ShelfActionViews(TestCase): + """tag views""" + + 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.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", + ) + self.work = models.Work.objects.create(title="Test Work") + self.book = models.Edition.objects.create( + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=self.work, + ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + self.shelf = models.Shelf.objects.create( + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) + models.SiteSettings.objects.create() + + def test_shelve(self, *_): + """shelve a book""" + request = self.factory.post( + "", {"book": self.book.id, "shelf": self.shelf.identifier} + ) + request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.shelve(request) + + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Add") + + item = models.ShelfBook.objects.get() + self.assertEqual(activity["object"]["id"], item.remote_id) + # make sure the book is on the shelf + self.assertEqual(self.shelf.books.get(), self.book) + + def test_shelve_to_read(self, *_): + """special behavior for the to-read shelf""" + shelf = models.Shelf.objects.get(identifier="to-read") + request = self.factory.post( + "", {"book": self.book.id, "shelf": shelf.identifier} + ) + request.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.shelve(request) + # make sure the book is on the shelf + self.assertEqual(shelf.books.get(), self.book) + + def test_shelve_reading(self, *_): + """special behavior for the reading shelf""" + shelf = models.Shelf.objects.get(identifier="reading") + request = self.factory.post( + "", {"book": self.book.id, "shelf": shelf.identifier} + ) + request.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.shelve(request) + # make sure the book is on the shelf + self.assertEqual(shelf.books.get(), self.book) + + def test_shelve_read(self, *_): + """special behavior for the read shelf""" + shelf = models.Shelf.objects.get(identifier="read") + request = self.factory.post( + "", {"book": self.book.id, "shelf": shelf.identifier} + ) + request.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.shelve(request) + # make sure the book is on the shelf + self.assertEqual(shelf.books.get(), self.book) + + def test_shelve_read_with_change_shelf(self, *_): + """special behavior for the read shelf""" + previous_shelf = models.Shelf.objects.get(identifier="reading") + models.ShelfBook.objects.create( + shelf=previous_shelf, user=self.local_user, book=self.book + ) + shelf = models.Shelf.objects.get(identifier="read") + + request = self.factory.post( + "", { + "book": self.book.id, + "shelf": shelf.identifier, + "change-shelf-from": previous_shelf.identifier, + } + ) + request.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.shelve(request) + # make sure the book is on the shelf + self.assertEqual(shelf.books.get(), self.book) + self.assertEqual(list(previous_shelf.books.all()), []) + + def test_unshelve(self, *_): + """remove a book from a shelf""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ShelfBook.objects.create( + book=self.book, user=self.local_user, shelf=self.shelf + ) + item = models.ShelfBook.objects.get() + + self.shelf.save() + self.assertEqual(self.shelf.books.count(), 1) + request = self.factory.post("", {"book": self.book.id, "shelf": self.shelf.id}) + request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.unshelve(request) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Remove") + self.assertEqual(activity["object"]["id"], item.remote_id) + self.assertEqual(self.shelf.books.count(), 0) + + def test_create_shelf(self, *_): + """a brand new custom shelf""" + form = forms.ShelfForm() + form.data["user"] = self.local_user.id + form.data["name"] = "new shelf name" + form.data["description"] = "desc" + form.data["privacy"] = "unlisted" + request = self.factory.post("", form.data) + request.user = self.local_user + + views.create_shelf(request) + + shelf = models.Shelf.objects.get(name="new shelf name") + self.assertEqual(shelf.privacy, "unlisted") + self.assertEqual(shelf.description, "desc") + self.assertEqual(shelf.user, self.local_user) + + def test_delete_shelf(self, *_): + """delete a brand new custom shelf""" + request = self.factory.post("") + request.user = self.local_user + shelf_id = self.shelf.id + + views.delete_shelf(request, shelf_id) + + self.assertFalse(models.Shelf.objects.filter(id=shelf_id).exists()) + + def test_delete_shelf_unauthorized(self, *_): + """delete a brand new custom shelf""" + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ): + rat = models.User.objects.create_user( + "rat@local.com", + "rat@mouse.mouse", + "password", + local=True, + localname="rat", + ) + request = self.factory.post("") + request.user = rat + + with self.assertRaises(PermissionDenied): + views.delete_shelf(request, self.shelf.id) + + self.assertTrue(models.Shelf.objects.filter(id=self.shelf.id).exists()) + + def test_delete_shelf_has_book(self, *_): + """delete a brand new custom shelf""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ShelfBook.objects.create( + book=self.book, user=self.local_user, shelf=self.shelf + ) + request = self.factory.post("") + request.user = self.local_user + + with self.assertRaises(PermissionDenied): + views.delete_shelf(request, self.shelf.id) + + self.assertTrue(models.Shelf.objects.filter(id=self.shelf.id).exists()) + + def test_delete_shelf_not_editable(self, *_): + """delete a brand new custom shelf""" + shelf = self.local_user.shelf_set.first() + self.assertFalse(shelf.editable) + request = self.factory.post("") + request.user = self.local_user + + with self.assertRaises(PermissionDenied): + views.delete_shelf(request, shelf.id) + + self.assertTrue(models.Shelf.objects.filter(id=shelf.id).exists()) From f65a54eb4abe8b5f0b089f1801fb598faa96e2ed Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 14:34:42 -0700 Subject: [PATCH 170/193] Python formatting --- bookwyrm/tests/views/shelf/test_shelf_actions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bookwyrm/tests/views/shelf/test_shelf_actions.py b/bookwyrm/tests/views/shelf/test_shelf_actions.py index 50b319d0e..3efae0f45 100644 --- a/bookwyrm/tests/views/shelf/test_shelf_actions.py +++ b/bookwyrm/tests/views/shelf/test_shelf_actions.py @@ -109,11 +109,12 @@ class ShelfActionViews(TestCase): shelf = models.Shelf.objects.get(identifier="read") request = self.factory.post( - "", { + "", + { "book": self.book.id, "shelf": shelf.identifier, "change-shelf-from": previous_shelf.identifier, - } + }, ) request.user = self.local_user From 89a385da0a96eece560aa569f4cb95c98b6e8dfe Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 17:36:52 -0700 Subject: [PATCH 171/193] Paginate books on author page --- bookwyrm/views/author.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/bookwyrm/views/author.py b/bookwyrm/views/author.py index e1e9247de..1265ad689 100644 --- a/bookwyrm/views/author.py +++ b/bookwyrm/views/author.py @@ -1,6 +1,7 @@ """ the good people stuff! the authors! """ from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import Q +from django.core.paginator import Paginator +from django.db.models import OuterRef, Subquery, F, Q from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator @@ -8,7 +9,8 @@ from django.views import View from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse -from .helpers import is_api_request +from bookwyrm.settings import PAGE_LENGTH +from bookwyrm.views.helpers import is_api_request # pylint: disable= no-self-use @@ -22,12 +24,26 @@ class Author(View): if is_api_request(request): return ActivitypubResponse(author.to_activity()) - books = models.Work.objects.filter( - Q(authors=author) | Q(editions__authors=author) - ).distinct() + default_editions = models.Edition.objects.filter( + parent_work=OuterRef("parent_work") + ).order_by("-edition_rank") + + books = ( + models.Edition.objects.filter( + Q(authors=author) | Q(parent_work__authors=author) + ) + .annotate(default_id=Subquery(default_editions.values("id")[:1])) + .filter(default_id=F("id")) + ) + + paginated = Paginator(books, PAGE_LENGTH) + page = paginated.get_page(request.GET.get("page")) data = { "author": author, - "books": [b.default_edition for b in books], + "books": page, + "page_range": paginated.get_elided_page_range( + page.number, on_each_side=2, on_ends=1 + ), } return TemplateResponse(request, "author/author.html", data) From 3eb3225d2c629abe72ca82f2ac09fa7413c9d6e5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 17:42:19 -0700 Subject: [PATCH 172/193] Adds pagination to the template --- bookwyrm/templates/author/author.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bookwyrm/templates/author/author.html b/bookwyrm/templates/author/author.html index 8a15cd0f0..5310f0df8 100644 --- a/bookwyrm/templates/author/author.html +++ b/bookwyrm/templates/author/author.html @@ -114,4 +114,9 @@ {% endfor %}
+ +
+ {% include 'snippets/pagination.html' with page=books %} +
+ {% endblock %} From de93beca84e5f2dcee9922486d99b6cc662a4525 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 17:51:42 -0700 Subject: [PATCH 173/193] Adds shelve buttons to books on author page --- bookwyrm/templates/author/author.html | 1 + bookwyrm/views/author.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/author/author.html b/bookwyrm/templates/author/author.html index 5310f0df8..6a67b50b3 100644 --- a/bookwyrm/templates/author/author.html +++ b/bookwyrm/templates/author/author.html @@ -110,6 +110,7 @@ {% for book in books %}
{% include 'landing/small-book.html' with book=book %} + {% include 'snippets/shelve_button/shelve_button.html' with book=book %}
{% endfor %}
diff --git a/bookwyrm/views/author.py b/bookwyrm/views/author.py index 1265ad689..9a3d582d6 100644 --- a/bookwyrm/views/author.py +++ b/bookwyrm/views/author.py @@ -29,7 +29,7 @@ class Author(View): ).order_by("-edition_rank") books = ( - models.Edition.objects.filter( + models.Edition.viewer_aware_objects(request.user).filter( Q(authors=author) | Q(parent_work__authors=author) ) .annotate(default_id=Subquery(default_editions.values("id")[:1])) From 14682ed8c687590b3ebb5be299a9399419fec0db Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 18:04:29 -0700 Subject: [PATCH 174/193] Prefect related data in author view --- bookwyrm/views/author.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/author.py b/bookwyrm/views/author.py index 9a3d582d6..4f29e2af6 100644 --- a/bookwyrm/views/author.py +++ b/bookwyrm/views/author.py @@ -34,7 +34,7 @@ class Author(View): ) .annotate(default_id=Subquery(default_editions.values("id")[:1])) .filter(default_id=F("id")) - ) + ).prefetch_related("authors") paginated = Paginator(books, PAGE_LENGTH) page = paginated.get_page(request.GET.get("page")) From d706b26ac98b3fbb0b2e82d1d2423defbe3168e4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 18:11:31 -0700 Subject: [PATCH 175/193] Python formatting --- bookwyrm/views/author.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bookwyrm/views/author.py b/bookwyrm/views/author.py index 4f29e2af6..4cb7ac2b5 100644 --- a/bookwyrm/views/author.py +++ b/bookwyrm/views/author.py @@ -29,9 +29,8 @@ class Author(View): ).order_by("-edition_rank") books = ( - models.Edition.viewer_aware_objects(request.user).filter( - Q(authors=author) | Q(parent_work__authors=author) - ) + models.Edition.viewer_aware_objects(request.user) + .filter(Q(authors=author) | Q(parent_work__authors=author)) .annotate(default_id=Subquery(default_editions.values("id")[:1])) .filter(default_id=F("id")) ).prefetch_related("authors") From 72dc21e82ab7c076eda0b984ea9f9ad57c687a4b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 18:27:19 -0700 Subject: [PATCH 176/193] Adds tests and fixes unset ordering warnings --- bookwyrm/tests/views/test_author.py | 27 +++++++++++++++++++++++++-- bookwyrm/views/author.py | 4 +++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/bookwyrm/tests/views/test_author.py b/bookwyrm/tests/views/test_author.py index 03c027fae..75c7433fe 100644 --- a/bookwyrm/tests/views/test_author.py +++ b/bookwyrm/tests/views/test_author.py @@ -1,6 +1,7 @@ """ test for app action functionality """ from unittest.mock import patch -from django.contrib.auth.models import Group, Permission + +from django.contrib.auth.models import AnonymousUser, Group, Permission from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.template.response import TemplateResponse @@ -44,6 +45,8 @@ class AuthorViews(TestCase): parent_work=self.work, ) + self.anonymous_user = AnonymousUser + self.anonymous_user.is_authenticated = False models.SiteSettings.objects.create() def test_author_page(self): @@ -51,6 +54,7 @@ class AuthorViews(TestCase): view = views.Author.as_view() author = models.Author.objects.create(name="Jessica") request = self.factory.get("") + request.user = self.local_user with patch("bookwyrm.views.author.is_api_request") as is_api: is_api.return_value = False result = view(request, author.id) @@ -59,7 +63,26 @@ class AuthorViews(TestCase): self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200) + def test_author_page_logged_out(self): + """there are so many views, this just makes sure it LOADS""" + view = views.Author.as_view() + author = models.Author.objects.create(name="Jessica") request = self.factory.get("") + request.user = self.anonymous_user + with patch("bookwyrm.views.author.is_api_request") as is_api: + is_api.return_value = False + result = view(request, author.id) + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, 200) + + def test_author_page_api_response(self): + """there are so many views, this just makes sure it LOADS""" + view = views.Author.as_view() + author = models.Author.objects.create(name="Jessica") + request = self.factory.get("") + request.user = self.local_user with patch("bookwyrm.views.author.is_api_request") as is_api: is_api.return_value = True result = view(request, author.id) @@ -126,5 +149,5 @@ class AuthorViews(TestCase): resp = view(request, author.id) author.refresh_from_db() self.assertEqual(author.name, "Test Author") - resp.render() + validate_html(resp.render()) self.assertEqual(resp.status_code, 200) diff --git a/bookwyrm/views/author.py b/bookwyrm/views/author.py index 4cb7ac2b5..6c3ee36ff 100644 --- a/bookwyrm/views/author.py +++ b/bookwyrm/views/author.py @@ -33,7 +33,9 @@ class Author(View): .filter(Q(authors=author) | Q(parent_work__authors=author)) .annotate(default_id=Subquery(default_editions.values("id")[:1])) .filter(default_id=F("id")) - ).prefetch_related("authors") + .order_by("-first_published_date", "-published_date", "-created_date") + .prefetch_related("authors") + ) paginated = Paginator(books, PAGE_LENGTH) page = paginated.get_page(request.GET.get("page")) From 278a9de673b54638342a31ea94f0fa5b0a08ae87 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 20 Oct 2021 18:29:00 -0700 Subject: [PATCH 177/193] Removes duplicate assertions in author view test --- bookwyrm/tests/views/test_author.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bookwyrm/tests/views/test_author.py b/bookwyrm/tests/views/test_author.py index 75c7433fe..ccbfe5493 100644 --- a/bookwyrm/tests/views/test_author.py +++ b/bookwyrm/tests/views/test_author.py @@ -61,7 +61,6 @@ class AuthorViews(TestCase): self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) - self.assertEqual(result.status_code, 200) def test_author_page_logged_out(self): """there are so many views, this just makes sure it LOADS""" @@ -75,7 +74,6 @@ class AuthorViews(TestCase): self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) - self.assertEqual(result.status_code, 200) def test_author_page_api_response(self): """there are so many views, this just makes sure it LOADS""" @@ -101,7 +99,6 @@ class AuthorViews(TestCase): self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) - self.assertEqual(result.status_code, 200) def test_edit_author(self): """edit an author""" From 19c7e43f50decbd848fc307be39a71aeb496a423 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Oct 2021 18:40:55 +1100 Subject: [PATCH 178/193] remove followers privacy option from group form --- bookwyrm/templates/groups/form.html | 2 +- .../snippets/privacy_select_no_followers.html | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/templates/snippets/privacy_select_no_followers.html diff --git a/bookwyrm/templates/groups/form.html b/bookwyrm/templates/groups/form.html index c1281172b..949a6e967 100644 --- a/bookwyrm/templates/groups/form.html +++ b/bookwyrm/templates/groups/form.html @@ -19,7 +19,7 @@
- {% include 'snippets/privacy_select.html' with current=group.privacy %} + {% include 'snippets/privacy_select_no_followers.html' with current=group.privacy %}
diff --git a/bookwyrm/templates/snippets/privacy_select_no_followers.html b/bookwyrm/templates/snippets/privacy_select_no_followers.html new file mode 100644 index 000000000..2c601e7ff --- /dev/null +++ b/bookwyrm/templates/snippets/privacy_select_no_followers.html @@ -0,0 +1,21 @@ +{% load i18n %} +{% load utilities %} +
+ {% firstof privacy_uuid 0|uuid as uuid %} + {% if not no_label %} + + {% endif %} + {% firstof current user.default_post_privacy "public" as privacy %} + +
+ From bd20c9ce2c37ff582241ea030e8f155bfab86b5c Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Oct 2021 18:42:18 +1100 Subject: [PATCH 179/193] remove followers group visibility test Also updates description of group.followers_filter() override --- bookwyrm/models/group.py | 2 +- bookwyrm/tests/models/test_group.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py index 552e2c287..bd5b8d410 100644 --- a/bookwyrm/models/group.py +++ b/bookwyrm/models/group.py @@ -23,7 +23,7 @@ class Group(BookWyrmModel): @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""" + group members to see the existence of group-curated lists""" return queryset.exclude( ~Q( # user is not a group member diff --git a/bookwyrm/tests/models/test_group.py b/bookwyrm/tests/models/test_group.py index 275fce196..33341d192 100644 --- a/bookwyrm/tests/models/test_group.py +++ b/bookwyrm/tests/models/test_group.py @@ -75,15 +75,6 @@ class Group(TestCase): ) models.GroupMember.objects.create(group=self.public_group, user=self.capybara) - def test_group_members_can_see_followers_only_groups(self, _): - """follower-only group should not be excluded from group listings for group members viewing""" - - rat_groups = models.Group.privacy_filter(self.rat).all() - badger_groups = models.Group.privacy_filter(self.badger).all() - - self.assertFalse(self.followers_only_group in rat_groups) - self.assertTrue(self.followers_only_group in badger_groups) - def test_group_members_can_see_private_groups(self, _): """direct privacy group should not be excluded from group listings for group members viewing""" From 80edc1e95e70cf14d88c6d5acbad1f0b0f88f1b8 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Oct 2021 20:16:48 +1100 Subject: [PATCH 180/193] remove trailing spaces --- bookwyrm/templates/notifications/items/join.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/bookwyrm/templates/notifications/items/join.html b/bookwyrm/templates/notifications/items/join.html index 9c766806f..93b356424 100644 --- a/bookwyrm/templates/notifications/items/join.html +++ b/bookwyrm/templates/notifications/items/join.html @@ -18,6 +18,3 @@ has joined your group "{{ group_name }}" {% endblocktrans %} {% endblock %} - - - From c9deda8fdd8a63203b55aa803126a35e6633fb6e Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Oct 2021 20:21:55 +1100 Subject: [PATCH 181/193] remove superfluous field --- bookwyrm/templates/groups/form.html | 1 - 1 file changed, 1 deletion(-) diff --git a/bookwyrm/templates/groups/form.html b/bookwyrm/templates/groups/form.html index 949a6e967..21c525ee0 100644 --- a/bookwyrm/templates/groups/form.html +++ b/bookwyrm/templates/groups/form.html @@ -4,7 +4,6 @@
-
{{ group_form.name }} From 6bc86f189fdc898fe8baeaf22f8bcdc0739636bf Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Oct 2021 20:23:45 +1100 Subject: [PATCH 182/193] notify group members of group changes Send a notification to all group members when group name, description, or privacy are changed. --- bookwyrm/models/notification.py | 2 +- bookwyrm/templates/notifications/item.html | 6 ++++ .../templates/notifications/items/update.html | 28 +++++++++++++++++++ bookwyrm/views/group.py | 25 +++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/templates/notifications/items/update.html diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index b15b95c2c..2f1aae4f3 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -7,7 +7,7 @@ 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", + "FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT INVITE ACCEPT JOIN LEAVE REMOVE GROUP_PRIVACY GROUP_NAME GROUP_DESCRIPTION", ) diff --git a/bookwyrm/templates/notifications/item.html b/bookwyrm/templates/notifications/item.html index 3ed43a96a..e8e2dcb26 100644 --- a/bookwyrm/templates/notifications/item.html +++ b/bookwyrm/templates/notifications/item.html @@ -27,4 +27,10 @@ {% include 'notifications/items/leave.html' %} {% elif notification.notification_type == 'REMOVE' %} {% include 'notifications/items/remove.html' %} +{% elif notification.notification_type == 'GROUP_PRIVACY' %} + {% include 'notifications/items/update.html' %} +{% elif notification.notification_type == 'GROUP_NAME' %} + {% include 'notifications/items/update.html' %} +{% elif notification.notification_type == 'GROUP_DESCRIPTION' %} + {% include 'notifications/items/update.html' %} {% endif %} diff --git a/bookwyrm/templates/notifications/items/update.html b/bookwyrm/templates/notifications/items/update.html new file mode 100644 index 000000000..5c60e5a24 --- /dev/null +++ b/bookwyrm/templates/notifications/items/update.html @@ -0,0 +1,28 @@ +{% extends 'notifications/items/item_layout.html' %} + +{% load i18n %} +{% load utilities %} + +{% block primary_link %}{% spaceless %} + {{ notification.related_group.local_path }} +{% endspaceless %}{% endblock %} + +{% block icon %} + +{% endblock %} + +{% block description %} + {% if notification.notification_type == 'GROUP_PRIVACY' %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + has changed the privacy level for {{ group_name }} + {% endblocktrans %} + {% elif notification.notification_type == 'GROUP_NAME' %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + has changed the name of {{ group_name }} + {% endblocktrans %} + {% else %} + {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + has changed the description of {{ group_name }} + {% endblocktrans %} + {% endif %} +{% endblock %} diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py index 1c0a28486..9ee99bffa 100644 --- a/bookwyrm/views/group.py +++ b/bookwyrm/views/group.py @@ -47,6 +47,31 @@ class Group(View): if not form.is_valid(): return redirect("group", user_group.id) user_group = form.save() + + # let the other members know something about the group changed + memberships = models.GroupMember.objects.filter(group=user_group) + model = apps.get_model("bookwyrm.Notification", require_ready=True) + for field in form.changed_data: + notification_type = ( + "GROUP_PRIVACY" + if field == "privacy" + else "GROUP_NAME" + if field == "name" + else "GROUP_DESCRIPTION" + if field == "description" + else None + ) + if notification_type: + for membership in memberships: + member = membership.user + if member != request.user: + model.objects.create( + user=member, + related_user=request.user, + related_group=user_group, + notification_type=notification_type, + ) + return redirect("group", user_group.id) From 1d791d950f3a851ca75411ec67685231f6352639 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Oct 2021 20:30:25 +1100 Subject: [PATCH 183/193] add migration for updated notification types --- .../migrations/0112_auto_20211022_0844.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 bookwyrm/migrations/0112_auto_20211022_0844.py diff --git a/bookwyrm/migrations/0112_auto_20211022_0844.py b/bookwyrm/migrations/0112_auto_20211022_0844.py new file mode 100644 index 000000000..246480b3a --- /dev/null +++ b/bookwyrm/migrations/0112_auto_20211022_0844.py @@ -0,0 +1,93 @@ +# Generated by Django 3.2.5 on 2021-10-22 08:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0111_merge_0107_auto_20211016_0639_0110_auto_20211015_1734"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="notification", + name="notification_type_valid", + ), + 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"), + ("GROUP_PRIVACY", "Group Privacy"), + ("GROUP_NAME", "Group Name"), + ("GROUP_DESCRIPTION", "Group Description"), + ], + max_length=255, + ), + ), + migrations.AlterField( + model_name="user", + name="preferred_language", + field=models.CharField( + blank=True, + choices=[ + ("en-us", "English"), + ("de-de", "Deutsch (German)"), + ("es-es", "Español (Spanish)"), + ("fr-fr", "Français (French)"), + ("pt-br", "Português - Brasil (Brazilian Portuguese)"), + ("zh-hans", "简体中文 (Simplified Chinese)"), + ("zh-hant", "繁體中文 (Traditional Chinese)"), + ], + max_length=255, + null=True, + ), + ), + 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", + "GROUP_PRIVACY", + "GROUP_NAME", + "GROUP_DESCRIPTION", + ], + ) + ), + name="notification_type_valid", + ), + ), + ] From bdb6e4c911b25bdf81f9c0d2da5dd4ddc9046e66 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Oct 2021 21:15:48 +1100 Subject: [PATCH 184/193] fix template indenting whoops --- bookwyrm/templates/notifications/items/update.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/notifications/items/update.html b/bookwyrm/templates/notifications/items/update.html index 5c60e5a24..f963fd3a9 100644 --- a/bookwyrm/templates/notifications/items/update.html +++ b/bookwyrm/templates/notifications/items/update.html @@ -20,7 +20,7 @@ {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has changed the name of {{ group_name }} {% endblocktrans %} - {% else %} + {% else %} {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has changed the description of {{ group_name }} {% endblocktrans %} From f39ff96a6417aad37b19607eb1711e0aac108318 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 22 Oct 2021 10:25:33 -0700 Subject: [PATCH 185/193] Adds a few more tests to the suggested users module --- bookwyrm/tests/test_suggested_users.py | 95 ++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/bookwyrm/tests/test_suggested_users.py b/bookwyrm/tests/test_suggested_users.py index 6b794b5dc..f625ac10c 100644 --- a/bookwyrm/tests/test_suggested_users.py +++ b/bookwyrm/tests/test_suggested_users.py @@ -35,11 +35,18 @@ class SuggestedUsers(TestCase): rank = suggested_users.get_rank(annotated_user_mock) self.assertEqual(rank, 3) # 3.9642857142857144) - def test_store_id(self, *_): - """redis key generation""" + def test_store_id_from_obj(self, *_): + """redis key generation by user obj""" self.assertEqual( suggested_users.store_id(self.local_user), - "{:d}-suggestions".format(self.local_user.id), + f"{self.local_user.id}-suggestions", + ) + + def test_store_id_from_id(self, *_): + """redis key generation by user id""" + self.assertEqual( + suggested_users.store_id(self.local_user.id), + f"{self.local_user.id}-suggestions", ) def test_get_counts_from_rank(self, *_): @@ -69,21 +76,74 @@ class SuggestedUsers(TestCase): suggestable_user.followers.add(mutual_user) results = suggested_users.get_objects_for_store( - "{:d}-suggestions".format(self.local_user.id) + f"{self.local_user.id}-suggestions" ) self.assertEqual(results.count(), 1) match = results.first() self.assertEqual(match.id, suggestable_user.id) self.assertEqual(match.mutuals, 1) - def test_create_user_signal(self, *_): - """build suggestions for new users""" - with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") as mock: - models.User.objects.create_user( - "nutria", "nutria@nu.tria", "password", local=True, localname="nutria" - ) + def test_get_stores_for_object(self, *_): + """possible follows""" + mutual_user = models.User.objects.create_user( + "rat", "rat@local.rat", "password", local=True, localname="rat" + ) + suggestable_user = models.User.objects.create_user( + "nutria", + "nutria@nutria.nutria", + "password", + local=True, + localname="nutria", + discoverable=True, + ) - self.assertEqual(mock.call_count, 1) + # you follow rat + mutual_user.followers.add(self.local_user) + # rat follows the suggested user + suggestable_user.followers.add(mutual_user) + + results = suggested_users.get_stores_for_object(self.local_user) + self.assertEqual(len(results), 1) + self.assertEqual(results[0], f"{suggestable_user.id}-suggestions") + + def test_get_users_for_object(self, *_): + """given a user, who might want to follow them""" + mutual_user = models.User.objects.create_user( + "rat", "rat@local.rat", "password", local=True, localname="rat" + ) + suggestable_user = models.User.objects.create_user( + "nutria", + "nutria@nutria.nutria", + "password", + local=True, + localname="nutria", + discoverable=True, + ) + # you follow rat + mutual_user.followers.add(self.local_user) + # rat follows the suggested user + suggestable_user.followers.add(mutual_user) + + results = suggested_users.get_users_for_object(self.local_user) + self.assertEqual(len(results), 1) + self.assertEqual(results[0], suggestable_user) + + def test_rerank_user_suggestions(self, *_): + """does it call the populate store function correctly""" + with patch( + "bookwyrm.suggested_users.SuggestedUsers.populate_store" + ) as store_mock: + suggested_users.rerank_user_suggestions(self.local_user) + args = store_mock.call_args[0] + self.assertEqual(args[0], f"{self.local_user.id}-suggestions") + + def test_get_suggestions(self, *_): + """load from store""" + with patch("bookwyrm.suggested_users.SuggestedUsers.get_store") as mock: + mock.return_value = [(self.local_user.id, 7.9)] + results = suggested_users.get_suggestions(self.local_user) + self.assertEqual(results[0], self.local_user) + self.assertEqual(results[0].mutuals, 7) def test_get_annotated_users(self, *_): """list of people you might know""" @@ -144,8 +204,8 @@ class SuggestedUsers(TestCase): ) for i in range(3): user = models.User.objects.create_user( - "{:d}@local.com".format(i), - "{:d}@nutria.com".format(i), + f"{i}@local.com", + f"{i}@nutria.com", "password", local=True, localname=i, @@ -175,3 +235,12 @@ class SuggestedUsers(TestCase): ) user_1_annotated = result.get(id=user_1.id) self.assertEqual(user_1_annotated.mutuals, 3) + + def test_create_user_signal(self, *_): + """build suggestions for new users""" + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") as mock: + models.User.objects.create_user( + "nutria", "nutria@nu.tria", "password", local=True, localname="nutria" + ) + + self.assertEqual(mock.call_count, 1) From 230c6f679893a15c9d0f64ecde7835273ab6fe18 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 22 Oct 2021 10:46:56 -0700 Subject: [PATCH 186/193] Updates locales --- locale/de_DE/LC_MESSAGES/django.mo | Bin 57128 -> 57136 bytes locale/de_DE/LC_MESSAGES/django.po | 8 +- locale/es_ES/LC_MESSAGES/django.mo | Bin 52929 -> 57328 bytes locale/es_ES/LC_MESSAGES/django.po | 145 +++++++++--------------- locale/fr_FR/LC_MESSAGES/django.mo | Bin 49128 -> 52510 bytes locale/fr_FR/LC_MESSAGES/django.po | 160 ++++++++++----------------- locale/pt_BR/LC_MESSAGES/django.mo | Bin 56440 -> 56435 bytes locale/pt_BR/LC_MESSAGES/django.po | 14 +-- locale/zh_Hans/LC_MESSAGES/django.mo | Bin 50813 -> 44096 bytes locale/zh_Hant/LC_MESSAGES/django.mo | Bin 38249 -> 38249 bytes locale/zh_Hant/LC_MESSAGES/django.po | 84 ++++---------- 11 files changed, 149 insertions(+), 262 deletions(-) diff --git a/locale/de_DE/LC_MESSAGES/django.mo b/locale/de_DE/LC_MESSAGES/django.mo index cff3c02e0f08cfd668de4985bdba3f4c87608ffb..516685a0ec3dd82a26bd6b98ded4e934ba7e7bc8 100644 GIT binary patch delta 6191 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 K@?x)e`}`kww;i_t diff --git a/locale/de_DE/LC_MESSAGES/django.po b/locale/de_DE/LC_MESSAGES/django.po index 0d2861fb2..8fd930fee 100644 --- a/locale/de_DE/LC_MESSAGES/django.po +++ b/locale/de_DE/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: bookwyrm\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-15 22:03+0000\n" -"PO-Revision-Date: 2021-10-20 16:40\n" +"PO-Revision-Date: 2021-10-20 17:38\n" "Last-Translator: Mouse Reeve \n" "Language-Team: German\n" "Language: de\n" @@ -858,8 +858,8 @@ msgstr "Gesperrtes Benutzer*inkonto" #: bookwyrm/templates/directory/user_card.html:40 msgid "follower you follow" msgid_plural "followers you follow" -msgstr[0] "gegenseitige Follower*innen" -msgstr[1] "gegenseitige Follower*innen" +msgstr[0] "Follower*in, dem*der du folgst" +msgstr[1] "Follower*innen, denen du folgst" #: bookwyrm/templates/directory/user_card.html:47 msgid "book on your shelves" @@ -3546,7 +3546,7 @@ msgstr "Folgt %(counter)s" msgid "%(mutuals_display)s follower you follow" msgid_plural "%(mutuals_display)s followers you follow" msgstr[0] "%(mutuals_display)s Follower*in, der*die du folgst" -msgstr[1] "%(mutuals_display)s Follower*in, der*die du folgst" +msgstr[1] "%(mutuals_display)s Follower*innen, denen du folgst" #: bookwyrm/templates/user/user_preview.html:38 msgid "No followers you follow" diff --git a/locale/es_ES/LC_MESSAGES/django.mo b/locale/es_ES/LC_MESSAGES/django.mo index 35ccb7f0598696b8d7d23e944253fed54f02d6d4..7df75031089d13dd2d6ea34634e0f135c306dd86 100644 GIT binary patch delta 19206 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 S|JCy^AJ_lim<7k59s57^$Y+@V delta 16073 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 k5Sx@38s9fQZgA33Z~uh2M9*K>7I-(_=\n" "Language-Team: Spanish\n" "Language: es\n" @@ -187,7 +187,7 @@ msgstr "Français (Francés)" #: bookwyrm/settings.py:169 msgid "Português - Brasil (Brazilian Portuguese)" -msgstr "" +msgstr "Português - Brasil (Portugués Brasileño)" #: bookwyrm/settings.py:170 msgid "简体中文 (Simplified Chinese)" @@ -225,7 +225,7 @@ msgstr "Editar Autor/Autora" #: bookwyrm/templates/author/author.html:34 #: bookwyrm/templates/author/edit_author.html:41 msgid "Aliases:" -msgstr "" +msgstr "Alias:" #: bookwyrm/templates/author/author.html:45 msgid "Born:" @@ -237,7 +237,7 @@ msgstr "Muerto:" #: bookwyrm/templates/author/author.html:61 msgid "Wikipedia" -msgstr "" +msgstr "Wikipedia" #: bookwyrm/templates/author/author.html:69 #: bookwyrm/templates/book/book.html:94 @@ -300,7 +300,7 @@ msgstr "Separar varios valores con comas." #: bookwyrm/templates/author/edit_author.html:50 msgid "Bio:" -msgstr "" +msgstr "Biografía:" #: bookwyrm/templates/author/edit_author.html:57 msgid "Wikipedia link:" @@ -488,7 +488,7 @@ msgstr "Número OCLC:" #: bookwyrm/templates/book/book_identifiers.html:22 #: bookwyrm/templates/book/edit/edit_book_form.html:240 msgid "ASIN:" -msgstr "" +msgstr "ASIN:" #: bookwyrm/templates/book/cover_modal.html:17 #: bookwyrm/templates/book/edit/edit_book_form.html:143 @@ -575,7 +575,7 @@ msgstr "Idiomas:" #: bookwyrm/templates/book/edit/edit_book_form.html:74 msgid "Publication" -msgstr "" +msgstr "Publicación" #: bookwyrm/templates/book/edit/edit_book_form.html:77 msgid "Publisher:" @@ -639,11 +639,11 @@ msgstr "Identificadores de libro" #: bookwyrm/templates/book/edit/edit_book_form.html:200 msgid "ISBN 13:" -msgstr "" +msgstr "ISBN 13:" #: bookwyrm/templates/book/edit/edit_book_form.html:208 msgid "ISBN 10:" -msgstr "" +msgstr "ISBN 10:" #: bookwyrm/templates/book/edit/edit_book_form.html:216 msgid "Openlibrary ID:" @@ -752,10 +752,8 @@ msgid "Help" msgstr "Ayuda" #: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 -#, fuzzy -#| msgid "View status" msgid "Edit status" -msgstr "Ver status" +msgstr "Editar estado" #: bookwyrm/templates/confirm_email/confirm_email.html:4 msgid "Confirm email" @@ -890,28 +888,24 @@ msgid "All known users" msgstr "Todos los usuarios conocidos" #: bookwyrm/templates/discover/card-header.html:9 -#, fuzzy, python-format -#| msgid "%(username)s wants to read %(book)s" +#, python-format msgid "%(username)s rated %(book_title)s" -msgstr "%(username)s quiere leer %(book)s" +msgstr "%(username)s calificó %(book_title)s" #: bookwyrm/templates/discover/card-header.html:13 -#, fuzzy, python-format -#| msgid "%(username)s wants to read %(book)s" +#, python-format msgid "%(username)s reviewed %(book_title)s" -msgstr "%(username)s quiere leer %(book)s" +msgstr "%(username)s reseñó %(book_title)s" #: bookwyrm/templates/discover/card-header.html:17 -#, fuzzy, python-format -#| msgid "%(username)s wants to read %(book)s" +#, python-format msgid "%(username)s commented on %(book_title)s" -msgstr "%(username)s quiere leer %(book)s" +msgstr "%(username)s comentó en %(book_title)s" #: bookwyrm/templates/discover/card-header.html:21 -#, fuzzy, python-format -#| msgid "%(username)s wants to read %(book)s" +#, python-format msgid "%(username)s quoted %(book_title)s" -msgstr "%(username)s quiere leer %(book)s" +msgstr "%(username)s citó %(book_title)s" #: bookwyrm/templates/discover/discover.html:4 #: bookwyrm/templates/discover/discover.html:10 @@ -978,10 +972,9 @@ msgid "Join Now" msgstr "Únete ahora" #: bookwyrm/templates/email/invite/html_content.html:15 -#, fuzzy, python-format -#| msgid "Learn more about this instance." +#, python-format msgid "Learn more about %(site_name)s." -msgstr "Aprenda más sobre esta instancia." +msgstr "Más información sobre %(site_name)s." #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format @@ -989,10 +982,9 @@ msgid "You're invited to join %(site_name)s! Click the link below to create an a msgstr "Estás invitado a unirte con %(site_name)s! Haz clic en el enlace a continuación para crear una cuenta." #: bookwyrm/templates/email/invite/text_content.html:8 -#, fuzzy, python-format -#| msgid "Learn more about this instance:" +#, python-format msgid "Learn more about %(site_name)s:" -msgstr "Aprende más sobre esta intancia:" +msgstr "Más información sobre %(site_name)s:" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 @@ -1206,7 +1198,7 @@ msgstr "Un poco sobre ti" #: bookwyrm/templates/get_started/profile.html:32 #: bookwyrm/templates/preferences/edit_user.html:27 msgid "Avatar:" -msgstr "" +msgstr "Avatar:" #: bookwyrm/templates/get_started/profile.html:42 #: bookwyrm/templates/preferences/edit_user.html:110 @@ -1346,10 +1338,8 @@ msgid "Imported" msgstr "Importado" #: bookwyrm/templates/import/tooltip.html:6 -#, fuzzy -#| msgid "You can download your GoodReads data from the Import/Export page of your GoodReads account." msgid "You can download your Goodreads data from the Import/Export page of your Goodreads account." -msgstr "Puedes descargar tus datos de GoodReads de la Página de Exportación/Importación de tu cuenta de GoodReads." +msgstr "Puede descargar sus datos de Goodreads desde la página de Importación/Exportación de su cuenta de Goodreads." #: bookwyrm/templates/invite.html:4 bookwyrm/templates/invite.html:8 #: bookwyrm/templates/login.html:49 @@ -1452,7 +1442,7 @@ msgstr "Invitaciones" #: bookwyrm/templates/layout.html:132 msgid "Admin" -msgstr "" +msgstr "Administrador" #: bookwyrm/templates/layout.html:139 msgid "Log out" @@ -1600,7 +1590,7 @@ msgstr "Cualquier usuario puede sugerir libros, en cuanto lo hayas aprobado" #: bookwyrm/templates/lists/form.html:31 msgctxt "curation type" msgid "Open" -msgstr "" +msgstr "Abrir" #: bookwyrm/templates/lists/form.html:32 msgid "Anyone can add books to this list" @@ -1710,12 +1700,12 @@ msgstr "Más sobre este sitio" #: bookwyrm/templates/notifications/items/add.html:24 #, python-format msgid "added %(book_title)s to your list \"%(list_name)s\"" -msgstr "" +msgstr "agregó %(book_title)s a su lista «%(list_name)s»" #: bookwyrm/templates/notifications/items/add.html:31 #, python-format msgid "suggested adding %(book_title)s to your list \"%(list_name)s\"" -msgstr "" +msgstr "sugirió agregar %(book_title)s a su lista «%(list_name)s»" #: bookwyrm/templates/notifications/items/boost.html:19 #, python-format @@ -1738,28 +1728,24 @@ msgid "boosted your status" msgstr "respaldó tu status" #: bookwyrm/templates/notifications/items/fav.html:19 -#, fuzzy, python-format -#| msgid "favorited your review of %(book_title)s" +#, python-format msgid "liked your review of %(book_title)s" msgstr "le gustó tu reseña de %(book_title)s" #: bookwyrm/templates/notifications/items/fav.html:25 -#, fuzzy, python-format -#| msgid "boosted your comment on%(book_title)s" +#, python-format msgid "liked your comment on%(book_title)s" -msgstr "respaldó tu comentario en%(book_title)s" +msgstr "le gustó tu comentario sobre %(book_title)s" #: bookwyrm/templates/notifications/items/fav.html:31 -#, fuzzy, python-format -#| msgid "favorited your quote from %(book_title)s" +#, python-format msgid "liked your quote from %(book_title)s" msgstr "le gustó tu cita de %(book_title)s" #: bookwyrm/templates/notifications/items/fav.html:37 -#, fuzzy, python-format -#| msgid "favorited your status" +#, python-format msgid "liked your status" -msgstr "le gustó tu status" +msgstr "le gustó tu estado" #: bookwyrm/templates/notifications/items/follow.html:15 msgid "followed you" @@ -2014,7 +2000,7 @@ msgstr "Editar anuncio" #: bookwyrm/templates/settings/announcements/announcement.html:35 msgid "Visible:" -msgstr "" +msgstr "Visible:" #: bookwyrm/templates/settings/announcements/announcement.html:38 msgid "True" @@ -2088,7 +2074,7 @@ msgstr "Fecha final" #: bookwyrm/templates/settings/users/user_admin.html:34 #: bookwyrm/templates/settings/users/user_info.html:20 msgid "Status" -msgstr "" +msgstr "Estado" #: bookwyrm/templates/settings/announcements/announcements.html:48 msgid "active" @@ -2120,7 +2106,7 @@ msgstr "Activos este mes" #: bookwyrm/templates/settings/dashboard/dashboard.html:27 msgid "Statuses" -msgstr "" +msgstr "Estados" #: bookwyrm/templates/settings/dashboard/dashboard.html:33 #: bookwyrm/templates/settings/dashboard/works_chart.html:11 @@ -2167,11 +2153,11 @@ msgstr "Actividad de status" #: bookwyrm/templates/settings/dashboard/dashboard.html:118 msgid "Works created" -msgstr "" +msgstr "Obras creadas" #: bookwyrm/templates/settings/dashboard/registration_chart.html:10 msgid "Registrations" -msgstr "" +msgstr "Inscripciones" #: bookwyrm/templates/settings/dashboard/status_chart.html:11 msgid "Statuses posted" @@ -2248,13 +2234,13 @@ msgstr "Instancia:" #: bookwyrm/templates/settings/federation/instance.html:28 #: bookwyrm/templates/settings/users/user_info.html:106 msgid "Status:" -msgstr "" +msgstr "Estado:" #: bookwyrm/templates/settings/federation/edit_instance.html:52 #: bookwyrm/templates/settings/federation/instance.html:22 #: bookwyrm/templates/settings/users/user_info.html:100 msgid "Software:" -msgstr "" +msgstr "Software:" #: bookwyrm/templates/settings/federation/edit_instance.html:61 #: bookwyrm/templates/settings/federation/instance.html:25 @@ -2368,7 +2354,7 @@ msgstr "Nombre de instancia" #: bookwyrm/templates/settings/federation/instance_list.html:40 msgid "Software" -msgstr "" +msgstr "Software" #: bookwyrm/templates/settings/federation/instance_list.html:63 msgid "No instances found" @@ -2647,10 +2633,8 @@ msgid "Short description:" msgstr "Descripción corta:" #: bookwyrm/templates/settings/site.html:37 -#, fuzzy -#| msgid "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." msgid "Used when the instance is previewed on joinbookwyrm.com. Does not support HTML or Markdown." -msgstr "Utilizado cuando la instancia se ve de una vista previa en joinbookwyrm.com. No es compatible con html o markdown." +msgstr "Se utiliza cuando se obtiene una vista previa de la instancia en joinbookwyrm.com. No es compatible con HTML ni Markdown." #: bookwyrm/templates/settings/site.html:41 msgid "Code of conduct:" @@ -2662,7 +2646,7 @@ msgstr "Política de privacidad:" #: bookwyrm/templates/settings/site.html:57 msgid "Logo:" -msgstr "" +msgstr "Logo:" #: bookwyrm/templates/settings/site.html:61 msgid "Logo small:" @@ -2670,7 +2654,7 @@ msgstr "Logo pequeño:" #: bookwyrm/templates/settings/site.html:65 msgid "Favicon:" -msgstr "" +msgstr "Favicon:" #: bookwyrm/templates/settings/site.html:77 msgid "Support link:" @@ -2773,7 +2757,7 @@ msgstr "Ver perfil de usuario" #: bookwyrm/templates/settings/users/user_info.html:36 msgid "Local" -msgstr "" +msgstr "Local" #: bookwyrm/templates/settings/users/user_info.html:38 msgid "Remote" @@ -2861,8 +2845,8 @@ msgstr "Crear estante" #, python-format msgid "%(formatted_count)s book" msgid_plural "%(formatted_count)s books" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(formatted_count)s libro" +msgstr[1] "%(formatted_count)s libros" #: bookwyrm/templates/shelf/shelf.html:84 #, python-format @@ -2905,8 +2889,8 @@ msgstr "Publicado por %(username)s" #, python-format msgid "and %(remainder_count_display)s other" msgid_plural "and %(remainder_count_display)s others" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "y %(remainder_count_display)s otro" +msgstr[1] "y %(remainder_count_display)s otros" #: bookwyrm/templates/snippets/book_cover.html:61 msgid "No cover" @@ -3332,7 +3316,7 @@ msgstr "(Página %(page)s)" #: bookwyrm/templates/snippets/status/content_status.html:103 #, python-format msgid "(%(percent)s%%)" -msgstr "" +msgstr "(%(percent)s%%)" #: bookwyrm/templates/snippets/status/content_status.html:125 msgid "Open image in new window" @@ -3343,10 +3327,9 @@ msgid "Hide status" msgstr "Ocultar status" #: bookwyrm/templates/snippets/status/header.html:45 -#, fuzzy, python-format -#| msgid "Joined %(date)s" +#, python-format msgid "edited %(date)s" -msgstr "Unido %(date)s" +msgstr "editado %(date)s" #: bookwyrm/templates/snippets/status/headers/comment.html:2 #, python-format @@ -3576,7 +3559,7 @@ msgstr "Archivo excede el tamaño máximo: 10MB" #: bookwyrm/templatetags/utilities.py:31 #, python-format msgid "%(title)s: %(subtitle)s" -msgstr "" +msgstr "%(title)s: %(subtitle)s" #: bookwyrm/views/import_data.py:67 msgid "Not a valid csv file" @@ -3600,23 +3583,3 @@ msgstr "Un enlace para reestablecer tu contraseña se envió a {email}" msgid "Status updates from {obj.display_name}" msgstr "Actualizaciones de status de {obj.display_name}" -#~ msgid "Compose status" -#~ msgstr "Componer status" - -#~ msgid "rated" -#~ msgstr "calificó" - -#~ msgid "reviewed" -#~ msgstr "reseñó" - -#~ msgid "commented on" -#~ msgstr "comentó en" - -#~ msgid "quoted" -#~ msgstr "citó" - -#~ msgid "About this instance" -#~ msgstr "Sobre esta instancia" - -#~ msgid "Delete & re-draft" -#~ msgstr "Eliminar y recomponer" diff --git a/locale/fr_FR/LC_MESSAGES/django.mo b/locale/fr_FR/LC_MESSAGES/django.mo index a1fbfd2cb5de46916399e1041b265435163bc70b..5e8dfa6646b38fbed1bbc9159b926c2d9839f176 100644 GIT binary patch delta 17666 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 MEB-yu^vJ9K1)pp=kpKVy delta 14847 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\n" "Language-Team: French\n" "Language: fr\n" @@ -752,10 +752,8 @@ msgid "Help" msgstr "Aide" #: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 -#, fuzzy -#| msgid "View status" msgid "Edit status" -msgstr "Afficher tous les status" +msgstr "" #: bookwyrm/templates/confirm_email/confirm_email.html:4 msgid "Confirm email" @@ -890,28 +888,24 @@ msgid "All known users" msgstr "Tous les comptes connus" #: bookwyrm/templates/discover/card-header.html:9 -#, fuzzy, python-format -#| msgid "replied to %(username)s's status" +#, python-format msgid "%(username)s rated %(book_title)s" -msgstr "a répondu au statut de %(username)s" +msgstr "" #: bookwyrm/templates/discover/card-header.html:13 -#, fuzzy, python-format -#| msgid "replied to %(username)s's status" +#, python-format msgid "%(username)s reviewed %(book_title)s" -msgstr "a répondu au statut de %(username)s" +msgstr "" #: bookwyrm/templates/discover/card-header.html:17 -#, fuzzy, python-format -#| msgid "replied to %(username)s's status" +#, python-format msgid "%(username)s commented on %(book_title)s" -msgstr "a répondu au statut de %(username)s" +msgstr "" #: bookwyrm/templates/discover/card-header.html:21 -#, fuzzy, python-format -#| msgid "replied to %(username)s's status" +#, python-format msgid "%(username)s quoted %(book_title)s" -msgstr "a répondu au statut de %(username)s" +msgstr "" #: bookwyrm/templates/discover/discover.html:4 #: bookwyrm/templates/discover/discover.html:10 @@ -978,10 +972,9 @@ msgid "Join Now" msgstr "S’enregistrer maintenant" #: bookwyrm/templates/email/invite/html_content.html:15 -#, fuzzy, python-format -#| msgid "Learn more about this instance." +#, python-format msgid "Learn more about %(site_name)s." -msgstr "En savoir plus sur cette instance." +msgstr "" #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format @@ -989,10 +982,9 @@ msgid "You're invited to join %(site_name)s! Click the link below to create an a msgstr "Vous avez reçu une invitation à rejoindre %(site_name)s ! Cliquez le lien suivant pour créer un compte." #: bookwyrm/templates/email/invite/text_content.html:8 -#, fuzzy, python-format -#| msgid "Learn more about this instance:" +#, python-format msgid "Learn more about %(site_name)s:" -msgstr "En savoir plus sur cete instance :" +msgstr "" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 @@ -1346,10 +1338,8 @@ msgid "Imported" msgstr "Importé" #: bookwyrm/templates/import/tooltip.html:6 -#, fuzzy -#| msgid "You can download your GoodReads data from the Import/Export page of your GoodReads account." msgid "You can download your Goodreads data from the Import/Export page of your Goodreads account." -msgstr "Vous pouvez télécharger vos données GoodReads depuis la page Importation/Exportation de votre compte GoodRead." +msgstr "" #: bookwyrm/templates/invite.html:4 bookwyrm/templates/invite.html:8 #: bookwyrm/templates/login.html:49 @@ -1738,28 +1728,24 @@ msgid "boosted your status" msgstr "a partagé votre statut" #: bookwyrm/templates/notifications/items/fav.html:19 -#, fuzzy, python-format -#| msgid "favorited your review of %(book_title)s" +#, python-format msgid "liked your review of %(book_title)s" -msgstr "a ajouté votre critique de %(book_title)s à ses favoris" +msgstr "" #: bookwyrm/templates/notifications/items/fav.html:25 -#, fuzzy, python-format -#| msgid "boosted your comment on%(book_title)s" +#, python-format msgid "liked your comment on%(book_title)s" -msgstr "a partagé votre commentaire sur %(book_title)s" +msgstr "" #: bookwyrm/templates/notifications/items/fav.html:31 -#, fuzzy, python-format -#| msgid "favorited your quote from %(book_title)s" +#, python-format msgid "liked your quote from %(book_title)s" -msgstr "a ajouté votre citation de %(book_title)s à ses favoris" +msgstr "" #: bookwyrm/templates/notifications/items/fav.html:37 -#, fuzzy, python-format -#| msgid "favorited your status" +#, python-format msgid "liked your status" -msgstr "a ajouté votre statut à ses favoris" +msgstr "" #: bookwyrm/templates/notifications/items/follow.html:15 msgid "followed you" @@ -1914,7 +1900,7 @@ msgstr "" #: bookwyrm/templates/preferences/edit_user.html:76 msgid "Show suggested users:" -msgstr "" +msgstr "Afficher les utilisateurs suggérés :" #: bookwyrm/templates/preferences/edit_user.html:85 #, python-format @@ -1927,7 +1913,7 @@ msgstr "Fuseau horaire préféré" #: bookwyrm/templates/preferences/edit_user.html:116 msgid "Default post privacy:" -msgstr "" +msgstr "Niveau de confidentialité des messages par défaut :" #: bookwyrm/templates/preferences/layout.html:11 msgid "Account" @@ -1940,12 +1926,12 @@ msgstr "Relations" #: bookwyrm/templates/reading_progress/finish.html:5 #, python-format msgid "Finish \"%(book_title)s\"" -msgstr "" +msgstr "Terminer \"%(book_title)s\"" #: bookwyrm/templates/reading_progress/start.html:5 #, python-format msgid "Start \"%(book_title)s\"" -msgstr "" +msgstr "Commencer \"%(book_title)s\"" #: bookwyrm/templates/reading_progress/want.html:5 #, python-format @@ -2048,11 +2034,11 @@ msgstr "Ajouter une annonce" #: bookwyrm/templates/settings/announcements/announcement_form.html:16 msgid "Preview:" -msgstr "" +msgstr "Aperçu :" #: bookwyrm/templates/settings/announcements/announcement_form.html:23 msgid "Content:" -msgstr "" +msgstr "Contenu :" #: bookwyrm/templates/settings/announcements/announcement_form.html:30 msgid "Event date:" @@ -2151,11 +2137,11 @@ msgstr "" #: bookwyrm/templates/settings/dashboard/dashboard.html:87 msgid "Days" -msgstr "" +msgstr "Jours" #: bookwyrm/templates/settings/dashboard/dashboard.html:88 msgid "Weeks" -msgstr "" +msgstr "Semaines" #: bookwyrm/templates/settings/dashboard/dashboard.html:106 msgid "User signup activity" @@ -2171,7 +2157,7 @@ msgstr "" #: bookwyrm/templates/settings/dashboard/registration_chart.html:10 msgid "Registrations" -msgstr "" +msgstr "Inscriptions" #: bookwyrm/templates/settings/dashboard/status_chart.html:11 msgid "Statuses posted" @@ -2198,7 +2184,7 @@ msgstr "" #: bookwyrm/templates/settings/email_blocklist/email_blocklist.html:18 msgid "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." -msgstr "" +msgstr "Quand quelqu'un essaiera de s'inscrire avec un e-mail de ce domaine, aucun compte ne sera créé. Le processus d'inscription semblera avoir fonctionné." #: bookwyrm/templates/settings/email_blocklist/email_blocklist.html:25 msgid "Domain" @@ -2207,14 +2193,14 @@ msgstr "" #: bookwyrm/templates/settings/email_blocklist/email_blocklist.html:29 #: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:27 msgid "Options" -msgstr "" +msgstr "Options" #: bookwyrm/templates/settings/email_blocklist/email_blocklist.html:38 #, python-format msgid "%(display_count)s user" msgid_plural "%(display_count)s users" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(display_count)s utilisateur" +msgstr[1] "%(display_count)s utilisateurs" #: bookwyrm/templates/settings/email_blocklist/email_blocklist.html:59 msgid "No email domains currently blocked" @@ -2372,7 +2358,7 @@ msgstr "Logiciel" #: bookwyrm/templates/settings/federation/instance_list.html:63 msgid "No instances found" -msgstr "" +msgstr "Aucune instance trouvée" #: bookwyrm/templates/settings/invites/manage_invite_requests.html:4 #: bookwyrm/templates/settings/invites/manage_invite_requests.html:11 @@ -2483,33 +2469,33 @@ msgstr "Aucune invitation active" #: bookwyrm/templates/settings/ip_blocklist/ip_address_form.html:5 #: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:10 msgid "Add IP address" -msgstr "" +msgstr "Ajouter une adresse IP" #: bookwyrm/templates/settings/ip_blocklist/ip_address_form.html:11 msgid "Use IP address blocks with caution, and consider using blocks only temporarily, as IP addresses are often shared or change hands. If you block your own IP, you will not be able to access this page." -msgstr "" +msgstr "Bloquez des adresses IP avec précaution, voire de façon temporaire, car les adresses IP sont souvent partagées, ou changent de main. Si vous bloquez votre propre adresse IP, vous ne pourrez plus accéder à cette page." #: bookwyrm/templates/settings/ip_blocklist/ip_address_form.html:18 msgid "IP Address:" -msgstr "" +msgstr "Adresse IP :" #: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:5 #: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:7 #: bookwyrm/templates/settings/layout.html:63 msgid "IP Address Blocklist" -msgstr "" +msgstr "Liste des adresses IP bloquées" #: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:18 msgid "Any traffic from this IP address will get a 404 response when trying to access any part of the application." -msgstr "" +msgstr "Tout trafic provenant de cette adresse IP obtiendra une réponse 404 en essayant d'accéder à n'importe quelle partie de l'application." #: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:24 msgid "Address" -msgstr "" +msgstr "Adresse" #: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:46 msgid "No IP addresses currently blocked" -msgstr "" +msgstr "Aucune adresse IP n'est actuellement bloquée" #: bookwyrm/templates/settings/ip_blocklist/ip_tooltip.html:6 msgid "You can block IP ranges using CIDR syntax." @@ -2647,10 +2633,8 @@ msgid "Short description:" msgstr "Description courte :" #: bookwyrm/templates/settings/site.html:37 -#, fuzzy -#| msgid "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." msgid "Used when the instance is previewed on joinbookwyrm.com. Does not support HTML or Markdown." -msgstr "Utilisé lorsque l'instance est prévisualisée sur joinbookwyrm.com. Ne supporte pas html ou markdown." +msgstr "" #: bookwyrm/templates/settings/site.html:41 msgid "Code of conduct:" @@ -2785,7 +2769,7 @@ msgstr "Détails du compte" #: bookwyrm/templates/settings/users/user_info.html:51 msgid "Email:" -msgstr "Email:" +msgstr "Email :" #: bookwyrm/templates/settings/users/user_info.html:61 msgid "(View reports)" @@ -2905,8 +2889,8 @@ msgstr "Publiée par %(username)s" #, python-format msgid "and %(remainder_count_display)s other" msgid_plural "and %(remainder_count_display)s others" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "et %(remainder_count_display)s autre" +msgstr[1] "et %(remainder_count_display)s autres" #: bookwyrm/templates/snippets/book_cover.html:61 msgid "No cover" @@ -3343,15 +3327,14 @@ msgid "Hide status" msgstr "" #: bookwyrm/templates/snippets/status/header.html:45 -#, fuzzy, python-format -#| msgid "Joined %(date)s" +#, python-format msgid "edited %(date)s" -msgstr "A rejoint ce serveur %(date)s" +msgstr "" #: bookwyrm/templates/snippets/status/headers/comment.html:2 #, python-format msgid "commented on %(book)s" -msgstr "" +msgstr "a commenté %(book)s" #: bookwyrm/templates/snippets/status/headers/note.html:15 #, python-format @@ -3361,32 +3344,32 @@ msgstr "a répondu au statut de %(book)s" -msgstr "" +msgstr "a cité un passage de %(book)s" #: bookwyrm/templates/snippets/status/headers/rating.html:3 #, python-format msgid "rated %(book)s:" -msgstr "" +msgstr "a noté %(book)s :" #: bookwyrm/templates/snippets/status/headers/read.html:7 #, python-format msgid "finished reading %(book)s" -msgstr "" +msgstr "a terminé %(book)s" #: bookwyrm/templates/snippets/status/headers/reading.html:7 #, python-format msgid "started reading %(book)s" -msgstr "" +msgstr "a commencé %(book)s" #: bookwyrm/templates/snippets/status/headers/review.html:3 #, python-format msgid "reviewed %(book)s" -msgstr "" +msgstr "a critiqué %(book)s" #: bookwyrm/templates/snippets/status/headers/to_read.html:7 #, python-format msgid "%(username)s wants to read %(book)s" -msgstr "" +msgstr "%(username)s veut lire %(book)s" #: bookwyrm/templates/snippets/status/layout.html:24 #: bookwyrm/templates/snippets/status/status_options.html:17 @@ -3429,7 +3412,7 @@ msgstr[1] "%(shared_books)s livres sur vos étagères" #: bookwyrm/templates/snippets/suggested_users.html:31 #: bookwyrm/templates/user/user_preview.html:36 msgid "Follows you" -msgstr "" +msgstr "Vous suit" #: bookwyrm/templates/snippets/switch_edition_button.html:5 msgid "Switch to this edition" @@ -3567,7 +3550,7 @@ msgstr[1] "%(mutuals_display)s abonné(e)s que vous suivez" #: bookwyrm/templates/user/user_preview.html:38 msgid "No followers you follow" -msgstr "" +msgstr "Aucun·e abonné·e que vous suivez" #: bookwyrm/templates/widgets/clearable_file_input_with_warning.html:28 msgid "File exceeds maximum size: 10MB" @@ -3584,7 +3567,7 @@ msgstr "Fichier CSV non valide" #: bookwyrm/views/login.py:69 msgid "Username or password are incorrect" -msgstr "" +msgstr "Identifiant ou mot de passe incorrect" #: bookwyrm/views/password.py:32 msgid "No user with that email address was found." @@ -3600,26 +3583,3 @@ msgstr "Un lien de réinitialisation a été envoyé à {email}." msgid "Status updates from {obj.display_name}" msgstr "" -#~ msgid "Compose status" -#~ msgstr "Rédiger un statut" - -#~ msgid "%(format)s" -#~ msgstr "%(format)s" - -#~ msgid "rated" -#~ msgstr "a noté" - -#~ msgid "reviewed" -#~ msgstr "a écrit une critique de" - -#~ msgid "commented on" -#~ msgstr "a commenté" - -#~ msgid "quoted" -#~ msgstr "a cité" - -#~ msgid "About this instance" -#~ msgstr "À propos de cette instance" - -#~ msgid "Delete & re-draft" -#~ msgstr "Supprimer & recommencer la rédaction" diff --git a/locale/pt_BR/LC_MESSAGES/django.mo b/locale/pt_BR/LC_MESSAGES/django.mo index 8dc869921a8dbc5749de6859d2bb30bf5dbdcac9..1ebe5280048618f0a5075466d95c52dd815070ec 100644 GIT binary patch delta 3378 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 diff --git a/locale/pt_BR/LC_MESSAGES/django.po b/locale/pt_BR/LC_MESSAGES/django.po index c02168e6f..2e729a2f2 100644 --- a/locale/pt_BR/LC_MESSAGES/django.po +++ b/locale/pt_BR/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: bookwyrm\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-15 22:03+0000\n" -"PO-Revision-Date: 2021-10-16 15:36\n" +"PO-Revision-Date: 2021-10-22 13:31\n" "Last-Translator: Mouse Reeve \n" "Language-Team: Portuguese, Brazilian\n" "Language: pt\n" @@ -23,15 +23,15 @@ msgstr "Já existe um usuário com este endereço de e-mail." #: bookwyrm/forms.py:256 msgid "One Day" -msgstr "Um Dia" +msgstr "Um dia" #: bookwyrm/forms.py:257 msgid "One Week" -msgstr "Uma Semana" +msgstr "Uma semana" #: bookwyrm/forms.py:258 msgid "One Month" -msgstr "Um Mês" +msgstr "Um mês" #: bookwyrm/forms.py:259 msgid "Does Not Expire" @@ -48,7 +48,7 @@ msgstr "Ilimitado" #: bookwyrm/forms.py:326 msgid "List Order" -msgstr "Ordem da lista" +msgstr "Ordem de inserção" #: bookwyrm/forms.py:327 msgid "Book Title" @@ -1189,7 +1189,7 @@ msgstr "Nome de exibição:" #: bookwyrm/templates/get_started/profile.html:22 #: bookwyrm/templates/preferences/edit_user.html:49 msgid "Summary:" -msgstr "Descrição:" +msgstr "Bio:" #: bookwyrm/templates/get_started/profile.html:23 msgid "A little bit about you" @@ -1496,7 +1496,7 @@ msgstr "Documentação" #: bookwyrm/templates/layout.html:245 #, python-format msgid "Support %(site_name)s on %(support_title)s" -msgstr "Apoie a instância %(site_name)s em %(support_title)s" +msgstr "Apoie a instância %(site_name)s: %(support_title)s" #: bookwyrm/templates/layout.html:249 msgid "BookWyrm's source code is freely available. You can contribute or report issues on GitHub." diff --git a/locale/zh_Hans/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo index e245fa536cb6e94cf153492b07f0e101ed301a8d..1d1227f8092b70c68bb692fb532759090142aa83 100644 GIT binary patch delta 15805 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 {wmB<%4)xG2Lrv(s@v`wt97lN` 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 HFaQ4lBmQE# diff --git a/locale/zh_Hant/LC_MESSAGES/django.mo b/locale/zh_Hant/LC_MESSAGES/django.mo index 9cfc176eb2f8441c795fd9488c814b2bea6351df..0bd0ad46e02da915244e3efe4ba13fe687a55bad 100644 GIT binary patch delta 23 fcmaF4is|JlrVTHWI1J4c3{9+z%{G5Xsx$%sdqoLs delta 23 fcmaF4is|JlrVTHWI1DTl3=FIcj5mKssx$%sdix1t diff --git a/locale/zh_Hant/LC_MESSAGES/django.po b/locale/zh_Hant/LC_MESSAGES/django.po index d95f0655a..7b9d3b778 100644 --- a/locale/zh_Hant/LC_MESSAGES/django.po +++ b/locale/zh_Hant/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: bookwyrm\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-15 22:03+0000\n" -"PO-Revision-Date: 2021-10-08 00:03\n" +"PO-Revision-Date: 2021-10-16 14:36\n" "Last-Translator: Mouse Reeve \n" "Language-Team: Chinese Traditional\n" "Language: zh\n" @@ -751,10 +751,8 @@ msgid "Help" msgstr "" #: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 -#, fuzzy -#| msgid "Like status" msgid "Edit status" -msgstr "喜歡狀態" +msgstr "" #: bookwyrm/templates/confirm_email/confirm_email.html:4 msgid "Confirm email" @@ -887,28 +885,24 @@ msgid "All known users" msgstr "所有已知使用者" #: bookwyrm/templates/discover/card-header.html:9 -#, fuzzy, python-format -#| msgid "replied to %(username)s's status" +#, python-format msgid "%(username)s rated %(book_title)s" -msgstr "回覆了 %(username)s狀態" +msgstr "" #: bookwyrm/templates/discover/card-header.html:13 -#, fuzzy, python-format -#| msgid "replied to %(username)s's status" +#, python-format msgid "%(username)s reviewed %(book_title)s" -msgstr "回覆了 %(username)s狀態" +msgstr "" #: bookwyrm/templates/discover/card-header.html:17 -#, fuzzy, python-format -#| msgid "replied to %(username)s's status" +#, python-format msgid "%(username)s commented on %(book_title)s" -msgstr "回覆了 %(username)s狀態" +msgstr "" #: bookwyrm/templates/discover/card-header.html:21 -#, fuzzy, python-format -#| msgid "replied to %(username)s's status" +#, python-format msgid "%(username)s quoted %(book_title)s" -msgstr "回覆了 %(username)s狀態" +msgstr "" #: bookwyrm/templates/discover/discover.html:4 #: bookwyrm/templates/discover/discover.html:10 @@ -975,10 +969,9 @@ msgid "Join Now" msgstr "立即加入" #: bookwyrm/templates/email/invite/html_content.html:15 -#, fuzzy, python-format -#| msgid "Learn more about this instance." +#, python-format msgid "Learn more about %(site_name)s." -msgstr "瞭解更多 有關本實例的資訊。" +msgstr "" #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format @@ -986,10 +979,9 @@ msgid "You're invited to join %(site_name)s! Click the link below to create an a msgstr "你受邀請加入 %(site_name)s!點選下面的連結來建立帳號。" #: bookwyrm/templates/email/invite/text_content.html:8 -#, fuzzy, python-format -#| msgid "Learn more about this instance:" +#, python-format msgid "Learn more about %(site_name)s:" -msgstr "瞭解更多有關本實例的資訊:" +msgstr "" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 @@ -1733,28 +1725,24 @@ msgid "boosted your status" msgstr "轉發了你的 狀態" #: bookwyrm/templates/notifications/items/fav.html:19 -#, fuzzy, python-format -#| msgid "favorited your review of %(book_title)s" +#, python-format msgid "liked your review of %(book_title)s" -msgstr "喜歡了你 %(book_title)s 的書評" +msgstr "" #: bookwyrm/templates/notifications/items/fav.html:25 -#, fuzzy, python-format -#| msgid "boosted your comment on%(book_title)s" +#, python-format msgid "liked your comment on%(book_title)s" -msgstr "轉發了你的 %(book_title)s 的評論" +msgstr "" #: bookwyrm/templates/notifications/items/fav.html:31 -#, fuzzy, python-format -#| msgid "favorited your quote from %(book_title)s" +#, python-format msgid "liked your quote from %(book_title)s" -msgstr "喜歡了你 來自 %(book_title)s 的引用" +msgstr "" #: bookwyrm/templates/notifications/items/fav.html:37 -#, fuzzy, python-format -#| msgid "favorited your status" +#, python-format msgid "liked your status" -msgstr "喜歡了你的 狀態" +msgstr "" #: bookwyrm/templates/notifications/items/follow.html:15 msgid "followed you" @@ -3326,10 +3314,9 @@ msgid "Hide status" msgstr "" #: bookwyrm/templates/snippets/status/header.html:45 -#, fuzzy, python-format -#| msgid "Joined %(date)s" +#, python-format msgid "edited %(date)s" -msgstr "在 %(date)s 加入" +msgstr "" #: bookwyrm/templates/snippets/status/headers/comment.html:2 #, python-format @@ -3579,26 +3566,3 @@ msgstr "密碼重置連結已傳送給 {email}" msgid "Status updates from {obj.display_name}" msgstr "" -#~ msgid "Compose status" -#~ msgstr "撰寫狀態" - -#~ msgid "%(format)s" -#~ msgstr "%(format)s" - -#~ msgid "rated" -#~ msgstr "評價了" - -#~ msgid "reviewed" -#~ msgstr "寫了書評給" - -#~ msgid "commented on" -#~ msgstr "評論了" - -#~ msgid "quoted" -#~ msgstr "引用了" - -#~ msgid "About this instance" -#~ msgstr "關於本實例" - -#~ msgid "Delete & re-draft" -#~ msgstr "刪除並重新起草" From 37ff68adb17c588e249cafd40611358c474d86d4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 22 Oct 2021 10:48:22 -0700 Subject: [PATCH 187/193] Updates with new translation strings --- locale/en_US/LC_MESSAGES/django.po | 576 ++++++++++++++++++++--------- 1 file changed, 393 insertions(+), 183 deletions(-) diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 3f04e5c5c..110eb7d16 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-15 22:03+0000\n" +"POT-Creation-Date: 2021-10-22 17:47+0000\n" "PO-Revision-Date: 2021-02-28 17:19-0800\n" "Last-Translator: Mouse Reeve \n" "Language-Team: English \n" @@ -47,29 +47,29 @@ msgstr "" msgid "Unlimited" msgstr "" -#: bookwyrm/forms.py:326 +#: bookwyrm/forms.py:332 msgid "List Order" msgstr "" -#: bookwyrm/forms.py:327 +#: bookwyrm/forms.py:333 msgid "Book Title" msgstr "" -#: bookwyrm/forms.py:328 bookwyrm/templates/shelf/shelf.html:136 -#: bookwyrm/templates/shelf/shelf.html:168 +#: bookwyrm/forms.py:334 bookwyrm/templates/shelf/shelf.html:149 +#: bookwyrm/templates/shelf/shelf.html:181 #: bookwyrm/templates/snippets/create_status/review.html:33 msgid "Rating" msgstr "" -#: bookwyrm/forms.py:330 bookwyrm/templates/lists/list.html:109 +#: bookwyrm/forms.py:336 bookwyrm/templates/lists/list.html:110 msgid "Sort By" msgstr "" -#: bookwyrm/forms.py:334 +#: bookwyrm/forms.py:340 msgid "Ascending" msgstr "" -#: bookwyrm/forms.py:335 +#: bookwyrm/forms.py:341 msgid "Descending" msgstr "" @@ -166,7 +166,7 @@ msgstr "" #: bookwyrm/settings.py:119 bookwyrm/templates/search/layout.html:21 #: bookwyrm/templates/search/layout.html:42 -#: bookwyrm/templates/user/layout.html:81 +#: bookwyrm/templates/user/layout.html:88 msgid "Books" msgstr "" @@ -224,7 +224,7 @@ msgid "Edit Author" msgstr "" #: bookwyrm/templates/author/author.html:34 -#: bookwyrm/templates/author/edit_author.html:41 +#: bookwyrm/templates/author/edit_author.html:43 msgid "Aliases:" msgstr "" @@ -277,71 +277,72 @@ msgstr "" msgid "Updated:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:15 +#: bookwyrm/templates/author/edit_author.html:16 #: bookwyrm/templates/book/edit/edit_book.html:25 msgid "Last edited by:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:31 +#: bookwyrm/templates/author/edit_author.html:33 #: bookwyrm/templates/book/edit/edit_book_form.html:15 msgid "Metadata" msgstr "" -#: bookwyrm/templates/author/edit_author.html:33 -#: bookwyrm/templates/lists/form.html:8 bookwyrm/templates/shelf/form.html:9 +#: bookwyrm/templates/author/edit_author.html:35 +#: bookwyrm/templates/lists/form.html:9 bookwyrm/templates/shelf/form.html:9 msgid "Name:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:43 +#: bookwyrm/templates/author/edit_author.html:45 #: bookwyrm/templates/book/edit/edit_book_form.html:65 #: bookwyrm/templates/book/edit/edit_book_form.html:79 #: bookwyrm/templates/book/edit/edit_book_form.html:124 msgid "Separate multiple values with commas." msgstr "" -#: bookwyrm/templates/author/edit_author.html:50 +#: bookwyrm/templates/author/edit_author.html:52 msgid "Bio:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:57 +#: bookwyrm/templates/author/edit_author.html:59 msgid "Wikipedia link:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:63 +#: bookwyrm/templates/author/edit_author.html:65 msgid "Birth date:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:71 +#: bookwyrm/templates/author/edit_author.html:73 msgid "Death date:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:79 +#: bookwyrm/templates/author/edit_author.html:81 msgid "Author Identifiers" msgstr "" -#: bookwyrm/templates/author/edit_author.html:81 +#: bookwyrm/templates/author/edit_author.html:83 msgid "Openlibrary key:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:89 +#: bookwyrm/templates/author/edit_author.html:91 #: bookwyrm/templates/book/edit/edit_book_form.html:224 msgid "Inventaire ID:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:97 +#: bookwyrm/templates/author/edit_author.html:99 msgid "Librarything key:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:105 +#: bookwyrm/templates/author/edit_author.html:107 msgid "Goodreads key:" msgstr "" -#: bookwyrm/templates/author/edit_author.html:116 +#: bookwyrm/templates/author/edit_author.html:118 #: bookwyrm/templates/book/book.html:140 #: bookwyrm/templates/book/edit/edit_book.html:110 #: bookwyrm/templates/book/readthrough.html:76 +#: bookwyrm/templates/groups/form.html:24 #: bookwyrm/templates/lists/bookmark_button.html:15 -#: bookwyrm/templates/lists/form.html:44 +#: bookwyrm/templates/lists/form.html:75 #: bookwyrm/templates/preferences/edit_user.html:124 #: bookwyrm/templates/settings/announcements/announcement_form.html:69 #: bookwyrm/templates/settings/federation/edit_instance.html:74 @@ -353,11 +354,13 @@ msgstr "" msgid "Save" msgstr "" -#: bookwyrm/templates/author/edit_author.html:117 +#: bookwyrm/templates/author/edit_author.html:119 #: bookwyrm/templates/book/book.html:141 bookwyrm/templates/book/book.html:190 #: bookwyrm/templates/book/cover_modal.html:32 -#: bookwyrm/templates/book/edit/edit_book.html:111 +#: bookwyrm/templates/book/edit/edit_book.html:112 +#: bookwyrm/templates/book/edit/edit_book.html:115 #: bookwyrm/templates/book/readthrough.html:77 +#: bookwyrm/templates/groups/delete_group_modal.html:17 #: bookwyrm/templates/lists/delete_list_modal.html:17 #: bookwyrm/templates/settings/federation/instance.html:88 #: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 @@ -398,7 +401,7 @@ msgstr "" #: bookwyrm/templates/book/book.html:136 #: bookwyrm/templates/book/edit/edit_book_form.html:34 -#: bookwyrm/templates/lists/form.html:12 bookwyrm/templates/shelf/form.html:17 +#: bookwyrm/templates/lists/form.html:13 bookwyrm/templates/shelf/form.html:17 msgid "Description:" msgstr "" @@ -461,7 +464,7 @@ msgstr "" #: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12 #: bookwyrm/templates/search/layout.html:25 #: bookwyrm/templates/search/layout.html:50 -#: bookwyrm/templates/user/layout.html:75 +#: bookwyrm/templates/user/layout.html:82 msgid "Lists" msgstr "" @@ -471,7 +474,7 @@ msgstr "" #: bookwyrm/templates/book/book.html:315 #: bookwyrm/templates/book/cover_modal.html:31 -#: bookwyrm/templates/lists/list.html:181 +#: bookwyrm/templates/lists/list.html:182 #: bookwyrm/templates/settings/email_blocklist/domain_form.html:26 #: bookwyrm/templates/settings/ip_blocklist/ip_address_form.html:32 msgid "Add" @@ -544,7 +547,8 @@ msgid "This is a new work" msgstr "" #: bookwyrm/templates/book/edit/edit_book.html:97 -#: bookwyrm/templates/password_reset.html:30 +#: bookwyrm/templates/groups/members.html:16 +#: bookwyrm/templates/landing/password_reset.html:30 msgid "Confirm" msgstr "" @@ -613,7 +617,7 @@ msgid "John Doe, Jane Smith" msgstr "" #: bookwyrm/templates/book/edit/edit_book_form.html:132 -#: bookwyrm/templates/shelf/shelf.html:127 +#: bookwyrm/templates/shelf/shelf.html:140 msgid "Cover" msgstr "" @@ -794,7 +798,7 @@ msgstr "" #: bookwyrm/templates/confirm_email/resend_form.html:11 #: bookwyrm/templates/landing/layout.html:67 -#: bookwyrm/templates/password_reset_request.html:18 +#: bookwyrm/templates/landing/password_reset_request.html:18 #: bookwyrm/templates/preferences/edit_user.html:56 #: bookwyrm/templates/snippets/register_form.html:13 msgid "Email address:" @@ -994,10 +998,10 @@ msgid "You requested to reset your %(site_name)s password. Click the link below msgstr "" #: bookwyrm/templates/email/password_reset/html_content.html:9 -#: bookwyrm/templates/password_reset.html:4 -#: bookwyrm/templates/password_reset.html:10 -#: bookwyrm/templates/password_reset_request.html:4 -#: bookwyrm/templates/password_reset_request.html:10 +#: bookwyrm/templates/landing/password_reset.html:4 +#: bookwyrm/templates/landing/password_reset.html:10 +#: bookwyrm/templates/landing/password_reset_request.html:4 +#: bookwyrm/templates/landing/password_reset_request.html:10 msgid "Reset Password" msgstr "" @@ -1103,7 +1107,7 @@ msgid "What are you reading?" msgstr "" #: bookwyrm/templates/get_started/books.html:9 -#: bookwyrm/templates/layout.html:45 bookwyrm/templates/lists/list.html:137 +#: bookwyrm/templates/layout.html:45 bookwyrm/templates/lists/list.html:138 msgid "Search for a book" msgstr "" @@ -1121,8 +1125,9 @@ msgstr "" #: bookwyrm/templates/get_started/books.html:17 #: bookwyrm/templates/get_started/users.html:18 #: bookwyrm/templates/get_started/users.html:19 -#: bookwyrm/templates/layout.html:51 bookwyrm/templates/layout.html:52 -#: bookwyrm/templates/lists/list.html:141 +#: bookwyrm/templates/groups/group.html:19 +#: bookwyrm/templates/groups/group.html:20 bookwyrm/templates/layout.html:51 +#: bookwyrm/templates/layout.html:52 bookwyrm/templates/lists/list.html:142 #: bookwyrm/templates/search/layout.html:4 #: bookwyrm/templates/search/layout.html:9 msgid "Search" @@ -1138,7 +1143,7 @@ msgid "Popular on %(site_name)s" msgstr "" #: bookwyrm/templates/get_started/books.html:58 -#: bookwyrm/templates/lists/list.html:154 +#: bookwyrm/templates/lists/list.html:155 msgid "No books found" msgstr "" @@ -1224,9 +1229,105 @@ msgstr "" msgid "No users found for \"%(query)s\"" msgstr "" +#: bookwyrm/templates/groups/create_form.html:5 +msgid "Create Group" +msgstr "" + +#: bookwyrm/templates/groups/created_text.html:4 +#, python-format +msgid "Managed by %(username)s" +msgstr "" + +#: bookwyrm/templates/groups/delete_group_modal.html:4 +msgid "Delete this group?" +msgstr "" + +#: bookwyrm/templates/groups/delete_group_modal.html:7 +#: bookwyrm/templates/lists/delete_list_modal.html:7 +msgid "This action cannot be un-done" +msgstr "" + +#: bookwyrm/templates/groups/delete_group_modal.html:15 +#: bookwyrm/templates/lists/delete_list_modal.html:15 +#: bookwyrm/templates/settings/announcements/announcement.html:20 +#: bookwyrm/templates/settings/email_blocklist/email_blocklist.html:49 +#: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:36 +#: bookwyrm/templates/snippets/delete_readthrough_modal.html:15 +#: bookwyrm/templates/snippets/follow_request_buttons.html:12 +#: bookwyrm/templates/snippets/join_invitation_buttons.html:13 +msgid "Delete" +msgstr "" + +#: bookwyrm/templates/groups/edit_form.html:5 +msgid "Edit Group" +msgstr "" + +#: bookwyrm/templates/groups/find_users.html:6 +msgid "Add new members!" +msgstr "" + +#: bookwyrm/templates/groups/form.html:8 +msgid "Group Name:" +msgstr "" + +#: bookwyrm/templates/groups/form.html:12 +msgid "Group Description:" +msgstr "" + +#: bookwyrm/templates/groups/form.html:30 +msgid "Delete group" +msgstr "" + +#: bookwyrm/templates/groups/group.html:15 +msgid "Search to add a user" +msgstr "" + +#: bookwyrm/templates/groups/group.html:36 +msgid "This group has no lists" +msgstr "" + +#: bookwyrm/templates/groups/layout.html:16 +msgid "Edit group" +msgstr "" + +#: bookwyrm/templates/groups/members.html:8 +msgid "Members can add and remove books on a group's book lists" +msgstr "" + +#: bookwyrm/templates/groups/members.html:19 +msgid "Leave group" +msgstr "" + +#: bookwyrm/templates/groups/members.html:41 +#: bookwyrm/templates/groups/suggested_users.html:32 +#: bookwyrm/templates/snippets/suggested_users.html:31 +#: bookwyrm/templates/user/user_preview.html:36 +msgid "Follows you" +msgstr "" + +#: bookwyrm/templates/groups/suggested_users.html:17 +#: bookwyrm/templates/snippets/suggested_users.html:16 +#, python-format +msgid "%(mutuals)s follower you follow" +msgid_plural "%(mutuals)s followers you follow" +msgstr[0] "" +msgstr[1] "" + +#: bookwyrm/templates/groups/suggested_users.html:24 +#: bookwyrm/templates/snippets/suggested_users.html:23 +#, python-format +msgid "%(shared_books)s book on your shelves" +msgid_plural "%(shared_books)s books on your shelves" +msgstr[0] "" +msgstr[1] "" + +#: bookwyrm/templates/groups/user_groups.html:15 +msgid "Manager" +msgstr "" + #: bookwyrm/templates/import/import.html:5 #: bookwyrm/templates/import/import.html:9 -#: bookwyrm/templates/shelf/shelf.html:57 +#: bookwyrm/templates/shelf/shelf.html:61 msgid "Import Books" msgstr "" @@ -1323,14 +1424,14 @@ msgid "Book" msgstr "" #: bookwyrm/templates/import/import_status.html:122 -#: bookwyrm/templates/shelf/shelf.html:128 -#: bookwyrm/templates/shelf/shelf.html:150 +#: bookwyrm/templates/shelf/shelf.html:141 +#: bookwyrm/templates/shelf/shelf.html:163 msgid "Title" msgstr "" #: bookwyrm/templates/import/import_status.html:125 -#: bookwyrm/templates/shelf/shelf.html:129 -#: bookwyrm/templates/shelf/shelf.html:153 +#: bookwyrm/templates/shelf/shelf.html:142 +#: bookwyrm/templates/shelf/shelf.html:166 msgid "Author" msgstr "" @@ -1342,19 +1443,6 @@ msgstr "" msgid "You can download your Goodreads data from the Import/Export page of your Goodreads account." msgstr "" -#: bookwyrm/templates/invite.html:4 bookwyrm/templates/invite.html:8 -#: bookwyrm/templates/login.html:49 -msgid "Create an Account" -msgstr "" - -#: bookwyrm/templates/invite.html:21 -msgid "Permission Denied" -msgstr "" - -#: bookwyrm/templates/invite.html:22 -msgid "Sorry! This invite code is no longer valid." -msgstr "" - #: bookwyrm/templates/landing/about.html:7 bookwyrm/templates/layout.html:230 #, python-format msgid "About %(site_name)s" @@ -1370,6 +1458,20 @@ msgstr "" msgid "Privacy Policy" msgstr "" +#: bookwyrm/templates/landing/invite.html:4 +#: bookwyrm/templates/landing/invite.html:8 +#: bookwyrm/templates/landing/login.html:49 +msgid "Create an Account" +msgstr "" + +#: bookwyrm/templates/landing/invite.html:21 +msgid "Permission Denied" +msgstr "" + +#: bookwyrm/templates/landing/invite.html:22 +msgid "Sorry! This invite code is no longer valid." +msgstr "" + #: bookwyrm/templates/landing/landing.html:6 msgid "Recent Books" msgstr "" @@ -1408,6 +1510,53 @@ msgstr "" msgid "Your Account" msgstr "" +#: bookwyrm/templates/landing/login.html:4 +msgid "Login" +msgstr "" + +#: bookwyrm/templates/landing/login.html:7 +#: bookwyrm/templates/landing/login.html:37 bookwyrm/templates/layout.html:179 +msgid "Log in" +msgstr "" + +#: bookwyrm/templates/landing/login.html:15 +msgid "Success! Email address confirmed." +msgstr "" + +#: bookwyrm/templates/landing/login.html:21 bookwyrm/templates/layout.html:170 +#: bookwyrm/templates/snippets/register_form.html:4 +msgid "Username:" +msgstr "" + +#: bookwyrm/templates/landing/login.html:27 +#: bookwyrm/templates/landing/password_reset.html:17 +#: bookwyrm/templates/layout.html:174 +#: bookwyrm/templates/snippets/register_form.html:22 +msgid "Password:" +msgstr "" + +#: bookwyrm/templates/landing/login.html:40 bookwyrm/templates/layout.html:176 +msgid "Forgot your password?" +msgstr "" + +#: bookwyrm/templates/landing/login.html:62 +msgid "More about this site" +msgstr "" + +#: bookwyrm/templates/landing/password_reset.html:23 +#: bookwyrm/templates/preferences/change_password.html:18 +#: bookwyrm/templates/preferences/delete_user.html:20 +msgid "Confirm password:" +msgstr "" + +#: bookwyrm/templates/landing/password_reset_request.html:14 +msgid "A link to reset your password will be sent to your email address" +msgstr "" + +#: bookwyrm/templates/landing/password_reset_request.html:28 +msgid "Reset password" +msgstr "" + #: bookwyrm/templates/layout.html:13 #, python-format msgid "%(site_name)s search" @@ -1455,25 +1604,10 @@ msgstr "" msgid "Notifications" msgstr "" -#: bookwyrm/templates/layout.html:170 bookwyrm/templates/layout.html:174 -#: bookwyrm/templates/login.html:21 -#: bookwyrm/templates/snippets/register_form.html:4 -msgid "Username:" -msgstr "" - #: bookwyrm/templates/layout.html:175 msgid "password" msgstr "" -#: bookwyrm/templates/layout.html:176 bookwyrm/templates/login.html:40 -msgid "Forgot your password?" -msgstr "" - -#: bookwyrm/templates/layout.html:179 bookwyrm/templates/login.html:7 -#: bookwyrm/templates/login.html:37 -msgid "Log in" -msgstr "" - #: bookwyrm/templates/layout.html:187 msgid "Join" msgstr "" @@ -1514,11 +1648,16 @@ msgstr "" #: bookwyrm/templates/lists/created_text.html:5 #, python-format -msgid "Created and curated by %(username)s" +msgid "Created by %(username)s and managed by %(groupname)s" msgstr "" #: bookwyrm/templates/lists/created_text.html:7 #, python-format +msgid "Created and curated by %(username)s" +msgstr "" + +#: bookwyrm/templates/lists/created_text.html:9 +#, python-format msgid "Created by %(username)s" msgstr "" @@ -1550,118 +1689,129 @@ msgstr "" msgid "Delete this list?" msgstr "" -#: bookwyrm/templates/lists/delete_list_modal.html:7 -msgid "This action cannot be un-done" -msgstr "" - -#: bookwyrm/templates/lists/delete_list_modal.html:15 -#: bookwyrm/templates/settings/announcements/announcement.html:20 -#: bookwyrm/templates/settings/email_blocklist/email_blocklist.html:49 -#: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:36 -#: bookwyrm/templates/snippets/delete_readthrough_modal.html:15 -#: bookwyrm/templates/snippets/follow_request_buttons.html:12 -msgid "Delete" -msgstr "" - #: bookwyrm/templates/lists/edit_form.html:5 #: bookwyrm/templates/lists/layout.html:16 msgid "Edit List" msgstr "" -#: bookwyrm/templates/lists/form.html:18 +#: bookwyrm/templates/lists/form.html:19 msgid "List curation:" msgstr "" -#: bookwyrm/templates/lists/form.html:21 +#: bookwyrm/templates/lists/form.html:22 msgid "Closed" msgstr "" -#: bookwyrm/templates/lists/form.html:22 +#: bookwyrm/templates/lists/form.html:23 msgid "Only you can add and remove books to this list" msgstr "" -#: bookwyrm/templates/lists/form.html:26 +#: bookwyrm/templates/lists/form.html:27 msgid "Curated" msgstr "" -#: bookwyrm/templates/lists/form.html:27 +#: bookwyrm/templates/lists/form.html:28 msgid "Anyone can suggest books, subject to your approval" msgstr "" -#: bookwyrm/templates/lists/form.html:31 +#: bookwyrm/templates/lists/form.html:32 msgctxt "curation type" msgid "Open" msgstr "" -#: bookwyrm/templates/lists/form.html:32 +#: bookwyrm/templates/lists/form.html:33 msgid "Anyone can add books to this list" msgstr "" -#: bookwyrm/templates/lists/form.html:50 +#: bookwyrm/templates/lists/form.html:37 +msgid "Group" +msgstr "" + +#: bookwyrm/templates/lists/form.html:38 +msgid "Group members can add to and remove from this list" +msgstr "" + +#: bookwyrm/templates/lists/form.html:41 +msgid "Select Group" +msgstr "" + +#: bookwyrm/templates/lists/form.html:45 +msgid "Select a group" +msgstr "" + +#: bookwyrm/templates/lists/form.html:56 +msgid "You don't have any Groups yet!" +msgstr "" + +#: bookwyrm/templates/lists/form.html:58 +msgid "Create a Group" +msgstr "" + +#: bookwyrm/templates/lists/form.html:81 msgid "Delete list" msgstr "" -#: bookwyrm/templates/lists/list.html:20 +#: bookwyrm/templates/lists/list.html:21 msgid "You successfully suggested a book for this list!" msgstr "" -#: bookwyrm/templates/lists/list.html:22 +#: bookwyrm/templates/lists/list.html:23 msgid "You successfully added a book to this list!" msgstr "" -#: bookwyrm/templates/lists/list.html:28 +#: bookwyrm/templates/lists/list.html:29 msgid "This list is currently empty" msgstr "" -#: bookwyrm/templates/lists/list.html:66 +#: bookwyrm/templates/lists/list.html:67 #, python-format msgid "Added by %(username)s" msgstr "" -#: bookwyrm/templates/lists/list.html:75 +#: bookwyrm/templates/lists/list.html:76 msgid "List position" msgstr "" -#: bookwyrm/templates/lists/list.html:81 +#: bookwyrm/templates/lists/list.html:82 msgid "Set" msgstr "" -#: bookwyrm/templates/lists/list.html:91 +#: bookwyrm/templates/lists/list.html:92 #: bookwyrm/templates/snippets/shelf_selector.html:26 msgid "Remove" msgstr "" -#: bookwyrm/templates/lists/list.html:105 -#: bookwyrm/templates/lists/list.html:122 +#: bookwyrm/templates/lists/list.html:106 +#: bookwyrm/templates/lists/list.html:123 msgid "Sort List" msgstr "" -#: bookwyrm/templates/lists/list.html:115 +#: bookwyrm/templates/lists/list.html:116 msgid "Direction" msgstr "" -#: bookwyrm/templates/lists/list.html:129 +#: bookwyrm/templates/lists/list.html:130 msgid "Add Books" msgstr "" -#: bookwyrm/templates/lists/list.html:131 +#: bookwyrm/templates/lists/list.html:132 msgid "Suggest Books" msgstr "" -#: bookwyrm/templates/lists/list.html:142 +#: bookwyrm/templates/lists/list.html:143 msgid "search" msgstr "" -#: bookwyrm/templates/lists/list.html:148 +#: bookwyrm/templates/lists/list.html:149 msgid "Clear search" msgstr "" -#: bookwyrm/templates/lists/list.html:153 +#: bookwyrm/templates/lists/list.html:154 #, python-format msgid "No books found matching the query \"%(query)s\"" msgstr "" -#: bookwyrm/templates/lists/list.html:181 +#: bookwyrm/templates/lists/list.html:182 msgid "Suggest" msgstr "" @@ -1673,29 +1823,20 @@ msgstr "" msgid "Your Lists" msgstr "" -#: bookwyrm/templates/lists/lists.html:35 +#: bookwyrm/templates/lists/lists.html:36 msgid "All Lists" msgstr "" -#: bookwyrm/templates/lists/lists.html:39 +#: bookwyrm/templates/lists/lists.html:40 msgid "Saved Lists" msgstr "" -#: bookwyrm/templates/login.html:4 -msgid "Login" -msgstr "" - -#: bookwyrm/templates/login.html:15 -msgid "Success! Email address confirmed." -msgstr "" - -#: bookwyrm/templates/login.html:27 bookwyrm/templates/password_reset.html:17 -#: bookwyrm/templates/snippets/register_form.html:22 -msgid "Password:" -msgstr "" - -#: bookwyrm/templates/login.html:62 -msgid "More about this site" +#: bookwyrm/templates/notifications/items/accept.html:16 +#, python-format +msgid "" +"\n" +" accepted your invitation to join group \"%(group_name)s\"\n" +" " msgstr "" #: bookwyrm/templates/notifications/items/add.html:24 @@ -1761,6 +1902,26 @@ msgstr "" msgid "Your import completed." msgstr "" +#: bookwyrm/templates/notifications/items/invite.html:15 +#, python-format +msgid "invited you to join the group %(group_name)s" +msgstr "" + +#: bookwyrm/templates/notifications/items/join.html:16 +#, python-format +msgid "" +"\n" +"has joined your group \"%(group_name)s\"\n" +msgstr "" + +#: bookwyrm/templates/notifications/items/leave.html:16 +#, python-format +msgid "" +"\n" +" has left your group \"%(group_name)s\"\n" +" " +msgstr "" + #: bookwyrm/templates/notifications/items/mention.html:20 #, python-format msgid "mentioned you in a review of %(book_title)s" @@ -1781,6 +1942,21 @@ msgstr "" msgid "mentioned you in a status" msgstr "" +#: bookwyrm/templates/notifications/items/remove.html:17 +#, python-format +msgid "" +" \n" +"has been removed from your group \"%(group_name)s\"\n" +msgstr "" + +#: bookwyrm/templates/notifications/items/remove.html:23 +#, python-format +msgid "" +"\n" +" You have been removed from the \"%(group_name)s group\"\n" +" " +msgstr "" + #: bookwyrm/templates/notifications/items/reply.html:21 #, python-format msgid "replied to your review of %(book_title)s" @@ -1806,6 +1982,30 @@ msgstr "" msgid "A new report needs moderation." msgstr "" +#: bookwyrm/templates/notifications/items/update.html:16 +#, python-format +msgid "" +"\n" +" has changed the privacy level for %(group_name)s\n" +" " +msgstr "" + +#: bookwyrm/templates/notifications/items/update.html:20 +#, python-format +msgid "" +"\n" +" has changed the name of %(group_name)s\n" +" " +msgstr "" + +#: bookwyrm/templates/notifications/items/update.html:24 +#, python-format +msgid "" +"\n" +" has changed the description of %(group_name)s\n" +" " +msgstr "" + #: bookwyrm/templates/notifications/notifications_page.html:18 msgid "Delete notifications" msgstr "" @@ -1822,20 +2022,6 @@ msgstr "" msgid "You're all caught up!" msgstr "" -#: bookwyrm/templates/password_reset.html:23 -#: bookwyrm/templates/preferences/change_password.html:18 -#: bookwyrm/templates/preferences/delete_user.html:20 -msgid "Confirm password:" -msgstr "" - -#: bookwyrm/templates/password_reset_request.html:14 -msgid "A link to reset your password will be sent to your email address" -msgstr "" - -#: bookwyrm/templates/password_reset_request.html:28 -msgid "Reset password" -msgstr "" - #: bookwyrm/templates/preferences/blocks.html:4 #: bookwyrm/templates/preferences/blocks.html:7 #: bookwyrm/templates/preferences/layout.html:31 @@ -2258,7 +2444,7 @@ msgid "Details" msgstr "" #: bookwyrm/templates/settings/federation/instance.html:35 -#: bookwyrm/templates/user/layout.html:63 +#: bookwyrm/templates/user/layout.html:64 msgid "Activity" msgstr "" @@ -2834,53 +3020,66 @@ msgstr "" msgid "Edit Shelf" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:28 bookwyrm/views/shelf.py:55 +#: bookwyrm/templates/shelf/shelf.html:28 bookwyrm/views/shelf/shelf.py:53 msgid "All books" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:55 +#: bookwyrm/templates/shelf/shelf.html:69 msgid "Create shelf" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:77 +#: bookwyrm/templates/shelf/shelf.html:90 #, python-format msgid "%(formatted_count)s book" msgid_plural "%(formatted_count)s books" msgstr[0] "" msgstr[1] "" -#: bookwyrm/templates/shelf/shelf.html:84 +#: bookwyrm/templates/shelf/shelf.html:97 #, python-format msgid "(showing %(start)s-%(end)s)" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:96 +#: bookwyrm/templates/shelf/shelf.html:109 msgid "Edit shelf" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:104 +#: bookwyrm/templates/shelf/shelf.html:117 msgid "Delete shelf" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:132 -#: bookwyrm/templates/shelf/shelf.html:158 +#: bookwyrm/templates/shelf/shelf.html:145 +#: bookwyrm/templates/shelf/shelf.html:171 msgid "Shelved" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:133 -#: bookwyrm/templates/shelf/shelf.html:161 +#: bookwyrm/templates/shelf/shelf.html:146 +#: bookwyrm/templates/shelf/shelf.html:174 msgid "Started" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:134 -#: bookwyrm/templates/shelf/shelf.html:164 +#: bookwyrm/templates/shelf/shelf.html:147 +#: bookwyrm/templates/shelf/shelf.html:177 msgid "Finished" msgstr "" -#: bookwyrm/templates/shelf/shelf.html:190 +#: bookwyrm/templates/shelf/shelf.html:203 msgid "This shelf is empty." msgstr "" +#: bookwyrm/templates/snippets/add_to_group_button.html:15 +msgid "Invite" +msgstr "" + +#: bookwyrm/templates/snippets/add_to_group_button.html:24 +msgid "Uninvite" +msgstr "" + +#: bookwyrm/templates/snippets/add_to_group_button.html:28 +#, python-format +msgid "Remove @%(username)s" +msgstr "" + #: bookwyrm/templates/snippets/announcement.html:31 #, python-format msgid "Posted by %(username)s" @@ -2976,6 +3175,7 @@ msgstr "" #: bookwyrm/templates/snippets/privacy-icons.html:15 #: bookwyrm/templates/snippets/privacy-icons.html:16 #: bookwyrm/templates/snippets/privacy_select.html:20 +#: bookwyrm/templates/snippets/privacy_select_no_followers.html:17 msgid "Private" msgstr "" @@ -3071,6 +3271,7 @@ msgid "Unfollow" msgstr "" #: bookwyrm/templates/snippets/follow_request_buttons.html:7 +#: bookwyrm/templates/snippets/join_invitation_buttons.html:8 msgid "Accept" msgstr "" @@ -3182,12 +3383,14 @@ msgstr "" #: bookwyrm/templates/snippets/privacy-icons.html:3 #: bookwyrm/templates/snippets/privacy-icons.html:4 #: bookwyrm/templates/snippets/privacy_select.html:11 +#: bookwyrm/templates/snippets/privacy_select_no_followers.html:11 msgid "Public" msgstr "" #: bookwyrm/templates/snippets/privacy-icons.html:7 #: bookwyrm/templates/snippets/privacy-icons.html:8 #: bookwyrm/templates/snippets/privacy_select.html:14 +#: bookwyrm/templates/snippets/privacy_select_no_followers.html:14 msgid "Unlisted" msgstr "" @@ -3196,6 +3399,7 @@ msgid "Followers-only" msgstr "" #: bookwyrm/templates/snippets/privacy_select.html:6 +#: bookwyrm/templates/snippets/privacy_select_no_followers.html:6 msgid "Post privacy" msgstr "" @@ -3256,6 +3460,14 @@ msgstr "" msgid "Sign Up" msgstr "" +#: bookwyrm/templates/snippets/remove_from_group_button.html:16 +msgid " Confirm " +msgstr "" + +#: bookwyrm/templates/snippets/remove_from_group_button.html:19 +msgid " Remove " +msgstr "" + #: bookwyrm/templates/snippets/report_button.html:6 msgid "Report" msgstr "" @@ -3396,25 +3608,6 @@ msgstr "" msgid "More options" msgstr "" -#: bookwyrm/templates/snippets/suggested_users.html:16 -#, python-format -msgid "%(mutuals)s follower you follow" -msgid_plural "%(mutuals)s followers you follow" -msgstr[0] "" -msgstr[1] "" - -#: bookwyrm/templates/snippets/suggested_users.html:23 -#, python-format -msgid "%(shared_books)s book on your shelves" -msgid_plural "%(shared_books)s books on your shelves" -msgstr[0] "" -msgstr[1] "" - -#: bookwyrm/templates/snippets/suggested_users.html:31 -#: bookwyrm/templates/user/user_preview.html:36 -msgid "Follows you" -msgstr "" - #: bookwyrm/templates/snippets/switch_edition_button.html:5 msgid "Switch to this edition" msgstr "" @@ -3464,18 +3657,35 @@ msgstr "" msgid "%(username)s's %(year)s Books" msgstr "" -#: bookwyrm/templates/user/layout.html:18 bookwyrm/templates/user/user.html:10 +#: bookwyrm/templates/user/groups.html:9 +msgid "Your Groups" +msgstr "" + +#: bookwyrm/templates/user/groups.html:11 +#, python-format +msgid "Groups: %(username)s" +msgstr "" + +#: bookwyrm/templates/user/groups.html:17 +msgid "Create group" +msgstr "" + +#: bookwyrm/templates/user/layout.html:19 bookwyrm/templates/user/user.html:10 msgid "User Profile" msgstr "" -#: bookwyrm/templates/user/layout.html:44 +#: bookwyrm/templates/user/layout.html:45 msgid "Follow Requests" msgstr "" -#: bookwyrm/templates/user/layout.html:69 +#: bookwyrm/templates/user/layout.html:70 msgid "Reading Goal" msgstr "" +#: bookwyrm/templates/user/layout.html:76 +msgid "Groups" +msgstr "" + #: bookwyrm/templates/user/lists.html:11 #, python-format msgid "Lists: %(username)s" @@ -3566,15 +3776,15 @@ msgstr "" msgid "Not a valid csv file" msgstr "" -#: bookwyrm/views/login.py:69 +#: bookwyrm/views/landing/login.py:69 msgid "Username or password are incorrect" msgstr "" -#: bookwyrm/views/password.py:32 +#: bookwyrm/views/landing/password.py:32 msgid "No user with that email address was found." msgstr "" -#: bookwyrm/views/password.py:41 +#: bookwyrm/views/landing/password.py:43 #, python-brace-format msgid "A password reset link was sent to {email}" msgstr "" From 1e6390a405f343a3076e869727e002ffccb9dd8c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 23 Oct 2021 06:59:06 -0700 Subject: [PATCH 188/193] Fixes whitespace in translation strings --- bookwyrm/templates/groups/suggested_users.html | 6 +++++- bookwyrm/templates/notifications/items/accept.html | 2 +- bookwyrm/templates/notifications/items/fav.html | 2 +- bookwyrm/templates/notifications/items/invite.html | 2 +- bookwyrm/templates/notifications/items/item_layout.html | 7 ++----- bookwyrm/templates/notifications/items/join.html | 2 +- bookwyrm/templates/notifications/items/leave.html | 2 +- bookwyrm/templates/notifications/items/remove.html | 8 ++++---- bookwyrm/templates/notifications/items/update.html | 6 +++--- 9 files changed, 19 insertions(+), 18 deletions(-) diff --git a/bookwyrm/templates/groups/suggested_users.html b/bookwyrm/templates/groups/suggested_users.html index 951e7bf05..64498eb85 100644 --- a/bookwyrm/templates/groups/suggested_users.html +++ b/bookwyrm/templates/groups/suggested_users.html @@ -36,7 +36,11 @@ {% endfor %}
{% else %} -

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

+

+ {% blocktrans trimmed %} + No potential members found for "{{ user_query }}" + {% endblocktrans %} +


{% endif %} diff --git a/bookwyrm/templates/notifications/items/accept.html b/bookwyrm/templates/notifications/items/accept.html index 19acab15e..045e23266 100644 --- a/bookwyrm/templates/notifications/items/accept.html +++ b/bookwyrm/templates/notifications/items/accept.html @@ -13,7 +13,7 @@ {% block description %} - {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} accepted your invitation to join group "{{ group_name }}" {% endblocktrans %} diff --git a/bookwyrm/templates/notifications/items/fav.html b/bookwyrm/templates/notifications/items/fav.html index afc18d73e..fbb865e4f 100644 --- a/bookwyrm/templates/notifications/items/fav.html +++ b/bookwyrm/templates/notifications/items/fav.html @@ -24,7 +24,7 @@ {% elif related_status.status_type == 'Comment' %} {% blocktrans trimmed %} - liked your comment on{{ book_title }} + liked your comment on {{ book_title }} {% endblocktrans %} {% elif related_status.status_type == 'Quotation' %} diff --git a/bookwyrm/templates/notifications/items/invite.html b/bookwyrm/templates/notifications/items/invite.html index ddfa3b345..de3b89e42 100644 --- a/bookwyrm/templates/notifications/items/invite.html +++ b/bookwyrm/templates/notifications/items/invite.html @@ -12,7 +12,7 @@ {% endblock %} {% block description %} -{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} +{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} invited you to join the group {{ group_name }} {% endblocktrans %}
diff --git a/bookwyrm/templates/notifications/items/item_layout.html b/bookwyrm/templates/notifications/items/item_layout.html index 382978d47..8db68dafb 100644 --- a/bookwyrm/templates/notifications/items/item_layout.html +++ b/bookwyrm/templates/notifications/items/item_layout.html @@ -1,4 +1,3 @@ -{% load humanize %} {% load bookwyrm_tags %} {% related_status notification as related_status %}
@@ -10,10 +9,8 @@

{% if notification.related_user %} - - {% include 'snippets/avatar.html' with user=notification.related_user %} - {{ notification.related_user.display_name }} - + {% include 'snippets/avatar.html' with user=notification.related_user %} + {{ notification.related_user.display_name }} {% endif %} {% block description %}{% endblock %}

diff --git a/bookwyrm/templates/notifications/items/join.html b/bookwyrm/templates/notifications/items/join.html index 93b356424..c10def456 100644 --- a/bookwyrm/templates/notifications/items/join.html +++ b/bookwyrm/templates/notifications/items/join.html @@ -13,7 +13,7 @@ {% block description %} -{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} +{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has joined your group "{{ group_name }}" {% endblocktrans %} diff --git a/bookwyrm/templates/notifications/items/leave.html b/bookwyrm/templates/notifications/items/leave.html index 9c7a71b61..422a31dea 100644 --- a/bookwyrm/templates/notifications/items/leave.html +++ b/bookwyrm/templates/notifications/items/leave.html @@ -13,7 +13,7 @@ {% block description %} - {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has left your group "{{ group_name }}" {% endblocktrans %} diff --git a/bookwyrm/templates/notifications/items/remove.html b/bookwyrm/templates/notifications/items/remove.html index 7ee38b4a7..18a0cf178 100644 --- a/bookwyrm/templates/notifications/items/remove.html +++ b/bookwyrm/templates/notifications/items/remove.html @@ -14,13 +14,13 @@ {% block description %} {% if notification.related_user %} -{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} -has been removed from your group "{{ group_name }}" -{% endblocktrans %} + {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + has been removed from your group "{{ group_name }}" + {% endblocktrans %} {% else %} - {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} You have been removed from the "{{ group_name }} group" {% endblocktrans %} diff --git a/bookwyrm/templates/notifications/items/update.html b/bookwyrm/templates/notifications/items/update.html index f963fd3a9..be796b785 100644 --- a/bookwyrm/templates/notifications/items/update.html +++ b/bookwyrm/templates/notifications/items/update.html @@ -13,15 +13,15 @@ {% block description %} {% if notification.notification_type == 'GROUP_PRIVACY' %} - {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has changed the privacy level for {{ group_name }} {% endblocktrans %} {% elif notification.notification_type == 'GROUP_NAME' %} - {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has changed the name of {{ group_name }} {% endblocktrans %} {% else %} - {% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} + {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has changed the description of {{ group_name }} {% endblocktrans %} {% endif %} From 353ccc1d7d05d5063a92aaf959fcd8a799b624cf Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 23 Oct 2021 07:11:38 -0700 Subject: [PATCH 189/193] Updates references locale --- bw-dev | 5 +--- locale/en_US/LC_MESSAGES/django.po | 47 ++++++++++-------------------- 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/bw-dev b/bw-dev index c9c2115ce..6c9def8ff 100755 --- a/bw-dev +++ b/bw-dev @@ -105,11 +105,8 @@ case "$CMD" in collectstatic) runweb python manage.py collectstatic --no-input ;; - add_locale) - runweb django-admin makemessages --no-wrap --ignore=venv -l $@ - ;; makemessages) - runweb django-admin makemessages --no-wrap --ignore=venv --all $@ + runweb django-admin makemessages --no-wrap --ignore=venv -l en_US $@ ;; compilemessages) runweb django-admin compilemessages --ignore venv $@ diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 110eb7d16..0d2cb22bf 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-22 17:47+0000\n" +"POT-Creation-Date: 2021-10-23 14:06+0000\n" "PO-Revision-Date: 2021-02-28 17:19-0800\n" "Last-Translator: Mouse Reeve \n" "Language-Team: English \n" @@ -1321,6 +1321,11 @@ msgid_plural "%(shared_books)s books on your shelves" msgstr[0] "" msgstr[1] "" +#: bookwyrm/templates/groups/suggested_users.html:40 +#, python-format +msgid "No potential members found for \"%(user_query)s\"" +msgstr "" + #: bookwyrm/templates/groups/user_groups.html:15 msgid "Manager" msgstr "" @@ -1833,10 +1838,7 @@ msgstr "" #: bookwyrm/templates/notifications/items/accept.html:16 #, python-format -msgid "" -"\n" -" accepted your invitation to join group \"%(group_name)s\"\n" -" " +msgid "accepted your invitation to join group \"%(group_name)s\"" msgstr "" #: bookwyrm/templates/notifications/items/add.html:24 @@ -1876,7 +1878,7 @@ msgstr "" #: bookwyrm/templates/notifications/items/fav.html:25 #, python-format -msgid "liked your comment on%(book_title)s" +msgid "liked your comment on %(book_title)s" msgstr "" #: bookwyrm/templates/notifications/items/fav.html:31 @@ -1909,17 +1911,12 @@ msgstr "" #: bookwyrm/templates/notifications/items/join.html:16 #, python-format -msgid "" -"\n" -"has joined your group \"%(group_name)s\"\n" +msgid "has joined your group \"%(group_name)s\"" msgstr "" #: bookwyrm/templates/notifications/items/leave.html:16 #, python-format -msgid "" -"\n" -" has left your group \"%(group_name)s\"\n" -" " +msgid "has left your group \"%(group_name)s\"" msgstr "" #: bookwyrm/templates/notifications/items/mention.html:20 @@ -1944,17 +1941,12 @@ msgstr "" #: bookwyrm/templates/notifications/items/remove.html:17 #, python-format -msgid "" -" \n" -"has been removed from your group \"%(group_name)s\"\n" +msgid "has been removed from your group \"%(group_name)s\"" msgstr "" #: bookwyrm/templates/notifications/items/remove.html:23 #, python-format -msgid "" -"\n" -" You have been removed from the \"%(group_name)s group\"\n" -" " +msgid "You have been removed from the \"%(group_name)s group\"" msgstr "" #: bookwyrm/templates/notifications/items/reply.html:21 @@ -1984,26 +1976,17 @@ msgstr "" #: bookwyrm/templates/notifications/items/update.html:16 #, python-format -msgid "" -"\n" -" has changed the privacy level for %(group_name)s\n" -" " +msgid "has changed the privacy level for %(group_name)s" msgstr "" #: bookwyrm/templates/notifications/items/update.html:20 #, python-format -msgid "" -"\n" -" has changed the name of %(group_name)s\n" -" " +msgid "has changed the name of %(group_name)s" msgstr "" #: bookwyrm/templates/notifications/items/update.html:24 #, python-format -msgid "" -"\n" -" has changed the description of %(group_name)s\n" -" " +msgid "has changed the description of %(group_name)s" msgstr "" #: bookwyrm/templates/notifications/notifications_page.html:18 From d80a28e12899c3f9d000840a6576d4a163373c0e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 23 Oct 2021 08:00:45 -0700 Subject: [PATCH 190/193] Consistent quotes around group names in notifications --- bookwyrm/templates/notifications/items/invite.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/notifications/items/invite.html b/bookwyrm/templates/notifications/items/invite.html index de3b89e42..abb8cd02f 100644 --- a/bookwyrm/templates/notifications/items/invite.html +++ b/bookwyrm/templates/notifications/items/invite.html @@ -13,7 +13,7 @@ {% block description %} {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} -invited you to join the group {{ group_name }} +invited you to join the group "{{ group_name }}" {% endblocktrans %}
{% include 'snippets/join_invitation_buttons.html' with group=notification.related_group %} From 0f9c363b00be6a89b52a62d063aed6857f93ab95 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 23 Oct 2021 08:13:07 -0700 Subject: [PATCH 191/193] Updates locale file for quotes fix --- locale/en_US/LC_MESSAGES/django.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 0d2cb22bf..0521e64ba 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-23 14:06+0000\n" +"POT-Creation-Date: 2021-10-23 15:11+0000\n" "PO-Revision-Date: 2021-02-28 17:19-0800\n" "Last-Translator: Mouse Reeve \n" "Language-Team: English \n" @@ -1906,7 +1906,7 @@ msgstr "" #: bookwyrm/templates/notifications/items/invite.html:15 #, python-format -msgid "invited you to join the group %(group_name)s" +msgid "invited you to join the group \"%(group_name)s\"" msgstr "" #: bookwyrm/templates/notifications/items/join.html:16 From 9a07c11b19818bc8133009765db7cd2306359973 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 24 Oct 2021 06:43:31 -0700 Subject: [PATCH 192/193] Fixes group quotes and button whitespace --- bookwyrm/templates/notifications/items/remove.html | 2 +- bookwyrm/templates/snippets/remove_from_group_button.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/notifications/items/remove.html b/bookwyrm/templates/notifications/items/remove.html index 18a0cf178..eba18fd89 100644 --- a/bookwyrm/templates/notifications/items/remove.html +++ b/bookwyrm/templates/notifications/items/remove.html @@ -21,7 +21,7 @@ {% else %} {% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %} - You have been removed from the "{{ group_name }} group" + You have been removed from the "{{ group_name }}" group {% endblocktrans %} {% endif %} diff --git a/bookwyrm/templates/snippets/remove_from_group_button.html b/bookwyrm/templates/snippets/remove_from_group_button.html index 6d1de0dd3..1672e0388 100644 --- a/bookwyrm/templates/snippets/remove_from_group_button.html +++ b/bookwyrm/templates/snippets/remove_from_group_button.html @@ -13,10 +13,10 @@
From 7502158e58c9ac9e068e294e96bf2faddd3f13cb Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 24 Oct 2021 07:10:20 -0700 Subject: [PATCH 193/193] Builds latest changes --- locale/en_US/LC_MESSAGES/django.po | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 0521e64ba..7d8fc801a 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-23 15:11+0000\n" +"POT-Creation-Date: 2021-10-24 14:09+0000\n" "PO-Revision-Date: 2021-02-28 17:19-0800\n" "Last-Translator: Mouse Reeve \n" "Language-Team: English \n" @@ -549,6 +549,7 @@ msgstr "" #: bookwyrm/templates/book/edit/edit_book.html:97 #: bookwyrm/templates/groups/members.html:16 #: bookwyrm/templates/landing/password_reset.html:30 +#: bookwyrm/templates/snippets/remove_from_group_button.html:16 msgid "Confirm" msgstr "" @@ -1782,6 +1783,7 @@ msgid "Set" msgstr "" #: bookwyrm/templates/lists/list.html:92 +#: bookwyrm/templates/snippets/remove_from_group_button.html:19 #: bookwyrm/templates/snippets/shelf_selector.html:26 msgid "Remove" msgstr "" @@ -1946,7 +1948,7 @@ msgstr "" #: bookwyrm/templates/notifications/items/remove.html:23 #, python-format -msgid "You have been removed from the \"%(group_name)s group\"" +msgid "You have been removed from the \"%(group_name)s\" group" msgstr "" #: bookwyrm/templates/notifications/items/reply.html:21 @@ -3443,14 +3445,6 @@ msgstr "" msgid "Sign Up" msgstr "" -#: bookwyrm/templates/snippets/remove_from_group_button.html:16 -msgid " Confirm " -msgstr "" - -#: bookwyrm/templates/snippets/remove_from_group_button.html:19 -msgid " Remove " -msgstr "" - #: bookwyrm/templates/snippets/report_button.html:6 msgid "Report" msgstr ""