Merge branch 'main' into export-fixes

This commit is contained in:
Hugh Rundle 2024-02-06 18:31:19 +11:00 committed by GitHub
commit 46a158d701
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 454 additions and 184 deletions

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,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)
@ -245,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

@ -350,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)

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -369,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

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

@ -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

@ -200,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

@ -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! --------------- ✨"