Merge from main into 'user-export'

Conflicts:
	bookwyrm/models/bookwyrm_export_job.py
	requirements.txt
This commit is contained in:
Adeodato Simó 2024-03-18 14:36:07 -03:00
commit 518da3b9cf
70 changed files with 8108 additions and 430 deletions

View file

@ -137,6 +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. # Time before being logged out (in seconds)
DATA_UPLOAD_MAX_MEMORY_SIZE = (1024**2 * 100) # SESSION_COOKIE_AGE=2592000 # current default: 30 days
# Maximum allowed memory for file uploads (increase if users are having trouble
# uploading BookWyrm export files).
# DATA_UPLOAD_MAX_MEMORY_MiB=100

View file

@ -1,17 +0,0 @@
name: Python Formatting (run ./bw-dev black to fix)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: psf/black@22.12.0
with:
version: 22.12.0

View file

@ -36,7 +36,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

View file

@ -10,7 +10,7 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install curlylint - name: Install curlylint
run: pip install curlylint run: pip install curlylint

View file

@ -1,70 +0,0 @@
name: Run Python Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-20.04
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: hunter2
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Check migrations up-to-date
run: |
python ./manage.py makemigrations --check
env:
SECRET_KEY: beepbeep
DOMAIN: your.domain.here
EMAIL_HOST: ""
EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: ""
- name: Run Tests
env:
SECRET_KEY: beepbeep
DEBUG: false
USE_HTTPS: true
DOMAIN: your.domain.here
BOOKWYRM_DATABASE_BACKEND: postgres
MEDIA_ROOT: images/
POSTGRES_PASSWORD: hunter2
POSTGRES_USER: postgres
POSTGRES_DB: github_actions
POSTGRES_HOST: 127.0.0.1
CELERY_BROKER: ""
REDIS_BROKER_PORT: 6379
REDIS_BROKER_PASSWORD: beep
USE_DUMMY_CACHE: true
FLOWER_PORT: 8888
EMAIL_HOST: "smtp.mailgun.org"
EMAIL_PORT: 587
EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: ""
EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: false
ENABLE_THUMBNAIL_GENERATION: true
HTTP_X_FORWARDED_PROTO: false
run: |
pytest -n 3

View file

@ -19,7 +19,7 @@ jobs:
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install modules - name: Install modules
run: npm install stylelint stylelint-config-recommended stylelint-config-standard stylelint-order eslint run: npm install stylelint stylelint-config-recommended stylelint-config-standard stylelint-order eslint

View file

@ -1,50 +0,0 @@
name: Mypy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Analysing the code with mypy
env:
SECRET_KEY: beepbeep
DEBUG: false
USE_HTTPS: true
DOMAIN: your.domain.here
BOOKWYRM_DATABASE_BACKEND: postgres
MEDIA_ROOT: images/
POSTGRES_PASSWORD: hunter2
POSTGRES_USER: postgres
POSTGRES_DB: github_actions
POSTGRES_HOST: 127.0.0.1
CELERY_BROKER: ""
REDIS_BROKER_PORT: 6379
REDIS_BROKER_PASSWORD: beep
USE_DUMMY_CACHE: true
FLOWER_PORT: 8888
EMAIL_HOST: "smtp.mailgun.org"
EMAIL_PORT: 587
EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: ""
EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: false
ENABLE_THUMBNAIL_GENERATION: true
HTTP_X_FORWARDED_PROTO: false
run: |
mypy bookwyrm celerywyrm

View file

@ -14,7 +14,7 @@ jobs:
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install modules - name: Install modules
run: npm install prettier@2.5.1 run: npm install prettier@2.5.1

View file

@ -1,27 +0,0 @@
name: Pylint
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Analysing the code with pylint
run: |
pylint bookwyrm/

99
.github/workflows/python.yml vendored Normal file
View file

@ -0,0 +1,99 @@
name: Python
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
# overrides for .env.example
env:
POSTGRES_HOST: 127.0.0.1
PGPORT: 5432
POSTGRES_USER: postgres
POSTGRES_PASSWORD: hunter2
POSTGRES_DB: github_actions
SECRET_KEY: beepbeep
EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: ""
jobs:
pytest:
name: Tests (pytest)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env: # does not inherit from jobs.build.env
POSTGRES_USER: postgres
POSTGRES_PASSWORD: hunter2
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: pip
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-github-actions-annotate-failures
- name: Set up .env
run: cp .env.example .env
- name: Check migrations up-to-date
run: python ./manage.py makemigrations --check
- name: Run Tests
run: pytest -n 3
pylint:
name: Linting (pylint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: pip
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Analyse code with pylint
run: pylint bookwyrm/
mypy:
name: Typing (mypy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: pip
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Set up .env
run: cp .env.example .env
- name: Analyse code with mypy
run: mypy bookwyrm celerywyrm
black:
name: Formatting (black; run ./bw-dev black to fix)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: psf/black@22.12.0
with:
version: 22.12.0

View file

@ -1,4 +1,4 @@
FROM python:3.9 FROM python:3.11
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1

View file

@ -1 +1 @@
0.7.1 0.7.2

View file

@ -20,6 +20,7 @@ from bookwyrm.tasks import app, MISC
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# pylint: disable=invalid-name
TBookWyrmModel = TypeVar("TBookWyrmModel", bound=base_model.BookWyrmModel) TBookWyrmModel = TypeVar("TBookWyrmModel", bound=base_model.BookWyrmModel)
@ -423,6 +424,7 @@ def get_activitypub_data(url):
"Date": now, "Date": now,
"Signature": make_signature("get", sender, url, now), "Signature": make_signature("get", sender, url, now),
}, },
timeout=15,
) )
except requests.RequestException: except requests.RequestException:
raise ConnectorException() raise ConnectorException()

View file

@ -3,7 +3,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional, TypedDict, Any, Callable, Union, Iterator from typing import Optional, TypedDict, Any, Callable, Union, Iterator
from urllib.parse import quote_plus from urllib.parse import quote_plus
import imghdr
# pylint: disable-next=deprecated-module
import imghdr # Deprecated in 3.11 for removal in 3.13; no good alternative yet
import logging import logging
import re import re
import asyncio import asyncio

View file

@ -1,4 +1,5 @@
""" using django model forms """ """ using django model forms """
from urllib.parse import urlparse from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -37,10 +38,9 @@ class FileLinkForm(CustomForm):
), ),
) )
if ( if (
not self.instance models.FileLink.objects.filter(url=url, book=book, filetype=filetype)
and models.FileLink.objects.filter( .exclude(pk=self.instance)
url=url, book=book, filetype=filetype .exists()
).exists()
): ):
# pylint: disable=line-too-long # pylint: disable=line-too-long
self.add_error( self.add_error(

View file

@ -26,7 +26,7 @@ class IsbnHyphenator:
def update_range_message(self) -> None: def update_range_message(self) -> None:
"""Download the range message xml file and save it locally""" """Download the range message xml file and save it locally"""
response = requests.get(self.__range_message_url) response = requests.get(self.__range_message_url, timeout=15)
with open(self.__range_file_path, "w", encoding="utf-8") as file: with open(self.__range_file_path, "w", encoding="utf-8") as file:
file.write(response.text) file.write(response.text)
self.__element_tree = None self.__element_tree = None

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

@ -0,0 +1,46 @@
# Generated by Django 3.2.23 on 2024-02-21 00:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0194_merge_20240203_1619"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("ca-es", "Català (Catalan)"),
("de-de", "Deutsch (German)"),
("eo-uy", "Esperanto (Esperanto)"),
("es-es", "Español (Spanish)"),
("eu-es", "Euskara (Basque)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("ko-kr", "한국어 (Korean)"),
("fi-fi", "Suomi (Finnish)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("nl-nl", "Nederlands (Dutch)"),
("no-no", "Norsk (Norwegian)"),
("pl-pl", "Polski (Polish)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("ro-ro", "Română (Romanian)"),
("sv-se", "Svenska (Swedish)"),
("uk-ua", "Українська (Ukrainian)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-03-18 17:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0193_auto_20240128_0249"),
("bookwyrm", "0195_alter_user_preferred_language"),
]
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

@ -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"
@ -318,6 +321,7 @@ LANGUAGES = [
("eu-es", _("Euskara (Basque)")), ("eu-es", _("Euskara (Basque)")),
("gl-es", _("Galego (Galician)")), ("gl-es", _("Galego (Galician)")),
("it-it", _("Italiano (Italian)")), ("it-it", _("Italiano (Italian)")),
("ko-kr", _("한국어 (Korean)")),
("fi-fi", _("Suomi (Finnish)")), ("fi-fi", _("Suomi (Finnish)")),
("fr-fr", _("Français (French)")), ("fr-fr", _("Français (French)")),
("lt-lt", _("Lietuvių (Lithuanian)")), ("lt-lt", _("Lietuvių (Lithuanian)")),
@ -347,8 +351,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)
@ -443,4 +446,6 @@ if HTTP_X_FORWARDED_PROTO:
# 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)) # We only allow specifying DATA_UPLOAD_MAX_MEMORY_SIZE in MiB from .env
# (note the difference in variable names).
DATA_UPLOAD_MAX_MEMORY_SIZE = env.int("DATA_UPLOAD_MAX_MEMORY_MiB", 100) << 20

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

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

@ -36,7 +36,7 @@
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{% trans "Search for a book, user, or list" as search_placeholder %} {% trans "Search for a book, author, user, or list" as search_placeholder %}
{% else %} {% else %}
{% trans "Search for a book" as search_placeholder %} {% trans "Search for a book" as search_placeholder %}
{% endif %} {% endif %}

View file

@ -14,31 +14,29 @@
<p> {% trans "You can create an export file here. This will allow you to migrate your data to another BookWyrm account." %}</p> <p> {% trans "You can create an export file here. This will allow you to migrate your data to another BookWyrm account." %}</p>
</div> </div>
<div class="block mx-5 columns"> <div class="block mx-5 columns">
{% blocktrans trimmed %}
<div class="column is-half"> <div class="column is-half">
<h2 class="is-size-5">Your file will include:</h2> <h2 class="is-size-5">{% trans "Your file will include:" %}</h2>
<ul> <ul>
<li>User profile</li> <li>{% trans "User profile" %}</li>
<li>Most user settings</li> <li>{% trans "Most user settings" %}</li>
<li>Reading goals</li> <li>{% trans "Reading goals" %}</li>
<li>Shelves</li> <li>{% trans "Shelves" %}</li>
<li>Reading history</li> <li>{% trans "Reading history" %}</li>
<li>Book reviews</li> <li>{% trans "Book reviews" %}</li>
<li>Statuses</li> <li>{% trans "Statuses" %}</li>
<li>Your own lists and saved lists</li> <li>{% trans "Your own lists and saved lists" %}</li>
<li>Which users you follow and block</li> <li>{% trans "Which users you follow and block" %}</li>
</ul> </ul>
</div> </div>
<div class="column is-half"> <div class="column is-half">
<h2 class="is-size-5">Your file will not include:</h2> <h2 class="is-size-5">{% trans "Your file will not include:" %}</h2>
<ul> <ul>
<li>Direct messages</li> <li>{% trans "Direct messages" %}</li>
<li>Replies to your statuses</li> <li>{% trans "Replies to your statuses" %}</li>
<li>Groups</li> <li>{% trans "Groups" %}</li>
<li>Favorites</li> <li>{% trans "Favorites" %}</li>
</ul> </ul>
</div> </div>
{% endblocktrans %}
</div> </div>
<p class="block">{% trans "In your new BookWyrm account can choose what to import: you will not have to import everything that is exported." %}</p> <p class="block">{% trans "In your new BookWyrm account can choose what to import: you will not have to import everything that is exported." %}</p>
<p class="notification is-warning"> <p class="notification is-warning">
@ -49,6 +47,13 @@
{% if not site.user_exports_enabled %} {% if not site.user_exports_enabled %}
<p class="notification is-danger"> <p class="notification is-danger">
{% trans "New user exports are currently disabled." %} {% trans "New user exports are currently disabled." %}
{% if perms.bookwyrm.edit_instance_settings %}
<br/>
{% url 'settings-imports' as url %}
{% blocktrans trimmed %}
User exports settings can be changed from <a href="{{ url }}">the Imports page</a> in the Admin dashboard.
{% endblocktrans %}
{% endif%}
</p> </p>
{% elif next_available %} {% elif next_available %}
<p class="notification is-warning"> <p class="notification is-warning">

View file

@ -0,0 +1,17 @@
{% extends 'search/layout.html' %}
{% block panel %}
{% if results %}
<ul class="block">
{% for author in results %}
<li class="">
<a href="{{ author.local_path }}" class="author" itemprop="author" itemscope itemtype="https://schema.org/Thing">
<span itemprop="name">{{ author.name }}</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View file

@ -20,6 +20,7 @@
<div class="select" aria-label="{% trans 'Search type' %}"> <div class="select" aria-label="{% trans 'Search type' %}">
<select name="type"> <select name="type">
<option value="book" {% if type == "book" %}selected{% endif %}>{% trans "Books" %}</option> <option value="book" {% if type == "book" %}selected{% endif %}>{% trans "Books" %}</option>
<option value="author" {% if type == "author" %}selected{% endif %}>{% trans "Authors" %}</option>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<option value="user" {% if type == "user" %}selected{% endif %}>{% trans "Users" %}</option> <option value="user" {% if type == "user" %}selected{% endif %}>{% trans "Users" %}</option>
{% endif %} {% endif %}
@ -42,6 +43,9 @@
<li{% if type == "book" %} class="is-active"{% endif %}> <li{% if type == "book" %} class="is-active"{% endif %}>
<a href="{% url 'search' %}?q={{ query }}&type=book">{% trans "Books" %}</a> <a href="{% url 'search' %}?q={{ query }}&type=book">{% trans "Books" %}</a>
</li> </li>
<li{% if type == "author" %} class="is-active"{% endif %}>
<a href="{% url 'search' %}?q={{ query }}&type=author">{% trans "Authors" %}</a>
</li>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li{% if type == "user" %} class="is-active"{% endif %}> <li{% if type == "user" %} class="is-active"{% endif %}>
<a href="{% url 'search' %}?q={{ query }}&type=user">{% trans "Users" %}</a> <a href="{% url 'search' %}?q={{ query }}&type=user">{% trans "Users" %}</a>

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

@ -3,7 +3,7 @@
{% block filter %} {% block filter %}
<div class="control"> <div class="control">
<label class="label" for="filter_query">{% trans 'Filter by keyword' %}</label> <label class="label" for="my-books-filter">{% trans 'Filter by keyword' %}</label>
<input aria-label="Filter by keyword" id="my-books-filter" class="input" type="text" name="filter" placeholder="{% trans 'Enter text here' %}" value="{{ shelves_filter_query|default:'' }}" spellcheck="false" /> <input id="my-books-filter" class="input" type="text" name="filter" placeholder="{% trans 'Enter text here' %}" value="{{ shelves_filter_query|default:'' }}" spellcheck="false" />
</div> </div>
{% endblock %} {% 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

@ -126,7 +126,7 @@ def id_to_username(user_id):
value = f"{name}@{domain}" value = f"{name}@{domain}"
return value return value
return "a new user account" 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

@ -13,16 +13,26 @@ def validate_html(html):
"warn-proprietary-attributes": False, "warn-proprietary-attributes": False,
}, },
) )
# idk how else to filter out these unescape amp errs # Tidy's parser is strict when validating unescaped/encoded ampersands found within
# the html document that are notpart of a character or entity reference
# (eg: `&amp;` or `&#38`). Despite the fact the HTML5 spec no longer recommends
# escaping ampersands in URLs, Tidy will still complain if they are used as query
# param keys. Unfortunately, there is no way currently to configure tidy to ignore
# this so we must explictly redlist related strings that will appear in Tidy's
# errors output.
#
# See further discussion: https://github.com/htacg/tidy-html5/issues/1017
excluded = [
"&book",
"&type",
"&resolved",
"id and name attribute",
"illegal characters found in URI",
"escaping malformed URI reference",
"&filter",
]
errors = "\n".join( errors = "\n".join(
e e for e in errors.split("\n") if not any(exclude in e for exclude in excluded)
for e in errors.split("\n")
if "&book" not in e
and "&type" not in e
and "&resolved" not in e
and "id and name attribute" not in e
and "illegal characters found in URI" not in e
and "escaping malformed URI reference" not in e
) )
if errors: if errors:
raise Exception(errors) raise Exception(errors)

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

@ -133,3 +133,73 @@ class BookViews(TestCase):
self.assertEqual(models.ShelfBook.objects.get().book, edition2) self.assertEqual(models.ShelfBook.objects.get().book, edition2)
self.assertEqual(models.ReadThrough.objects.get().book, edition2) self.assertEqual(models.ReadThrough.objects.get().book, edition2)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_move_ratings_on_switch_edition(self, *_):
"""updates user's rating on a book to new edition"""
work = models.Work.objects.create(title="test work")
edition1 = models.Edition.objects.create(title="first ed", parent_work=work)
edition2 = models.Edition.objects.create(title="second ed", parent_work=work)
models.ReviewRating.objects.create(
book=edition1,
user=self.local_user,
rating=3,
)
self.assertIsInstance(
models.ReviewRating.objects.get(user=self.local_user, book=edition1),
models.ReviewRating,
)
with self.assertRaises(models.ReviewRating.DoesNotExist):
models.ReviewRating.objects.get(user=self.local_user, book=edition2)
request = self.factory.post("", {"edition": edition2.id})
request.user = self.local_user
views.switch_edition(request)
self.assertIsInstance(
models.ReviewRating.objects.get(user=self.local_user, book=edition2),
models.ReviewRating,
)
with self.assertRaises(models.ReviewRating.DoesNotExist):
models.ReviewRating.objects.get(user=self.local_user, book=edition1)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_move_reviews_on_switch_edition(self, *_):
"""updates user's review on a book to new edition"""
work = models.Work.objects.create(title="test work")
edition1 = models.Edition.objects.create(title="first ed", parent_work=work)
edition2 = models.Edition.objects.create(title="second ed", parent_work=work)
models.Review.objects.create(
book=edition1,
user=self.local_user,
name="blah",
rating=3,
content="not bad",
)
self.assertIsInstance(
models.Review.objects.get(user=self.local_user, book=edition1),
models.Review,
)
with self.assertRaises(models.Review.DoesNotExist):
models.Review.objects.get(user=self.local_user, book=edition2)
request = self.factory.post("", {"edition": edition2.id})
request.user = self.local_user
views.switch_edition(request)
self.assertIsInstance(
models.Review.objects.get(user=self.local_user, book=edition2),
models.Review,
)
with self.assertRaises(models.Review.DoesNotExist):
models.Review.objects.get(user=self.local_user, book=edition1)

View file

@ -219,3 +219,48 @@ class ShelfViews(TestCase):
view(request, request.user.username, shelf.identifier) view(request, request.user.username, shelf.identifier)
self.assertEqual(shelf.name, "To Read") self.assertEqual(shelf.name, "To Read")
def test_filter_shelf_found(self, *_):
"""display books that match a filter keyword"""
models.ShelfBook.objects.create(
book=self.book,
shelf=self.shelf,
user=self.local_user,
)
shelf_book = models.ShelfBook.objects.create(
book=self.book,
shelf=self.local_user.shelf_set.first(),
user=self.local_user,
)
view = views.Shelf.as_view()
request = self.factory.get("", {"filter": shelf_book.book.title})
request.user = self.local_user
with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.local_user.username)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["books"].object_list), 1)
self.assertEqual(
result.context_data["books"].object_list[0].title,
shelf_book.book.title,
)
def test_filter_shelf_none(self, *_):
"""display a message when no books match a filter keyword"""
models.ShelfBook.objects.create(
book=self.book,
shelf=self.shelf,
user=self.local_user,
)
view = views.Shelf.as_view()
request = self.factory.get("", {"filter": "NOPE"})
request.user = self.local_user
with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.local_user.username)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["books"].object_list), 0)

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

@ -93,6 +93,7 @@ def switch_edition(request):
user=shelfbook.user, user=shelfbook.user,
shelf=shelfbook.shelf, shelf=shelfbook.shelf,
book=new_edition, book=new_edition,
shelved_date=shelfbook.shelved_date,
) )
shelfbook.delete() shelfbook.delete()
@ -103,4 +104,13 @@ def switch_edition(request):
readthrough.book = new_edition readthrough.book = new_edition
readthrough.save() readthrough.save()
reviews = models.Review.objects.filter(
book__parent_work=new_edition.parent_work, user=request.user
)
for review in reviews.all():
# because ratings are a subclass of reviews,
# this will pick up both ratings and reviews
review.book = new_edition
review.save()
return redirect(f"/book/{new_edition.id}") return redirect(f"/book/{new_edition.id}")

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

@ -1,4 +1,5 @@
""" search views""" """ search views"""
import re import re
from django.contrib.postgres.search import TrigramSimilarity from django.contrib.postgres.search import TrigramSimilarity
@ -39,6 +40,7 @@ class Search(View):
endpoints = { endpoints = {
"book": book_search, "book": book_search,
"author": author_search,
"user": user_search, "user": user_search,
"list": list_search, "list": list_search,
} }
@ -90,6 +92,31 @@ def book_search(request):
return TemplateResponse(request, "search/book.html", data) return TemplateResponse(request, "search/book.html", data)
def author_search(request):
"""search for an author"""
query = request.GET.get("q")
query = query.strip()
data = {"type": "author", "query": query}
results = (
models.Author.objects.annotate(
similarity=TrigramSimilarity("name", query),
)
.filter(
similarity__gt=0.1,
)
.order_by("-similarity")
)
paginated = Paginator(results, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data["results"] = page
data["page_range"] = paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
)
return TemplateResponse(request, "search/author.html", data)
def user_search(request): def user_search(request):
"""user search: search for a user""" """user search: search for a user"""
viewer = request.user viewer = request.user

1
bw-dev
View file

@ -156,6 +156,7 @@ case "$CMD" in
git checkout l10n_main locale/fi_FI git checkout l10n_main locale/fi_FI
git checkout l10n_main locale/fr_FR git checkout l10n_main locale/fr_FR
git checkout l10n_main locale/gl_ES git checkout l10n_main locale/gl_ES
git checkout l10n_main locale/ko_KR
git checkout l10n_main locale/it_IT git checkout l10n_main locale/it_IT
git checkout l10n_main locale/lt_LT git checkout l10n_main locale/lt_LT
git checkout l10n_main locale/nl_NL git checkout l10n_main locale/nl_NL

View file

@ -1,4 +1,4 @@
FROM python:3.9-bookworm FROM python:3.11-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"

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -61,17 +61,25 @@ server {
proxy_pass http://web; proxy_pass http://web;
} }
# directly serve images and static files from the # directly serve 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 ~ \.(bmp|ico|jpg|jpeg|png|svg|tif|tiff|ttf|webp|css|js)$ { location ~ ^/static/ {
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 # same with image files not in static folder
location ~ \.(bmp|ico|jpg|jpeg|png|svg|tif|tiff|webp)$ {
root /app;
try_files $uri =404;
add_header X-Cache-Status STATIC;
access_log off;
}
# block access to any non-image files from images
location ~ ^/images/ { location ~ ^/images/ {
return 403; return 403;
} }

View file

@ -93,19 +93,27 @@ server {
# proxy_pass http://web; # proxy_pass http://web;
# } # }
# #
# # directly serve images and static files from the # # directly serve 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 ~ \.(bmp|ico|jpg|jpeg|png|svg|tif|tiff|ttf|webp|css|js)$ { # location ~ ^/static/ {
# 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 # # same with image files not in static folder
# location ~ \.(bmp|ico|jpg|jpeg|png|svg|tif|tiff|webp)$ {
# root /app;
# try_files $uri =404;
# add_header X-Cache-Status STATIC;
# access_log off;
# }
# # block access to any non-image files from images
# location ~ ^/images/ { # location ~ ^/images/ {
# return 403; # return 403;
# } # }
# #
# # monitor the celery queues with flower, no caching enabled # # monitor the celery queues with flower, no caching enabled

View file

@ -11,6 +11,7 @@ env =
DEBUG = false DEBUG = false
USE_HTTPS = true USE_HTTPS = true
DOMAIN = your.domain.here DOMAIN = your.domain.here
ALLOWED_HOSTS = your.domain.here
BOOKWYRM_DATABASE_BACKEND = postgres BOOKWYRM_DATABASE_BACKEND = postgres
MEDIA_ROOT = images/ MEDIA_ROOT = images/
CELERY_BROKER = "" CELERY_BROKER = ""

View file

@ -1,53 +1,56 @@
aiohttp==3.9.0 aiohttp==3.9.2
bleach==5.0.1 bleach==5.0.1
celery==5.2.7 boto3==1.26.57
colorthief==0.2.1
Django==3.2.23
django-celery-beat==2.4.0
bw-file-resubmit==0.6.0rc2 bw-file-resubmit==0.6.0rc2
django-compressor==4.3.1 celery==5.3.1
colorthief==0.2.1
Django==3.2.24
django-celery-beat==2.5.0
django-compressor==4.4
django-csp==3.7
django-imagekit==4.1.0 django-imagekit==4.1.0
django-model-utils==4.3.1 django-model-utils==4.3.1
django-redis==5.2.0
django-sass-processor==1.2.2 django-sass-processor==1.2.2
django-csp==3.7
environs==9.5.0
flower==1.2.0
libsass==0.22.0
Markdown==3.4.1
Pillow==10.0.1
psycopg2==2.9.5
pycryptodome==3.19.1
python-dateutil==2.8.2
redis==4.5.4
requests==2.31.0
responses==0.22.0
pytz>=2022.7
boto3==1.26.57
django-storages==1.13.2 django-storages==1.13.2
django-storages[azure] django-storages[azure]
django-redis==5.2.0 environs==9.5.0
flower==2.0.0
grpcio==1.57.0 # Not a direct dependency, pinned to get a security fix
libsass==0.22.0
Markdown==3.4.1
opentelemetry-api==1.16.0 opentelemetry-api==1.16.0
opentelemetry-exporter-otlp-proto-grpc==1.16.0 opentelemetry-exporter-otlp-proto-grpc==1.16.0
opentelemetry-instrumentation-celery==0.37b0 opentelemetry-instrumentation-celery==0.37b0
opentelemetry-instrumentation-django==0.37b0 opentelemetry-instrumentation-django==0.37b0
opentelemetry-instrumentation-psycopg2==0.37b0 opentelemetry-instrumentation-psycopg2==0.37b0
opentelemetry-sdk==1.16.0 opentelemetry-sdk==1.16.0
Pillow==10.0.1
protobuf==3.20.* protobuf==3.20.*
psycopg2==2.9.5
pycryptodome==3.19.1
pyotp==2.8.0 pyotp==2.8.0
python-dateutil==2.8.2
pytz>=2022.7
qrcode==7.3.1 qrcode==7.3.1
redis==4.5.4
requests==2.31.0
responses==0.22.0
s3-tar==0.1.13 s3-tar==0.1.13
setuptools>=65.5.1 # Not a direct dependency, pinned to get a security fix
tornado==6.3.3 # Not a direct dependency, pinned to get a security fix
# Dev # Dev
pytest-django==4.1.0 celery-types==0.18.0
pytest==6.1.2 django-stubs[compatible-mypy]==4.2.4
mypy==1.5.1
pylint==2.15.0
pytest==6.2.5
pytest-cov==2.10.1 pytest-cov==2.10.1
pytest-django==4.1.0
pytest-env==0.6.2 pytest-env==0.6.2
pytest-xdist==2.3.0 pytest-xdist==2.3.0
pytidylib==0.3.2 pytidylib==0.3.2
pylint==2.14.0
mypy==1.5.1
celery-types==0.18.0
django-stubs[compatible-mypy]==4.2.4
types-bleach==6.0.0.4 types-bleach==6.0.0.4
types-dataclasses==0.6.6 types-dataclasses==0.6.6
types-Markdown==3.4.2.10 types-Markdown==3.4.2.10
@ -55,4 +58,3 @@ types-Pillow==10.0.0.3
types-psycopg2==2.9.21.11 types-psycopg2==2.9.21.11
types-python-dateutil==2.8.19.14 types-python-dateutil==2.8.19.14
types-requests==2.31.0.2 types-requests==2.31.0.2
types-requests==2.31.0.2

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