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_question_text",
"require_confirm_email",
"default_user_auth_group",
]
widgets = {

View file

@ -117,10 +117,12 @@ def init_connectors():
def init_settings():
"""info about the instance"""
group_editor = Group.objects.filter(name="editor").first()
models.SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm",
support_title="Patreon",
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
import uuid
import django.contrib.auth.models as auth_models
from django.core.exceptions import PermissionDenied
from django.db import models, IntegrityError
from django.dispatch import receiver
@ -70,6 +71,9 @@ class SiteSettings(SiteModel):
allow_invite_requests = models.BooleanField(default=True)
invite_request_question = models.BooleanField(default=False)
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(
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 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.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.dispatch import receiver
from django.db import models, transaction
from django.utils import timezone
@ -356,8 +356,14 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# make users editors by default
try:
self.groups.add(Group.objects.get(name="editor"))
except Group.DoesNotExist:
group = (
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
pass

View file

@ -38,6 +38,23 @@
{% trans "Allow registration" %}
</label>
</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">
<label class="label mb-0" for="id_require_confirm_email">
{{ form.require_confirm_email }}

View file

@ -70,10 +70,52 @@
<h2 class="title column">{% trans "User Activity" %}</h2>
{% if user.local %}
<div class="column is-narrow">
<a target="_blank" href="{{ user.local_path }}/rss" rel="nofollow noopener noreferrer">
<span class="icon icon-rss" aria-hidden="true"></span>
<span class="is-hidden-mobile">{% trans "RSS feed" %}</span>
</a>
<details class="dropdown">
<summary
class="is-relative pulldown-menu dropdown-trigger"
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>
{% endif %}
</div>

View file

@ -63,9 +63,19 @@ class InitDB(TestCase):
def test_init_settings(self):
"""Create the settings file"""
initdb.init_groups()
group_editor = Group.objects.get(name="editor")
initdb.init_settings()
settings = models.SiteSettings.objects.get()
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):
"""Common trusted domains for links"""

View file

@ -109,6 +109,36 @@ class User(TestCase):
self.assertEqual(activity["id"], self.user.outbox)
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):
server = models.FederatedServer.objects.create(
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.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"^@(?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-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(
rf"{USER_PATH}/(?P<direction>(followers|following))(.json)?/?$",
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 ReadingStatus
from .report import Report
from .rss_feed import RssFeed
from .rss_feed import (
RssFeed,
RssReviewsOnlyFeed,
RssQuotesOnlyFeed,
RssCommentsOnlyFeed,
)
from .search import Search
from .setup import InstanceConfig, CreateAdmin
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress

View file

@ -3,6 +3,7 @@
from django.contrib.syndication.views import Feed
from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _
from ..models import Review, Quotation, Comment
from .helpers import get_user_from_username
@ -42,3 +43,117 @@ class RssFeed(Feed):
def item_link(self, item):
"""link to the status"""
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