Merge branch 'themes' into dark-theme

This commit is contained in:
Mouse Reeve 2022-02-28 13:32:37 -08:00 committed by GitHub
commit 2c8fa5cd9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 289 additions and 109 deletions

View file

@ -1,3 +0,0 @@
@charset "utf-8";
// Copy this file to bookwyrm/static/css/ and set your instance custom styles.

3
.gitignore vendored
View file

@ -17,7 +17,8 @@
.env .env
/images/ /images/
bookwyrm/static/css/bookwyrm.css bookwyrm/static/css/bookwyrm.css
bookwyrm/static/css/_instance-settings.scss bookwyrm/static/css/themes/
!bookwyrm/static/css/themes/bookwyrm-*.scss
# Testing # Testing
.coverage .coverage

View file

@ -13,7 +13,7 @@ Social reading and reviewing, decentralized with ActivityPub
- [Set up Bookwyrm](#set-up-bookwyrm) - [Set up Bookwyrm](#set-up-bookwyrm)
## Joining BookWyrm ## Joining BookWyrm
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://docs.joinbookwyrm.com/instances.html) list. BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
You can request an invite by entering your email address at https://bookwyrm.social. You can request an invite by entering your email address at https://bookwyrm.social.

View file

@ -39,4 +39,5 @@ class Person(ActivityObject):
bookwyrmUser: bool = False bookwyrmUser: bool = False
manuallyApprovesFollowers: str = False manuallyApprovesFollowers: str = False
discoverable: str = False discoverable: str = False
hideFollows: str = False
type: str = "Person" type: str = "Person"

View file

@ -153,8 +153,10 @@ class EditUserForm(CustomForm):
"manually_approves_followers", "manually_approves_followers",
"default_post_privacy", "default_post_privacy",
"discoverable", "discoverable",
"hide_follows",
"preferred_timezone", "preferred_timezone",
"preferred_language", "preferred_language",
"theme",
] ]
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
widgets = { widgets = {

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2022-02-28 19:44
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0141_alter_report_status"),
]
operations = [
migrations.AddField(
model_name="user",
name="hide_follows",
field=bookwyrm.models.fields.BooleanField(default=False),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.12 on 2022-02-28 21:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0142_auto_20220227_1752"),
("bookwyrm", "0142_user_hide_follows"),
]
operations = []

View file

@ -227,7 +227,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@classmethod @classmethod
def privacy_filter(cls, viewer, privacy_levels=None): def privacy_filter(cls, viewer, privacy_levels=None):
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels) queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
return queryset.filter(deleted=False) return queryset.filter(deleted=False, user__is_active=True)
@classmethod @classmethod
def direct_filter(cls, queryset, viewer): def direct_filter(cls, queryset, viewer):

View file

@ -137,6 +137,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
last_active_date = models.DateTimeField(default=timezone.now) last_active_date = models.DateTimeField(default=timezone.now)
manually_approves_followers = fields.BooleanField(default=False) manually_approves_followers = fields.BooleanField(default=False)
theme = models.ForeignKey("Theme", null=True, blank=True, on_delete=models.SET_NULL) theme = models.ForeignKey("Theme", null=True, blank=True, on_delete=models.SET_NULL)
hide_follows = fields.BooleanField(default=False)
# options to turn features on and off # options to turn features on and off
show_goal = models.BooleanField(default=True) show_goal = models.BooleanField(default=True)
@ -490,10 +491,13 @@ def set_remote_server(user_id):
get_remote_reviews.delay(user.outbox) get_remote_reviews.delay(user.outbox)
def get_or_create_remote_server(domain): def get_or_create_remote_server(domain, refresh=False):
"""get info on a remote server""" """get info on a remote server"""
server = FederatedServer()
try: try:
return FederatedServer.objects.get(server_name=domain) server = FederatedServer.objects.get(server_name=domain)
if not refresh:
return server
except FederatedServer.DoesNotExist: except FederatedServer.DoesNotExist:
pass pass
@ -508,13 +512,15 @@ def get_or_create_remote_server(domain):
application_type = data.get("software", {}).get("name") application_type = data.get("software", {}).get("name")
application_version = data.get("software", {}).get("version") application_version = data.get("software", {}).get("version")
except ConnectorException: except ConnectorException:
if server.id:
return server
application_type = application_version = None application_type = application_version = None
server = FederatedServer.objects.create( server.server_name = domain
server_name=domain, server.application_type = application_type
application_type=application_type, server.application_version = application_version
application_version=application_version,
) server.save()
return server return server

View file

@ -1,6 +1,4 @@
@charset "utf-8"; @charset "utf-8";
@import "instance-settings";
@import "vendor/bulma/bulma.sass"; @import "vendor/bulma/bulma.sass";
@import "vendor/icons.css";
@import "bookwyrm/all.scss"; @import "bookwyrm/all.scss";

View file

@ -80,4 +80,5 @@ $family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif; $family-secondary: $family-sans-serif;
@import "../bookwyrm.scss"; @import "../bookwyrm.scss";
@import "../vendor/icons.css";

View file

@ -55,5 +55,5 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66);
$family-primary: $family-sans-serif; $family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif; $family-secondary: $family-sans-serif;
@import "../bookwyrm.scss";
@import "../bookwyrm.scss"; @import "../vendor/icons.css";

View file

@ -352,7 +352,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% if request.user.list_set.exists %} {% if list_options.exists %}
<form name="list-add" method="post" action="{% url 'list-add-book' %}"> <form name="list-add" method="post" action="{% url 'list-add-book' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
@ -361,7 +361,7 @@
<div class="field has-addons"> <div class="field has-addons">
<div class="select control is-clipped"> <div class="select control is-clipped">
<select name="book_list" id="id_list"> <select name="book_list" id="id_list">
{% for list in user.list_set.all %} {% for list in list_options %}
<option value="{{ list.id }}">{{ list.name }}</option> <option value="{{ list.id }}">{{ list.name }}</option>
{% endfor %} {% endfor %}
</select> </select>

View file

@ -33,7 +33,7 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
{% if user != request.user %} {% if user != request.user %}
{% if user.mutuals %} {% if user.mutuals and not user.hide_follows %}
<div class="card-footer-item"> <div class="card-footer-item">
<div class="has-text-centered"> <div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.mutuals }}</p> <p class="title is-6 mb-0">{{ user.mutuals }}</p>

View file

@ -15,7 +15,7 @@
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span> <span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
</a> </a>
{% include 'snippets/add_to_group_button.html' with user=user group=group %} {% include 'snippets/add_to_group_button.html' with user=user group=group %}
{% if user.mutuals %} {% if user.mutuals and not user.hide_follows %}
<p class="help"> <p class="help">
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %} {% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
{{ mutuals }} follower you follow {{ mutuals }} follower you follow

View file

@ -30,13 +30,23 @@
<div class="columns mt-3"> <div class="columns mt-3">
<section class="column is-three-quarters"> <section class="column is-three-quarters">
{% if request.GET.updated %} {% if add_failed %}
<div class="notification is-primary"> <div class="notification is-danger is-light">
{% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %} <span class="icon icon-x" aria-hidden="true"></span>
{% trans "You successfully suggested a book for this list!" %} <span>
{% else %} {% trans "That book is already on this list." %}
{% trans "You successfully added a book to this list!" %} </span>
{% endif %} </div>
{% elif add_succeeded %}
<div class="notification is-success is-light">
<span class="icon icon-check" aria-hidden="true"></span>
<span>
{% 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!" %}
{% endif %}
</span>
</div> </div>
{% endif %} {% endif %}

View file

@ -97,6 +97,12 @@
{{ form.preferred_language }} {{ form.preferred_language }}
</div> </div>
</div> </div>
<div class="field">
<label class="label" for="id_them">{% trans "Theme:" %}</label>
<div class="select">
{{ form.theme }}
</div>
</div>
</div> </div>
</section> </section>
@ -111,6 +117,12 @@
{% trans "Manually approve followers" %} {% trans "Manually approve followers" %}
</label> </label>
</div> </div>
<div class="field">
<label class="checkbox label" for="id_hide_follows">
{{ form.hide_follows }}
{% trans "Hide followers and following on profile" %}
</label>
</div>
<div class="field"> <div class="field">
<label class="label" for="id_default_post_privacy"> <label class="label" for="id_default_post_privacy">
{% trans "Default post privacy:" %} {% trans "Default post privacy:" %}

View file

@ -4,7 +4,19 @@
{% block header %} {% block header %}
{% trans "Add instance" %} {% trans "Add instance" %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a> {% endblock %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% trans "Add instance" %}
</a>
</li>
</ul>
</nav>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}
@ -73,9 +85,13 @@
<label class="label" for="id_notes"> <label class="label" for="id_notes">
{% trans "Notes:" %} {% trans "Notes:" %}
</label> </label>
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes"> <textarea
{{ form.notes.value|default:'' }} name="notes"
</textarea> cols="40"
rows="5"
class="textarea"
id="id_notes"
>{{ form.notes.value|default:'' }}</textarea>
</div> </div>
<button type="submit" class="button is-primary"> <button type="submit" class="button is-primary">

View file

@ -9,8 +9,26 @@
{% if server.status == "blocked" %}<span class="icon icon-x has-text-danger is-size-5" title="{% trans 'Blocked' %}"><span class="is-sr-only">{% trans "Blocked" %}</span></span> {% if server.status == "blocked" %}<span class="icon icon-x has-text-danger is-size-5" title="{% trans 'Blocked' %}"><span class="is-sr-only">{% trans "Blocked" %}</span></span>
{% endif %} {% endif %}
{% endblock %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to list" %}</a> {% block edit-button %}
<form name="reload" method="POST" action="{% url 'settings-federated-server-refresh' server.id %}">
{% csrf_token %}
<button class="button" type="submit">{% trans "Refresh data" %}</button>
</form>
{% endblock %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{{ server.server_name }}
</a>
</li>
</ul>
</nav>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}

View file

@ -4,7 +4,19 @@
{% block header %} {% block header %}
{% trans "Import Blocklist" %} {% trans "Import Blocklist" %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a> {% endblock %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% trans "Import Blocklist" %}
</a>
</li>
</ul>
</nav>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}

View file

@ -16,11 +16,11 @@
<ul> <ul>
{% url 'settings-federation' status='federated' as url %} {% url 'settings-federation' status='federated' as url %}
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}> <li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Federated" %}</a> <a href="{{ url }}">{% trans "Federated" %} ({{ federated_count }})</a>
</li> </li>
{% url 'settings-federation' status='blocked' as url %} {% url 'settings-federation' status='blocked' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}> <li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Blocked" %}</a> <a href="{{ url }}">{% trans "Blocked" %} ({{ blocked_count }})</a>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -5,6 +5,12 @@
{% block header %}{% trans "Themes" %}{% endblock %} {% block header %}{% trans "Themes" %}{% endblock %}
{% block breadcrumbs %}
<a class="subtitle help is-link" href="{% url 'settings-site' %}/#display">
{% trans "Set instance default theme" %}
</a>
{% endblock %}
{% block panel %} {% block panel %}
{% if success %} {% if success %}
<div class="notification is-success is-light"> <div class="notification is-success is-light">
@ -117,8 +123,12 @@
<td>{{ theme.name }}</td> <td>{{ theme.name }}</td>
<td><code>{{ theme.path }}</code></td> <td><code>{{ theme.path }}</code></td>
<td> <td>
<form> <form method="POST" action="{% url 'settings-themes-delete' theme.id %}">
<button type="submit" class="button is-danger is-light">{% trans "Remove theme" %}</button> {% csrf_token %}
<button type="submit" class="button is-danger is-light is-small">
<span class="icon icon-x" aria-hideen="true"></span>
<span>{% trans "Remove theme" %}</span>
</button>
</form> </form>
</td> </td>
</tr> </tr>

View file

@ -71,7 +71,9 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ user_shelf.id }}"> <input type="hidden" name="shelf" value="{{ user_shelf.id }}">
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove from" %} {{ user_shelf.name }}</button> <button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">
{% blocktrans with name=user_shelf|translate_shelf_name %}Remove from {{ name }}{% endblocktrans %}
</button>
</form> </form>
</li> </li>
{% endif %} {% endif %}

View file

@ -63,7 +63,7 @@
<input type="hidden" name="book" value="{{ active_shelf.book.id }}"> <input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}"> <input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
<button class="button is-fullwidth is-small{% if dropdown %} is-radiusless{% endif %} is-danger is-light" type="submit"> <button class="button is-fullwidth is-small{% if dropdown %} is-radiusless{% endif %} is-danger is-light" type="submit">
{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %} {% blocktrans with name=active_shelf.shelf|translate_shelf_name %}Remove from {{ name }}{% endblocktrans %}
</button> </button>
</form> </form>
</li> </li>

View file

@ -11,7 +11,7 @@
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span> <span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
</a> </a>
{% include 'snippets/follow_button.html' with user=user minimal=True %} {% include 'snippets/follow_button.html' with user=user minimal=True %}
{% if user.mutuals %} {% if user.mutuals and not user.hide_follows %}
<p class="help"> <p class="help">
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %} {% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
{{ mutuals }} follower you follow {{ mutuals }} follower you follow

View file

@ -1,12 +1,4 @@
{% load i18n %} {% load i18n %}
{% if shelf.identifier == 'all' %} {% load shelf_tags %}
{% trans "All books" %}
{% elif shelf.identifier == 'to-read' %} {{ shelf|translate_shelf_name }}
{% trans "To Read" %}
{% elif shelf.identifier == 'reading' %}
{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}
{% trans "Read" %}
{% else %}
{{ shelf.name }}
{% endif %}

View file

@ -28,16 +28,22 @@
{% elif request.user.is_authenticated %} {% elif request.user.is_authenticated %}
{% mutuals_count user as mutuals %} {% if user.hide_follows %}
<a href="{% url 'user-followers' user|username %}"> {% if request.user in user.following.all %}
{% if mutuals %} {% trans "Follows you" %}
{% blocktrans with mutuals_display=mutuals|intcomma count counter=mutuals %}{{ mutuals_display }} follower you follow{% plural %}{{ mutuals_display }} followers you follow{% endblocktrans %}
{% elif request.user in user.following.all %}
{% trans "Follows you" %}
{% else %}
{% trans "No followers you follow" %}
{% endif %} {% endif %}
</a> {% else %}
{% mutuals_count user as mutuals %}
<a href="{% url 'user-followers' user|username %}">
{% if mutuals %}
{% blocktrans with mutuals_display=mutuals|intcomma count counter=mutuals %}{{ mutuals_display }} follower you follow{% plural %}{{ mutuals_display }} followers you follow{% endblocktrans %}
{% elif request.user in user.following.all %}
{% trans "Follows you" %}
{% else %}
{% trans "No followers you follow" %}
{% endif %}
</a>
{% endif %}
{% endif %} {% endif %}
</p> </p>

View file

@ -1,5 +1,6 @@
""" Filters and tags related to shelving books """ """ Filters and tags related to shelving books """
from django import template from django import template
from django.utils.translation import gettext_lazy as _
from bookwyrm import models from bookwyrm import models
from bookwyrm.utils import cache from bookwyrm.utils import cache
@ -32,6 +33,24 @@ def get_next_shelf(current_shelf):
return "to-read" return "to-read"
@register.filter(name="translate_shelf_name")
def get_translated_shelf_name(shelf):
"""produced translated shelf nidentifierame"""
if not shelf:
return ""
# support obj or dict
identifier = shelf["identifier"] if isinstance(shelf, dict) else shelf.identifier
if identifier == "all":
return _("All books")
if identifier == "to-read":
return _("To Read")
if identifier == "reading":
return _("Currently Reading")
if identifier == "read":
return _("Read")
return shelf["name"] if isinstance(shelf, dict) else shelf.name
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def active_shelf(context, book): def active_shelf(context, book):
"""check what shelf a user has a book on, if any""" """check what shelf a user has a book on, if any"""

View file

@ -87,6 +87,11 @@ urlpatterns = [
), ),
re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"), re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
re_path(r"^settings/themes/?$", views.Themes.as_view(), name="settings-themes"), re_path(r"^settings/themes/?$", views.Themes.as_view(), name="settings-themes"),
re_path(
r"^settings/themes/(?P<theme_id>\d+)/delete/?$",
views.delete_theme,
name="settings-themes-delete",
),
re_path( re_path(
r"^settings/announcements/?$", r"^settings/announcements/?$",
views.Announcements.as_view(), views.Announcements.as_view(),
@ -145,6 +150,11 @@ urlpatterns = [
views.unblock_server, views.unblock_server,
name="settings-federated-server-unblock", name="settings-federated-server-unblock",
), ),
re_path(
r"^settings/federation/(?P<server>\d+)/refresh/?$",
views.refresh_server,
name="settings-federated-server-refresh",
),
re_path( re_path(
r"^settings/federation/add/?$", r"^settings/federation/add/?$",
views.AddFederatedServer.as_view(), views.AddFederatedServer.as_view(),

View file

@ -6,7 +6,7 @@ from .admin.automod import AutoMod, automod_delete, run_automod
from .admin.dashboard import Dashboard from .admin.dashboard import Dashboard
from .admin.federation import Federation, FederatedServer from .admin.federation import Federation, FederatedServer
from .admin.federation import AddFederatedServer, ImportServerBlocklist from .admin.federation import AddFederatedServer, ImportServerBlocklist
from .admin.federation import block_server, unblock_server from .admin.federation import block_server, unblock_server, refresh_server
from .admin.email_blocklist import EmailBlocklist from .admin.email_blocklist import EmailBlocklist
from .admin.ip_blocklist import IPBlocklist from .admin.ip_blocklist import IPBlocklist
from .admin.invite import ManageInvites, Invite, InviteRequest from .admin.invite import ManageInvites, Invite, InviteRequest
@ -21,7 +21,7 @@ from .admin.reports import (
moderator_delete_user, moderator_delete_user,
) )
from .admin.site import Site from .admin.site import Site
from .admin.themes import Themes from .admin.themes import Themes, delete_theme
from .admin.user_admin import UserAdmin, UserAdminList from .admin.user_admin import UserAdmin, UserAdminList
# user preferences # user preferences

View file

@ -11,6 +11,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.models.user import get_or_create_remote_server
# pylint: disable= no-self-use # pylint: disable= no-self-use
@ -37,6 +38,12 @@ class Federation(View):
page = paginated.get_page(request.GET.get("page")) page = paginated.get_page(request.GET.get("page"))
data = { data = {
"federated_count": models.FederatedServer.objects.filter(
status="federated"
).count(),
"blocked_count": models.FederatedServer.objects.filter(
status="blocked"
).count(),
"servers": page, "servers": page,
"page_range": paginated.get_elided_page_range( "page_range": paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1 page.number, on_each_side=2, on_ends=1
@ -157,3 +164,14 @@ def unblock_server(request, server):
server = get_object_or_404(models.FederatedServer, id=server) server = get_object_or_404(models.FederatedServer, id=server)
server.unblock() server.unblock()
return redirect("settings-federated-server", server.id) return redirect("settings-federated-server", server.id)
@login_required
@require_POST
@permission_required("bookwyrm.control_federation", raise_exception=True)
# pylint: disable=unused-argument
def refresh_server(request, server):
"""unblock a server"""
server = get_object_or_404(models.FederatedServer, id=server)
get_or_create_remote_server(server.server_name, refresh=True)
return redirect("settings-federated-server", server.id)

View file

@ -2,9 +2,11 @@
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.staticfiles.utils import get_files from django.contrib.staticfiles.utils import get_files
from django.contrib.staticfiles.storage import StaticFilesStorage from django.contrib.staticfiles.storage import StaticFilesStorage
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models from bookwyrm import forms, models
@ -46,3 +48,12 @@ def get_view_data():
"choices": [c for c in choices if c not in current and c[-5:] == ".scss"], "choices": [c for c in choices if c not in current and c[-5:] == ".scss"],
"theme_form": forms.ThemeForm(), "theme_form": forms.ThemeForm(),
} }
@require_POST
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
# pylint: disable=unused-argument
def delete_theme(request, theme_id):
"""Remove a theme"""
get_object_or_404(models.Theme, id=theme_id).delete()
return redirect("settings-themes")

View file

@ -83,6 +83,7 @@ class Book(View):
} }
if request.user.is_authenticated: if request.user.is_authenticated:
data["list_options"] = request.user.list_set.exclude(id__in=data["lists"])
data["file_link_form"] = forms.FileLinkForm() data["file_link_form"] = forms.FileLinkForm()
readthroughs = models.ReadThrough.objects.filter( readthroughs = models.ReadThrough.objects.filter(
user=request.user, user=request.user,

View file

@ -1,11 +1,10 @@
""" book list views""" """ book list views"""
from typing import Optional from typing import Optional
from urllib.parse import urlencode
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db import IntegrityError, transaction from django.db import transaction
from django.db.models import Avg, DecimalField, Q, Max from django.db.models import Avg, DecimalField, Q, Max
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.http import HttpResponseBadRequest, HttpResponse from django.http import HttpResponseBadRequest, HttpResponse
@ -26,7 +25,7 @@ from bookwyrm.views.helpers import is_api_request
class List(View): class List(View):
"""book list page""" """book list page"""
def get(self, request, list_id): def get(self, request, list_id, add_failed=False, add_succeeded=False):
"""display a book list""" """display a book list"""
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
book_list.raise_visible_to_user(request.user) book_list.raise_visible_to_user(request.user)
@ -37,33 +36,10 @@ class List(View):
query = request.GET.get("q") query = request.GET.get("q")
suggestions = None suggestions = None
# sort_by shall be "order" unless a valid alternative is given items = book_list.listitem_set.filter(approved=True).prefetch_related(
sort_by = request.GET.get("sort_by", "order") "user", "book", "book__authors"
if sort_by not in ("order", "title", "rating"): )
sort_by = "order" items = sort_list(request, items)
# direction shall be "ascending" unless a valid alternative is given
direction = request.GET.get("direction", "ascending")
if direction not in ("ascending", "descending"):
direction = "ascending"
directional_sort_by = {
"order": "order",
"title": "book__title",
"rating": "average_rating",
}[sort_by]
if direction == "descending":
directional_sort_by = "-" + directional_sort_by
items = book_list.listitem_set.prefetch_related("user", "book", "book__authors")
if sort_by == "rating":
items = items.annotate(
average_rating=Avg(
Coalesce("book__review__rating", 0.0),
output_field=DecimalField(),
)
)
items = items.filter(approved=True).order_by(directional_sort_by)
paginated = Paginator(items, PAGE_LENGTH) paginated = Paginator(items, PAGE_LENGTH)
@ -106,10 +82,10 @@ class List(View):
"suggested_books": suggestions, "suggested_books": suggestions,
"list_form": forms.ListForm(instance=book_list), "list_form": forms.ListForm(instance=book_list),
"query": query or "", "query": query or "",
"sort_form": forms.SortListForm( "sort_form": forms.SortListForm(request.GET),
{"direction": direction, "sort_by": sort_by}
),
"embed_url": embed_url, "embed_url": embed_url,
"add_failed": add_failed,
"add_succeeded": add_succeeded,
} }
return TemplateResponse(request, "lists/list.html", data) return TemplateResponse(request, "lists/list.html", data)
@ -131,6 +107,36 @@ class List(View):
return redirect(book_list.local_path) return redirect(book_list.local_path)
def sort_list(request, items):
"""helper to handle the surprisngly involved sorting"""
# sort_by shall be "order" unless a valid alternative is given
sort_by = request.GET.get("sort_by", "order")
if sort_by not in ("order", "title", "rating"):
sort_by = "order"
# direction shall be "ascending" unless a valid alternative is given
direction = request.GET.get("direction", "ascending")
if direction not in ("ascending", "descending"):
direction = "ascending"
directional_sort_by = {
"order": "order",
"title": "book__title",
"rating": "average_rating",
}[sort_by]
if direction == "descending":
directional_sort_by = "-" + directional_sort_by
if sort_by == "rating":
items = items.annotate(
average_rating=Avg(
Coalesce("book__review__rating", 0.0),
output_field=DecimalField(),
)
)
return items.order_by(directional_sort_by)
@require_POST @require_POST
@login_required @login_required
def save_list(request, list_id): def save_list(request, list_id):
@ -179,8 +185,8 @@ def add_book(request):
form = forms.ListItemForm(request.POST) form = forms.ListItemForm(request.POST)
if not form.is_valid(): if not form.is_valid():
# this shouldn't happen, there aren't validated fields return List().get(request, book_list.id, add_failed=True)
raise Exception(form.errors)
item = form.save(commit=False) item = form.save(commit=False)
if book_list.curation == "curated": if book_list.curation == "curated":
@ -196,17 +202,9 @@ def add_book(request):
) or 0 ) or 0
increment_order_in_reverse(book_list.id, order_max + 1) increment_order_in_reverse(book_list.id, order_max + 1)
item.order = order_max + 1 item.order = order_max + 1
item.save()
try: return List().get(request, book_list.id, add_succeeded=True)
item.save()
except IntegrityError:
# if the book is already on the list, don't flip out
pass
path = reverse("list", args=[book_list.id])
params = request.GET.copy()
params["updated"] = True
return redirect(f"{path}?{urlencode(params)}")
@require_POST @require_POST

View file

@ -1,5 +1,6 @@
""" non-interactive pages """ """ non-interactive pages """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q, Count from django.db.models import Q, Count
from django.http import Http404 from django.http import Http404
@ -105,6 +106,9 @@ class Followers(View):
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(user.to_followers_activity(**request.GET)) return ActivitypubResponse(user.to_followers_activity(**request.GET))
if user.hide_follows:
raise PermissionDenied()
followers = annotate_if_follows(request.user, user.followers) followers = annotate_if_follows(request.user, user.followers)
paginated = Paginator(followers.all(), PAGE_LENGTH) paginated = Paginator(followers.all(), PAGE_LENGTH)
data = { data = {
@ -125,6 +129,9 @@ class Following(View):
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(user.to_following_activity(**request.GET)) return ActivitypubResponse(user.to_following_activity(**request.GET))
if user.hide_follows:
raise PermissionDenied()
following = annotate_if_follows(request.user, user.following) following = annotate_if_follows(request.user, user.following)
paginated = Paginator(following.all(), PAGE_LENGTH) paginated = Paginator(following.all(), PAGE_LENGTH)
data = { data = {