Merge branch 'main' into more-tests
This commit is contained in:
@ -38,7 +38,7 @@ class Create(Verb):
|
||||
class Delete(Verb):
|
||||
"""Create activity"""
|
||||
|
||||
to: List[str]
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
type: str = "Delete"
|
||||
|
||||
@ -137,8 +137,8 @@ class Accept(Verb):
|
||||
type: str = "Accept"
|
||||
|
||||
def action(self):
|
||||
"""find and remove the activity object"""
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
"""accept a request"""
|
||||
obj = self.object.to_model(save=False, allow_create=True)
|
||||
obj.accept()
|
||||
|
||||
|
||||
@ -150,7 +150,7 @@ class Reject(Verb):
|
||||
type: str = "Reject"
|
||||
|
||||
def action(self):
|
||||
"""find and remove the activity object"""
|
||||
"""reject a follow request"""
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
obj.reject()
|
||||
|
||||
|
46
bookwyrm/apps.py
Normal file
46
bookwyrm/apps.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Do further startup configuration and initialization"""
|
||||
import os
|
||||
import urllib
|
||||
import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from bookwyrm import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def download_file(url, destination):
|
||||
"""Downloads a file to the given path"""
|
||||
try:
|
||||
# Ensure our destination directory exists
|
||||
os.makedirs(os.path.dirname(destination))
|
||||
with urllib.request.urlopen(url) as stream:
|
||||
with open(destination, "b+w") as outfile:
|
||||
outfile.write(stream.read())
|
||||
except (urllib.error.HTTPError, urllib.error.URLError):
|
||||
logger.error("Failed to download file %s", url)
|
||||
except OSError:
|
||||
logger.error("Couldn't open font file %s for writing", destination)
|
||||
except: # pylint: disable=bare-except
|
||||
logger.exception("Unknown error in file download")
|
||||
|
||||
|
||||
class BookwyrmConfig(AppConfig):
|
||||
"""Handles additional configuration"""
|
||||
|
||||
name = "bookwyrm"
|
||||
verbose_name = "BookWyrm"
|
||||
|
||||
def ready(self):
|
||||
if settings.ENABLE_PREVIEW_IMAGES and settings.FONTS:
|
||||
# Download any fonts that we don't have yet
|
||||
logger.debug("Downloading fonts..")
|
||||
for name, config in settings.FONTS.items():
|
||||
font_path = os.path.join(
|
||||
settings.FONT_DIR, config["directory"], config["filename"]
|
||||
)
|
||||
|
||||
if "url" in config and not os.path.exists(font_path):
|
||||
logger.info("Just a sec, downloading %s", name)
|
||||
download_file(config["url"], font_path)
|
@ -1,7 +1,9 @@
|
||||
""" functionality outline for a book data connector """
|
||||
from abc import ABC, abstractmethod
|
||||
import imghdr
|
||||
import logging
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
@ -290,10 +292,18 @@ def get_image(url, timeout=10):
|
||||
)
|
||||
except RequestException as err:
|
||||
logger.exception(err)
|
||||
return None
|
||||
return None, None
|
||||
|
||||
if not resp.ok:
|
||||
return None
|
||||
return resp
|
||||
return None, None
|
||||
|
||||
image_content = ContentFile(resp.content)
|
||||
extension = imghdr.what(None, image_content.read())
|
||||
if not extension:
|
||||
logger.exception("File requested was not an image: %s", url)
|
||||
return None, None
|
||||
|
||||
return image_content, extension
|
||||
|
||||
|
||||
class Mapping:
|
||||
|
@ -48,7 +48,9 @@ def moderation_report_email(report):
|
||||
data["reportee"] = report.user.localname or report.user.username
|
||||
data["report_link"] = report.remote_id
|
||||
|
||||
for admin in models.User.objects.filter(groups__name__in=["admin", "moderator"]):
|
||||
for admin in models.User.objects.filter(
|
||||
groups__name__in=["admin", "moderator"]
|
||||
).distinct():
|
||||
data["user"] = admin.display_name
|
||||
send_email.delay(admin.email, *format_email("moderation_report", data))
|
||||
|
||||
|
37
bookwyrm/migrations/0132_alter_user_preferred_language.py
Normal file
37
bookwyrm/migrations/0132_alter_user_preferred_language.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.2.10 on 2022-02-02 20:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0131_merge_20220125_1644"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es-es", "Español (Spanish)"),
|
||||
("gl-es", "Galego (Galician)"),
|
||||
("it-it", "Italiano (Italian)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||
("no-no", "Norsk (Norwegian)"),
|
||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||
("sv-se", "Svenska (Swedish)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
@ -1,6 +1,5 @@
|
||||
""" activitypub-aware django model fields """
|
||||
from dataclasses import MISSING
|
||||
import imghdr
|
||||
import re
|
||||
from uuid import uuid4
|
||||
from urllib.parse import urljoin
|
||||
@ -9,7 +8,6 @@ import dateutil.parser
|
||||
from dateutil.parser import ParserError
|
||||
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from django.forms import ClearableFileInput, ImageField as DjangoImageField
|
||||
from django.utils import timezone
|
||||
@ -443,12 +441,10 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
response = get_image(url)
|
||||
if not response:
|
||||
image_content, extension = get_image(url)
|
||||
if not image_content:
|
||||
return None
|
||||
|
||||
image_content = ContentFile(response.content)
|
||||
extension = imghdr.what(None, image_content.read()) or ""
|
||||
image_name = f"{uuid4()}.{extension}"
|
||||
return [image_name, image_content]
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
""" flagged for moderation """
|
||||
from django.db import models
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .base_model import BookWyrmModel
|
||||
|
||||
|
||||
@ -15,6 +16,9 @@ class Report(BookWyrmModel):
|
||||
links = models.ManyToManyField("Link", blank=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
|
||||
def get_remote_id(self):
|
||||
return f"https://{DOMAIN}/settings/reports/{self.id}"
|
||||
|
||||
class Meta:
|
||||
"""set order by default"""
|
||||
|
||||
|
@ -4,6 +4,7 @@ import os
|
||||
import textwrap
|
||||
from io import BytesIO
|
||||
from uuid import uuid4
|
||||
import logging
|
||||
|
||||
import colorsys
|
||||
from colorthief import ColorThief
|
||||
@ -17,34 +18,49 @@ from django.db.models import Avg
|
||||
from bookwyrm import models, settings
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
|
||||
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
|
||||
BG_COLOR = settings.PREVIEW_BG_COLOR
|
||||
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
|
||||
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
|
||||
DEFAULT_FONT = settings.PREVIEW_DEFAULT_FONT
|
||||
TRANSPARENT_COLOR = (0, 0, 0, 0)
|
||||
|
||||
margin = math.floor(IMG_HEIGHT / 10)
|
||||
gutter = math.floor(margin / 2)
|
||||
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
|
||||
inner_img_width = math.floor(inner_img_height * 0.7)
|
||||
font_dir = os.path.join(settings.STATIC_ROOT, "fonts/public_sans")
|
||||
|
||||
|
||||
def get_font(font_name, size=28):
|
||||
"""Loads custom font"""
|
||||
if font_name == "light":
|
||||
font_path = os.path.join(font_dir, "PublicSans-Light.ttf")
|
||||
if font_name == "regular":
|
||||
font_path = os.path.join(font_dir, "PublicSans-Regular.ttf")
|
||||
elif font_name == "bold":
|
||||
font_path = os.path.join(font_dir, "PublicSans-Bold.ttf")
|
||||
def get_imagefont(name, size):
|
||||
"""Loads an ImageFont based on config"""
|
||||
try:
|
||||
config = settings.FONTS[name]
|
||||
path = os.path.join(settings.FONT_DIR, config["directory"], config["filename"])
|
||||
return ImageFont.truetype(path, size)
|
||||
except KeyError:
|
||||
logger.error("Font %s not found in config", name)
|
||||
except OSError:
|
||||
logger.error("Could not load font %s from file", name)
|
||||
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def get_font(weight, size=28):
|
||||
"""Gets a custom font with the given weight and size"""
|
||||
font = get_imagefont(DEFAULT_FONT, size)
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, size)
|
||||
except OSError:
|
||||
font = ImageFont.load_default()
|
||||
if weight == "light":
|
||||
font.set_variation_by_name("Light")
|
||||
if weight == "bold":
|
||||
font.set_variation_by_name("Bold")
|
||||
if weight == "regular":
|
||||
font.set_variation_by_name("Regular")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return font
|
||||
|
||||
|
@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.2.0"
|
||||
VERSION = "0.2.1"
|
||||
|
||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
@ -35,6 +35,9 @@ LOCALE_PATHS = [
|
||||
]
|
||||
LANGUAGE_COOKIE_NAME = env.str("LANGUAGE_COOKIE_NAME", "django_language")
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
# Preview image
|
||||
@ -44,6 +47,17 @@ PREVIEW_TEXT_COLOR = env.str("PREVIEW_TEXT_COLOR", "#363636")
|
||||
PREVIEW_IMG_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
|
||||
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
|
||||
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
|
||||
PREVIEW_DEFAULT_FONT = env.str("PREVIEW_DEFAULT_FONT", "Source Han Sans")
|
||||
|
||||
FONTS = {
|
||||
# pylint: disable=line-too-long
|
||||
"Source Han Sans": {
|
||||
"directory": "source_han_sans",
|
||||
"filename": "SourceHanSans-VF.ttf.ttc",
|
||||
"url": "https://github.com/adobe-fonts/source-han-sans/raw/release/Variable/OTC/SourceHanSans-VF.ttf.ttc",
|
||||
}
|
||||
}
|
||||
FONT_DIR = os.path.join(STATIC_ROOT, "fonts")
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
@ -150,6 +164,9 @@ LOGGING = {
|
||||
"handlers": ["console", "mail_admins"],
|
||||
"level": LOG_LEVEL,
|
||||
},
|
||||
"django.utils.autoreload": {
|
||||
"level": "INFO",
|
||||
},
|
||||
# Add a bookwyrm-specific logger
|
||||
"bookwyrm": {
|
||||
"handlers": ["console"],
|
||||
@ -255,7 +272,7 @@ LANGUAGES = [
|
||||
("no-no", _("Norsk (Norwegian)")),
|
||||
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||
("sv-se", _("Swedish (Svenska)")),
|
||||
("sv-se", _("Svenska (Swedish)")),
|
||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||
]
|
||||
@ -311,13 +328,8 @@ if USE_S3:
|
||||
MEDIA_FULL_URL = MEDIA_URL
|
||||
STATIC_FULL_URL = STATIC_URL
|
||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
||||
# I don't know if it's used, but the site crashes without it
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||
else:
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||
MEDIA_URL = "/images/"
|
||||
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
||||
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||
|
96
bookwyrm/static/fonts/source_han_sans/LICENSE.txt
Normal file
96
bookwyrm/static/fonts/source_han_sans/LICENSE.txt
Normal file
@ -0,0 +1,96 @@
|
||||
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font
|
||||
Name 'Source'. Source is a trademark of Adobe in the United States
|
||||
and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
9
bookwyrm/static/fonts/source_han_sans/README.txt
Normal file
9
bookwyrm/static/fonts/source_han_sans/README.txt
Normal file
@ -0,0 +1,9 @@
|
||||
The font file itself is not included in the Git repository to avoid putting
|
||||
large files in the repo history. The Docker image should download the correct
|
||||
font into this folder automatically.
|
||||
|
||||
In case something goes wrong, the font used is the Variable OTC TTF, available
|
||||
as of this writing from the Adobe Fonts GitHub repository:
|
||||
https://github.com/adobe-fonts/source-han-sans/tree/release#user-content-variable-otcs
|
||||
|
||||
BookWyrm expects the file to be in this folder, named SourceHanSans-VF.ttf.ttc
|
@ -28,7 +28,7 @@
|
||||
|
||||
<div class="columns">
|
||||
{% if superlatives.top_rated %}
|
||||
{% with book=superlatives.top_rated.default_edition rating=top_rated.rating %}
|
||||
{% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %}
|
||||
<div class="column is-one-third is-flex">
|
||||
<div class="media notification">
|
||||
<div class="media-left">
|
||||
|
@ -356,10 +356,11 @@
|
||||
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
<label class="label" for="id_list">{% trans "Add to list" %}</label>
|
||||
<div class="field has-addons">
|
||||
<div class="select control is-clipped">
|
||||
<select name="list" id="id_list">
|
||||
<select name="book_list" id="id_list">
|
||||
{% for list in user.list_set.all %}
|
||||
<option value="{{ list.id }}">{{ list.name }}</option>
|
||||
{% endfor %}
|
||||
|
@ -2,15 +2,17 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block filter %}
|
||||
<label class="label is-block" for="id_format">{% trans "Format:" %}</label>
|
||||
<div class="select">
|
||||
<select id="id_format" name="format">
|
||||
<option value="">{% trans "Any" %}</option>
|
||||
{% for format in formats %}{% if format %}
|
||||
<option value="{{ format }}" {% if request.GET.format == format %}selected{% endif %}>
|
||||
{{ format|title }}
|
||||
</option>
|
||||
{% endif %}{% endfor %}
|
||||
</select>
|
||||
<div class="control">
|
||||
<label class="label is-block" for="id_format">{% trans "Format:" %}</label>
|
||||
<div class="select">
|
||||
<select id="id_format" name="format">
|
||||
<option value="">{% trans "Any" %}</option>
|
||||
{% for format in formats %}{% if format %}
|
||||
<option value="{{ format }}" {% if request.GET.format == format %}selected{% endif %}>
|
||||
{{ format|title }}
|
||||
</option>
|
||||
{% endif %}{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -2,15 +2,17 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block filter %}
|
||||
<label class="label is-block" for="id_language">{% trans "Language:" %}</label>
|
||||
<div class="select">
|
||||
<select id="id_language" name="language">
|
||||
<option value="">{% trans "Any" %}</option>
|
||||
{% for language in languages %}
|
||||
<option value="{{ language }}" {% if request.GET.language == language %}selected{% endif %}>
|
||||
{{ language }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="control">
|
||||
<label class="label is-block" for="id_language">{% trans "Language:" %}</label>
|
||||
<div class="select">
|
||||
<select id="id_language" name="language">
|
||||
<option value="">{% trans "Any" %}</option>
|
||||
{% for language in languages %}
|
||||
<option value="{{ language }}" {% if request.GET.language == language %}selected{% endif %}>
|
||||
{{ language }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -2,7 +2,9 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block filter %}
|
||||
<label class="label" for="id_search">{% trans "Search editions" %}</label>
|
||||
<input type="text" class="input" name="q" value="{{ request.GET.q|default:'' }}" id="id_search">
|
||||
<div class="control">
|
||||
<label class="label" for="id_search">{% trans "Search editions" %}</label>
|
||||
<input type="text" class="input" name="q" value="{{ request.GET.q|default:'' }}" id="id_search">
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -97,7 +97,7 @@
|
||||
<span class="details-close icon icon-pencil" aria-hidden></span>
|
||||
</span>
|
||||
</summary>
|
||||
{% include "lists/edit_item_form.html" %}
|
||||
{% include "lists/edit_item_form.html" with book=item.book %}
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -112,7 +112,7 @@
|
||||
<span class="details-close icon icon-plus" aria-hidden></span>
|
||||
</span>
|
||||
</summary>
|
||||
{% include "lists/edit_item_form.html" %}
|
||||
{% include "lists/edit_item_form.html" with book=item.book %}
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -17,6 +17,19 @@
|
||||
{% include 'settings/reports/report_preview.html' with report=report %}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<details class="details-panel box">
|
||||
<summary>
|
||||
<span class="title is-4">{% trans "Message reporter" %}</span>
|
||||
<span class="details-close icon icon-x" aria-hidden></span>
|
||||
</summary>
|
||||
<div class="box">
|
||||
{% trans "Update on your report:" as dm_template %}
|
||||
{% include 'snippets/create_status/status.html' with type="direct" uuid=1 mention=report.reporter prepared_content=dm_template no_script=True %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{% if report.statuses.exists %}
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
|
||||
@ -68,9 +81,13 @@
|
||||
{% endfor %}
|
||||
<form class="block" name="report-comment" method="post" action="{% url 'settings-report' report.id %}">
|
||||
{% csrf_token %}
|
||||
<label for="report_comment" class="label">Comment on report</label>
|
||||
<textarea name="note" id="report_comment" class="textarea"></textarea>
|
||||
<button class="button">{% trans "Comment" %}</button>
|
||||
<div class="field">
|
||||
<label for="report_comment" class="label">Comment on report</label>
|
||||
<textarea name="note" id="report_comment" class="textarea"></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="button">{% trans "Comment" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -45,7 +45,7 @@
|
||||
href="{{ shelf_tab.local_path }}"
|
||||
{% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %}
|
||||
>
|
||||
{% include 'user/books_header.html' with shelf=shelf_tab %}
|
||||
{% include "snippets/translated_shelf_name.html" with shelf=shelf_tab %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -8,13 +8,14 @@ reply_parent: if applicable, the Status object that this post is in reply to
|
||||
mention: a user who is @ mentioned by default in the post
|
||||
draft: an existing Status object that is providing default values for input fields
|
||||
{% endcomment %}
|
||||
<textarea
|
||||
name="content"
|
||||
class="textarea save-draft"
|
||||
{% if not draft %}data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"{% endif %}
|
||||
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
|
||||
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}
|
||||
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{% firstof draft.raw_content draft.content '' %}</textarea>
|
||||
|
||||
<div class="control">
|
||||
<textarea
|
||||
name="content"
|
||||
class="textarea save-draft"
|
||||
{% if not draft %}data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"{% endif %}
|
||||
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
|
||||
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}
|
||||
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{{ prepared_content }}{% firstof draft.raw_content draft.content '' %}</textarea>
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="column is-narrow control">
|
||||
<button class="button is-link" type="submit">
|
||||
<span class="icon icon-spinner" aria-hidden="true"></span>
|
||||
<span>{% trans "Post" %}</span>
|
||||
|
@ -38,9 +38,11 @@
|
||||
{% block filter_fields %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<button type="submit" class="button is-primary is-small">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Apply filters" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
@ -22,7 +22,7 @@
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="reporter" value="{{ request.user.id }}">
|
||||
<input type="hidden" name="user" value="{{ user.id }}">
|
||||
{% if status %}
|
||||
{% if status_id %}
|
||||
<input type="hidden" name="statuses" value="{{ status_id }}">
|
||||
{% endif %}
|
||||
{% if link %}
|
||||
|
@ -3,10 +3,8 @@
|
||||
{% load book_display_tags %}
|
||||
{% load markdown %}
|
||||
{% load i18n %}
|
||||
{% load cache %}
|
||||
|
||||
{% if not hide_book %}
|
||||
{% cache 259200 generated_status_book status.id %}
|
||||
{% with book=status.book|default:status.mention_books.first %}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<a class="column is-cover is-narrow" href="{{ book.local_path }}">
|
||||
@ -26,7 +24,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endcache %}
|
||||
{% endif %}
|
||||
|
||||
{% endspaceless %}
|
||||
|
@ -10,7 +10,9 @@
|
||||
|
||||
{% block dropdown-list %}
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'direct-messages-user' user|username %}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
||||
<div class="control">
|
||||
<a href="{% url 'direct-messages-user' user|username %}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
||||
</div>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
{% include 'snippets/report_button.html' with user=user class="is-fullwidth" %}
|
||||
|
@ -36,22 +36,19 @@ def get_next_shelf(current_shelf):
|
||||
def active_shelf(context, book):
|
||||
"""check what shelf a user has a book on, if any"""
|
||||
user = context["request"].user
|
||||
return (
|
||||
cache.get_or_set(
|
||||
f"active_shelf-{user.id}-{book.id}",
|
||||
lambda u, b: (
|
||||
models.ShelfBook.objects.filter(
|
||||
shelf__user=u,
|
||||
book__parent_work__editions=b,
|
||||
).first()
|
||||
or False
|
||||
),
|
||||
user,
|
||||
book,
|
||||
timeout=15552000,
|
||||
)
|
||||
or {"book": book}
|
||||
)
|
||||
return cache.get_or_set(
|
||||
f"active_shelf-{user.id}-{book.id}",
|
||||
lambda u, b: (
|
||||
models.ShelfBook.objects.filter(
|
||||
shelf__user=u,
|
||||
book__parent_work__editions=b,
|
||||
).first()
|
||||
or False
|
||||
),
|
||||
user,
|
||||
book,
|
||||
timeout=15552000,
|
||||
) or {"book": book}
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=False)
|
||||
|
@ -430,7 +430,7 @@ class ModelFields(TestCase):
|
||||
output = instance.field_to_activity(user.avatar)
|
||||
self.assertIsNotNone(
|
||||
re.match(
|
||||
fr"https:\/\/{DOMAIN}\/.*\.jpg",
|
||||
rf"https:\/\/{DOMAIN}\/.*\.jpg",
|
||||
output.url,
|
||||
)
|
||||
)
|
||||
@ -443,18 +443,17 @@ class ModelFields(TestCase):
|
||||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
|
||||
instance = fields.ImageField()
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image.tobytes(),
|
||||
status=200,
|
||||
)
|
||||
with open(image_file, "rb") as image_data:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image_data.read(),
|
||||
status=200,
|
||||
content_type="image/jpeg",
|
||||
stream=True,
|
||||
)
|
||||
loaded_image = instance.field_from_activity("http://www.example.com/image.jpg")
|
||||
self.assertIsInstance(loaded_image, list)
|
||||
self.assertIsInstance(loaded_image[1], ContentFile)
|
||||
@ -465,18 +464,18 @@ class ModelFields(TestCase):
|
||||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
|
||||
instance = fields.ImageField(activitypub_field="cover", name="cover")
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image.tobytes(),
|
||||
status=200,
|
||||
)
|
||||
with open(image_file, "rb") as image_data:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image_data.read(),
|
||||
content_type="image/jpeg",
|
||||
status=200,
|
||||
stream=True,
|
||||
)
|
||||
book = Edition.objects.create(title="hello")
|
||||
|
||||
MockActivity = namedtuple("MockActivity", ("cover"))
|
||||
@ -491,18 +490,18 @@ class ModelFields(TestCase):
|
||||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
|
||||
instance = fields.ImageField(activitypub_field="cover", name="cover")
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image.tobytes(),
|
||||
status=200,
|
||||
)
|
||||
with open(image_file, "rb") as image_data:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image_data.read(),
|
||||
status=200,
|
||||
content_type="image/jpeg",
|
||||
stream=True,
|
||||
)
|
||||
book = Edition.objects.create(title="hello")
|
||||
|
||||
MockActivity = namedtuple("MockActivity", ("cover"))
|
||||
@ -565,18 +564,18 @@ class ModelFields(TestCase):
|
||||
another_image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/logo.png"
|
||||
)
|
||||
another_image = Image.open(another_image_file)
|
||||
another_output = BytesIO()
|
||||
another_image.save(another_output, format=another_image.format)
|
||||
|
||||
instance = fields.ImageField(activitypub_field="cover", name="cover")
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=another_image.tobytes(),
|
||||
status=200,
|
||||
)
|
||||
with open(another_image_file, "rb") as another_image:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=another_image.read(),
|
||||
status=200,
|
||||
content_type="image/jpeg",
|
||||
stream=True,
|
||||
)
|
||||
|
||||
MockActivity = namedtuple("MockActivity", ("cover"))
|
||||
mock_activity = MockActivity("http://www.example.com/image.jpg")
|
||||
|
@ -34,9 +34,11 @@ urlpatterns = [
|
||||
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
|
||||
),
|
||||
# federation endpoints
|
||||
re_path(r"^inbox/?$", views.Inbox.as_view()),
|
||||
re_path(rf"{LOCAL_USER_PATH}/inbox/?$", views.Inbox.as_view()),
|
||||
re_path(rf"{LOCAL_USER_PATH}/outbox/?$", views.Outbox.as_view()),
|
||||
re_path(r"^inbox/?$", views.Inbox.as_view(), name="inbox"),
|
||||
re_path(rf"{LOCAL_USER_PATH}/inbox/?$", views.Inbox.as_view(), name="user_inbox"),
|
||||
re_path(
|
||||
rf"{LOCAL_USER_PATH}/outbox/?$", views.Outbox.as_view(), name="user_outbox"
|
||||
),
|
||||
re_path(r"^\.well-known/webfinger/?$", views.webfinger),
|
||||
re_path(r"^\.well-known/nodeinfo/?$", views.nodeinfo_pointer),
|
||||
re_path(r"^\.well-known/host-meta/?$", views.host_meta),
|
||||
@ -46,9 +48,15 @@ urlpatterns = [
|
||||
re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"),
|
||||
re_path(r"^ostatus_subscribe/?$", views.ostatus_follow_request),
|
||||
# polling updates
|
||||
re_path("^api/updates/notifications/?$", views.get_notification_count),
|
||||
re_path(
|
||||
"^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_string
|
||||
"^api/updates/notifications/?$",
|
||||
views.get_notification_count,
|
||||
name="notification-updates",
|
||||
),
|
||||
re_path(
|
||||
"^api/updates/stream/(?P<stream>[a-z]+)/?$",
|
||||
views.get_unread_status_string,
|
||||
name="stream-updates",
|
||||
),
|
||||
# authentication
|
||||
re_path(r"^login/?$", views.Login.as_view(), name="login"),
|
||||
@ -149,7 +157,9 @@ urlpatterns = [
|
||||
re_path(
|
||||
r"^invite-request/?$", views.InviteRequest.as_view(), name="invite-request"
|
||||
),
|
||||
re_path(r"^invite/(?P<code>[A-Za-z0-9]+)/?$", views.Invite.as_view()),
|
||||
re_path(
|
||||
r"^invite/(?P<code>[A-Za-z0-9]+)/?$", views.Invite.as_view(), name="invite"
|
||||
),
|
||||
re_path(
|
||||
r"^settings/email-blocklist/?$",
|
||||
views.EmailBlocklist.as_view(),
|
||||
|
@ -58,6 +58,7 @@ class ReportAdmin(View):
|
||||
"""load a report"""
|
||||
data = {
|
||||
"report": get_object_or_404(models.Report, id=report_id),
|
||||
"group_form": forms.UserGroupForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/reports/report.html", data)
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Avg, Q
|
||||
from django.http import Http404
|
||||
@ -144,13 +143,12 @@ def upload_cover(request, book_id):
|
||||
def set_cover_from_url(url):
|
||||
"""load it from a url"""
|
||||
try:
|
||||
image_file = get_image(url)
|
||||
image_content, extension = get_image(url)
|
||||
except: # pylint: disable=bare-except
|
||||
return None
|
||||
if not image_file:
|
||||
if not image_content:
|
||||
return None
|
||||
image_name = str(uuid4()) + "." + url.split(".")[-1]
|
||||
image_content = ContentFile(image_file.content)
|
||||
image_name = str(uuid4()) + "." + extension
|
||||
return [image_name, image_content]
|
||||
|
||||
|
||||
|
@ -19,4 +19,6 @@ class ListItem(View):
|
||||
form = forms.ListItemForm(request.POST, instance=list_item)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
else:
|
||||
raise Exception(form.errors)
|
||||
return redirect("list", list_item.book_list.id)
|
||||
|
@ -31,12 +31,13 @@ class User(View):
|
||||
shelf_preview = []
|
||||
|
||||
# only show shelves that should be visible
|
||||
shelves = user.shelf_set
|
||||
is_self = request.user.id == user.id
|
||||
if not is_self:
|
||||
shelves = models.Shelf.privacy_filter(
|
||||
request.user, privacy_levels=["public", "followers"]
|
||||
).filter(user=user, books__isnull=False)
|
||||
else:
|
||||
shelves = user.shelf_set.filter(books__isnull=False).distinct()
|
||||
|
||||
for user_shelf in shelves.all()[:3]:
|
||||
shelf_preview.append(
|
||||
|
Reference in New Issue
Block a user