1
0
Fork 0

Merge branch 'main' into wikidata

This commit is contained in:
Carlos Cámara 2024-02-04 20:34:51 +01:00 committed by GitHub
commit 71f527eb1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 659 additions and 207 deletions

View file

@ -137,3 +137,10 @@ TWO_FACTOR_LOGIN_MAX_SECONDS=60
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default. # and AWS_S3_CUSTOM_DOMAIN (if used) are added by default.
# Value should be a comma-separated list of host names. # Value should be a comma-separated list of host names.
CSP_ADDITIONAL_HOSTS= CSP_ADDITIONAL_HOSTS=
# The last number here means "megabytes"
# Increase if users are having trouble uploading BookWyrm export files.
DATA_UPLOAD_MAX_MEMORY_SIZE = (1024**2 * 100)
# Time before being logged out (in seconds)
# SESSION_COOKIE_AGE=2592000 # current default: 30 days

View file

@ -1 +1 @@
0.7.1 0.7.2

View file

@ -1,54 +0,0 @@
""" Get your admin code to allow install """
from django.core.management.base import BaseCommand
from bookwyrm import models
from bookwyrm.settings import VERSION
# pylint: disable=no-self-use
class Command(BaseCommand):
"""command-line options"""
help = "What version is this?"
def add_arguments(self, parser):
"""specify which function to run"""
parser.add_argument(
"--current",
action="store_true",
help="Version stored in database",
)
parser.add_argument(
"--target",
action="store_true",
help="Version stored in settings",
)
parser.add_argument(
"--update",
action="store_true",
help="Update database version",
)
# pylint: disable=unused-argument
def handle(self, *args, **options):
"""execute init"""
site = models.SiteSettings.objects.get()
current = site.version or "0.0.1"
target = VERSION
if options.get("current"):
print(current)
return
if options.get("target"):
print(target)
return
if options.get("update"):
site.version = target
site.save()
return
if current != target:
print(f"{current}/{target}")
else:
print(current)

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.23 on 2024-01-04 23:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0191_merge_20240102_0326"),
]
operations = [
migrations.AlterField(
model_name="quotation",
name="endposition",
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="quotation",
name="position",
field=models.TextField(blank=True, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-01-02 19:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0191_merge_20240102_0326"),
]
operations = [
migrations.RenameField(
model_name="sitesettings",
old_name="version",
new_name="available_version",
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-01-16 10:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0191_merge_20240102_0326"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="user_exports_enabled",
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-02-03 15:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0192_make_page_positions_text"),
("bookwyrm", "0192_sitesettings_user_exports_enabled"),
]
operations = []

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-02-03 16:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0192_rename_version_sitesettings_available_version"),
("bookwyrm", "0193_merge_20240203_1539"),
]
operations = []

View file

@ -152,8 +152,9 @@ class ActivitypubMixin:
# find anyone who's tagged in a status, for example # find anyone who's tagged in a status, for example
mentions = self.recipients if hasattr(self, "recipients") else [] mentions = self.recipients if hasattr(self, "recipients") else []
# we always send activities to explicitly mentioned users' inboxes # we always send activities to explicitly mentioned users (using shared inboxes
recipients = [u.inbox for u in mentions or [] if not u.local] # where available to avoid duplicate submissions to a given instance)
recipients = {u.shared_inbox or u.inbox for u in mentions if not u.local}
# unless it's a dm, all the followers should receive the activity # unless it's a dm, all the followers should receive the activity
if privacy != "direct": if privacy != "direct":
@ -173,18 +174,18 @@ class ActivitypubMixin:
if user: if user:
queryset = queryset.filter(following=user) queryset = queryset.filter(following=user)
# ideally, we will send to shared inboxes for efficiency # as above, we prefer shared inboxes if available
shared_inboxes = ( recipients.update(
queryset.filter(shared_inbox__isnull=False) queryset.filter(shared_inbox__isnull=False).values_list(
.values_list("shared_inbox", flat=True) "shared_inbox", flat=True
.distinct() )
) )
# but not everyone has a shared inbox recipients.update(
inboxes = queryset.filter(shared_inbox__isnull=True).values_list( queryset.filter(shared_inbox__isnull=True).values_list(
"inbox", flat=True "inbox", flat=True
)
) )
recipients += list(shared_inboxes) + list(inboxes) return list(recipients)
return list(set(recipients))
def to_activity_dataclass(self): def to_activity_dataclass(self):
"""convert from a model to an activity""" """convert from a model to an activity"""

View file

@ -10,8 +10,11 @@ from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from model_utils import FieldTracker from model_utils import FieldTracker
from bookwyrm.connectors.abstract_connector import get_data
from bookwyrm.preview_images import generate_site_preview_image_task from bookwyrm.preview_images import generate_site_preview_image_task
from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
from bookwyrm.settings import RELEASE_API
from bookwyrm.tasks import app, MISC
from .base_model import BookWyrmModel, new_access_code from .base_model import BookWyrmModel, new_access_code
from .user import User from .user import User
from .fields import get_absolute_url from .fields import get_absolute_url
@ -45,7 +48,7 @@ class SiteSettings(SiteModel):
default_theme = models.ForeignKey( default_theme = models.ForeignKey(
"Theme", null=True, blank=True, on_delete=models.SET_NULL "Theme", null=True, blank=True, on_delete=models.SET_NULL
) )
version = models.CharField(null=True, blank=True, max_length=10) available_version = models.CharField(null=True, blank=True, max_length=10)
# admin setup options # admin setup options
install_mode = models.BooleanField(default=False) install_mode = models.BooleanField(default=False)
@ -96,6 +99,7 @@ class SiteSettings(SiteModel):
imports_enabled = models.BooleanField(default=True) imports_enabled = models.BooleanField(default=True)
import_size_limit = models.IntegerField(default=0) import_size_limit = models.IntegerField(default=0)
import_limit_reset = models.IntegerField(default=0) import_limit_reset = models.IntegerField(default=0)
user_exports_enabled = models.BooleanField(default=False)
user_import_time_limit = models.IntegerField(default=48) user_import_time_limit = models.IntegerField(default=48)
field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"]) field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"])
@ -244,3 +248,14 @@ def preview_image(instance, *args, **kwargs):
if len(changed_fields) > 0: if len(changed_fields) > 0:
generate_site_preview_image_task.delay() generate_site_preview_image_task.delay()
@app.task(queue=MISC)
def check_for_updates_task():
"""See if git remote knows about a new version"""
site = SiteSettings.objects.get()
release = get_data(RELEASE_API, timeout=3)
available_version = release.get("tag_name", None)
if available_version:
site.available_version = available_version
site.save(update_fields=["available_version"])

View file

@ -12,6 +12,8 @@ from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.template.loader import get_template from django.template.loader import get_template
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
from model_utils import FieldTracker from model_utils import FieldTracker
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
@ -107,14 +109,14 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@property @property
def recipients(self): def recipients(self):
"""tagged users who definitely need to get this status in broadcast""" """tagged users who definitely need to get this status in broadcast"""
mentions = [u for u in self.mention_users.all() if not u.local] mentions = {u for u in self.mention_users.all() if not u.local}
if ( if (
hasattr(self, "reply_parent") hasattr(self, "reply_parent")
and self.reply_parent and self.reply_parent
and not self.reply_parent.user.local and not self.reply_parent.user.local
): ):
mentions.append(self.reply_parent.user) mentions.add(self.reply_parent.user)
return list(set(mentions)) return list(mentions)
@classmethod @classmethod
def ignore_activity( def ignore_activity(
@ -178,6 +180,24 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
"""you can't boost dms""" """you can't boost dms"""
return self.privacy in ["unlisted", "public"] return self.privacy in ["unlisted", "public"]
@property
def page_title(self):
"""title of the page when only this status is shown"""
return _("%(display_name)s's status") % {"display_name": self.user.display_name}
@property
def page_description(self):
"""description of the page in meta tags when only this status is shown"""
return None
@property
def page_image(self):
"""image to use as preview in meta tags when only this status is shown"""
if self.mention_books.exists():
book = self.mention_books.first()
return book.preview_image or book.cover
return self.user.preview_image
def to_replies(self, **kwargs): def to_replies(self, **kwargs):
"""helper function for loading AP serialized replies to a status""" """helper function for loading AP serialized replies to a status"""
return self.to_ordered_collection( return self.to_ordered_collection(
@ -301,6 +321,10 @@ class BookStatus(Status):
abstract = True abstract = True
@property
def page_image(self):
return self.book.preview_image or self.book.cover or super().page_image
class Comment(BookStatus): class Comment(BookStatus):
"""like a review but without a rating and transient""" """like a review but without a rating and transient"""
@ -332,17 +356,26 @@ class Comment(BookStatus):
activity_serializer = activitypub.Comment activity_serializer = activitypub.Comment
@property
def page_title(self):
return _("%(display_name)s's comment on %(book_title)s") % {
"display_name": self.user.display_name,
"book_title": self.book.title,
}
class Quotation(BookStatus): class Quotation(BookStatus):
"""like a review but without a rating and transient""" """like a review but without a rating and transient"""
quote = fields.HtmlField() quote = fields.HtmlField()
raw_quote = models.TextField(blank=True, null=True) raw_quote = models.TextField(blank=True, null=True)
position = models.IntegerField( position = models.TextField(
validators=[MinValueValidator(0)], null=True, blank=True null=True,
blank=True,
) )
endposition = models.IntegerField( endposition = models.TextField(
validators=[MinValueValidator(0)], null=True, blank=True null=True,
blank=True,
) )
position_mode = models.CharField( position_mode = models.CharField(
max_length=3, max_length=3,
@ -374,6 +407,13 @@ class Quotation(BookStatus):
activity_serializer = activitypub.Quotation activity_serializer = activitypub.Quotation
@property
def page_title(self):
return _("%(display_name)s's quote from %(book_title)s") % {
"display_name": self.user.display_name,
"book_title": self.book.title,
}
class Review(BookStatus): class Review(BookStatus):
"""a book review""" """a book review"""
@ -403,6 +443,13 @@ class Review(BookStatus):
"""indicate the book in question for mastodon (or w/e) users""" """indicate the book in question for mastodon (or w/e) users"""
return self.content return self.content
@property
def page_title(self):
return _("%(display_name)s's review of %(book_title)s") % {
"display_name": self.user.display_name,
"book_title": self.book.title,
}
activity_serializer = activitypub.Review activity_serializer = activitypub.Review
pure_type = "Article" pure_type = "Article"
@ -426,6 +473,18 @@ class ReviewRating(Review):
template = get_template("snippets/generated_status/rating.html") template = get_template("snippets/generated_status/rating.html")
return template.render({"book": self.book, "rating": self.rating}).strip() return template.render({"book": self.book, "rating": self.rating}).strip()
@property
def page_description(self):
return ngettext_lazy(
"%(display_name)s rated %(book_title)s: %(display_rating).1f star",
"%(display_name)s rated %(book_title)s: %(display_rating).1f stars",
"display_rating",
) % {
"display_name": self.user.display_name,
"book_title": self.book.title,
"display_rating": self.rating,
}
activity_serializer = activitypub.Rating activity_serializer = activitypub.Rating
pure_type = "Note" pure_type = "Note"

View file

@ -30,6 +30,9 @@ RELEASE_API = env(
PAGE_LENGTH = env.int("PAGE_LENGTH", 15) PAGE_LENGTH = env.int("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
# TODO: extend maximum age to 1 year once termination of active sessions
# is implemented (see bookwyrm-social#2278, bookwyrm-social#3082).
SESSION_COOKIE_AGE = env.int("SESSION_COOKIE_AGE", 3600 * 24 * 30) # 1 month
JS_CACHE = "8a89cad7" JS_CACHE = "8a89cad7"
@ -347,8 +350,7 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
agent = requests.utils.default_user_agent() USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +https://{DOMAIN}/)"
USER_AGENT = f"{agent} (BookWyrm/{VERSION}; +https://{DOMAIN}/)"
# Imagekit generated thumbnails # Imagekit generated thumbnails
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False) ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
@ -442,3 +444,5 @@ if HTTP_X_FORWARDED_PROTO:
# Do not change this setting unless you already have an existing # Do not change this setting unless you already have an existing
# user with the same username - in which case you should change it! # user with the same username - in which case you should change it!
INSTANCE_ACTOR_USERNAME = "bookwyrm.instance.actor" INSTANCE_ACTOR_USERNAME = "bookwyrm.instance.actor"
DATA_UPLOAD_MAX_MEMORY_SIZE = env.int("DATA_UPLOAD_MAX_MEMORY_SIZE", (1024**2 * 100))

View file

@ -111,6 +111,10 @@ const tries = {
}, },
}, },
f: { f: {
b: {
2: "FB2",
3: "FB3",
},
l: { l: {
a: { a: {
c: "FLAC", c: "FLAC",

View file

@ -31,10 +31,10 @@
</p> </p>
</div> </div>
<div class="columns"> <div class="columns is-multiline">
{% if superlatives.top_rated %} {% if superlatives.top_rated %}
{% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %} {% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %}
<div class="column is-one-third is-flex"> <div class="column is-half-tablet is-one-third-desktop is-flex-grow-1 is-flex">
<div class="media notification is-clipped"> <div class="media notification is-clipped">
<div class="media-left"> <div class="media-left">
<a href="{{ book.local_path }}"> <a href="{{ book.local_path }}">
@ -53,7 +53,7 @@
{% if superlatives.wanted %} {% if superlatives.wanted %}
{% with book=superlatives.wanted.default_edition %} {% with book=superlatives.wanted.default_edition %}
<div class="column is-one-third is-flex"> <div class="column is-half-tablet is-one-third-desktop is-flex-grow-1 is-flex">
<div class="media notification is-clipped"> <div class="media notification is-clipped">
<div class="media-left"> <div class="media-left">
<a href="{{ book.local_path }}"> <a href="{{ book.local_path }}">
@ -72,7 +72,7 @@
{% if superlatives.controversial %} {% if superlatives.controversial %}
{% with book=superlatives.controversial.default_edition %} {% with book=superlatives.controversial.default_edition %}
<div class="column is-one-third is-flex"> <div class="column is-half-tablet is-one-third-desktop is-flex-grow-1 is-flex">
<div class="media notification is-clipped"> <div class="media notification is-clipped">
<div class="media-left"> <div class="media-left">
<a href="{{ book.local_path }}"> <a href="{{ book.local_path }}">

View file

@ -9,7 +9,8 @@
{% block title %}{{ book|book_title }}{% endblock %} {% block title %}{{ book|book_title }}{% endblock %}
{% block opengraph %} {% block opengraph %}
{% include 'snippets/opengraph.html' with title=book.title description=book|book_description image=book.preview_image %} {% firstof book.preview_image book.cover as book_image %}
{% include 'snippets/opengraph.html' with title=book.title description=book|book_description image=book_image %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View file

@ -6,8 +6,8 @@
{% block content %} {% block content %}
<h1 class="title">{% trans "Confirm your email address" %}</h1> <h1 class="title">{% trans "Confirm your email address" %}</h1>
<div class="columns"> <div class="columns is-multiline">
<div class="column"> <div class="column is-full is-half-desktop">
<div class="block content"> <div class="block content">
<section class="block"> <section class="block">
<p>{% trans "A confirmation code has been sent to the email address you used to register your account." %}</p> <p>{% trans "A confirmation code has been sent to the email address you used to register your account." %}</p>

View file

@ -41,7 +41,7 @@
</section> </section>
{% endif %} {% endif %}
{% if annual_summary_year and tab.key == 'home' %} {% if annual_summary_year and tab.key == 'home' and has_summary_read_throughs %}
<section class="block is-hidden" data-hide="hide_annual_summary_{{ annual_summary_year }}"> <section class="block is-hidden" data-hide="hide_annual_summary_{{ annual_summary_year }}">
{% include 'feed/summary_card.html' with year=annual_summary_year %} {% include 'feed/summary_card.html' with year=annual_summary_year %}
<hr> <hr>

View file

@ -2,13 +2,11 @@
{% load feed_page_tags %} {% load feed_page_tags %}
{% load i18n %} {% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block opengraph %} {% block opengraph %}
{% firstof status.book status.mention_books.first as book %} {% include 'snippets/opengraph.html' with image=page_image %}
{% if book %}
{% include 'snippets/opengraph.html' with image=preview %}
{% else %}
{% include 'snippets/opengraph.html' %}
{% endif %}
{% endblock %} {% endblock %}

View file

@ -6,8 +6,8 @@
{% block content %} {% block content %}
<h1 class="title">{% trans "Create an Account" %}</h1> <h1 class="title">{% trans "Create an Account" %}</h1>
<div class="columns"> <div class="columns is-multiline">
<div class="column"> <div class="column is-full is-half-desktop">
<div class="block"> <div class="block">
{% if valid %} {% if valid %}
<div> <div>

View file

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<h1 class="title">{% trans "Log in" %}</h1> <h1 class="title">{% trans "Log in" %}</h1>
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-half"> <div class="column {% if site.allow_registration %}is-half{% else %}is-full is-half-desktop{% endif %}">
{% if login_form.non_field_errors %} {% if login_form.non_field_errors %}
<p class="notification is-danger">{{ login_form.non_field_errors }}</p> <p class="notification is-danger">{{ login_form.non_field_errors }}</p>
{% endif %} {% endif %}
@ -20,13 +20,15 @@
<div class="field"> <div class="field">
<label class="label" for="id_localname_confirm">{% trans "Username:" %}</label> <label class="label" for="id_localname_confirm">{% trans "Username:" %}</label>
<div class="control"> <div class="control">
<input type="text" name="localname" maxlength="255" class="input" required="" id="id_localname_confirm" value="{{ login_form.localname.value|default:'' }}"> <input type="text" name="localname" maxlength="255" class="input" required=""
id="id_localname_confirm" value="{{ login_form.localname.value|default:'' }}">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="id_password_confirm">{% trans "Password:" %}</label> <label class="label" for="id_password_confirm">{% trans "Password:" %}</label>
<div class="control"> <div class="control">
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm" aria-describedby="desc_password"> <input type="password" name="password" maxlength="128" class="input" required=""
id="id_password_confirm" aria-describedby="desc_password">
</div> </div>
{% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %} {% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %}
@ -58,10 +60,10 @@
{% include 'snippets/about.html' %} {% include 'snippets/about.html' %}
<p class="block"> <p class="block">
<a href="{% url 'about' %}">{% trans "More about this site" %}</a> <a href="{% url 'about' %}">{% trans "More about this site" %}</a>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -4,8 +4,8 @@
{% block title %}{% trans "Reset Password" %}{% endblock %} {% block title %}{% trans "Reset Password" %}{% endblock %}
{% block content %} {% block content %}
<div class="columns"> <div class="columns is-multiline">
<div class="column"> <div class="column is-full is-half-desktop">
<div class="block"> <div class="block">
<h1 class="title">{% trans "Reset Password" %}</h1> <h1 class="title">{% trans "Reset Password" %}</h1>

View file

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<h1 class="title">{% trans "Reactivate Account" %}</h1> <h1 class="title">{% trans "Reactivate Account" %}</h1>
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-half"> <div class="column {% if site.allow_registration %}is-half{% else %}is-full is-half-desktop{% endif %}">
{% if login_form.non_field_errors %} {% if login_form.non_field_errors %}
<p class="notification is-danger">{{ login_form.non_field_errors }}</p> <p class="notification is-danger">{{ login_form.non_field_errors }}</p>
{% endif %} {% endif %}
@ -16,13 +16,15 @@
<div class="field"> <div class="field">
<label class="label" for="id_localname_confirm">{% trans "Username:" %}</label> <label class="label" for="id_localname_confirm">{% trans "Username:" %}</label>
<div class="control"> <div class="control">
<input type="text" name="localname" maxlength="255" class="input" required="" id="id_localname_confirm" value="{{ login_form.localname.value|default:'' }}"> <input type="text" name="localname" maxlength="255" class="input" required=""
id="id_localname_confirm" value="{{ login_form.localname.value|default:'' }}">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="id_password_confirm">{% trans "Password:" %}</label> <label class="label" for="id_password_confirm">{% trans "Password:" %}</label>
<div class="control"> <div class="control">
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm" aria-describedby="desc_password"> <input type="password" name="password" maxlength="128" class="input" required=""
id="id_password_confirm" aria-describedby="desc_password">
</div> </div>
{% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %} {% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %}
@ -51,10 +53,10 @@
{% include 'snippets/about.html' %} {% include 'snippets/about.html' %}
<p class="block"> <p class="block">
<a href="{% url 'about' %}">{% trans "More about this site" %}</a> <a href="{% url 'about' %}">{% trans "More about this site" %}</a>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -23,7 +23,7 @@
<div class="notification is-warning"> <div class="notification is-warning">
<p> <p>
{% id_to_username request.user.moved_to as username %} {% id_to_username request.user.moved_to as username %}
{% blocktrans trimmed with moved_to=user.moved_to %} {% blocktrans trimmed with moved_to=user.moved_to %}
<strong>You have moved your account</strong> to <a href="{{ moved_to }}">{{ username }}</a> <strong>You have moved your account</strong> to <a href="{{ moved_to }}">{{ username }}</a>
{% endblocktrans %} {% endblocktrans %}

View file

@ -14,7 +14,7 @@
{% block description %} {% block description %}
{% if related_user_moved_to %} {% if related_user_moved_to %}
{% id_to_username request.user.moved_to as username %} {% id_to_username related_user_moved_to as username %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
{{ related_user }} has moved to <a href="{{ related_user_moved_to }}">{{ username }}</a> {{ related_user }} has moved to <a href="{{ related_user_moved_to }}">{{ username }}</a>
{% endblocktrans %} {% endblocktrans %}

View file

@ -46,7 +46,11 @@
{% trans "If you wish to migrate any statuses (comments, reviews, or quotes) you must either set the account you are moving to as an <strong>alias</strong> of this one, or <strong>move</strong> this account to the new account, before you import your user data." %} {% trans "If you wish to migrate any statuses (comments, reviews, or quotes) you must either set the account you are moving to as an <strong>alias</strong> of this one, or <strong>move</strong> this account to the new account, before you import your user data." %}
{% endspaceless %} {% endspaceless %}
</p> </p>
{% if next_available %} {% if not site.user_exports_enabled %}
<p class="notification is-danger">
{% trans "New user exports are currently disabled." %}
</p>
{% elif next_available %}
<p class="notification is-warning"> <p class="notification is-warning">
{% blocktrans trimmed %} {% blocktrans trimmed %}
You will be able to create a new export file at {{ next_available }} You will be able to create a new export file at {{ next_available }}

View file

@ -45,6 +45,10 @@
{% include 'settings/dashboard/warnings/update_version.html' with warning_level="warning" fullwidth=True %} {% include 'settings/dashboard/warnings/update_version.html' with warning_level="warning" fullwidth=True %}
{% endif %} {% endif %}
{% if schedule_form %}
{% include 'settings/dashboard/warnings/check_for_updates.html' with warning_level="success" fullwidth=True %}
{% endif %}
{% if missing_privacy or missing_conduct %} {% if missing_privacy or missing_conduct %}
<div class="column is-12 columns m-0 p-0"> <div class="column is-12 columns m-0 p-0">
{% if missing_privacy %} {% if missing_privacy %}

View file

@ -0,0 +1,24 @@
{% extends 'settings/dashboard/warnings/layout.html' %}
{% load i18n %}
{% block warning_link %}#{% endblock %}
{% block warning_text %}
<form name="check-version" method="POST" action="{% url 'settings-dashboard' %}" class="is-flex is-align-items-center">
{% csrf_token %}
<p class="pr-2">
{% blocktrans trimmed with current=current_version available=available_version %}
Would you like to automatically check for new BookWyrm releases? (recommended)
{% endblocktrans %}
</p>
{{ schedule_form.every.as_hidden }}
{{ schedule_form.period.as_hidden }}
<button class="button is-small" type="submit">{% trans "Schedule checks" %}</button>
</form>
{% endblock %}

View file

@ -90,6 +90,33 @@
</div> </div>
</form> </form>
</details> </details>
{% if site.user_exports_enabled %}
<details class="details-panel box">
<summary>
<span role="heading" aria-level="2" class="title is-6">
{% trans "Disable starting new user exports" %}
</span>
<span class="details-close icon icon-x" aria-hidden="true"></span>
</summary>
<form
name="disable-user-exports"
id="disable-user-exports"
method="POST"
action="{% url 'settings-user-exports-disable' %}"
>
<div class="notification">
{% trans "This is only intended to be used when things have gone very wrong with exports and you need to pause the feature while addressing issues." %}
{% trans "While exports are disabled, users will not be allowed to start new user exports, but existing exports will not be affected." %}
</div>
{% csrf_token %}
<div class="control">
<button type="submit" class="button is-danger">
{% trans "Disable user exports" %}
</button>
</div>
</form>
</details>
<details class="details-panel box"> <details class="details-panel box">
<summary> <summary>
<span role="heading" aria-level="2" class="title is-6"> <span role="heading" aria-level="2" class="title is-6">
@ -108,7 +135,7 @@
{% trans "Set the value to 0 to not enforce any limit." %} {% trans "Set the value to 0 to not enforce any limit." %}
</div> </div>
<div class="align.to-t"> <div class="align.to-t">
<label for="limit">{% trans "Restrict user imports and exports to once every " %}</label> <label for="limit">{% trans "Limit how often users can import and export user data" %}</label>
<input name="limit" class="input is-w-xs is-h-em" type="text" placeholder="0" value="{{ user_import_time_limit }}"> <input name="limit" class="input is-w-xs is-h-em" type="text" placeholder="0" value="{{ user_import_time_limit }}">
<label>{% trans "hours" %}</label> <label>{% trans "hours" %}</label>
{% csrf_token %} {% csrf_token %}
@ -120,6 +147,28 @@
</div> </div>
</form> </form>
</details> </details>
{% else %}
<form
name="enable-user-imports"
id="enable-user-imports"
method="POST"
action="{% url 'settings-user-exports-enable' %}"
class="box"
>
<div class="notification is-danger is-light">
<p class="my-2">{% trans "Users are currently unable to start new user exports. This is the default setting." %}</p>
{% if use_s3 %}
<p>{% trans "It is not currently possible to provide user exports when using s3 storage. The BookWyrm development team are working on a fix for this." %}</p>
{% endif %}
</div>
{% csrf_token %}
<div class="control">
<button type="submit" class="button is-success" {% if use_s3 %}disabled{% endif %}>
{% trans "Enable user exports" %}
</button>
</div>
</form>
{% endif %}
</div> </div>
<div class="block"> <div class="block">
<h4 class="title is-4">{% trans "Book Imports" %}</h4> <h4 class="title is-4">{% trans "Book Imports" %}</h4>

View file

@ -85,6 +85,10 @@
{% url 'settings-celery' as url %} {% url 'settings-celery' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Celery status" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Celery status" %}</a>
</li> </li>
<li>
{% url 'settings-schedules' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Scheduled tasks" %}</a>
</li>
<li> <li>
{% url 'settings-email-config' as url %} {% url 'settings-email-config' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Configuration" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Configuration" %}</a>

View file

@ -0,0 +1,127 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% load humanize %}
{% load utilities %}
{% block title %}
{% trans "Scheduled tasks" %}
{% endblock %}
{% block header %}
{% trans "Scheduled tasks" %}
{% endblock %}
{% block panel %}
<div class="block content">
<h3>{% trans "Tasks" %}</h3>
<div class="table-container">
<table class="table is-striped is-fullwidth">
<tr>
<th>
{% trans "Name" %}
</th>
<th>
{% trans "Celery task" %}
</th>
<th>
{% trans "Date changed" %}
</th>
<th>
{% trans "Last run at" %}
</th>
<th>
{% trans "Schedule" %}
</th>
<th>
{% trans "Schedule ID" %}
</th>
<th>
{% trans "Enabled" %}
</th>
</tr>
{% for task in tasks %}
<tr>
<td>
{{ task.name }}
</td>
<td class="overflow-wrap-anywhere">
{{ task.task }}
</td>
<td>
{{ task.date_changed }}
</td>
<td>
{{ task.last_run_at }}
</td>
<td>
{% firstof task.interval task.crontab "None" %}
</td>
<td>
{{ task.interval.id }}
</td>
<td>
<span class="tag">
{% if task.enabled %}
<span class="icon icon-check" aria-hidden="true"></span>
{% endif %}
{{ task.enabled|yesno }}
</span>
{% if task.name != "celery.backend_cleanup" %}
<form name="unschedule-{{ task.id }}" method="POST" action="{% url 'settings-schedules' task.id %}">
{% csrf_token %}
<button type="submit" class="button is-danger is-small">{% trans "Un-schedule" %}</button>
</form>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="2">
{% trans "No scheduled tasks" %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="block content">
<h3>{% trans "Schedules" %}</h3>
<div class="table-container">
<table class="table is-striped is-fullwidth">
<tr>
<th>
{% trans "ID" %}
</th>
<th>
{% trans "Schedule" %}
</th>
<th>
{% trans "Tasks" %}
</th>
</tr>
{% for schedule in schedules %}
<tr>
<td>
{{ schedule.id }}
</td>
<td class="overflow-wrap-anywhere">
{{ schedule }}
</td>
<td>
{{ schedule.periodictask_set.count }}
</td>
</tr>
{% empty %}
<tr>
<td colspan="2">
{% trans "No schedules found" %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}

View file

@ -56,8 +56,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
<input <input
aria-label="{% if draft.position_mode == 'PG' %}Page{% else %}Percent{% endif %}" aria-label="{% if draft.position_mode == 'PG' %}Page{% else %}Percent{% endif %}"
class="input" class="input"
type="number" type="text"
min="0"
name="position" name="position"
size="3" size="3"
value="{% firstof draft.position '' %}" value="{% firstof draft.position '' %}"
@ -72,8 +71,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
<input <input
aria-label="{% if draft.position_mode == 'PG' %}Page{% else %}Percent{% endif %}" aria-label="{% if draft.position_mode == 'PG' %}Page{% else %}Percent{% endif %}"
class="input" class="input"
type="number" type="text"
min="0"
name="endposition" name="endposition"
size="3" size="3"
value="{% firstof draft.endposition '' %}" value="{% firstof draft.endposition '' %}"

View file

@ -1,24 +1,25 @@
{% load static %} {% load static %}
{% if preview_images_enabled is True %} {% firstof image site.preview_image as page_image %}
{% if page_image %}
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
{% if image %} <meta name="twitter:image" content="{{ media_full_url }}{{ page_image }}">
<meta name="twitter:image" content="{{ media_full_url }}{{ image }}"> <meta name="og:image" content="{{ media_full_url }}{{ page_image }}">
<meta name="og:image" content="{{ media_full_url }}{{ image }}"> {% elif site.logo %}
{% else %} <meta name="twitter:card" content="summary">
<meta name="twitter:image" content="{{ media_full_url }}{{ site.preview_image }}"> <meta name="twitter:image" content="{{ media_full_url }}{{ site.logo }}">
<meta name="og:image" content="{{ media_full_url }}{{ site.preview_image }}"> <meta name="twitter:image:alt" content="{{ site.name }} Logo">
{% endif %} <meta name="og:image" content="{{ media_full_url }}{{ site.logo }}">
{% else %} {% else %}
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:image" content="{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}"> <meta name="twitter:image" content="{% static "images/logo.png" %}">
<meta name="og:image" content="{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}"> <meta name="twitter:image:alt" content="BookWyrm Logo">
<meta name="og:image" content="{% static "images/logo.png" %}">
{% endif %} {% endif %}
<meta name="twitter:image:alt" content="BookWyrm Logo">
<meta name="twitter:title" content="{% if title %}{{ title }} - {% endif %}{{ site.name }}"> <meta name="twitter:title" content="{% if title %}{{ title }} - {% endif %}{{ site.name }}">
<meta name="og:title" content="{% if title %}{{ title }} - {% endif %}{{ site.name }}"> <meta name="og:title" content="{% if title %}{{ title }} - {% endif %}{{ site.name }}">
<meta name="twitter:description" content="{% if description %}{{ description }}{% else %}{{ site.instance_tagline }}{% endif %}"> {% firstof description site.instance_tagline as description %}
<meta name="og:description" content="{% if description %}{{ description }}{% else %}{{ site.instance_tagline }}{% endif %}"> <meta name="twitter:description" content="{{ description }}">
<meta name="og:description" content="{{ description }}">

View file

@ -125,7 +125,8 @@ def id_to_username(user_id):
name = parts[-1] name = parts[-1]
value = f"{name}@{domain}" value = f"{name}@{domain}"
return value return value
return "a new user account"
@register.filter(name="get_file_size") @register.filter(name="get_file_size")

View file

@ -227,14 +227,18 @@ class ActivitypubMixins(TestCase):
shared_inbox="http://example.com/inbox", shared_inbox="http://example.com/inbox",
outbox="https://example.com/users/nutria/outbox", outbox="https://example.com/users/nutria/outbox",
) )
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user", "recipients"))
mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
self.local_user.followers.add(another_remote_user) self.local_user.followers.add(another_remote_user)
mock_self = MockSelf("public", self.local_user, [])
recipients = ActivitypubMixin.get_recipients(mock_self) recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 1) self.assertCountEqual(recipients, ["http://example.com/inbox"])
self.assertEqual(recipients[0], "http://example.com/inbox")
# should also work with recipient that is a follower
mock_self.recipients.append(another_remote_user)
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertCountEqual(recipients, ["http://example.com/inbox"])
def test_get_recipients_software(self, *_): def test_get_recipients_software(self, *_):
"""should differentiate between bookwyrm and other remote users""" """should differentiate between bookwyrm and other remote users"""

View file

@ -272,8 +272,8 @@ class BookViews(TestCase):
book=self.book, book=self.book,
content="hi", content="hi",
quote="wow", quote="wow",
position=12, position="12",
endposition=13, endposition="13",
) )
request = self.factory.get("") request = self.factory.get("")
@ -286,7 +286,9 @@ class BookViews(TestCase):
validate_html(result.render()) validate_html(result.render())
print(result.render()) print(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["statuses"].object_list[0].endposition, 13) self.assertEqual(
result.context_data["statuses"].object_list[0].endposition, "13"
)
def _setup_cover_url(): def _setup_cover_url():

View file

@ -18,7 +18,9 @@ class ExportViews(TestCase):
"""viewing and creating statuses""" """viewing and creating statuses"""
@classmethod @classmethod
def setUpTestData(self): # pylint: disable=bad-classmethod-argument def setUpTestData(
self,
): # pylint: disable=bad-classmethod-argument, disable=invalid-name
"""we need basic test data and mocks""" """we need basic test data and mocks"""
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay" "bookwyrm.activitystreams.populate_stream_task.delay"
@ -40,6 +42,7 @@ class ExportViews(TestCase):
bnf_id="beep", bnf_id="beep",
) )
# pylint: disable=invalid-name
def setUp(self): def setUp(self):
"""individual test setup""" """individual test setup"""
self.factory = RequestFactory() self.factory = RequestFactory()
@ -53,11 +56,12 @@ class ExportViews(TestCase):
def test_export_file(self, *_): def test_export_file(self, *_):
"""simple export""" """simple export"""
models.ShelfBook.objects.create( shelfbook = models.ShelfBook.objects.create(
shelf=self.local_user.shelf_set.first(), shelf=self.local_user.shelf_set.first(),
user=self.local_user, user=self.local_user,
book=self.book, book=self.book,
) )
book_date = str.encode(f"{shelfbook.shelved_date.date()}")
request = self.factory.post("") request = self.factory.post("")
request.user = self.local_user request.user = self.local_user
export = views.Export.as_view()(request) export = views.Export.as_view()(request)
@ -66,7 +70,7 @@ class ExportViews(TestCase):
# pylint: disable=line-too-long # pylint: disable=line-too-long
self.assertEqual( self.assertEqual(
export.content, export.content,
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,aasin,isfdb,isbn_10,isbn_13,oclc_number,start_date,finish_date,stopped_date,rating,review_name,review_cw,review_content\r\nTest Book,," b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,aasin,isfdb,isbn_10,isbn_13,oclc_number,start_date,finish_date,stopped_date,rating,review_name,review_cw,review_content,review_published,shelf,shelf_name,shelf_date\r\n"
+ self.book.remote_id.encode("utf-8") + b"Test Book,,%b,,,,,beep,,,,,,123456789X,9781234567890,,,,,,,,,,to-read,To Read,%b\r\n"
+ b",,,,,beep,,,,,,123456789X,9781234567890,,,,,,,,\r\n", % (self.book.remote_id.encode("utf-8"), book_date),
) )

View file

@ -338,6 +338,16 @@ urlpatterns = [
views.disable_imports, views.disable_imports,
name="settings-imports-disable", name="settings-imports-disable",
), ),
re_path(
r"^settings/user-exports/enable/?$",
views.enable_user_exports,
name="settings-user-exports-enable",
),
re_path(
r"^settings/user-exports/disable/?$",
views.disable_user_exports,
name="settings-user-exports-disable",
),
re_path( re_path(
r"^settings/imports/enable/?$", r"^settings/imports/enable/?$",
views.enable_imports, views.enable_imports,
@ -359,6 +369,11 @@ urlpatterns = [
re_path( re_path(
r"^settings/celery/ping/?$", views.celery_ping, name="settings-celery-ping" r"^settings/celery/ping/?$", views.celery_ping, name="settings-celery-ping"
), ),
re_path(
r"^settings/schedules/(?P<task_id>\d+)?$",
views.ScheduledTasks.as_view(),
name="settings-schedules",
),
re_path( re_path(
r"^settings/email-config/?$", r"^settings/email-config/?$",
views.EmailConfig.as_view(), views.EmailConfig.as_view(),

View file

@ -5,6 +5,7 @@ from .admin.announcements import EditAnnouncement, delete_announcement
from .admin.automod import AutoMod, automod_delete, run_automod from .admin.automod import AutoMod, automod_delete, run_automod
from .admin.automod import schedule_automod_task, unschedule_automod_task from .admin.automod import schedule_automod_task, unschedule_automod_task
from .admin.celery_status import CeleryStatus, celery_ping from .admin.celery_status import CeleryStatus, celery_ping
from .admin.schedule import ScheduledTasks
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
@ -18,6 +19,8 @@ from .admin.imports import (
set_import_size_limit, set_import_size_limit,
set_user_import_completed, set_user_import_completed,
set_user_import_limit, set_user_import_limit,
enable_user_exports,
disable_user_exports,
) )
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

View file

@ -6,7 +6,7 @@ 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 django.views.decorators.http import require_POST
from django_celery_beat.models import PeriodicTask from django_celery_beat.models import PeriodicTask, IntervalSchedule
from bookwyrm import forms, models from bookwyrm import forms, models
@ -54,7 +54,7 @@ def schedule_automod_task(request):
return TemplateResponse(request, "settings/automod/rules.html", data) return TemplateResponse(request, "settings/automod/rules.html", data)
with transaction.atomic(): with transaction.atomic():
schedule = form.save(request) schedule, _ = IntervalSchedule.objects.get_or_create(**form.cleaned_data)
PeriodicTask.objects.get_or_create( PeriodicTask.objects.get_or_create(
interval=schedule, interval=schedule,
name="automod-task", name="automod-task",

View file

@ -6,16 +6,18 @@ from dateutil.parser import parse
from packaging import version from packaging import version
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
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_celery_beat.models import PeriodicTask, IntervalSchedule
from csp.decorators import csp_update from csp.decorators import csp_update
from bookwyrm import models, settings from bookwyrm import forms, models, settings
from bookwyrm.connectors.abstract_connector import get_data
from bookwyrm.utils import regex from bookwyrm.utils import regex
@ -59,21 +61,36 @@ class Dashboard(View):
== site._meta.get_field("privacy_policy").get_default() == site._meta.get_field("privacy_policy").get_default()
) )
# check version if site.available_version and version.parse(
site.available_version
) > version.parse(settings.VERSION):
data["current_version"] = settings.VERSION
data["available_version"] = site.available_version
try: if not PeriodicTask.objects.filter(name="check-for-updates").exists():
release = get_data(settings.RELEASE_API, timeout=3) data["schedule_form"] = forms.IntervalScheduleForm(
available_version = release.get("tag_name", None) {"every": 1, "period": "days"}
if available_version and version.parse(available_version) > version.parse( )
settings.VERSION
):
data["current_version"] = settings.VERSION
data["available_version"] = available_version
except: # pylint: disable= bare-except
pass
return TemplateResponse(request, "settings/dashboard/dashboard.html", data) return TemplateResponse(request, "settings/dashboard/dashboard.html", data)
def post(self, request):
"""Create a schedule task to check for updates"""
schedule_form = forms.IntervalScheduleForm(request.POST)
if not schedule_form.is_valid():
raise schedule_form.ValidationError(schedule_form.errors)
with transaction.atomic():
schedule, _ = IntervalSchedule.objects.get_or_create(
**schedule_form.cleaned_data
)
PeriodicTask.objects.get_or_create(
interval=schedule,
name="check-for-updates",
task="bookwyrm.models.site.check_for_updates_task",
)
return redirect("settings-dashboard")
def get_charts_and_stats(request): def get_charts_and_stats(request):
"""Defines the dashboard charts""" """Defines the dashboard charts"""

View file

@ -9,7 +9,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import models from bookwyrm import models
from bookwyrm.views.helpers import redirect_to_referer from bookwyrm.views.helpers import redirect_to_referer
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH, USE_S3
# pylint: disable=no-self-use # pylint: disable=no-self-use
@ -59,6 +59,7 @@ class ImportList(View):
"import_size_limit": site_settings.import_size_limit, "import_size_limit": site_settings.import_size_limit,
"import_limit_reset": site_settings.import_limit_reset, "import_limit_reset": site_settings.import_limit_reset,
"user_import_time_limit": site_settings.user_import_time_limit, "user_import_time_limit": site_settings.user_import_time_limit,
"use_s3": USE_S3,
} }
return TemplateResponse(request, "settings/imports/imports.html", data) return TemplateResponse(request, "settings/imports/imports.html", data)
@ -126,3 +127,25 @@ def set_user_import_limit(request):
site.user_import_time_limit = int(request.POST.get("limit")) site.user_import_time_limit = int(request.POST.get("limit"))
site.save(update_fields=["user_import_time_limit"]) site.save(update_fields=["user_import_time_limit"])
return redirect("settings-imports") return redirect("settings-imports")
@require_POST
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
# pylint: disable=unused-argument
def enable_user_exports(request):
"""Allow users to export account data"""
site = models.SiteSettings.objects.get()
site.user_exports_enabled = True
site.save(update_fields=["user_exports_enabled"])
return redirect("settings-imports")
@require_POST
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
# pylint: disable=unused-argument
def disable_user_exports(request):
"""Don't allow users to export account data"""
site = models.SiteSettings.objects.get()
site.user_exports_enabled = False
site.save(update_fields=["user_exports_enabled"])
return redirect("settings-imports")

View file

@ -0,0 +1,31 @@
""" Scheduled celery tasks """
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django_celery_beat.models import PeriodicTask, IntervalSchedule
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
name="dispatch",
)
# pylint: disable=no-self-use
class ScheduledTasks(View):
"""Manage automated flagging"""
def get(self, request):
"""view schedules"""
data = {}
data["tasks"] = PeriodicTask.objects.all()
data["schedules"] = IntervalSchedule.objects.all()
return TemplateResponse(request, "settings/schedules.html", data)
# pylint: disable=unused-argument
def post(self, request, task_id):
"""un-schedule a task"""
task = PeriodicTask.objects.get(id=task_id)
task.delete()
return redirect("settings-schedules")

View file

@ -1,4 +1,5 @@
""" non-interactive pages """ """ non-interactive pages """
from datetime import date
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q from django.db.models import Q
@ -52,6 +53,19 @@ class Feed(View):
suggestions = suggested_users.get_suggestions(request.user) suggestions = suggested_users.get_suggestions(request.user)
cutoff = (
date(get_annual_summary_year(), 12, 31)
if get_annual_summary_year()
else None
)
readthroughs = (
models.ReadThrough.objects.filter(
user=request.user, finish_date__lte=cutoff
)
if get_annual_summary_year()
else []
)
data = { data = {
**feed_page_data(request.user), **feed_page_data(request.user),
**{ **{
@ -66,6 +80,7 @@ class Feed(View):
"path": f"/{tab['key']}", "path": f"/{tab['key']}",
"annual_summary_year": get_annual_summary_year(), "annual_summary_year": get_annual_summary_year(),
"has_tour": True, "has_tour": True,
"has_summary_read_throughs": len(readthroughs),
}, },
} }
return TemplateResponse(request, "feed/feed.html", data) return TemplateResponse(request, "feed/feed.html", data)
@ -185,19 +200,15 @@ class Status(View):
params=[status.id, visible_thread, visible_thread], params=[status.id, visible_thread, visible_thread],
) )
preview = None
if hasattr(status, "book"):
preview = status.book.preview_image
elif status.mention_books.exists():
preview = status.mention_books.first().preview_image
data = { data = {
**feed_page_data(request.user), **feed_page_data(request.user),
**{ **{
"status": status, "status": status,
"children": children, "children": children,
"ancestors": ancestors, "ancestors": ancestors,
"preview": preview, "title": status.page_title,
"description": status.page_description,
"page_image": status.page_image,
}, },
} }
return TemplateResponse(request, "feed/status.html", data) return TemplateResponse(request, "feed/status.html", data)

View file

@ -17,6 +17,7 @@ from bookwyrm import models
from bookwyrm.models.bookwyrm_export_job import BookwyrmExportJob from bookwyrm.models.bookwyrm_export_job import BookwyrmExportJob
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
# pylint: disable=no-self-use,too-many-locals # pylint: disable=no-self-use,too-many-locals
@method_decorator(login_required, name="dispatch") @method_decorator(login_required, name="dispatch")
class Export(View): class Export(View):
@ -54,8 +55,19 @@ class Export(View):
fields = ( fields = (
["title", "author_text"] ["title", "author_text"]
+ deduplication_fields + deduplication_fields
+ ["start_date", "finish_date", "stopped_date"] + [
+ ["rating", "review_name", "review_cw", "review_content"] "start_date",
"finish_date",
"stopped_date",
"rating",
"review_name",
"review_cw",
"review_content",
"review_published",
"shelf",
"shelf_name",
"shelf_date",
]
) )
writer.writerow(fields) writer.writerow(fields)
@ -97,9 +109,27 @@ class Export(View):
.first() .first()
) )
if review: if review:
book.review_published = (
review.published_date.date() if review.published_date else None
)
book.review_name = review.name book.review_name = review.name
book.review_cw = review.content_warning book.review_cw = review.content_warning
book.review_content = review.raw_content book.review_content = (
review.raw_content if review.raw_content else review.content
) # GoodReads imported reviews do not have raw_content, but content.
shelfbook = (
models.ShelfBook.objects.filter(user=request.user, book=book)
.order_by("-shelved_date", "-created_date", "-updated_date")
.last()
)
if shelfbook:
book.shelf = shelfbook.shelf.identifier
book.shelf_name = shelfbook.shelf.name
book.shelf_date = (
shelfbook.shelved_date.date() if shelfbook.shelved_date else None
)
writer.writerow([getattr(book, field, "") or "" for field in fields]) writer.writerow([getattr(book, field, "") or "" for field in fields])
return HttpResponse( return HttpResponse(

View file

@ -1,4 +1,4 @@
FROM python:3.9 FROM python:3.9-bookworm
WORKDIR /app/dev-tools WORKDIR /app/dev-tools
ENV PATH="/app/dev-tools/node_modules/.bin:$PATH" ENV PATH="/app/dev-tools/node_modules/.bin:$PATH"

View file

@ -64,13 +64,18 @@ server {
# directly serve images and static files from the # directly serve images and static files from the
# bookwyrm filesystem using sendfile. # bookwyrm filesystem using sendfile.
# make the logs quieter by not reporting these requests # make the logs quieter by not reporting these requests
location ~ ^/(images|static)/ { location ~ \.(bmp|ico|jpg|jpeg|png|svg|tif|tiff|ttf|webp|css|js)$ {
root /app; root /app;
try_files $uri =404; try_files $uri =404;
add_header X-Cache-Status STATIC; add_header X-Cache-Status STATIC;
access_log off; access_log off;
} }
# block access to any non-image files from images or static
location ~ ^/images/ {
return 403;
}
# monitor the celery queues with flower, no caching enabled # monitor the celery queues with flower, no caching enabled
location /flower/ { location /flower/ {
proxy_pass http://flower:8888; proxy_pass http://flower:8888;

View file

@ -96,12 +96,17 @@ server {
# # directly serve images and static files from the # # directly serve images and static files from the
# # bookwyrm filesystem using sendfile. # # bookwyrm filesystem using sendfile.
# # make the logs quieter by not reporting these requests # # make the logs quieter by not reporting these requests
# location ~ ^/(images|static)/ { # location ~ \.(bmp|ico|jpg|jpeg|png|svg|tif|tiff|ttf|webp|css|js)$ {
# root /app; # root /app;
# try_files $uri =404; # try_files $uri =404;
# add_header X-Cache-Status STATIC; # add_header X-Cache-Status STATIC;
# access_log off; # access_log off;
# } # }
# # block access to any non-image files from images or static
# location ~ ^/images/ {
# return 403;
# }
# #
# # monitor the celery queues with flower, no caching enabled # # monitor the celery queues with flower, no caching enabled
# location /flower/ { # location /flower/ {

View file

@ -2,6 +2,9 @@ bind 127.0.0.1 ::1
protected-mode yes protected-mode yes
port 6379 port 6379
auto-aof-rewrite-percentage 50
auto-aof-rewrite-min-size 128mb
rename-command FLUSHDB "" rename-command FLUSHDB ""
rename-command FLUSHALL "" rename-command FLUSHALL ""
rename-command DEBUG "" rename-command DEBUG ""

View file

@ -1,4 +1,4 @@
aiohttp==3.9.0 aiohttp==3.9.2
bleach==5.0.1 bleach==5.0.1
celery==5.2.7 celery==5.2.7
colorthief==0.2.1 colorthief==0.2.1

View file

@ -1,37 +0,0 @@
#!/usr/bin/env bash
set -e
# determine inital and target versions
initial_version="`./bw-dev runweb python manage.py instance_version --current`"
target_version="`./bw-dev runweb python manage.py instance_version --target`"
initial_version="`echo $initial_version | tail -n 1 | xargs`"
target_version="`echo $target_version | tail -n 1 | xargs`"
if [[ "$initial_version" = "$target_version" ]]; then
echo "Already up to date; version $initial_version"
exit
fi
echo "---------------------------------------"
echo "Updating from version: $initial_version"
echo ".......... to version: $target_version"
echo "---------------------------------------"
function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
# execute scripts between initial and target
for version in `ls -A updates/ | sort -V `; do
if version_gt $initial_version $version; then
# too early
continue
fi
if version_gt $version $target_version; then
# too late
continue
fi
echo "Running tasks for version $version"
./updates/$version
done
./bw-dev runweb python manage.py instance_version --update
echo "✨ ----------- Done! --------------- ✨"