Merge branch 'main' into book-series-v1

This commit is contained in:
Dustin 2023-01-26 06:50:22 +00:00 committed by GitHub
commit 1d909ee8e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 335 additions and 9 deletions

View file

@ -91,6 +91,7 @@ class RegistrationForm(CustomForm):
"invite_request_question", "invite_request_question",
"invite_question_text", "invite_question_text",
"require_confirm_email", "require_confirm_email",
"default_user_auth_group",
] ]
widgets = { widgets = {

View file

@ -117,10 +117,12 @@ def init_connectors():
def init_settings(): def init_settings():
"""info about the instance""" """info about the instance"""
group_editor = Group.objects.filter(name="editor").first()
models.SiteSettings.objects.create( models.SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm", support_link="https://www.patreon.com/bookwyrm",
support_title="Patreon", support_title="Patreon",
install_mode=True, install_mode=True,
default_user_auth_group=group_editor,
) )

View file

@ -0,0 +1,34 @@
# Generated by Django 3.2.16 on 2022-12-27 21:34
from django.db import migrations, models
import django.db.models.deletion
def backfill_sitesettings(apps, schema_editor):
db_alias = schema_editor.connection.alias
group_model = apps.get_model("auth", "Group")
editor_group = group_model.objects.using(db_alias).filter(name="editor").first()
sitesettings_model = apps.get_model("bookwyrm", "SiteSettings")
sitesettings_model.objects.update(default_user_auth_group=editor_group)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0175_merge_0173_author_website_0174_merge_20230111_1523"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="default_user_auth_group",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.RESTRICT,
to="auth.group",
),
),
migrations.RunPython(backfill_sitesettings, migrations.RunPython.noop),
]

View file

@ -3,6 +3,7 @@ import datetime
from urllib.parse import urljoin from urllib.parse import urljoin
import uuid import uuid
import django.contrib.auth.models as auth_models
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models, IntegrityError from django.db import models, IntegrityError
from django.dispatch import receiver from django.dispatch import receiver
@ -70,6 +71,9 @@ class SiteSettings(SiteModel):
allow_invite_requests = models.BooleanField(default=True) allow_invite_requests = models.BooleanField(default=True)
invite_request_question = models.BooleanField(default=False) invite_request_question = models.BooleanField(default=False)
require_confirm_email = models.BooleanField(default=True) require_confirm_email = models.BooleanField(default=True)
default_user_auth_group = models.ForeignKey(
auth_models.Group, null=True, blank=True, on_delete=models.PROTECT
)
invite_question_text = models.CharField( invite_question_text = models.CharField(
max_length=255, blank=True, default="What is your favourite book?" max_length=255, blank=True, default="What is your favourite book?"

View file

@ -3,9 +3,9 @@ import re
from urllib.parse import urlparse from urllib.parse import urlparse
from django.apps import apps from django.apps import apps
from django.contrib.auth.models import AbstractUser, Group from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField, CICharField from django.contrib.postgres.fields import ArrayField, CICharField
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.dispatch import receiver from django.dispatch import receiver
from django.db import models, transaction from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
@ -356,8 +356,14 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# make users editors by default # make users editors by default
try: try:
self.groups.add(Group.objects.get(name="editor")) group = (
except Group.DoesNotExist: apps.get_model("bookwyrm.SiteSettings")
.objects.get()
.default_user_auth_group
)
if group:
self.groups.add(group)
except ObjectDoesNotExist:
# this should only happen in tests # this should only happen in tests
pass pass

View file

@ -38,6 +38,23 @@
{% trans "Allow registration" %} {% trans "Allow registration" %}
</label> </label>
</div> </div>
<div class="block">
<label class="label" for="id_default_user_auth_group">
{% trans "Default access level:" %}
</label>
<div class="select">
<select name="default_user_auth_group" id="id_default_user_auth_group" desc_default_user_auth_group="desc_default_user_auth_group">
{% for instance in form.default_user_auth_group.field.queryset %}
<option value="{{ instance.pk }}" {% if instance.pk == form.default_user_auth_group.value %}selected{% endif %}>
{{ instance.name|title }}
</option>
{% endfor %}
<option value="" {% if not form.default_user_auth_group.value %}selected{% endif %}>
User
</option>
</select>
</div>
</div>
<div class="field"> <div class="field">
<label class="label mb-0" for="id_require_confirm_email"> <label class="label mb-0" for="id_require_confirm_email">
{{ form.require_confirm_email }} {{ form.require_confirm_email }}

View file

@ -70,10 +70,52 @@
<h2 class="title column">{% trans "User Activity" %}</h2> <h2 class="title column">{% trans "User Activity" %}</h2>
{% if user.local %} {% if user.local %}
<div class="column is-narrow"> <div class="column is-narrow">
<a target="_blank" href="{{ user.local_path }}/rss" rel="nofollow noopener noreferrer"> <details class="dropdown">
<span class="icon icon-rss" aria-hidden="true"></span> <summary
<span class="is-hidden-mobile">{% trans "RSS feed" %}</span> class="is-relative pulldown-menu dropdown-trigger"
</a> aria-label="{% trans 'Show RSS Options' %}"
role="button"
aria-haspopup="menu"
>
<span class="">
<span class="icon icon-rss" aria-hidden="true"></span>
<span class="">{% trans "RSS feed" %}</span>
</span>
<span class="icon icon-arrow-down is-hidden-mobile" aria-hidden="true"></span>
<span class="summary-on-open">
<span class="icon icon-arrow-left is-small" aria-hidden="true"></span>
{% trans "Back" %}
</span>
</summary>
<div class="dropdown-menu">
<ul
class="dropdown-content"
role="menu"
>
<li role="menuitem">
<a target="_blank" href="{{ user.local_path }}/rss" class="navbar-item" rel="nofollow noopener noreferrer">
{% trans "Complete feed" %}
</a>
</li>
<li role="menuitem">
<a target="_blank" href="{{ user.local_path }}/rss-reviews" class="navbar-item" rel="nofollow noopener noreferrer">
{% trans "Reviews only" %}
</a>
</li>
<li role="menuitem">
<a target="_blank" href="{{ user.local_path }}/rss-quotes" class="navbar-item" rel="nofollow noopener noreferrer">
{% trans "Quotes only" %}
</a>
</li>
<li role="menuitem">
<a target="_blank" href="{{ user.local_path }}/rss-comments" class="navbar-item" rel="nofollow noopener noreferrer">
{% trans "Comments only" %}
</a>
</li>
</ul>
</div>
</details>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -63,9 +63,19 @@ class InitDB(TestCase):
def test_init_settings(self): def test_init_settings(self):
"""Create the settings file""" """Create the settings file"""
initdb.init_groups()
group_editor = Group.objects.get(name="editor")
initdb.init_settings() initdb.init_settings()
settings = models.SiteSettings.objects.get() settings = models.SiteSettings.objects.get()
self.assertEqual(settings.name, "BookWyrm") self.assertEqual(settings.name, "BookWyrm")
self.assertEqual(settings.default_user_auth_group, group_editor)
def test_init_settings_without_groups(self):
"""Create the settings, but without groups existing already"""
initdb.init_settings()
settings = models.SiteSettings.objects.get()
self.assertIsNone(settings.default_user_auth_group)
def test_init_link_domains(self): def test_init_link_domains(self):
"""Common trusted domains for links""" """Common trusted domains for links"""

View file

@ -109,6 +109,36 @@ class User(TestCase):
self.assertEqual(activity["id"], self.user.outbox) self.assertEqual(activity["id"], self.user.outbox)
self.assertEqual(activity["totalItems"], 0) self.assertEqual(activity["totalItems"], 0)
def test_save_auth_group(self):
user_attrs = {"local": True}
site = models.SiteSettings.get()
site.default_user_auth_group = Group.objects.get(name="editor")
site.save()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
user = models.User.objects.create_user(
f"test2{DOMAIN}",
"test2@bookwyrm.test",
localname="test2",
**user_attrs,
)
self.assertEqual(list(user.groups.all()), [Group.objects.get(name="editor")])
site.default_user_auth_group = None
site.save()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
user = models.User.objects.create_user(
f"test1{DOMAIN}",
"test1@bookwyrm.test",
localname="test1",
**user_attrs,
)
self.assertEqual(len(user.groups.all()), 0)
def test_set_remote_server(self): def test_set_remote_server(self):
server = models.FederatedServer.objects.create( server = models.FederatedServer.objects.create(
server_name=DOMAIN, application_type="test type", application_version=3 server_name=DOMAIN, application_type="test type", application_version=3

View file

@ -82,3 +82,48 @@ class RssFeedView(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertIn(b"a sickening sense", result.content) self.assertIn(b"a sickening sense", result.content)
def test_rss_comment_only(self, *_):
"""load an rss feed"""
models.Comment.objects.create(
content="comment test content",
user=self.local_user,
book=self.book,
)
view = rss_feed.RssCommentsOnlyFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
self.assertIn(b"Example Edition", result.content)
def test_rss_review_only(self, *_):
"""load an rss feed"""
models.Review.objects.create(
name="Review name",
content="test content",
rating=3,
user=self.local_user,
book=self.book,
)
view = rss_feed.RssReviewsOnlyFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
def test_rss_quotation_only(self, *_):
"""load an rss feed"""
models.Quotation.objects.create(
quote="a sickening sense",
content="test content",
user=self.local_user,
book=self.book,
)
view = rss_feed.RssQuotesOnlyFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
self.assertIn(b"a sickening sense", result.content)

View file

@ -424,6 +424,21 @@ urlpatterns = [
re_path(rf"{USER_PATH}/?$", views.User.as_view(), name="user-feed"), re_path(rf"{USER_PATH}/?$", views.User.as_view(), name="user-feed"),
re_path(rf"^@(?P<username>{regex.USERNAME})$", views.user_redirect), re_path(rf"^@(?P<username>{regex.USERNAME})$", views.user_redirect),
re_path(rf"{USER_PATH}/rss/?$", views.rss_feed.RssFeed(), name="user-rss"), re_path(rf"{USER_PATH}/rss/?$", views.rss_feed.RssFeed(), name="user-rss"),
re_path(
rf"{USER_PATH}/rss-reviews/?$",
views.rss_feed.RssReviewsOnlyFeed(),
name="user-reviews-rss",
),
re_path(
rf"{USER_PATH}/rss-quotes/?$",
views.rss_feed.RssQuotesOnlyFeed(),
name="user-quotes-rss",
),
re_path(
rf"{USER_PATH}/rss-comments/?$",
views.rss_feed.RssCommentsOnlyFeed(),
name="user-comments-rss",
),
re_path( re_path(
rf"{USER_PATH}/(?P<direction>(followers|following))(.json)?/?$", rf"{USER_PATH}/(?P<direction>(followers|following))(.json)?/?$",
views.Relationships.as_view(), views.Relationships.as_view(),

View file

@ -138,7 +138,12 @@ from .outbox import Outbox
from .reading import ReadThrough, delete_readthrough, delete_progressupdate from .reading import ReadThrough, delete_readthrough, delete_progressupdate
from .reading import ReadingStatus from .reading import ReadingStatus
from .report import Report from .report import Report
from .rss_feed import RssFeed from .rss_feed import (
RssFeed,
RssReviewsOnlyFeed,
RssQuotesOnlyFeed,
RssCommentsOnlyFeed,
)
from .search import Search from .search import Search
from .setup import InstanceConfig, CreateAdmin from .setup import InstanceConfig, CreateAdmin
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress from .status import CreateStatus, EditStatus, DeleteStatus, update_progress

View file

@ -3,6 +3,7 @@
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ..models import Review, Quotation, Comment
from .helpers import get_user_from_username from .helpers import get_user_from_username
@ -42,3 +43,117 @@ class RssFeed(Feed):
def item_link(self, item): def item_link(self, item):
"""link to the status""" """link to the status"""
return item.local_path return item.local_path
class RssReviewsOnlyFeed(Feed):
"""serialize user's reviews in rss feed"""
description_template = "rss/content.html"
def item_title(self, item):
"""render the item title"""
if hasattr(item, "pure_name") and item.pure_name:
return item.pure_name
title_template = get_template("snippets/status/header_content.html")
title = title_template.render({"status": item})
template = get_template("rss/title.html")
return template.render({"user": item.user, "item_title": title}).strip()
def get_object(self, request, username): # pylint: disable=arguments-differ
"""the user who's posts get serialized"""
return get_user_from_username(request.user, username)
def link(self, obj):
"""link to the user's profile"""
return obj.local_path
def title(self, obj):
"""title of the rss feed entry"""
return _(f"Reviews from {obj.display_name}")
def items(self, obj):
"""the user's activity feed"""
return Review.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path
class RssQuotesOnlyFeed(Feed):
"""serialize user's quotes in rss feed"""
description_template = "rss/content.html"
def item_title(self, item):
"""render the item title"""
if hasattr(item, "pure_name") and item.pure_name:
return item.pure_name
title_template = get_template("snippets/status/header_content.html")
title = title_template.render({"status": item})
template = get_template("rss/title.html")
return template.render({"user": item.user, "item_title": title}).strip()
def get_object(self, request, username): # pylint: disable=arguments-differ
"""the user who's posts get serialized"""
return get_user_from_username(request.user, username)
def link(self, obj):
"""link to the user's profile"""
return obj.local_path
def title(self, obj):
"""title of the rss feed entry"""
return _(f"Quotes from {obj.display_name}")
def items(self, obj):
"""the user's activity feed"""
return Quotation.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path
class RssCommentsOnlyFeed(Feed):
"""serialize user's quotes in rss feed"""
description_template = "rss/content.html"
def item_title(self, item):
"""render the item title"""
if hasattr(item, "pure_name") and item.pure_name:
return item.pure_name
title_template = get_template("snippets/status/header_content.html")
title = title_template.render({"status": item})
template = get_template("rss/title.html")
return template.render({"user": item.user, "item_title": title}).strip()
def get_object(self, request, username): # pylint: disable=arguments-differ
"""the user who's posts get serialized"""
return get_user_from_username(request.user, username)
def link(self, obj):
"""link to the user's profile"""
return obj.local_path
def title(self, obj):
"""title of the rss feed entry"""
return _(f"Comments from {obj.display_name}")
def items(self, obj):
"""the user's activity feed"""
return Comment.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path