mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-28 11:00:36 +00:00
Merge branch 'main' into tidy-header
This commit is contained in:
commit
c3a32b3a54
125 changed files with 7488 additions and 2794 deletions
|
@ -69,7 +69,7 @@ AWS_SECRET_ACCESS_KEY=
|
|||
# or use_dominant_color_light / use_dominant_color_dark
|
||||
PREVIEW_BG_COLOR=use_dominant_color_light
|
||||
# Change to #FFF if you use use_dominant_color_dark
|
||||
PREVIEW_TEXT_COLOR="#363636"
|
||||
PREVIEW_TEXT_COLOR=#363636
|
||||
PREVIEW_IMG_WIDTH=1200
|
||||
PREVIEW_IMG_HEIGHT=630
|
||||
PREVIEW_DEFAULT_COVER_COLOR="#002549"
|
||||
PREVIEW_DEFAULT_COVER_COLOR=#002549
|
||||
|
|
|
@ -69,7 +69,7 @@ AWS_SECRET_ACCESS_KEY=
|
|||
# or use_dominant_color_light / use_dominant_color_dark
|
||||
PREVIEW_BG_COLOR=use_dominant_color_light
|
||||
# Change to #FFF if you use use_dominant_color_dark
|
||||
PREVIEW_TEXT_COLOR="#363636"
|
||||
PREVIEW_TEXT_COLOR=#363636
|
||||
PREVIEW_IMG_WIDTH=1200
|
||||
PREVIEW_IMG_HEIGHT=630
|
||||
PREVIEW_DEFAULT_COVER_COLOR="#002549"
|
||||
PREVIEW_DEFAULT_COVER_COLOR=#002549
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@
|
|||
*.swp
|
||||
**/__pycache__
|
||||
.local
|
||||
/nginx/nginx.conf
|
||||
|
||||
# VSCode
|
||||
/.vscode
|
||||
|
|
|
@ -22,6 +22,11 @@ class ActivityStream(RedisStore):
|
|||
stream_id = self.stream_id(user)
|
||||
return f"{stream_id}-unread"
|
||||
|
||||
def unread_by_status_type_id(self, user):
|
||||
"""the redis key for this user's unread count for this stream"""
|
||||
stream_id = self.stream_id(user)
|
||||
return f"{stream_id}-unread-by-type"
|
||||
|
||||
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||
"""statuses are sorted by date published"""
|
||||
return obj.published_date.timestamp()
|
||||
|
@ -35,6 +40,10 @@ class ActivityStream(RedisStore):
|
|||
for user in self.get_audience(status):
|
||||
# add to the unread status count
|
||||
pipeline.incr(self.unread_id(user))
|
||||
# add to the unread status count for status type
|
||||
pipeline.hincrby(
|
||||
self.unread_by_status_type_id(user), get_status_type(status), 1
|
||||
)
|
||||
|
||||
# and go!
|
||||
pipeline.execute()
|
||||
|
@ -55,6 +64,7 @@ class ActivityStream(RedisStore):
|
|||
"""load the statuses to be displayed"""
|
||||
# clear unreads for this feed
|
||||
r.set(self.unread_id(user), 0)
|
||||
r.delete(self.unread_by_status_type_id(user))
|
||||
|
||||
statuses = self.get_store(self.stream_id(user))
|
||||
return (
|
||||
|
@ -75,6 +85,14 @@ class ActivityStream(RedisStore):
|
|||
"""get the unread status count for this user's feed"""
|
||||
return int(r.get(self.unread_id(user)) or 0)
|
||||
|
||||
def get_unread_count_by_status_type(self, user):
|
||||
"""get the unread status count for this user's feed's status types"""
|
||||
status_types = r.hgetall(self.unread_by_status_type_id(user))
|
||||
return {
|
||||
str(key.decode("utf-8")): int(value) or 0
|
||||
for key, value in status_types.items()
|
||||
}
|
||||
|
||||
def populate_streams(self, user):
|
||||
"""go from zero to a timeline"""
|
||||
self.populate_store(self.stream_id(user))
|
||||
|
@ -460,7 +478,7 @@ def remove_status_task(status_ids):
|
|||
@app.task(queue=HIGH)
|
||||
def add_status_task(status_id, increment_unread=False):
|
||||
"""add a status to any stream it should be in"""
|
||||
status = models.Status.objects.get(id=status_id)
|
||||
status = models.Status.objects.select_subclasses().get(id=status_id)
|
||||
# we don't want to tick the unread count for csv import statuses, idk how better
|
||||
# to check than just to see if the states is more than a few days old
|
||||
if status.created_date < timezone.now() - timedelta(days=2):
|
||||
|
@ -507,3 +525,20 @@ def handle_boost_task(boost_id):
|
|||
stream.remove_object_from_related_stores(boosted, stores=audience)
|
||||
for status in old_versions:
|
||||
stream.remove_object_from_related_stores(status, stores=audience)
|
||||
|
||||
|
||||
def get_status_type(status):
|
||||
"""return status type even for boosted statuses"""
|
||||
status_type = status.status_type.lower()
|
||||
|
||||
# Check if current status is a boost
|
||||
if hasattr(status, "boost"):
|
||||
# Act in accordance of your findings
|
||||
if hasattr(status.boost.boosted_status, "review"):
|
||||
status_type = "review"
|
||||
if hasattr(status.boost.boosted_status, "comment"):
|
||||
status_type = "comment"
|
||||
if hasattr(status.boost.boosted_status, "quotation"):
|
||||
status_type = "quotation"
|
||||
|
||||
return status_type
|
||||
|
|
|
@ -111,7 +111,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
return existing.default_edition
|
||||
return existing
|
||||
|
||||
# load the json
|
||||
# load the json data from the remote data source
|
||||
data = self.get_book_data(remote_id)
|
||||
if self.is_work_data(data):
|
||||
try:
|
||||
|
@ -150,27 +150,37 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
"""this allows connectors to override the default behavior"""
|
||||
return get_data(remote_id)
|
||||
|
||||
def create_edition_from_data(self, work, edition_data):
|
||||
def create_edition_from_data(self, work, edition_data, instance=None):
|
||||
"""if we already have the work, we're ready"""
|
||||
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
||||
mapped_data["work"] = work.remote_id
|
||||
edition_activity = activitypub.Edition(**mapped_data)
|
||||
edition = edition_activity.to_model(model=models.Edition, overwrite=False)
|
||||
edition.connector = self.connector
|
||||
edition.save()
|
||||
edition = edition_activity.to_model(
|
||||
model=models.Edition, overwrite=False, instance=instance
|
||||
)
|
||||
|
||||
# if we're updating an existing instance, we don't need to load authors
|
||||
if instance:
|
||||
return edition
|
||||
|
||||
if not edition.connector:
|
||||
edition.connector = self.connector
|
||||
edition.save(broadcast=False, update_fields=["connector"])
|
||||
|
||||
for author in self.get_authors_from_data(edition_data):
|
||||
edition.authors.add(author)
|
||||
# use the authors from the work if none are found for the edition
|
||||
if not edition.authors.exists() and work.authors.exists():
|
||||
edition.authors.set(work.authors.all())
|
||||
|
||||
return edition
|
||||
|
||||
def get_or_create_author(self, remote_id):
|
||||
def get_or_create_author(self, remote_id, instance=None):
|
||||
"""load that author"""
|
||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
return existing
|
||||
if not instance:
|
||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
data = self.get_book_data(remote_id)
|
||||
|
||||
|
@ -181,7 +191,24 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
return None
|
||||
|
||||
# this will dedupe
|
||||
return activity.to_model(model=models.Author, overwrite=False)
|
||||
return activity.to_model(
|
||||
model=models.Author, overwrite=False, instance=instance
|
||||
)
|
||||
|
||||
def get_remote_id_from_model(self, obj):
|
||||
"""given the data stored, how can we look this up"""
|
||||
return getattr(obj, getattr(self, "generated_remote_link_field"))
|
||||
|
||||
def update_author_from_remote(self, obj):
|
||||
"""load the remote data from this connector and add it to an existing author"""
|
||||
remote_id = self.get_remote_id_from_model(obj)
|
||||
return self.get_or_create_author(remote_id, instance=obj)
|
||||
|
||||
def update_book_from_remote(self, obj):
|
||||
"""load the remote data from this connector and add it to an existing book"""
|
||||
remote_id = self.get_remote_id_from_model(obj)
|
||||
data = self.get_book_data(remote_id)
|
||||
return self.create_edition_from_data(obj.parent_work, data, instance=obj)
|
||||
|
||||
@abstractmethod
|
||||
def is_work_data(self, data):
|
||||
|
@ -227,15 +254,17 @@ def get_data(url, params=None, timeout=10):
|
|||
resp = requests.get(
|
||||
url,
|
||||
params=params,
|
||||
headers={
|
||||
"Accept": "application/json; charset=utf-8",
|
||||
headers={ # pylint: disable=line-too-long
|
||||
"Accept": (
|
||||
'application/json, application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'
|
||||
),
|
||||
"User-Agent": settings.USER_AGENT,
|
||||
},
|
||||
timeout=timeout,
|
||||
)
|
||||
except RequestException as err:
|
||||
logger.exception(err)
|
||||
raise ConnectorException()
|
||||
raise ConnectorException(err)
|
||||
|
||||
if not resp.ok:
|
||||
raise ConnectorException()
|
||||
|
@ -243,7 +272,7 @@ def get_data(url, params=None, timeout=10):
|
|||
data = resp.json()
|
||||
except ValueError as err:
|
||||
logger.exception(err)
|
||||
raise ConnectorException()
|
||||
raise ConnectorException(err)
|
||||
|
||||
return data
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ from .connector_manager import ConnectorException
|
|||
class Connector(AbstractConnector):
|
||||
"""instantiate a connector for inventaire"""
|
||||
|
||||
generated_remote_link_field = "inventaire_id"
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
|
@ -210,6 +212,11 @@ class Connector(AbstractConnector):
|
|||
return ""
|
||||
return data.get("extract")
|
||||
|
||||
def get_remote_id_from_model(self, obj):
|
||||
"""use get_remote_id to figure out the link from a model obj"""
|
||||
remote_id_value = obj.inventaire_id
|
||||
return self.get_remote_id(remote_id_value)
|
||||
|
||||
|
||||
def get_language_code(options, code="en"):
|
||||
"""when there are a bunch of translation but we need a single field"""
|
||||
|
|
|
@ -12,6 +12,8 @@ from .openlibrary_languages import languages
|
|||
class Connector(AbstractConnector):
|
||||
"""instantiate a connector for OL"""
|
||||
|
||||
generated_remote_link_field = "openlibrary_link"
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
|
@ -66,6 +68,7 @@ class Connector(AbstractConnector):
|
|||
Mapping("born", remote_field="birth_date"),
|
||||
Mapping("died", remote_field="death_date"),
|
||||
Mapping("bio", formatter=get_description),
|
||||
Mapping("isni", remote_field="remote_ids", formatter=get_isni),
|
||||
]
|
||||
|
||||
def get_book_data(self, remote_id):
|
||||
|
@ -224,6 +227,13 @@ def get_languages(language_blob):
|
|||
return langs
|
||||
|
||||
|
||||
def get_isni(remote_ids_blob):
|
||||
"""extract the isni from the remote id data for the author"""
|
||||
if not remote_ids_blob or not isinstance(remote_ids_blob, dict):
|
||||
return None
|
||||
return remote_ids_blob.get("isni")
|
||||
|
||||
|
||||
def pick_default_edition(options):
|
||||
"""favor physical copies with covers in english"""
|
||||
if not options:
|
||||
|
|
|
@ -9,6 +9,8 @@ from django.utils import timezone
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||
from bookwyrm.models.user import FeedFilterChoices
|
||||
|
||||
|
||||
class CustomForm(ModelForm):
|
||||
|
@ -147,6 +149,17 @@ class EditUserForm(CustomForm):
|
|||
"preferred_language",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"avatar": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_avatar"}
|
||||
),
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
||||
"email": forms.EmailInput(attrs={"aria-describedby": "desc_email"}),
|
||||
"discoverable": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_discoverable"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class LimitedEditUserForm(CustomForm):
|
||||
|
@ -160,6 +173,16 @@ class LimitedEditUserForm(CustomForm):
|
|||
"discoverable",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"avatar": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_avatar"}
|
||||
),
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
||||
"discoverable": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_discoverable"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeleteUserForm(CustomForm):
|
||||
|
@ -174,6 +197,18 @@ class UserGroupForm(CustomForm):
|
|||
fields = ["groups"]
|
||||
|
||||
|
||||
class FeedStatusTypesForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["feed_status_types"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"feed_status_types": widgets.CheckboxSelectMultiple(
|
||||
choices=FeedFilterChoices,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class CoverForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Book
|
||||
|
@ -196,6 +231,51 @@ class EditionForm(CustomForm):
|
|||
"connector",
|
||||
"search_vector",
|
||||
]
|
||||
widgets = {
|
||||
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
|
||||
"subtitle": forms.TextInput(attrs={"aria-describedby": "desc_subtitle"}),
|
||||
"description": forms.Textarea(
|
||||
attrs={"aria-describedby": "desc_description"}
|
||||
),
|
||||
"series": forms.TextInput(attrs={"aria-describedby": "desc_series"}),
|
||||
"series_number": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_series_number"}
|
||||
),
|
||||
"languages": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_languages_help desc_languages"}
|
||||
),
|
||||
"publishers": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
||||
),
|
||||
"first_published_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_first_published_date"}
|
||||
),
|
||||
"published_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_published_date"}
|
||||
),
|
||||
"cover": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_cover"}
|
||||
),
|
||||
"physical_format": forms.Select(
|
||||
attrs={"aria-describedby": "desc_physical_format"}
|
||||
),
|
||||
"physical_format_detail": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_physical_format_detail"}
|
||||
),
|
||||
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
|
||||
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
|
||||
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
|
||||
"openlibrary_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_openlibrary_key"}
|
||||
),
|
||||
"inventaire_id": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
||||
),
|
||||
"oclc_number": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_oclc_number"}
|
||||
),
|
||||
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
||||
}
|
||||
|
||||
|
||||
class AuthorForm(CustomForm):
|
||||
|
@ -213,7 +293,30 @@ class AuthorForm(CustomForm):
|
|||
"inventaire_id",
|
||||
"librarything_key",
|
||||
"goodreads_key",
|
||||
"isni",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"aliases": forms.TextInput(attrs={"aria-describedby": "desc_aliases"}),
|
||||
"bio": forms.Textarea(attrs={"aria-describedby": "desc_bio"}),
|
||||
"wikipedia_link": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_wikipedia_link"}
|
||||
),
|
||||
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
|
||||
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
|
||||
"oepnlibrary_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_oepnlibrary_key"}
|
||||
),
|
||||
"inventaire_id": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
||||
),
|
||||
"librarything_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_librarything_key"}
|
||||
),
|
||||
"goodreads_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_goodreads_key"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ImportForm(forms.Form):
|
||||
|
@ -288,12 +391,37 @@ class SiteForm(CustomForm):
|
|||
class Meta:
|
||||
model = models.SiteSettings
|
||||
exclude = []
|
||||
widgets = {
|
||||
"instance_short_description": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_instance_short_description"}
|
||||
),
|
||||
"require_confirm_email": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_require_confirm_email"}
|
||||
),
|
||||
"invite_request_text": forms.Textarea(
|
||||
attrs={"aria-describedby": "desc_invite_request_text"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class AnnouncementForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Announcement
|
||||
exclude = ["remote_id"]
|
||||
widgets = {
|
||||
"preview": forms.TextInput(attrs={"aria-describedby": "desc_preview"}),
|
||||
"content": forms.Textarea(attrs={"aria-describedby": "desc_content"}),
|
||||
"event_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_event_date"}
|
||||
),
|
||||
"start_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_start_date"}
|
||||
),
|
||||
"end_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_end_date"}
|
||||
),
|
||||
"active": forms.CheckboxInput(attrs={"aria-describedby": "desc_active"}),
|
||||
}
|
||||
|
||||
|
||||
class ListForm(CustomForm):
|
||||
|
@ -318,6 +446,9 @@ class EmailBlocklistForm(CustomForm):
|
|||
class Meta:
|
||||
model = models.EmailBlocklist
|
||||
fields = ["domain"]
|
||||
widgets = {
|
||||
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
|
||||
}
|
||||
|
||||
|
||||
class IPBlocklistForm(CustomForm):
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
from .importer import Importer
|
||||
from .goodreads_import import GoodreadsImporter
|
||||
from .librarything_import import LibrarythingImporter
|
||||
from .openlibrary_import import OpenLibraryImporter
|
||||
from .storygraph_import import StorygraphImporter
|
||||
|
|
|
@ -26,7 +26,7 @@ class Importer:
|
|||
("authors", ["author", "authors", "primary author"]),
|
||||
("isbn_10", ["isbn10", "isbn"]),
|
||||
("isbn_13", ["isbn13", "isbn", "isbns"]),
|
||||
("shelf", ["shelf", "exclusive shelf", "read status"]),
|
||||
("shelf", ["shelf", "exclusive shelf", "read status", "bookshelf"]),
|
||||
("review_name", ["review name"]),
|
||||
("review_body", ["my review", "review"]),
|
||||
("rating", ["my rating", "rating", "star rating"]),
|
||||
|
@ -36,9 +36,9 @@ class Importer:
|
|||
]
|
||||
date_fields = ["date_added", "date_started", "date_finished"]
|
||||
shelf_mapping_guesses = {
|
||||
"to-read": ["to-read"],
|
||||
"read": ["read"],
|
||||
"reading": ["currently-reading", "reading"],
|
||||
"to-read": ["to-read", "want to read"],
|
||||
"read": ["read", "already read"],
|
||||
"reading": ["currently-reading", "reading", "currently reading"],
|
||||
}
|
||||
|
||||
def create_job(self, user, csv_file, include_reviews, privacy):
|
||||
|
@ -90,7 +90,10 @@ class Importer:
|
|||
|
||||
def get_shelf(self, normalized_row):
|
||||
"""determine which shelf to use"""
|
||||
shelf_name = normalized_row["shelf"]
|
||||
shelf_name = normalized_row.get("shelf")
|
||||
if not shelf_name:
|
||||
return None
|
||||
shelf_name = shelf_name.lower()
|
||||
shelf = [
|
||||
s for (s, gs) in self.shelf_mapping_guesses.items() if shelf_name in gs
|
||||
]
|
||||
|
@ -106,6 +109,7 @@ class Importer:
|
|||
user=user,
|
||||
include_reviews=original_job.include_reviews,
|
||||
privacy=original_job.privacy,
|
||||
source=original_job.source,
|
||||
# TODO: allow users to adjust mappings
|
||||
mappings=original_job.mappings,
|
||||
retry=True,
|
||||
|
|
13
bookwyrm/importers/openlibrary_import.py
Normal file
13
bookwyrm/importers/openlibrary_import.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
""" handle reading a csv from openlibrary"""
|
||||
from . import Importer
|
||||
|
||||
|
||||
class OpenLibraryImporter(Importer):
|
||||
"""csv downloads from OpenLibrary"""
|
||||
|
||||
service = "OpenLibrary"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.row_mappings_guesses.append(("openlibrary_key", ["edition id"]))
|
||||
self.row_mappings_guesses.append(("openlibrary_work_key", ["work id"]))
|
||||
super().__init__(*args, **kwargs)
|
|
@ -3,6 +3,6 @@ from . import Importer
|
|||
|
||||
|
||||
class StorygraphImporter(Importer):
|
||||
"""csv downloads from librarything"""
|
||||
"""csv downloads from Storygraph"""
|
||||
|
||||
service = "Storygraph"
|
||||
|
|
32
bookwyrm/migrations/0119_user_feed_status_types.py
Normal file
32
bookwyrm/migrations/0119_user_feed_status_types.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 3.2.5 on 2021-11-24 10:15
|
||||
|
||||
import bookwyrm.models.user
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0118_alter_user_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="feed_status_types",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[
|
||||
("review", "Reviews"),
|
||||
("comment", "Comments"),
|
||||
("quotation", "Quotations"),
|
||||
("everything", "Everything else"),
|
||||
],
|
||||
max_length=10,
|
||||
),
|
||||
default=bookwyrm.models.user.get_feed_filter_choices,
|
||||
size=8,
|
||||
),
|
||||
),
|
||||
]
|
29
bookwyrm/migrations/0120_list_embed_key.py
Normal file
29
bookwyrm/migrations/0120_list_embed_key.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.2.5 on 2021-12-04 10:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
def gen_uuid(apps, schema_editor):
|
||||
"""sets an unique UUID for embed_key"""
|
||||
book_lists = apps.get_model("bookwyrm", "List")
|
||||
db_alias = schema_editor.connection.alias
|
||||
for book_list in book_lists.objects.using(db_alias).all():
|
||||
book_list.embed_key = uuid.uuid4()
|
||||
book_list.save(broadcast=False)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0119_user_feed_status_types"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="list",
|
||||
name="embed_key",
|
||||
field=models.UUIDField(editable=False, null=True, unique=True),
|
||||
),
|
||||
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
|
||||
]
|
|
@ -1,4 +1,5 @@
|
|||
""" database schema for info about authors """
|
||||
import re
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.db import models
|
||||
|
||||
|
@ -33,6 +34,17 @@ class Author(BookDataModel):
|
|||
)
|
||||
bio = fields.HtmlField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def isni_link(self):
|
||||
"""generate the url from the isni id"""
|
||||
clean_isni = re.sub(r"\s", "", self.isni)
|
||||
return f"https://isni.org/isni/{clean_isni}"
|
||||
|
||||
@property
|
||||
def openlibrary_link(self):
|
||||
"""generate the url from the openlibrary id"""
|
||||
return f"https://openlibrary.org/authors/{self.openlibrary_key}"
|
||||
|
||||
def get_remote_id(self):
|
||||
"""editions and works both use "book" instead of model_name"""
|
||||
return f"https://{DOMAIN}/author/{self.id}"
|
||||
|
|
|
@ -52,6 +52,16 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
|||
null=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def openlibrary_link(self):
|
||||
"""generate the url from the openlibrary id"""
|
||||
return f"https://openlibrary.org/books/{self.openlibrary_key}"
|
||||
|
||||
@property
|
||||
def inventaire_link(self):
|
||||
"""generate the url from the inventaire id"""
|
||||
return f"https://inventaire.io/entity/{self.inventaire_id}"
|
||||
|
||||
class Meta:
|
||||
"""can't initialize this model, that wouldn't make sense"""
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ class GroupMember(models.Model):
|
|||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
# accepts and requests are handled by the GroupInvitation model
|
||||
# accepts and requests are handled by the GroupMemberInvitation model
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
|
@ -150,31 +150,30 @@ class GroupMemberInvitation(models.Model):
|
|||
notification_type=notification_type,
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def accept(self):
|
||||
"""turn this request into the real deal"""
|
||||
GroupMember.from_request(self)
|
||||
|
||||
with transaction.atomic():
|
||||
GroupMember.from_request(self)
|
||||
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||
# tell the group owner
|
||||
model.objects.create(
|
||||
user=self.group.user,
|
||||
related_user=self.user,
|
||||
related_group=self.group,
|
||||
notification_type="ACCEPT",
|
||||
)
|
||||
|
||||
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||
# tell the group owner
|
||||
model.objects.create(
|
||||
user=self.group.user,
|
||||
related_user=self.user,
|
||||
related_group=self.group,
|
||||
notification_type="ACCEPT",
|
||||
)
|
||||
|
||||
# let the other members know about it
|
||||
for membership in self.group.memberships.all():
|
||||
member = membership.user
|
||||
if member not in (self.user, self.group.user):
|
||||
model.objects.create(
|
||||
user=member,
|
||||
related_user=self.user,
|
||||
related_group=self.group,
|
||||
notification_type="JOIN",
|
||||
)
|
||||
# let the other members know about it
|
||||
for membership in self.group.memberships.all():
|
||||
member = membership.user
|
||||
if member not in (self.user, self.group.user):
|
||||
model.objects.create(
|
||||
user=member,
|
||||
related_user=self.user,
|
||||
related_group=self.group,
|
||||
notification_type="JOIN",
|
||||
)
|
||||
|
||||
def reject(self):
|
||||
"""generate a Reject for this membership request"""
|
||||
|
|
|
@ -25,7 +25,7 @@ def construct_search_term(title, author):
|
|||
# Strip brackets (usually series title from search term)
|
||||
title = re.sub(r"\s*\([^)]*\)\s*", "", title)
|
||||
# Open library doesn't like including author initials in search term.
|
||||
author = re.sub(r"(\w\.)+\s*", "", author)
|
||||
author = re.sub(r"(\w\.)+\s*", "", author) if author else ""
|
||||
|
||||
return " ".join([title, author])
|
||||
|
||||
|
@ -88,7 +88,9 @@ class ImportItem(models.Model):
|
|||
return
|
||||
|
||||
if self.isbn:
|
||||
self.book = self.get_book_from_isbn()
|
||||
self.book = self.get_book_from_identifier()
|
||||
elif self.openlibrary_key:
|
||||
self.book = self.get_book_from_identifier(field="openlibrary_key")
|
||||
else:
|
||||
# don't fall back on title/author search if isbn is present.
|
||||
# you're too likely to mismatch
|
||||
|
@ -98,10 +100,10 @@ class ImportItem(models.Model):
|
|||
else:
|
||||
self.book_guess = book
|
||||
|
||||
def get_book_from_isbn(self):
|
||||
"""search by isbn"""
|
||||
def get_book_from_identifier(self, field="isbn"):
|
||||
"""search by isbn or other unique identifier"""
|
||||
search_result = connector_manager.first_search_result(
|
||||
self.isbn, min_confidence=0.999
|
||||
getattr(self, field), min_confidence=0.999
|
||||
)
|
||||
if search_result:
|
||||
# it's already in the right format
|
||||
|
@ -114,6 +116,8 @@ class ImportItem(models.Model):
|
|||
|
||||
def get_book_from_title_author(self):
|
||||
"""search by title and author"""
|
||||
if not self.title:
|
||||
return None, 0
|
||||
search_term = construct_search_term(self.title, self.author)
|
||||
search_result = connector_manager.first_search_result(
|
||||
search_term, min_confidence=0.1
|
||||
|
@ -145,6 +149,13 @@ class ImportItem(models.Model):
|
|||
self.normalized_data.get("isbn_10")
|
||||
)
|
||||
|
||||
@property
|
||||
def openlibrary_key(self):
|
||||
"""the edition identifier is preferable to the work key"""
|
||||
return self.normalized_data.get("openlibrary_key") or self.normalized_data.get(
|
||||
"openlibrary_work_key"
|
||||
)
|
||||
|
||||
@property
|
||||
def shelf(self):
|
||||
"""the goodreads shelf field"""
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
""" make a list of books!! """
|
||||
import uuid
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
@ -43,6 +45,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
|||
through="ListItem",
|
||||
through_fields=("book_list", "book"),
|
||||
)
|
||||
embed_key = models.UUIDField(unique=True, null=True, editable=False)
|
||||
activity_serializer = activitypub.BookList
|
||||
|
||||
def get_remote_id(self):
|
||||
|
@ -105,6 +108,12 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
|||
group=None, curation="closed"
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""on save, update embed_key and avoid clash with existing code"""
|
||||
if not self.embed_key:
|
||||
self.embed_key = uuid.uuid4()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||
"""ok"""
|
||||
|
|
|
@ -4,11 +4,12 @@ from urllib.parse import urlparse
|
|||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import AbstractUser, Group
|
||||
from django.contrib.postgres.fields import CICharField
|
||||
from django.contrib.postgres.fields import ArrayField, CICharField
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.dispatch import receiver
|
||||
from django.db import models, transaction
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from model_utils import FieldTracker
|
||||
import pytz
|
||||
|
||||
|
@ -27,6 +28,19 @@ from .federated_server import FederatedServer
|
|||
from . import fields, Review
|
||||
|
||||
|
||||
FeedFilterChoices = [
|
||||
("review", _("Reviews")),
|
||||
("comment", _("Comments")),
|
||||
("quotation", _("Quotations")),
|
||||
("everything", _("Everything else")),
|
||||
]
|
||||
|
||||
|
||||
def get_feed_filter_choices():
|
||||
"""return a list of filter choice keys"""
|
||||
return [f[0] for f in FeedFilterChoices]
|
||||
|
||||
|
||||
def site_link():
|
||||
"""helper for generating links to the site"""
|
||||
protocol = "https" if USE_HTTPS else "http"
|
||||
|
@ -128,6 +142,13 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
show_suggested_users = models.BooleanField(default=True)
|
||||
discoverable = fields.BooleanField(default=False)
|
||||
|
||||
# feed options
|
||||
feed_status_types = ArrayField(
|
||||
models.CharField(max_length=10, blank=False, choices=FeedFilterChoices),
|
||||
size=8,
|
||||
default=get_feed_filter_choices,
|
||||
)
|
||||
|
||||
preferred_timezone = models.CharField(
|
||||
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
|
||||
default=str(pytz.utc),
|
||||
|
|
|
@ -49,6 +49,28 @@ def get_font(font_name, size=28):
|
|||
return font
|
||||
|
||||
|
||||
def get_wrapped_text(text, font, content_width):
|
||||
"""text wrap length depends on the max width of the content"""
|
||||
|
||||
low = 0
|
||||
high = len(text)
|
||||
|
||||
try:
|
||||
# ideal length is determined via binary search
|
||||
while low < high:
|
||||
mid = math.floor(low + high)
|
||||
wrapped_text = textwrap.fill(text, width=mid)
|
||||
width = font.getsize_multiline(wrapped_text)[0]
|
||||
if width < content_width:
|
||||
low = mid
|
||||
else:
|
||||
high = mid - 1
|
||||
except AttributeError:
|
||||
wrapped_text = text
|
||||
|
||||
return wrapped_text
|
||||
|
||||
|
||||
def generate_texts_layer(texts, content_width):
|
||||
"""Adds text for images"""
|
||||
font_text_zero = get_font("bold", size=20)
|
||||
|
@ -63,7 +85,8 @@ def generate_texts_layer(texts, content_width):
|
|||
|
||||
if "text_zero" in texts and texts["text_zero"]:
|
||||
# Text one (Book title)
|
||||
text_zero = textwrap.fill(texts["text_zero"], width=72)
|
||||
text_zero = get_wrapped_text(texts["text_zero"], font_text_zero, content_width)
|
||||
|
||||
text_layer_draw.multiline_text(
|
||||
(0, text_y), text_zero, font=font_text_zero, fill=TEXT_COLOR
|
||||
)
|
||||
|
@ -75,7 +98,8 @@ def generate_texts_layer(texts, content_width):
|
|||
|
||||
if "text_one" in texts and texts["text_one"]:
|
||||
# Text one (Book title)
|
||||
text_one = textwrap.fill(texts["text_one"], width=28)
|
||||
text_one = get_wrapped_text(texts["text_one"], font_text_one, content_width)
|
||||
|
||||
text_layer_draw.multiline_text(
|
||||
(0, text_y), text_one, font=font_text_one, fill=TEXT_COLOR
|
||||
)
|
||||
|
@ -87,7 +111,8 @@ def generate_texts_layer(texts, content_width):
|
|||
|
||||
if "text_two" in texts and texts["text_two"]:
|
||||
# Text one (Book subtitle)
|
||||
text_two = textwrap.fill(texts["text_two"], width=36)
|
||||
text_two = get_wrapped_text(texts["text_two"], font_text_two, content_width)
|
||||
|
||||
text_layer_draw.multiline_text(
|
||||
(0, text_y), text_two, font=font_text_two, fill=TEXT_COLOR
|
||||
)
|
||||
|
@ -99,7 +124,10 @@ def generate_texts_layer(texts, content_width):
|
|||
|
||||
if "text_three" in texts and texts["text_three"]:
|
||||
# Text three (Book authors)
|
||||
text_three = textwrap.fill(texts["text_three"], width=36)
|
||||
text_three = get_wrapped_text(
|
||||
texts["text_three"], font_text_three, content_width
|
||||
)
|
||||
|
||||
text_layer_draw.multiline_text(
|
||||
(0, text_y), text_three, font=font_text_three, fill=TEXT_COLOR
|
||||
)
|
||||
|
|
|
@ -14,7 +14,7 @@ VERSION = "0.1.0"
|
|||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "3eb4edb1"
|
||||
JS_CACHE = "3891b373"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
|
|
@ -25,6 +25,10 @@ body {
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.card.has-border {
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.scroll-x {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
|
@ -140,6 +144,34 @@ input[type=file]::file-selector-button:hover {
|
|||
color: #363636;
|
||||
}
|
||||
|
||||
details .dropdown-menu {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
details.dropdown[open] summary.dropdown-trigger::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.detail-pinned-button summary {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.detail-pinned-button form {
|
||||
float: left;
|
||||
width: -webkit-fill-available;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/** Shelving
|
||||
******************************************************************************/
|
||||
|
||||
|
|
Binary file not shown.
|
@ -46,4 +46,5 @@
|
|||
<glyph unicode="" glyph-name="star-full" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" />
|
||||
<glyph unicode="" glyph-name="heart" d="M755.188 896c-107.63 0-200.258-87.554-243.164-179-42.938 91.444-135.578 179-243.216 179-148.382 0-268.808-120.44-268.808-268.832 0-301.846 304.5-380.994 512.022-679.418 196.154 296.576 511.978 387.206 511.978 679.418 0 148.392-120.43 268.832-268.812 268.832z" />
|
||||
<glyph unicode="" glyph-name="plus" d="M992 576h-352v352c0 17.672-14.328 32-32 32h-192c-17.672 0-32-14.328-32-32v-352h-352c-17.672 0-32-14.328-32-32v-192c0-17.672 14.328-32 32-32h352v-352c0-17.672 14.328-32 32-32h192c17.672 0 32 14.328 32 32v352h352c17.672 0 32 14.328 32 32v192c0 17.672-14.328 32-32 32z" />
|
||||
<glyph unicode="" glyph-name="download" d="M512-32l480 480h-288v512h-384v-512h-288z" />
|
||||
</font></defs></svg>
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Binary file not shown.
13
bookwyrm/static/css/vendor/icons.css
vendored
13
bookwyrm/static/css/vendor/icons.css
vendored
|
@ -1,10 +1,10 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('../fonts/icomoon.eot?36x4a3');
|
||||
src: url('../fonts/icomoon.eot?36x4a3#iefix') format('embedded-opentype'),
|
||||
url('../fonts/icomoon.ttf?36x4a3') format('truetype'),
|
||||
url('../fonts/icomoon.woff?36x4a3') format('woff'),
|
||||
url('../fonts/icomoon.svg?36x4a3#icomoon') format('svg');
|
||||
src: url('../fonts/icomoon.eot?r7jc98');
|
||||
src: url('../fonts/icomoon.eot?r7jc98#iefix') format('embedded-opentype'),
|
||||
url('../fonts/icomoon.ttf?r7jc98') format('truetype'),
|
||||
url('../fonts/icomoon.woff?r7jc98') format('woff'),
|
||||
url('../fonts/icomoon.svg?r7jc98#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@ -142,3 +142,6 @@
|
|||
.icon-spinner:before {
|
||||
content: "\e97a";
|
||||
}
|
||||
.icon-download:before {
|
||||
content: "\ea36";
|
||||
}
|
||||
|
|
|
@ -72,6 +72,9 @@ let BookWyrm = new class {
|
|||
document.querySelectorAll('input[type="file"]').forEach(
|
||||
bookwyrm.disableIfTooLarge.bind(bookwyrm)
|
||||
);
|
||||
document.querySelectorAll('[data-copytext]').forEach(
|
||||
bookwyrm.copyText.bind(bookwyrm)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -126,9 +129,44 @@ let BookWyrm = new class {
|
|||
* @return {undefined}
|
||||
*/
|
||||
updateCountElement(counter, data) {
|
||||
let count = data.count;
|
||||
const count_by_type = data.count_by_type;
|
||||
const currentCount = counter.innerText;
|
||||
const count = data.count;
|
||||
const hasMentions = data.has_mentions;
|
||||
const allowedStatusTypesEl = document.getElementById('unread-notifications-wrapper');
|
||||
|
||||
// If we're on the right counter element
|
||||
if (counter.closest('[data-poll-wrapper]').contains(allowedStatusTypesEl)) {
|
||||
const allowedStatusTypes = JSON.parse(allowedStatusTypesEl.textContent);
|
||||
|
||||
// For keys in common between allowedStatusTypes and count_by_type
|
||||
// This concerns 'review', 'quotation', 'comment'
|
||||
count = allowedStatusTypes.reduce(function(prev, currentKey) {
|
||||
const currentValue = count_by_type[currentKey] | 0;
|
||||
|
||||
return prev + currentValue;
|
||||
}, 0);
|
||||
|
||||
// Add all the "other" in count_by_type if 'everything' is allowed
|
||||
if (allowedStatusTypes.includes('everything')) {
|
||||
// Clone count_by_type with 0 for reviews/quotations/comments
|
||||
const count_by_everything_else = Object.assign(
|
||||
{},
|
||||
count_by_type,
|
||||
{review: 0, quotation: 0, comment: 0}
|
||||
);
|
||||
|
||||
count = Object.keys(count_by_everything_else).reduce(
|
||||
function(prev, currentKey) {
|
||||
const currentValue =
|
||||
count_by_everything_else[currentKey] | 0
|
||||
|
||||
return prev + currentValue;
|
||||
},
|
||||
count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (count != currentCount) {
|
||||
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
|
||||
|
@ -470,6 +508,21 @@ let BookWyrm = new class {
|
|||
handleModalOpen(modal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display pop up window.
|
||||
*
|
||||
* @param {string} url Url to open
|
||||
* @param {string} windowName windowName
|
||||
* @return {undefined}
|
||||
*/
|
||||
displayPopUp(url, windowName) {
|
||||
window.open(
|
||||
url,
|
||||
windowName,
|
||||
"left=100,top=100,width=430,height=600"
|
||||
);
|
||||
}
|
||||
|
||||
duplicateInput (event ) {
|
||||
const trigger = event.currentTarget;
|
||||
const input_id = trigger.dataset['duplicate']
|
||||
|
@ -489,4 +542,38 @@ let BookWyrm = new class {
|
|||
parent.appendChild(label)
|
||||
parent.appendChild(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a "click-to-copy" component from a textarea element
|
||||
* with `data-copytext`, `data-copytext-label`, `data-copytext-success`
|
||||
* attributes.
|
||||
*
|
||||
* @param {object} node - DOM node of the text container
|
||||
* @return {undefined}
|
||||
*/
|
||||
|
||||
copyText(textareaEl) {
|
||||
const text = textareaEl.textContent;
|
||||
|
||||
const copyButtonEl = document.createElement('button');
|
||||
|
||||
copyButtonEl.textContent = textareaEl.dataset.copytextLabel;
|
||||
copyButtonEl.classList.add(
|
||||
"mt-2",
|
||||
"button",
|
||||
"is-small",
|
||||
"is-fullwidth",
|
||||
"is-primary",
|
||||
"is-light"
|
||||
);
|
||||
copyButtonEl.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
textareaEl.classList.add('is-success');
|
||||
copyButtonEl.classList.replace('is-primary', 'is-success');
|
||||
copyButtonEl.textContent = textareaEl.dataset.copytextSuccess;
|
||||
});
|
||||
});
|
||||
|
||||
textareaEl.parentNode.appendChild(copyButtonEl)
|
||||
}
|
||||
}();
|
||||
|
|
|
@ -210,10 +210,10 @@ let StatusCache = new class {
|
|||
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
||||
|
||||
// Close menu
|
||||
let menu = button.querySelector(".dropdown-trigger[aria-expanded=true]");
|
||||
let menu = button.querySelector("details[open]");
|
||||
|
||||
if (menu) {
|
||||
menu.click();
|
||||
menu.removeAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ author.local_path }}/edit">
|
||||
<a href="{% url 'edit-author' author.id %}">
|
||||
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
|
||||
<span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
|
||||
</a>
|
||||
|
@ -23,102 +23,130 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block columns content" itemscope itemtype="https://schema.org/Person">
|
||||
<div class="block columns is-flex-direction-row-reverse" itemscope itemtype="https://schema.org/Person">
|
||||
<meta itemprop="name" content="{{ author.name }}">
|
||||
{% if author.bio %}
|
||||
<div class="column">
|
||||
{% include "snippets/trimmed_text.html" with full=author.bio trim_length=200 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id or author.isni %}
|
||||
{% firstof author.aliases author.born author.died as details %}
|
||||
{% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni as links %}
|
||||
{% if details or links %}
|
||||
<div class="column is-two-fifths">
|
||||
<div class="box py-2">
|
||||
<dl>
|
||||
{% if details %}
|
||||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Author details" %}</h2>
|
||||
<dl class="box">
|
||||
{% if author.aliases %}
|
||||
<div class="is-flex is-flex-wrap-wrap my-1">
|
||||
<div class="is-flex is-flex-wrap-wrap mr-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Aliases:" %}</dt>
|
||||
{% for alias in author.aliases %}
|
||||
<dd itemprop="alternateName" content="{{alias}}">
|
||||
{{alias}}{% if not forloop.last %}, {% endif %}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
<dd>
|
||||
{% include "snippets/trimmed_list.html" with items=author.aliases itemprop="alternateName" %}
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.born %}
|
||||
<div class="is-flex my-1">
|
||||
<div class="is-flex mt-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Born:" %}</dt>
|
||||
<dd itemprop="birthDate">{{ author.born|naturalday }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.died %}
|
||||
<div class="is-flex my-1">
|
||||
<div class="is-flex mt-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Died:" %}</dt>
|
||||
<dd itemprop="deathDate">{{ author.died|naturalday }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if author.wikipedia_link %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
|
||||
{% trans "Wikipedia" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if links %}
|
||||
<section>
|
||||
<h2 class="title is-4">{% trans "External links" %}</h2>
|
||||
<div class="box">
|
||||
{% if author.wikipedia_link %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
|
||||
{% trans "Wikipedia" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.isni %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://isni.org/isni/{{ author.isni|remove_spaces }}" rel="noopener" target="_blank">
|
||||
{% trans "View ISNI record" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if author.isni %}
|
||||
<div class="mt-1">
|
||||
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener" target="_blank">
|
||||
{% trans "View ISNI record" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.openlibrary_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% trans "Load data" as button_text %}
|
||||
{% if author.openlibrary_key %}
|
||||
<div class="mt-1 is-flex">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener">
|
||||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="ol_sync" controls_uid=author.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_ol_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "author/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.inventaire_id %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Inventaire" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if author.inventaire_id %}
|
||||
<div class="mt-1 is-flex">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Inventaire" %}
|
||||
</a>
|
||||
|
||||
{% if author.librarything_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on LibraryThing" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="iv_sync" controls_uid=author.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "author/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.goodreads_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Goodreads" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="column">
|
||||
{% if author.bio %}
|
||||
{{ author.bio|to_markdown|safe }}
|
||||
{% if author.librarything_key %}
|
||||
<div class="mt-1">
|
||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on LibraryThing" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.goodreads_key %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Goodreads" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr aria-hidden="true">
|
||||
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h3>
|
||||
<h2 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h2>
|
||||
<div class="columns is-multiline is-mobile">
|
||||
{% for book in books %}
|
||||
<div class="column is-one-fifth">
|
||||
{% include 'landing/small-book.html' with book=book %}
|
||||
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
|
||||
<div class="is-flex-grow-1">
|
||||
{% include 'landing/small-book.html' with book=book %}
|
||||
</div>
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -34,47 +34,41 @@
|
|||
<div class="field">
|
||||
<label class="label" for="id_name">{% trans "Name:" %}</label>
|
||||
{{ form.name }}
|
||||
{% for error in form.name.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_aliases">{% trans "Aliases:" %}</label>
|
||||
{{ form.aliases }}
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
{% for error in form.aliases.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.aliases.errors id="desc_aliases" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_bio">{% trans "Bio:" %}</label>
|
||||
{{ form.bio }}
|
||||
{% for error in form.bio.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.bio.errors id="desc_bio" %}
|
||||
</div>
|
||||
|
||||
<p class="field"><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
|
||||
{% for error in form.wikipedia_link.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.wikipedia_link.errors id="desc_wikipedia_link" %}
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_born">{% trans "Birth date:" %}</label>
|
||||
<input type="date" name="born" value="{{ form.born.value|date:'Y-m-d' }}" class="input" id="id_born">
|
||||
{% for error in form.born.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.born.errors id="desc_born" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_died">{% trans "Death date:" %}</label>
|
||||
<input type="date" name="died" value="{{ form.died.value|date:'Y-m-d' }}" class="input" id="id_died">
|
||||
{% for error in form.died.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.died.errors id="desc_died" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
|
@ -82,33 +76,36 @@
|
|||
<div class="field">
|
||||
<label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label>
|
||||
{{ form.openlibrary_key }}
|
||||
{% for error in form.openlibrary_key.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.oepnlibrary_key.errors id="desc_oepnlibrary_key" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label>
|
||||
{{ form.inventaire_id }}
|
||||
{% for error in form.inventaire_id.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label>
|
||||
{{ form.librarything_key }}
|
||||
{% for error in form.librarything_key.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.librarything_key.errors id="desc_librarything_key" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label>
|
||||
{{ form.goodreads_key }}
|
||||
{% for error in form.goodreads_key.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_isni">{% trans "ISNI:" %}</label>
|
||||
{{ form.isni }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.isni.errors id="desc_isni" %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
30
bookwyrm/templates/author/sync_modal.html
Normal file
30
bookwyrm/templates/author/sync_modal.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Load data" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="{{ source }}-update" method="POST" action="{% url 'author-update-remote' author.id source %}">
|
||||
{% csrf_token %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Loading data will connect to <strong>{{ source_name }}</strong> and check for any metadata about this author which aren't present here. Existing metadata will not be overwritten.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">
|
||||
<span>{% trans "Confirm" %}</span>
|
||||
</button>
|
||||
|
||||
{% trans "Cancel" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
|
@ -90,11 +90,28 @@
|
|||
</div>
|
||||
{% endwith %}
|
||||
|
||||
{% trans "Load data" as button_text %}
|
||||
{% if book.openlibrary_key %}
|
||||
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
|
||||
<p>
|
||||
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="ol_sync" controls_uid=book.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_ol_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "book/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if book.inventaire_id %}
|
||||
<p><a href="https://inventaire.io/entity/{{ book.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a></p>
|
||||
<p>
|
||||
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="iv_sync" controls_uid=book.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "book/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
@ -160,7 +177,7 @@
|
|||
<ul>
|
||||
{% for shelf in user_shelfbooks %}
|
||||
<li class="box">
|
||||
{% blocktrans with path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}<a href="{{ path }}">{{ shelf_name }}</a>{% endblocktrans %}
|
||||
<a href="{{ shelf.shelf.local_path }}">{{ shelf.shelf.name }}</a>
|
||||
<div class="mb-3">
|
||||
{% include 'snippets/shelf_selector.html' with shelf=shelf.shelf class="is-small" readthrough=readthrough %}
|
||||
</div>
|
||||
|
|
|
@ -12,106 +12,125 @@
|
|||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Metadata" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_title">{% trans "Title:" %}</label>
|
||||
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
|
||||
{% for error in form.title.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_title">
|
||||
{% trans "Title:" %}
|
||||
</label>
|
||||
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title" aria-describedby="desc_title">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.title.errors id="desc_title" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label>
|
||||
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle">
|
||||
{% for error in form.subtitle.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_subtitle">
|
||||
{% trans "Subtitle:" %}
|
||||
</label>
|
||||
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle" aria-describedby="desc_subtitle">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.subtitle.errors id="desc_subtitle" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_description">{% trans "Description:" %}</label>
|
||||
<label class="label" for="id_description">
|
||||
{% trans "Description:" %}
|
||||
</label>
|
||||
{{ form.description }}
|
||||
{% for error in form.description.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.description.errors id="desc_description" %}
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<div class="field">
|
||||
<label class="label" for="id_series">{% trans "Series:" %}</label>
|
||||
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
|
||||
{% for error in form.series.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_series">
|
||||
{% trans "Series:" %}
|
||||
</label>
|
||||
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}" aria-describedby="desc_series">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.series.errors id="desc_series" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="field">
|
||||
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
|
||||
<label class="label" for="id_series_number">
|
||||
{% trans "Series number:" %}
|
||||
</label>
|
||||
{{ form.series_number }}
|
||||
{% for error in form.series_number.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.series_number.errors id="desc_series_number" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_languages">{% trans "Languages:" %}</label>
|
||||
<label class="label" for="id_languages">
|
||||
{% trans "Languages:" %}
|
||||
</label>
|
||||
{{ form.languages }}
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
{% for error in form.languages.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<span class="help" id="desc_languages_help">
|
||||
{% trans "Separate multiple values with commas." %}
|
||||
</span>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Publication" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Publication" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
||||
<label class="label" for="id_publishers">
|
||||
{% trans "Publisher:" %}
|
||||
</label>
|
||||
{{ form.publishers }}
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
{% for error in form.publishers.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<span class="help" id="desc_publishers_help">
|
||||
{% trans "Separate multiple values with commas." %}
|
||||
</span>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.publishers.errors id="desc_publishers" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label>
|
||||
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %}>
|
||||
{% for error in form.first_published_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_first_published_date">
|
||||
{% trans "First published date:" %}
|
||||
</label>
|
||||
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %} aria-describedby="desc_first_published_date">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_published_date">{% trans "Published date:" %}</label>
|
||||
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %}>
|
||||
{% for error in form.published_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_published_date">
|
||||
{% trans "Published date:" %}
|
||||
</label>
|
||||
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %} aria-describedby="desc_published_date">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Authors" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Authors" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
{% if book.authors.exists %}
|
||||
<fieldset>
|
||||
{% for author in book.authors.all %}
|
||||
<div class="is-flex is-justify-content-space-between">
|
||||
<label class="label mb-2">
|
||||
<input type="checkbox" name="remove_authors" value="{{ author.id }}" {% if author.id|stringformat:"i" in remove_authors %}checked{% endif %}>
|
||||
<input type="checkbox" name="remove_authors" value="{{ author.id }}" {% if author.id|stringformat:"i" in remove_authors %}checked{% endif %} aria-describedby="desc_remove_author_{{author.id}}">
|
||||
{% blocktrans with name=author.name %}Remove {{ name }}{% endblocktrans %}
|
||||
</label>
|
||||
<p class="help">
|
||||
<p class="help" id="desc_remove_author_{{author.id}}">
|
||||
<a href="{{ author.local_path }}">{% blocktrans with name=author.name %}Author page for {{ name }}{% endblocktrans %}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -119,7 +138,9 @@
|
|||
</fieldset>
|
||||
{% endif %}
|
||||
<div class="field">
|
||||
<label class="label">{% trans "Add Authors:" %}</label>
|
||||
<label class="label" for="id_add_author">
|
||||
{% trans "Add Authors:" %}
|
||||
</label>
|
||||
{% for author in add_author %}
|
||||
<label class="label is-sr-only" for="id_add_author{% if not forloop.first %}-{{forloop.counter}}{% endif %}">{% trans "Add Author" %}</label>
|
||||
<input class="input" type="text" name="add_author" id="id_add_author{% if not forloop.first %}-{{forloop.counter}}{% endif %}" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}>
|
||||
|
@ -135,7 +156,9 @@
|
|||
|
||||
<div class="column is-half">
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Cover" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Cover" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
{% if book.cover %}
|
||||
|
@ -146,108 +169,122 @@
|
|||
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
|
||||
<label class="label" for="id_cover">
|
||||
{% trans "Upload cover:" %}
|
||||
</label>
|
||||
{{ form.cover }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_cover_url">
|
||||
{% trans "Load cover from url:" %}
|
||||
</label>
|
||||
<input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}">
|
||||
<input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}" aria-describedby="desc_cover">
|
||||
</div>
|
||||
{% for error in form.cover.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.cover.errors id="desc_cover" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Physical Properties" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third">
|
||||
<div class="field">
|
||||
<label class="label" for="id_physical_format">{% trans "Format:" %}</label>
|
||||
<label class="label" for="id_physical_format">
|
||||
{% trans "Format:" %}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ form.physical_format }}
|
||||
</div>
|
||||
{% for error in form.physical_format.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label" for="id_physical_format_detail">{% trans "Format details:" %}</label>
|
||||
<label class="label" for="id_physical_format_detail">
|
||||
{% trans "Format details:" %}
|
||||
</label>
|
||||
{{ form.physical_format_detail }}
|
||||
{% for error in form.physical_format_detail.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.physical_format_detail.errors id="desc_physical_format_detail" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_pages">{% trans "Pages:" %}</label>
|
||||
<label class="label" for="id_pages">
|
||||
{% trans "Pages:" %}
|
||||
</label>
|
||||
{{ form.pages }}
|
||||
{% for error in form.pages.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.pages.errors id="desc_pages" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Book Identifiers" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
|
||||
<label class="label" for="id_isbn_13">
|
||||
{% trans "ISBN 13:" %}
|
||||
</label>
|
||||
{{ form.isbn_13 }}
|
||||
{% for error in form.isbn_13.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.isbn_13.errors id="desc_isbn_13" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_isbn_10">{% trans "ISBN 10:" %}</label>
|
||||
<label class="label" for="id_isbn_10">
|
||||
{% trans "ISBN 10:" %}
|
||||
</label>
|
||||
{{ form.isbn_10 }}
|
||||
{% for error in form.isbn_10.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.isbn_10.errors id="desc_isbn_10" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_openlibrary_key">{% trans "Openlibrary ID:" %}</label>
|
||||
<label class="label" for="id_openlibrary_key">
|
||||
{% trans "Openlibrary ID:" %}
|
||||
</label>
|
||||
{{ form.openlibrary_key }}
|
||||
{% for error in form.openlibrary_key.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.openlibrary_key.errors id="desc_openlibrary_key" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label>
|
||||
<label class="label" for="id_inventaire_id">
|
||||
{% trans "Inventaire ID:" %}
|
||||
</label>
|
||||
{{ form.inventaire_id }}
|
||||
{% for error in form.inventaire_id.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_oclc_number">{% trans "OCLC Number:" %}</label>
|
||||
<label class="label" for="id_oclc_number">
|
||||
{% trans "OCLC Number:" %}
|
||||
</label>
|
||||
{{ form.oclc_number }}
|
||||
{% for error in form.oclc_number.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.oclc_number.errors id="desc_oclc_number" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_asin">{% trans "ASIN:" %}</label>
|
||||
<label class="label" for="id_asin">
|
||||
{% trans "ASIN:" %}
|
||||
</label>
|
||||
{{ form.asin }}
|
||||
{% for error in form.ASIN.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
30
bookwyrm/templates/book/sync_modal.html
Normal file
30
bookwyrm/templates/book/sync_modal.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Load data" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="{{ source }}-update" method="POST" action="{% url 'book-update-remote' book.id source %}">
|
||||
{% csrf_token %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Loading data will connect to <strong>{{ source_name }}</strong> and check for any metadata about this book which aren't present here. Existing metadata will not be overwritten.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">
|
||||
<span>{% trans "Confirm" %}</span>
|
||||
</button>
|
||||
|
||||
{% trans "Cancel" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
|
@ -2,25 +2,20 @@
|
|||
{% load utilities %}
|
||||
|
||||
{% with 0|uuid as uuid %}
|
||||
<div
|
||||
<details
|
||||
id="menu_{{ uuid }}"
|
||||
class="
|
||||
dropdown control
|
||||
{% if right %}is-right{% endif %}
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="button dropdown-trigger pulldown-menu {{ class }}"
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-controls="menu_options_{{ uuid }}"
|
||||
data-controls="menu_{{ uuid }}"
|
||||
<summary
|
||||
class="button control dropdown-trigger pulldown-menu {{ class }}"
|
||||
>
|
||||
{% block dropdown-trigger %}{% endblock %}
|
||||
</button>
|
||||
</summary>
|
||||
|
||||
<div class="dropdown-menu">
|
||||
<div class="dropdown-menu control">
|
||||
<ul
|
||||
id="menu_options_{{ uuid }}"
|
||||
class="dropdown-content p-0 is-clipped"
|
||||
|
@ -29,6 +24,6 @@
|
|||
{% block dropdown-list %}{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</p>
|
||||
<form name="directory" method="POST" action="{% url 'directory' %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-primary" type="submit">Join Directory</button>
|
||||
<button class="button is-primary" type="submit">{% trans "Join Directory" %}</button>
|
||||
<p class="help">
|
||||
{% url 'prefs-profile' as path %}
|
||||
{% blocktrans with path=path %}You can opt-out at any time in your <a href="{{ path }}">profile settings.</a>{% endblocktrans %}
|
||||
|
@ -28,7 +28,7 @@
|
|||
<div class="column is-narrow">
|
||||
{% trans "Dismiss message" as button_text %}
|
||||
<button type="button" class="delete set-display" data-id="hide_join_directory" data-value="true">
|
||||
<span>Dismiss message</span>
|
||||
<span>{% trans "Dismiss message" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div></div>
|
||||
|
|
53
bookwyrm/templates/embed-layout.html
Normal file
53
bookwyrm/templates/embed-layout.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% load layout %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{% get_lang %}">
|
||||
<head>
|
||||
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="{% static "css/vendor/bulma.min.css" %}">
|
||||
<link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
|
||||
<link rel="stylesheet" href="{% static "css/bookwyrm.css" %}">
|
||||
|
||||
<base target="_blank">
|
||||
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="section py-3">
|
||||
<a href="/" class="is-flex is-align-items-center">
|
||||
<img class="image logo is-flex-shrink-0" style="height: 32px" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
|
||||
<span class="title is-5 ml-2">{{ site.name }}</span>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<main class="section py-3">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="section py-3">
|
||||
<p>
|
||||
<a href="{% url 'about' %}">
|
||||
{% blocktrans with site_name=site.name %}About {{ site_name }}{% endblocktrans %}
|
||||
</a>
|
||||
</p>
|
||||
{% if site.admin_email %}
|
||||
<p>
|
||||
<a href="mailto:{{ site.admin_email }}">
|
||||
{% trans "Contact site admin" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="https://joinbookwyrm.com/">
|
||||
{% trans "Join Bookwyrm" %}
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -6,20 +6,62 @@
|
|||
<h1 class="title">
|
||||
{{ tab.name }}
|
||||
</h1>
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% for stream in streams %}
|
||||
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
||||
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="block is-clipped">
|
||||
<div class="is-pulled-left">
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% for stream in streams %}
|
||||
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
||||
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# feed settings #}
|
||||
<details class="detail-pinned-button" {% if settings_saved %}open{% endif %}>
|
||||
<summary class="control">
|
||||
<span class="button">
|
||||
<span class="icon icon-dots-three m-0-mobile" aria-hidden="true"></span>
|
||||
<span class="is-sr-only-mobile">{{ _("Feed settings") }}</span>
|
||||
</span>
|
||||
</summary>
|
||||
<form class="notification level is-align-items-flex-end" method="post" action="/{{ tab.key }}#feed">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="level-left">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<span class="is-flex is-align-items-baseline">
|
||||
<label class="label mt-2 mb-1">Status types</label>
|
||||
{% if settings_saved %}
|
||||
<span class="tag is-success is-light ml-2">{{ _("Saved!") }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% for name, value in feed_status_types_options %}
|
||||
<label class="mr-2">
|
||||
<input type="checkbox" name="feed_status_types" value="{{ name }}" {% if name in user.feed_status_types %}checked=""{% endif %}/>
|
||||
{{ value }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right control">
|
||||
<button class="button is-small is-primary is-outlined" type="submit">
|
||||
{{ _("Save settings") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{# announcements and system messages #}
|
||||
{% if not activities.number > 1 %}
|
||||
<a href="{{ request.path }}" class="transition-y is-hidden notification is-primary is-block" data-poll-wrapper>
|
||||
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
|
||||
{{ allowed_status_types|json_script:"unread-notifications-wrapper" }}
|
||||
</a>
|
||||
|
||||
{% if request.user.show_goal and not goal and tab.key == 'home' %}
|
||||
|
@ -36,6 +78,7 @@
|
|||
{% if not activities %}
|
||||
<div class="block content">
|
||||
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
|
||||
<p>{% if user.feed_status_types|length < 4 %}{% trans "Alternatively, you can try enabling more status types" %}{% endif %}</p>
|
||||
|
||||
{% if request.user.show_suggested_users and suggested_users %}
|
||||
{# suggested users for when things are very lonely #}
|
||||
|
|
|
@ -2,6 +2,18 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% block opengraph_images %}
|
||||
|
||||
{% firstof status.book status.mention_books.first as book %}
|
||||
{% if book %}
|
||||
{% include 'snippets/opengraph_images.html' with image=preview %}
|
||||
{% else %}
|
||||
{% include 'snippets/opengraph_images.html' %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block panel %}
|
||||
<header class="block">
|
||||
<a href="/#feed" class="button" data-back>
|
||||
|
|
|
@ -4,9 +4,14 @@
|
|||
|
||||
<div class="select is-small mt-1 mb-3">
|
||||
<select name="{{ book.id }}" aria-label="{% blocktrans with book_title=book.title %}Have you read {{ book_title }}?{% endblocktrans %}">
|
||||
<option disabled selected value>Add to your books</option>
|
||||
<option disabled selected value>{% trans 'Add to your books' %}</option>
|
||||
{% for shelf in user_shelves %}
|
||||
<option value="{{ shelf.id }}">{{ shelf.name }}</option>
|
||||
<option value="{{ shelf.id }}">
|
||||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||
{% else %}{{ shelf.name }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -14,16 +14,14 @@
|
|||
<div class="block">
|
||||
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
||||
<input type="text" name="name" maxlength="100" class="input" id="id_name" placeholder="{{ user.localname }}" value="{% if request.user.name %}{{ request.user.name }}{% endif %}">
|
||||
{% for error in form.name.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||
</div>
|
||||
<div class="block">
|
||||
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
||||
<textarea name="summary" cols="None" rows="None" class="textarea" id="id_summary" placeholder="{% trans 'A little bit about you' %}">{% if request.user.summary %}{{ request.user.summary }}{% endif %}</textarea>
|
||||
{% for error in form.summary.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -31,9 +29,8 @@
|
|||
<div class="block">
|
||||
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
|
||||
{{ form.avatar }}
|
||||
{% for error in form.avatar.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.avatar.errors id="desc_avatar" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
|
||||
LibraryThing (TSV)
|
||||
</option>
|
||||
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
||||
OpenLibrary (CSV)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
|
@ -105,6 +105,11 @@
|
|||
<th>
|
||||
{% trans "ISBN" %}
|
||||
</th>
|
||||
{% if job.source == "OpenLibrary" %}
|
||||
<th>
|
||||
{% trans "Openlibrary key" %}
|
||||
</th>
|
||||
{% endif %}
|
||||
<th>
|
||||
{% trans "Author" %}
|
||||
</th>
|
||||
|
@ -145,6 +150,11 @@
|
|||
<td>
|
||||
{{ item.isbn|default:'' }}
|
||||
</td>
|
||||
{% if job.source == "OpenLibrary" %}
|
||||
<td>
|
||||
{{ item.openlibrary_key }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{{ item.normalized_data.authors }}
|
||||
</td>
|
||||
|
|
|
@ -65,10 +65,9 @@
|
|||
{% csrf_token %}
|
||||
<div class="block">
|
||||
<label for="id_request_email" class="label">{% trans "Email address:" %}</label>
|
||||
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
|
||||
{% for error in request_form.email.errors %}
|
||||
<p class="help is-danger">{{ error|escape }}</p>
|
||||
{% endfor %}
|
||||
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email" aria-describedby="desc_request_email">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=request_form.email.errors id="desc_request_email" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
|
|
|
@ -26,11 +26,10 @@
|
|||
<div class="field">
|
||||
<label class="label" for="id_password_confirm">{% trans "Password:" %}</label>
|
||||
<div class="control">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm" aria-describedby="desc_password">
|
||||
</div>
|
||||
{% for error in login_form.password.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
|
|
|
@ -8,21 +8,33 @@
|
|||
<div class="column">
|
||||
<div class="block">
|
||||
<h1 class="title">{% trans "Reset Password" %}</h1>
|
||||
{% for error in errors %}
|
||||
<p class="is-danger">{{ error }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% if errors %}
|
||||
<div id="form_errors">
|
||||
{% for error in errors %}
|
||||
<p class="is-danger">
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<form name="password-reset" method="post" action="/password-reset/{{ code }}">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_new_password">{% trans "Password:" %}</label>
|
||||
<label class="label" for="id_new_password">
|
||||
{% trans "Password:" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_new_password">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_new_password" aria-describedby="form_errors">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_confirm_password">{% trans "Confirm password:" %}</label>
|
||||
<label class="label" for="id_confirm_password">
|
||||
{% trans "Confirm password:" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
|
||||
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password" aria-describedby="form_errors">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="Home page">
|
||||
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
|
||||
</a>
|
||||
<form class="navbar-item column" action="{% url 'search' %}">
|
||||
<div class="field has-addons">
|
||||
|
@ -298,5 +298,6 @@
|
|||
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
59
bookwyrm/templates/lists/embed-list.html
Normal file
59
bookwyrm/templates/lists/embed-list.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends 'embed-layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load bookwyrm_group_tags %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}{% blocktrans with list_name=list.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}}{% endblocktrans %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mt-3">
|
||||
<h1 class="title is-4">
|
||||
{{ list.name }}
|
||||
<span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||
</h1>
|
||||
<p class="subtitle is-size-6">
|
||||
{% include 'lists/created_text.html' with list=list %}
|
||||
{% blocktrans with site_name=site.name %}on <a href="/">{{ site_name }}</a>{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="block content">
|
||||
{% include 'snippets/trimmed_text.html' with full=list.description %}
|
||||
</div>
|
||||
|
||||
<section>
|
||||
{% if not items.object_list.exists %}
|
||||
<p>{% trans "This list is currently empty" %}</p>
|
||||
{% else %}
|
||||
<ol start="{{ items.start_index }}" class="ordered-list">
|
||||
{% for item in items %}
|
||||
{% with book=item.book %}
|
||||
<li class="mb-5 card is-shadowless has-border">
|
||||
<div class="card-content p-0 mb-0 columns is-gapless is-mobile">
|
||||
<div class="column is-3-mobile is-2-tablet is-cover align to-t">
|
||||
<a href="{{ item.book.local_path }}" aria-hidden="true">
|
||||
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet is-align-items-flex-start' size='medium' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="column mx-3 my-2">
|
||||
<h2 class="title is-6 mb-1">
|
||||
{% include 'snippets/book_titleby.html' %}
|
||||
</h2>
|
||||
<p>
|
||||
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
|
||||
</p>
|
||||
<div>
|
||||
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
{% include "snippets/pagination.html" with page=items %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -186,6 +186,13 @@
|
|||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div>
|
||||
<h2 class="title is-5 mt-6" id="embed-label">
|
||||
{% trans "Embed this list on a website" %}
|
||||
</h2>
|
||||
<textarea readonly class="textarea is-small" aria-labelledby="embed-label" data-copytext data-copytext-label="{% trans 'Copy embed code' %}" data-copytext-success="{% trans 'Copied!' %}"><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans with list_name=list.name site_name=site.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}} on {{ site_name }}{% endblocktrans %}" src="{{ embed_url }}"></iframe></textarea>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
<div class="column is-one-quarter">
|
||||
<div class="card is-stretchable">
|
||||
<header class="card-header">
|
||||
<h4 class="card-header-title">
|
||||
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||
<h4 class="card-header-title is-clipped">
|
||||
<a href="{{ list.local_path }}" class="is-clipped">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||
</h4>
|
||||
{% if request.user.is_authenticated and request.user|saved:list %}
|
||||
<div class="card-header-icon">
|
||||
|
|
70
bookwyrm/templates/ostatus/error.html
Normal file
70
bookwyrm/templates/ostatus/error.html
Normal file
|
@ -0,0 +1,70 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
{% if error == 'invalid_username' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> is not a valid username{% endblocktrans %}.</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again' %}.</p>
|
||||
</div>
|
||||
{% elif error == 'user_not_found' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> could not be found or <code>{{ remote_domain }}</code> does not support identity discovery{% endblocktrans %}.</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again' %}.</p>
|
||||
</div>
|
||||
{% elif error == 'not_supported' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> was found but <code>{{ remote_domain }}</code> does not support 'remote follow'{% endblocktrans %}.</p>
|
||||
<p>{% blocktrans %}Try searching for <strong>{{ user }}</strong> on <code>{{ remote_domain }}</code> instead{% endblocktrans %}.</p>
|
||||
</div>
|
||||
{% elif not request.user.is_authenticated %}
|
||||
<div class="navbar-item">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<form name="login" method="post" action="{% url 'login' %}?next={{ request.path }}?acct={{ user.remote_id }}">
|
||||
{% csrf_token %}
|
||||
<div class="columns is-variable is-1">
|
||||
<div class="column">
|
||||
<label class="is-sr-only" for="id_localname">{% trans "Username:" %}</label>
|
||||
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname" placeholder="{% trans 'username' %}">
|
||||
</div>
|
||||
<div class="column">
|
||||
<label class="is-sr-only" for="id_password">{% trans "Password:" %}</label>
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
|
||||
<p class="help"><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif error == 'ostatus_subscribe' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}Something went wrong trying to follow <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again.' %}</p>
|
||||
</div>
|
||||
{% elif error == 'is_blocked' %}
|
||||
<div class="notification is-danger has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You have blocked <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'has_blocked' %}
|
||||
<div class="notification is-danger has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> has blocked you{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'already_following' %}
|
||||
<div class="notification is-success has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You are already following <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'already_requested' %}
|
||||
<div class="notification is-success has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You have already requested to follow <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="block is-pulled-right">
|
||||
<button type="button" class="button" onclick="closeWindow()">Close window</button>
|
||||
</div>
|
||||
{% endblock %}
|
46
bookwyrm/templates/ostatus/remote_follow.html
Normal file
46
bookwyrm/templates/ostatus/remote_follow.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block heading %}
|
||||
{% blocktrans with username=user.localname sitename=site.name %}Follow {{ username }} on the fediverse{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<p>{% blocktrans with username=user.display_name %}Follow {{ username }} from another Fediverse account like BookWyrm, Mastodon, or Pleroma.{% endblocktrans %}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<section class="card-content content">
|
||||
<form name="remote-follow" method="post" action="{% url 'remote-follow' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ user.id }}">
|
||||
<label class="label" for="remote_user">{% trans 'User handle to follow from:' %}</label>
|
||||
<input class="input" type="text" name="remote_user" id="remote_user" placeholder="user@example.social" required>
|
||||
<button class="button mt-1 is-primary" type="submit">{% trans 'Follow!' %}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
15
bookwyrm/templates/ostatus/remote_follow_button.html
Normal file
15
bookwyrm/templates/ostatus/remote_follow_button.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% load i18n %}
|
||||
{% if request.user == user %}
|
||||
{% else %}
|
||||
|
||||
<div class="field mb-0">
|
||||
<div class="control">
|
||||
<a class="button is-small is-link" href="{% url 'remote-follow-page' %}?user={{ user.username }}" target="_blank" rel="noopener noreferrer" onclick="BookWyrm.displayPopUp(`{% url 'remote-follow-page' %}?user={{ user.username }}`, `remoteFollow`); return false;" aria-describedby="remote_follow_warning">
|
||||
{% blocktrans with username=user.localname %}Follow on Fediverse{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
<p id="remote_follow_warning" class="mt-1 is-size-7 has-text-weight-light">
|
||||
{% trans 'This link opens in a pop-up window' %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
63
bookwyrm/templates/ostatus/subscribe.html
Normal file
63
bookwyrm/templates/ostatus/subscribe.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}
|
||||
{% if not request.user.is_authenticated %}
|
||||
{% blocktrans with sitename=site.name %}Log in to {{ sitename }}{% endblocktrans %}
|
||||
{% elif error %}
|
||||
{% blocktrans with sitename=site.name %}Error following from {{ sitename }}{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with sitename=site.name %}Follow from {{ sitename }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% if error %}
|
||||
{% trans 'Uh oh...' %}
|
||||
{% elif not request.user.is_authenticated %}
|
||||
{% trans "Let's log in first..." %}
|
||||
{% else %}
|
||||
{% blocktrans with sitename=site.name %}Follow from {{ sitename }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if error or not request.user.is_authenticated %}
|
||||
{% include 'ostatus/error.html' with error=error user=user account=account %}
|
||||
{% else %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans 'Locked account' %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
<form name="follow" method="post" action="{% url 'follow' %}/?next={% url 'ostatus-success' %}?following={{ user.username }}">
|
||||
{% csrf_token %}
|
||||
<input name="user" value="{{ user.username }}" hidden>
|
||||
<button class="button is-link" type="submit">{% blocktrans with username=user.display_name %}Follow {{ username }}{% endblocktrans %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if user.summary %}
|
||||
{{ user.summary|to_markdown|safe|truncatechars_html:120 }}
|
||||
{% else %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
35
bookwyrm/templates/ostatus/success.html
Normal file
35
bookwyrm/templates/ostatus/success.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="notification is-success">
|
||||
<span class="icon icon-check m-0-mobile" aria-hidden="true"></span>
|
||||
<span>{% blocktrans with display_name=user.display_name %}You are now following {{ display_name }}!{% endblocktrans %}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block is-pulled-right">
|
||||
<button type="button" class="button" onclick="closeWindow()">Close window</button>
|
||||
</div>
|
||||
{% endblock %}
|
41
bookwyrm/templates/ostatus/template.html
Normal file
41
bookwyrm/templates/ostatus/template.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
{% load layout %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{% get_lang %}">
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="{% static 'css/vendor/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/vendor/icons.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bookwyrm.css' %}">
|
||||
<script>
|
||||
function closeWindow() {
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<img class="image logo navbar-item" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static 'images/logo-small.png' %}{% endif %}" alt="Home page">
|
||||
<h2 class="navbar-item subtitle">{% block heading %}{% endblock %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="section is-flex-grow-1 columns is-centered">
|
||||
<div class="block column is-one-third">
|
||||
{% block content%}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var csrf_token = '{{ csrf_token }}';
|
||||
</script>
|
||||
<script src="{% static 'js/bookwyrm.js' %}?v={{ js_cache }}"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -18,10 +18,9 @@
|
|||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">{% trans "Confirm password:" %}</label>
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required>
|
||||
{% for error in form.password.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
||||
</form>
|
||||
|
|
|
@ -33,31 +33,27 @@
|
|||
{% endif %}
|
||||
<div class="column">
|
||||
{{ form.avatar }}
|
||||
{% for error in form.avatar.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.avatar.errors id="desc_avatar" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
||||
{{ form.name }}
|
||||
{% for error in form.name.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
||||
{{ form.summary }}
|
||||
{% for error in form.summary.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_email">{% trans "Email address:" %}</label>
|
||||
{{ form.email }}
|
||||
{% for error in form.email.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.email.errors id="desc_email" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -69,19 +65,23 @@
|
|||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_show_goal">
|
||||
{% trans "Show reading goal prompt in feed:" %}
|
||||
{{ form.show_goal }}
|
||||
{% trans "Show reading goal prompt in feed" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_show_suggested_users">
|
||||
{% trans "Show suggested users:" %}
|
||||
{{ form.show_suggested_users }}
|
||||
{% trans "Show suggested users" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_discoverable">
|
||||
{% trans "Show this account in suggested users:" %}
|
||||
{{ form.discoverable }}
|
||||
{% trans "Show this account in suggested users" %}
|
||||
</label>
|
||||
{% url 'directory' as path %}
|
||||
<p class="help">
|
||||
<p class="help" id="desc_discoverable">
|
||||
{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -107,8 +107,8 @@
|
|||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_manually_approves_followers">
|
||||
{% trans "Manually approve followers:" %}
|
||||
{{ form.manually_approves_followers }}
|
||||
{% trans "Manually approve followers" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<header class="columns is-mobile">
|
||||
<div class="column">
|
||||
<h3 class="title is-5">
|
||||
Results from
|
||||
{% trans 'Results from' %}
|
||||
<a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
|
|
@ -13,60 +13,68 @@
|
|||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
<p>
|
||||
<label class="label" for="id_preview">{% trans "Preview:" %}</label>
|
||||
<label class="label" for="id_preview">
|
||||
{% trans "Preview:" %}
|
||||
</label>
|
||||
{{ form.preview }}
|
||||
{% for error in form.preview.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.preview.errors id="desc_preview" %}
|
||||
</p>
|
||||
<p>
|
||||
<label class="label" for="id_content">{% trans "Content:" %}</label>
|
||||
<label class="label" for="id_content">
|
||||
{% trans "Content:" %}
|
||||
</label>
|
||||
{{ form.content }}
|
||||
{% for error in form.content.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.content.errors id="desc_content" %}
|
||||
</p>
|
||||
<p>
|
||||
<label class="label" for="id_event_date">{% trans "Event date:" %}</label>
|
||||
<label class="label" for="id_event_date">
|
||||
{% trans "Event date:" %}
|
||||
</label>
|
||||
<input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
|
||||
{% for error in form.event_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.event_date.errors id="desc_event_date" %}
|
||||
</p>
|
||||
<hr aria-hidden="true">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p>
|
||||
<label class="label" for="id_start_date">{% trans "Start date:" %}</label>
|
||||
<label class="label" for="id_start_date">
|
||||
{% trans "Start date:" %}
|
||||
</label>
|
||||
<input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
|
||||
{% for error in form.start_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.start_date.errors id="desc_start_date" %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>
|
||||
<label class="label" for="id_end_date">{% trans "End date:" %}</label>
|
||||
<label class="label" for="id_end_date">
|
||||
{% trans "End date:" %}
|
||||
</label>
|
||||
<input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
|
||||
{% for error in form.end_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.end_date.errors id="desc_end_date" %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<p>
|
||||
<label class="label" for="id_active">{% trans "Active:" %}</label>
|
||||
<label class="label" for="id_active">
|
||||
{% trans "Active:" %}
|
||||
</label>
|
||||
{{ form.active }}
|
||||
{% for error in form.active.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.active.errors id="desc_active" %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -17,10 +17,8 @@
|
|||
{{ form.domain }}
|
||||
</div>
|
||||
</div>
|
||||
{% for error in form.domain.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.domain.errors id="desc_domain" %}
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
|
||||
|
|
|
@ -27,11 +27,12 @@
|
|||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label" for="id_server_name">{% trans "Instance:" %}</label>
|
||||
<input type="text" name="server_name" maxlength="255" class="input" required id="id_server_name" value="{{ form.server_name.value|default:'' }}" placeholder="domain.com">
|
||||
{% for error in form.server_name.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_server_name">
|
||||
{% trans "Instance:" %}
|
||||
</label>
|
||||
<input type="text" name="server_name" maxlength="255" class="input" required id="id_server_name" value="{{ form.server_name.value|default:'' }}" placeholder="domain.com" aria-describedby="desc_server_name">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.server_name.errors id="desc_server_name" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
|
@ -49,29 +50,37 @@
|
|||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label" for="id_application_type">{% trans "Software:" %}</label>
|
||||
<input type="text" name="application_type" maxlength="255" class="input" id="id_application_type" value="{{ form.application_type.value|default:'' }}">
|
||||
{% for error in form.application_type.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_application_type">
|
||||
{% trans "Software:" %}
|
||||
</label>
|
||||
<input type="text" name="application_type" maxlength="255" class="input" id="id_application_type" value="{{ form.application_type.value|default:'' }}" aria-describedby="desc_application_type">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.application_type.errors id="desc_application_type" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label" for="id_application_version">{% trans "Version:" %}</label>
|
||||
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}">
|
||||
{% for error in form.application_version.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_application_version">
|
||||
{% trans "Version:" %}
|
||||
</label>
|
||||
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}" aria-describedby="desc_application_version">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.application_version.errors id="desc_application_version" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_notes">{% trans "Notes:" %}</label>
|
||||
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea>
|
||||
<label class="label" for="id_notes">
|
||||
{% trans "Notes:" %}
|
||||
</label>
|
||||
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">
|
||||
{{ form.notes.value|default:'' }}
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -20,16 +20,16 @@
|
|||
</div>
|
||||
|
||||
<div class="field">
|
||||
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24">
|
||||
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24" aria-describedby="desc_address">
|
||||
</div>
|
||||
|
||||
{% for error in form.address.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.address.errors id="desc_address" %}
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -33,8 +33,8 @@
|
|||
{{ site_form.instance_description }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label>
|
||||
<p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support HTML or Markdown." %}</p>
|
||||
<label class="label mb-0" for="id_instance_short_description">{% trans "Short description:" %}</label>
|
||||
<p class="help" id="desc_instance_short_description">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support HTML or Markdown." %}</p>
|
||||
{{ site_form.instance_short_description }}
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -114,7 +114,7 @@
|
|||
{{ site_form.require_confirm_email }}
|
||||
{% trans "Require users to confirm email address" %}
|
||||
</label>
|
||||
<p class="help">{% trans "(Recommended if registration is open)" %}</p>
|
||||
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||
|
@ -123,9 +123,8 @@
|
|||
<div class="field">
|
||||
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
||||
{{ site_form.invite_request_text }}
|
||||
{% for error in site_form.invite_request_text.errors %}
|
||||
<p class="help is-danger">{{ error|escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -15,10 +15,9 @@
|
|||
</p>
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">{% trans "Your password:" %}</label>
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required>
|
||||
{% for error in form.password.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
||||
</form>
|
||||
|
|
|
@ -50,18 +50,23 @@
|
|||
{% endif %}
|
||||
{% with group=user.groups.first %}
|
||||
<div class="select">
|
||||
<select name="groups" id="id_user_group">
|
||||
<select name="groups" id="id_user_group" aria-describedby="desc_user_group">
|
||||
{% for value, name in group_form.fields.groups.choices %}
|
||||
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>{{ name|title }}</option>
|
||||
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>
|
||||
{{ name|title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
<option value="" {% if not group %}selected{% endif %}>User</option>
|
||||
<option value="" {% if not group %}selected{% endif %}>
|
||||
User
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
{% for error in group_form.groups.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
|
||||
{% endwith %}
|
||||
<button class="button">{% trans "Save" %}</button>
|
||||
<button class="button">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -80,7 +80,10 @@
|
|||
<div class="block columns is-mobile">
|
||||
<div class="column">
|
||||
<h2 class="title is-3">
|
||||
{{ shelf.name }}
|
||||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||
{% else %}{{ shelf.name }}{% endif %}
|
||||
<span class="subtitle">
|
||||
{% include 'snippets/privacy-icons.html' with item=shelf %}
|
||||
</span>
|
||||
|
|
9
bookwyrm/templates/snippets/form_errors.html
Normal file
9
bookwyrm/templates/snippets/form_errors.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% if errors_list %}
|
||||
<div id="{{ id }}">
|
||||
{% for error in errors_list %}
|
||||
<p class="help is-danger">
|
||||
{{ error | escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
|
@ -3,32 +3,31 @@
|
|||
<div class="field">
|
||||
<label class="label" for="id_localname_register">{% trans "Username:" %}</label>
|
||||
<div class="control">
|
||||
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname_register" value="{% if register_form.localname.value %}{{ register_form.localname.value }}{% endif %}">
|
||||
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname_register" value="{% if register_form.localname.value %}{{ register_form.localname.value }}{% endif %}" aria-describedby="desc_localname_register">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=register_form.localname.errors id="desc_localname_register" %}
|
||||
</div>
|
||||
{% for error in register_form.localname.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
|
||||
<div class="control">
|
||||
<input type="email" name="email" maxlength="254" class="input" id="id_email_register" value="{% if register_form.email.value %}{{ register_form.email.value }}{% endif %}" required>
|
||||
{% for error in register_form.email.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<input type="email" name="email" maxlength="254" class="input" id="id_email_register" value="{% if register_form.email.value %}{{ register_form.email.value }}{% endif %}" required aria-describedby="desc_email_register">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=register_form.email.errors id="desc_email_register" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_password_register">{% trans "Password:" %}</label>
|
||||
<div class="control">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_register">
|
||||
{% for error in register_form.password.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_register" aria-describedby="desc_password_register">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=register_form.password.errors id="desc_password_register" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">{% trans "Sign Up" %}</button>
|
||||
<button class="button is-primary" type="submit">
|
||||
{% trans "Sign Up" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -22,7 +22,15 @@
|
|||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<input type="hidden" name="change-shelf-from" value="{{ current.identifier }}">
|
||||
<input type="hidden" name="shelf" value="{{ shelf.identifier }}">
|
||||
<button class="button is-fullwidth is-small shelf-option is-radiusless is-white" type="submit" {% if shelf in book.shelf_set.all %} disabled {% endif %}><span>{{ shelf.name }}</span></button>
|
||||
|
||||
<button class="button is-fullwidth is-small shelf-option is-radiusless is-white" type="submit" {% if shelf.identifier == current.identifier %}disabled{% endif %}>
|
||||
<span>
|
||||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||
{% else %}{{ shelf.name }}{% endif %}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else%}
|
||||
|
@ -78,11 +86,11 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book controls_text="want_to_read" controls_uid=uuid move_from=current.id refresh=True %}
|
||||
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book controls_text="want_to_read" controls_uid=uuid move_from=current.id refresh=True class="" %}
|
||||
|
||||
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book controls_text="start_reading" controls_uid=uuid move_from=current.id refresh=True %}
|
||||
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book controls_text="start_reading" controls_uid=uuid move_from=current.id refresh=True class="" %}
|
||||
|
||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid move_from=current.id readthrough=readthrough refresh=True %}
|
||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid move_from=current.id readthrough=readthrough refresh=True class="" %}
|
||||
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
|
34
bookwyrm/templates/snippets/trimmed_list.html
Normal file
34
bookwyrm/templates/snippets/trimmed_list.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% spaceless %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% firstof limit 3 as limit %}
|
||||
{% with subtraction_value='-'|add:limit %}
|
||||
{% with remainder_count=items|length|add:subtraction_value %}
|
||||
{% with remainder_count_display=remainder_count|intcomma %}
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
{% for item in items|slice:limit %}
|
||||
<span
|
||||
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
||||
>{{ item }}</span>{% if not forloop.last %}, {% elif remainder_count > 0 %}, {% blocktrans trimmed count counter=remainder_count %}
|
||||
and {{ remainder_count_display }} other
|
||||
{% plural %}
|
||||
and {{ remainder_count_display }} others
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</summary>
|
||||
|
||||
{% for item in items|slice:"3:" %}
|
||||
<span
|
||||
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
||||
>{{ item }}</span>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</details>
|
||||
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
|
@ -39,6 +39,9 @@
|
|||
{% if not is_self and request.user.is_authenticated %}
|
||||
{% include 'snippets/follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
{% if not is_self %}
|
||||
{% include 'ostatus/remote_follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_self and user.follower_requests.all %}
|
||||
<div class="follow-requests">
|
||||
|
|
|
@ -29,8 +29,13 @@
|
|||
<div class="columns is-mobile scroll-x">
|
||||
{% for shelf in shelves %}
|
||||
<div class="column is-narrow">
|
||||
<h3>{{ shelf.name }}
|
||||
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}</h3>
|
||||
<h3>
|
||||
{% if shelf.name == 'To Read' %}{% trans "To Read" %}
|
||||
{% elif shelf.name == 'Currently Reading' %}{% trans "Currently Reading" %}
|
||||
{% elif shelf.name == 'Read' %}{% trans "Read" %}
|
||||
{% else %}{{ shelf.name }}{% endif %}
|
||||
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}
|
||||
</h3>
|
||||
<div class="is-mobile field is-grouped">
|
||||
{% for book in shelf.books %}
|
||||
<div class="control">
|
||||
|
@ -49,7 +54,8 @@
|
|||
|
||||
{% if goal %}
|
||||
<div class="block">
|
||||
<h2 class="title">{% now 'Y' %} Reading Goal</h2>
|
||||
{% now 'Y' as current_year%}
|
||||
<h2 class="title">{% blocktrans %}{{ current_year }} Reading Goal{% endblocktrans %}</h2>
|
||||
{% include 'snippets/goal_progress.html' with goal=goal %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -5,7 +5,6 @@ from uuid import uuid4
|
|||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.templatetags.static import static
|
||||
|
||||
|
||||
|
@ -98,10 +97,3 @@ def get_isni(existing, author, autoescape=True):
|
|||
f'<input type="text" name="isni-for-{author.id}" value="{isni}" hidden>'
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@register.filter(name="remove_spaces")
|
||||
@stringfilter
|
||||
def remove_spaces(arg):
|
||||
"""Removes spaces from argument passed in"""
|
||||
return re.sub(r"\s", "", str(arg))
|
||||
|
|
|
@ -40,6 +40,8 @@ class AbstractConnector(TestCase):
|
|||
class TestConnector(abstract_connector.AbstractConnector):
|
||||
"""nothing added here"""
|
||||
|
||||
generated_remote_link_field = "openlibrary_link"
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
return search_result
|
||||
|
||||
|
@ -87,9 +89,7 @@ class AbstractConnector(TestCase):
|
|||
def test_get_or_create_book_existing(self):
|
||||
"""find an existing book by remote/origin id"""
|
||||
self.assertEqual(models.Book.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id)
|
||||
)
|
||||
self.assertEqual(self.book.remote_id, f"https://{DOMAIN}/book/{self.book.id}")
|
||||
self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
|
||||
|
||||
# dedupe by origin id
|
||||
|
@ -99,7 +99,7 @@ class AbstractConnector(TestCase):
|
|||
|
||||
# dedupe by remote id
|
||||
result = self.connector.get_or_create_book(
|
||||
"https://%s/book/%d" % (DOMAIN, self.book.id)
|
||||
f"https://{DOMAIN}/book/{self.book.id}"
|
||||
)
|
||||
self.assertEqual(models.Book.objects.count(), 1)
|
||||
self.assertEqual(result, self.book)
|
||||
|
@ -119,7 +119,8 @@ class AbstractConnector(TestCase):
|
|||
@responses.activate
|
||||
def test_get_or_create_author(self):
|
||||
"""load an author"""
|
||||
self.connector.author_mappings = [ # pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
]
|
||||
|
@ -139,3 +140,26 @@ class AbstractConnector(TestCase):
|
|||
author = models.Author.objects.create(name="Test Author")
|
||||
result = self.connector.get_or_create_author(author.remote_id)
|
||||
self.assertEqual(author, result)
|
||||
|
||||
@responses.activate
|
||||
def test_update_author_from_remote(self):
|
||||
"""trigger the function that looks up the remote data"""
|
||||
author = models.Author.objects.create(name="Test", openlibrary_key="OL123A")
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
Mapping("isni"),
|
||||
]
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://openlibrary.org/authors/OL123A",
|
||||
json={"id": "https://www.example.com/author", "name": "Beep", "isni": "hi"},
|
||||
)
|
||||
|
||||
self.connector.update_author_from_remote(author)
|
||||
|
||||
author.refresh_from_db()
|
||||
self.assertEqual(author.name, "Test")
|
||||
self.assertEqual(author.isni, "hi")
|
||||
|
|
|
@ -306,3 +306,11 @@ class Inventaire(TestCase):
|
|||
|
||||
extract = self.connector.get_description({"enwiki": "test_path"})
|
||||
self.assertEqual(extract, "hi hi")
|
||||
|
||||
def test_remote_id_from_model(self):
|
||||
"""figure out a url from an id"""
|
||||
obj = models.Author.objects.create(name="hello", inventaire_id="123")
|
||||
self.assertEqual(
|
||||
self.connector.get_remote_id_from_model(obj),
|
||||
"https://inventaire.io?action=by-uris&uris=123",
|
||||
)
|
||||
|
|
|
@ -98,6 +98,9 @@ class Openlibrary(TestCase):
|
|||
"type": "/type/datetime",
|
||||
"value": "2008-08-31 10:09:33.413686",
|
||||
},
|
||||
"remote_ids": {
|
||||
"isni": "000111",
|
||||
},
|
||||
"key": "/authors/OL453734A",
|
||||
"type": {"key": "/type/author"},
|
||||
"id": 1259965,
|
||||
|
@ -110,6 +113,7 @@ class Openlibrary(TestCase):
|
|||
self.assertIsInstance(result, models.Author)
|
||||
self.assertEqual(result.name, "George Elliott")
|
||||
self.assertEqual(result.openlibrary_key, "OL453734A")
|
||||
self.assertEqual(result.isni, "000111")
|
||||
|
||||
def test_get_cover_url(self):
|
||||
"""formats a url that should contain the cover image"""
|
||||
|
|
5
bookwyrm/tests/data/openlibrary.csv
Normal file
5
bookwyrm/tests/data/openlibrary.csv
Normal file
|
@ -0,0 +1,5 @@
|
|||
Work Id,Edition Id,Bookshelf
|
||||
OL102749W,,Currently Reading
|
||||
OL361393W,OL7798182M,Currently Reading
|
||||
OL1652392W,OL7194114M,Want to Read
|
||||
OL17062644W,OL25726365M,Already Read
|
|
|
@ -128,7 +128,7 @@ class GenericImporter(TestCase):
|
|||
|
||||
import_item = models.ImportItem.objects.get(job=import_job, index=0)
|
||||
with patch(
|
||||
"bookwyrm.models.import_job.ImportItem.get_book_from_isbn"
|
||||
"bookwyrm.models.import_job.ImportItem.get_book_from_identifier"
|
||||
) as resolve:
|
||||
resolve.return_value = self.book
|
||||
|
||||
|
@ -158,7 +158,7 @@ class GenericImporter(TestCase):
|
|||
).exists()
|
||||
)
|
||||
|
||||
item = items[3]
|
||||
item = items.last()
|
||||
item.fail_reason = "hello"
|
||||
item.save()
|
||||
item.update_job()
|
||||
|
|
85
bookwyrm/tests/importers/test_openlibrary_import.py
Normal file
85
bookwyrm/tests/importers/test_openlibrary_import.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
""" testing import """
|
||||
import pathlib
|
||||
from unittest.mock import patch
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.importers import OpenLibraryImporter
|
||||
from bookwyrm.importers.importer import handle_imported_book
|
||||
|
||||
|
||||
def make_date(*args):
|
||||
"""helper function to easily generate a date obj"""
|
||||
return datetime.datetime(*args, tzinfo=pytz.UTC)
|
||||
|
||||
|
||||
# pylint: disable=consider-using-with
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
|
||||
class OpenLibraryImport(TestCase):
|
||||
"""importing from openlibrary csv"""
|
||||
|
||||
def setUp(self):
|
||||
"""use a test csv"""
|
||||
self.importer = OpenLibraryImporter()
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../data/openlibrary.csv")
|
||||
self.csv = open(datafile, "r", encoding=self.importer.encoding)
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse", "mouse@mouse.mouse", "password", local=True
|
||||
)
|
||||
|
||||
work = models.Work.objects.create(title="Test Work")
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
parent_work=work,
|
||||
)
|
||||
|
||||
def test_create_job(self, *_):
|
||||
"""creates the import job entry and checks csv"""
|
||||
import_job = self.importer.create_job(
|
||||
self.local_user, self.csv, False, "public"
|
||||
)
|
||||
|
||||
import_items = models.ImportItem.objects.filter(job=import_job).all()
|
||||
self.assertEqual(len(import_items), 4)
|
||||
self.assertEqual(import_items[0].index, 0)
|
||||
self.assertEqual(import_items[0].data["Work Id"], "OL102749W")
|
||||
self.assertEqual(import_items[1].data["Work Id"], "OL361393W")
|
||||
self.assertEqual(import_items[1].data["Edition Id"], "OL7798182M")
|
||||
|
||||
self.assertEqual(import_items[0].normalized_data["shelf"], "reading")
|
||||
self.assertEqual(import_items[0].normalized_data["openlibrary_key"], "")
|
||||
self.assertEqual(
|
||||
import_items[0].normalized_data["openlibrary_work_key"], "OL102749W"
|
||||
)
|
||||
self.assertEqual(
|
||||
import_items[1].normalized_data["openlibrary_key"], "OL7798182M"
|
||||
)
|
||||
self.assertEqual(import_items[2].normalized_data["shelf"], "to-read")
|
||||
self.assertEqual(import_items[3].normalized_data["shelf"], "read")
|
||||
|
||||
def test_handle_imported_book(self, *_):
|
||||
"""openlibrary import added a book, this adds related connections"""
|
||||
shelf = self.local_user.shelf_set.filter(identifier="reading").first()
|
||||
self.assertIsNone(shelf.books.first())
|
||||
|
||||
import_job = self.importer.create_job(
|
||||
self.local_user, self.csv, False, "public"
|
||||
)
|
||||
import_item = import_job.items.first()
|
||||
import_item.book = self.book
|
||||
import_item.save()
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
handle_imported_book(import_item)
|
||||
|
||||
shelf.refresh_from_db()
|
||||
self.assertEqual(shelf.books.first(), self.book)
|
|
@ -1,5 +1,7 @@
|
|||
""" testing models """
|
||||
from dateutil.parser import parse
|
||||
|
||||
from imagekit.models import ImageSpecField
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
|
@ -26,11 +28,22 @@ class Book(TestCase):
|
|||
|
||||
def test_remote_id(self):
|
||||
"""fanciness with remote/origin ids"""
|
||||
remote_id = "https://%s/book/%d" % (settings.DOMAIN, self.work.id)
|
||||
remote_id = f"https://{settings.DOMAIN}/book/{self.work.id}"
|
||||
self.assertEqual(self.work.get_remote_id(), remote_id)
|
||||
self.assertEqual(self.work.remote_id, remote_id)
|
||||
|
||||
def test_create_book(self):
|
||||
def test_generated_links(self):
|
||||
"""links produced from identifiers"""
|
||||
book = models.Edition.objects.create(
|
||||
title="ExEd",
|
||||
parent_work=self.work,
|
||||
openlibrary_key="OL123M",
|
||||
inventaire_id="isbn:123",
|
||||
)
|
||||
self.assertEqual(book.openlibrary_link, "https://openlibrary.org/books/OL123M")
|
||||
self.assertEqual(book.inventaire_link, "https://inventaire.io/entity/isbn:123")
|
||||
|
||||
def test_create_book_invalid(self):
|
||||
"""you shouldn't be able to create Books (only editions and works)"""
|
||||
self.assertRaises(ValueError, models.Book.objects.create, title="Invalid Book")
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from unittest.mock import patch
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models, settings
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||
|
@ -76,7 +76,8 @@ class Group(TestCase):
|
|||
models.GroupMember.objects.create(group=self.public_group, user=self.capybara)
|
||||
|
||||
def test_group_members_can_see_private_groups(self, _):
|
||||
"""direct privacy group should not be excluded from group listings for group members viewing"""
|
||||
"""direct privacy group should not be excluded from group listings for group
|
||||
members viewing"""
|
||||
|
||||
rat_groups = models.Group.privacy_filter(self.rat).all()
|
||||
badger_groups = models.Group.privacy_filter(self.badger).all()
|
||||
|
@ -85,7 +86,8 @@ class Group(TestCase):
|
|||
self.assertTrue(self.private_group in badger_groups)
|
||||
|
||||
def test_group_members_can_see_followers_only_lists(self, _):
|
||||
"""follower-only group booklists should not be excluded from group booklist listing for group members who do not follower list owner"""
|
||||
"""follower-only group booklists should not be excluded from group booklist
|
||||
listing for group members who do not follower list owner"""
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
followers_list = models.List.objects.create(
|
||||
|
@ -105,7 +107,8 @@ class Group(TestCase):
|
|||
self.assertTrue(followers_list in capybara_lists)
|
||||
|
||||
def test_group_members_can_see_private_lists(self, _):
|
||||
"""private group booklists should not be excluded from group booklist listing for group members"""
|
||||
"""private group booklists should not be excluded from group booklist listing
|
||||
for group members"""
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
|
||||
|
|
|
@ -139,7 +139,7 @@ class ImportJob(TestCase):
|
|||
self.assertEqual(item.reads, expected)
|
||||
|
||||
@responses.activate
|
||||
def test_get_book_from_isbn(self):
|
||||
def test_get_book_from_identifier(self):
|
||||
"""search and load books by isbn (9780356506999)"""
|
||||
item = models.ImportItem.objects.create(
|
||||
index=1,
|
||||
|
@ -197,6 +197,6 @@ class ImportJob(TestCase):
|
|||
with patch(
|
||||
"bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
|
||||
):
|
||||
book = item.get_book_from_isbn()
|
||||
book = item.get_book_from_identifier()
|
||||
|
||||
self.assertEqual(book.title, "Sabriel")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" testing models """
|
||||
from unittest.mock import patch
|
||||
from django.test import TestCase
|
||||
from uuid import UUID
|
||||
|
||||
from bookwyrm import models, settings
|
||||
|
||||
|
@ -80,3 +81,12 @@ class List(TestCase):
|
|||
self.assertEqual(item.book_list.privacy, "public")
|
||||
self.assertEqual(item.privacy, "direct")
|
||||
self.assertEqual(item.recipients, [])
|
||||
|
||||
def test_embed_key(self, _):
|
||||
"""embed_key should never be empty"""
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
book_list = models.List.objects.create(
|
||||
name="Test List", user=self.local_user
|
||||
)
|
||||
|
||||
self.assertIsInstance(book_list.embed_key, UUID)
|
||||
|
|
|
@ -209,6 +209,54 @@ class BookViews(TestCase):
|
|||
self.assertEqual(self.book.description, "new description hi")
|
||||
self.assertEqual(self.book.last_edited_by, self.local_user)
|
||||
|
||||
def test_update_book_from_remote(self):
|
||||
"""call out to sync with remote connector"""
|
||||
models.Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://openlibrary.org",
|
||||
books_url="https://openlibrary.org",
|
||||
covers_url="https://covers.openlibrary.org",
|
||||
search_url="https://openlibrary.org/search?q=",
|
||||
isbn_search_url="https://openlibrary.org/isbn",
|
||||
)
|
||||
self.local_user.groups.add(self.group)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.connectors.openlibrary.Connector.update_book_from_remote"
|
||||
) as mock:
|
||||
views.update_book_from_remote(request, self.book.id, "openlibrary.org")
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
||||
def test_resolve_book(self):
|
||||
"""load a book from search results"""
|
||||
models.Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://openlibrary.org",
|
||||
books_url="https://openlibrary.org",
|
||||
covers_url="https://covers.openlibrary.org",
|
||||
search_url="https://openlibrary.org/search?q=",
|
||||
isbn_search_url="https://openlibrary.org/isbn",
|
||||
)
|
||||
request = self.factory.post(
|
||||
"", {"remote_id": "https://openlibrary.org/book/123"}
|
||||
)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.connectors.openlibrary.Connector.get_or_create_book"
|
||||
) as mock:
|
||||
mock.return_value = self.book
|
||||
result = views.resolve_book(request)
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
self.assertEqual(mock.call_args[0][0], "https://openlibrary.org/book/123")
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
|
||||
def _setup_cover_url():
|
||||
"""creates cover url mock"""
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
""" test for app action functionality """
|
||||
import pathlib
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class ImportViews(TestCase):
|
||||
|
@ -44,15 +45,29 @@ class ImportViews(TestCase):
|
|||
import_job = models.ImportJob.objects.create(user=self.local_user, mappings={})
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.tasks.app.AsyncResult") as async_result:
|
||||
async_result.return_value = []
|
||||
result = view(request, import_job.id)
|
||||
|
||||
result = view(request, import_job.id)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_import_status_reformat(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.ImportStatus.as_view()
|
||||
import_job = models.ImportJob.objects.create(user=self.local_user, mappings={})
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
with patch(
|
||||
"bookwyrm.importers.goodreads_import.GoodreadsImporter.update_legacy_job"
|
||||
) as mock:
|
||||
result = view(request, import_job.id)
|
||||
self.assertEqual(mock.call_args[0][0], import_job)
|
||||
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
def test_start_import(self):
|
||||
"""retry failed items"""
|
||||
"""start a job"""
|
||||
view = views.Import.as_view()
|
||||
form = forms.ImportForm()
|
||||
form.data["source"] = "Goodreads"
|
||||
|
@ -74,3 +89,19 @@ class ImportViews(TestCase):
|
|||
job = models.ImportJob.objects.get()
|
||||
self.assertFalse(job.include_reviews)
|
||||
self.assertEqual(job.privacy, "public")
|
||||
|
||||
def test_retry_item(self):
|
||||
"""try again on a single row"""
|
||||
job = models.ImportJob.objects.create(user=self.local_user, mappings={})
|
||||
item = models.ImportItem.objects.create(
|
||||
index=0,
|
||||
job=job,
|
||||
fail_reason="no match",
|
||||
data={},
|
||||
normalized_data={},
|
||||
)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.importers.importer.import_item_task.delay") as mock:
|
||||
views.retry_item(request, job.id, item.id)
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
|
|
@ -148,3 +148,26 @@ class AuthorViews(TestCase):
|
|||
self.assertEqual(author.name, "Test Author")
|
||||
validate_html(resp.render())
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_update_author_from_remote(self):
|
||||
"""call out to sync with remote connector"""
|
||||
author = models.Author.objects.create(name="Test Author")
|
||||
models.Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://openlibrary.org",
|
||||
books_url="https://openlibrary.org",
|
||||
covers_url="https://covers.openlibrary.org",
|
||||
search_url="https://openlibrary.org/search?q=",
|
||||
isbn_search_url="https://openlibrary.org/isbn",
|
||||
)
|
||||
self.local_user.groups.add(self.group)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.connectors.openlibrary.Connector.update_author_from_remote"
|
||||
) as mock:
|
||||
views.update_author_from_remote(request, author.id, "openlibrary.org")
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
|
|
@ -4,10 +4,12 @@ from unittest.mock import patch
|
|||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
|
||||
|
@ -16,6 +18,7 @@ class FollowViews(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
models.SiteSettings.objects.create()
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
|
@ -174,3 +177,43 @@ class FollowViews(TestCase):
|
|||
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
||||
# follow relationship should not exist
|
||||
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)
|
||||
|
||||
def test_ostatus_follow_request(self, _):
|
||||
"""check ostatus subscribe template loads"""
|
||||
request = self.factory.get(
|
||||
"", {"acct": "https%3A%2F%2Fexample.com%2Fusers%2Frat"}
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.ostatus_follow_request(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_remote_follow_page(self, _):
|
||||
"""check remote follow page loads"""
|
||||
request = self.factory.get("", {"acct": "mouse@local.com"})
|
||||
request.user = self.remote_user
|
||||
result = views.remote_follow_page(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_ostatus_follow_success(self, _):
|
||||
"""check remote follow success page loads"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.remote_user
|
||||
request.following = "mouse@local.com"
|
||||
result = views.ostatus_follow_success(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_remote_follow(self, _):
|
||||
"""check follow from remote page loads"""
|
||||
request = self.factory.post("", {"user": self.remote_user.id})
|
||||
request.user = self.remote_user
|
||||
request.remote_user = "mouse@local.com"
|
||||
result = views.remote_follow(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from django.contrib.auth import decorators
|
||||
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views, forms
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -27,16 +26,23 @@ class GroupViews(TestCase):
|
|||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
self.rat = models.User.objects.create_user(
|
||||
"rat@local.com",
|
||||
"rat@rat.rat",
|
||||
"password",
|
||||
local=True,
|
||||
localname="rat",
|
||||
)
|
||||
|
||||
self.testgroup = models.Group.objects.create(
|
||||
name="Test Group",
|
||||
description="Initial description",
|
||||
user=self.local_user,
|
||||
privacy="public",
|
||||
)
|
||||
self.membership = models.GroupMember.objects.create(
|
||||
group=self.testgroup, user=self.local_user
|
||||
)
|
||||
self.testgroup = models.Group.objects.create(
|
||||
name="Test Group",
|
||||
description="Initial description",
|
||||
user=self.local_user,
|
||||
privacy="public",
|
||||
)
|
||||
self.membership = models.GroupMember.objects.create(
|
||||
group=self.testgroup, user=self.local_user
|
||||
)
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
@ -98,7 +104,6 @@ class GroupViews(TestCase):
|
|||
|
||||
def test_group_edit(self, _):
|
||||
"""test editing a "group" database entry"""
|
||||
|
||||
view = views.Group.as_view()
|
||||
request = self.factory.post(
|
||||
"",
|
||||
|
@ -117,3 +122,137 @@ class GroupViews(TestCase):
|
|||
self.assertEqual(self.testgroup.name, "Updated Group name")
|
||||
self.assertEqual(self.testgroup.description, "wow")
|
||||
self.assertEqual(self.testgroup.privacy, "direct")
|
||||
|
||||
def test_delete_group(self, _):
|
||||
"""delete a group"""
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
views.delete_group(request, self.testgroup.id)
|
||||
self.assertFalse(models.Group.objects.exists())
|
||||
|
||||
def test_invite_member(self, _):
|
||||
"""invite a member to a group"""
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"group": self.testgroup.id,
|
||||
"user": self.rat.localname,
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.invite_member(request)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
invite = models.GroupMemberInvitation.objects.get()
|
||||
self.assertEqual(invite.user, self.rat)
|
||||
self.assertEqual(invite.group, self.testgroup)
|
||||
|
||||
def test_invite_member_twice(self, _):
|
||||
"""invite a member to a group again"""
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"group": self.testgroup.id,
|
||||
"user": self.rat.localname,
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.invite_member(request)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
result = views.invite_member(request)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
def test_remove_member_denied(self, _):
|
||||
"""remove member"""
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"group": self.testgroup.id,
|
||||
"user": self.local_user.localname,
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.remove_member(request)
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
def test_remove_member_non_member(self, _):
|
||||
"""remove member but wait, that's not a member"""
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"group": self.testgroup.id,
|
||||
"user": self.rat.localname,
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.remove_member(request)
|
||||
# nothing happens
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
def test_remove_member_invited(self, _):
|
||||
"""remove an invited member"""
|
||||
models.GroupMemberInvitation.objects.create(
|
||||
user=self.rat,
|
||||
group=self.testgroup,
|
||||
)
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"group": self.testgroup.id,
|
||||
"user": self.rat.localname,
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.remove_member(request)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertFalse(models.GroupMemberInvitation.objects.exists())
|
||||
|
||||
def test_remove_member_existing_member(self, _):
|
||||
"""remove an invited member"""
|
||||
models.GroupMember.objects.create(
|
||||
user=self.rat,
|
||||
group=self.testgroup,
|
||||
)
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"group": self.testgroup.id,
|
||||
"user": self.rat.localname,
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.remove_member(request)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(models.GroupMember.objects.count(), 1)
|
||||
self.assertEqual(models.GroupMember.objects.first().user, self.local_user)
|
||||
notification = models.Notification.objects.get()
|
||||
self.assertEqual(notification.user, self.rat)
|
||||
self.assertEqual(notification.related_group, self.testgroup)
|
||||
self.assertEqual(notification.notification_type, "REMOVE")
|
||||
|
||||
def test_accept_membership(self, _):
|
||||
"""accept an invite"""
|
||||
models.GroupMemberInvitation.objects.create(
|
||||
user=self.rat,
|
||||
group=self.testgroup,
|
||||
)
|
||||
request = self.factory.post("", {"group": self.testgroup.id})
|
||||
request.user = self.rat
|
||||
views.accept_membership(request)
|
||||
|
||||
self.assertFalse(models.GroupMemberInvitation.objects.exists())
|
||||
self.assertTrue(self.rat in [m.user for m in self.testgroup.memberships.all()])
|
||||
|
||||
def test_reject_membership(self, _):
|
||||
"""reject an invite"""
|
||||
models.GroupMemberInvitation.objects.create(
|
||||
user=self.rat,
|
||||
group=self.testgroup,
|
||||
)
|
||||
request = self.factory.post("", {"group": self.testgroup.id})
|
||||
request.user = self.rat
|
||||
views.reject_membership(request)
|
||||
|
||||
self.testgroup.refresh_from_db()
|
||||
self.assertFalse(models.GroupMemberInvitation.objects.exists())
|
||||
self.assertFalse(self.rat in [m.user for m in self.testgroup.memberships.all()])
|
||||
|
|
|
@ -3,6 +3,7 @@ import json
|
|||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http.response import Http404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
@ -385,3 +386,46 @@ class ListViews(TestCase):
|
|||
|
||||
result = view(request, self.local_user.username)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
def test_embed_call_without_key(self):
|
||||
"""there are so many views, this just makes sure it DOESN’T load"""
|
||||
view = views.unsafe_embed_list
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
approved=True,
|
||||
order=1,
|
||||
)
|
||||
|
||||
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
with self.assertRaises(Http404):
|
||||
result = view(request, self.list.id, "")
|
||||
|
||||
def test_embed_call_with_key(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.unsafe_embed_list
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
approved=True,
|
||||
order=1,
|
||||
)
|
||||
|
||||
embed_key = str(self.list.embed_key.hex)
|
||||
|
||||
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.list.id, embed_key)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
|
|
@ -50,10 +50,17 @@ class UpdateViews(TestCase):
|
|||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
|
||||
with patch("bookwyrm.activitystreams.ActivityStream.get_unread_count") as mock:
|
||||
mock.return_value = 3
|
||||
result = views.get_unread_status_count(request, "home")
|
||||
with patch(
|
||||
"bookwyrm.activitystreams.ActivityStream.get_unread_count"
|
||||
) as mock_count:
|
||||
with patch(
|
||||
"bookwyrm.activitystreams.ActivityStream.get_unread_count_by_status_type"
|
||||
) as mock_count_by_status:
|
||||
mock_count.return_value = 3
|
||||
mock_count_by_status.return_value = {"review": 5}
|
||||
result = views.get_unread_status_count(request, "home")
|
||||
|
||||
self.assertIsInstance(result, JsonResponse)
|
||||
data = json.loads(result.getvalue())
|
||||
self.assertEqual(data["count"], 3)
|
||||
self.assertEqual(data["count_by_type"]["review"], 5)
|
||||
|
|
|
@ -51,6 +51,11 @@ class UserViews(TestCase):
|
|||
|
||||
def test_user_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
# extras that are rendered on the user page
|
||||
models.AnnualGoal.objects.create(
|
||||
user=self.local_user, goal=12, privacy="followers"
|
||||
)
|
||||
|
||||
view = views.User.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
|
@ -104,6 +109,18 @@ class UserViews(TestCase):
|
|||
self.assertIsInstance(result, ActivitypubResponse)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_followers_page_anonymous(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Followers.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
with patch("bookwyrm.views.user.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, "mouse")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||
def test_followers_page_blocked(self, *_):
|
||||
|
@ -135,6 +152,18 @@ class UserViews(TestCase):
|
|||
self.assertIsInstance(result, ActivitypubResponse)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_following_page_anonymous(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Following.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
with patch("bookwyrm.views.user.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, "mouse")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_following_page_blocked(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Following.as_view()
|
||||
|
@ -145,3 +174,15 @@ class UserViews(TestCase):
|
|||
is_api.return_value = False
|
||||
with self.assertRaises(Http404):
|
||||
view(request, "rat")
|
||||
|
||||
def test_hide_suggestions(self):
|
||||
"""update suggestions settings"""
|
||||
self.assertTrue(self.local_user.show_suggested_users)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
|
||||
result = views.hide_suggestions(request)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
self.local_user.refresh_from_db()
|
||||
self.assertFalse(self.local_user.show_suggested_users)
|
||||
|
|
|
@ -44,6 +44,7 @@ urlpatterns = [
|
|||
re_path(r"^api/v1/instance/?$", views.instance_info),
|
||||
re_path(r"^api/v1/instance/peers/?$", views.peers),
|
||||
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_count),
|
||||
|
@ -336,6 +337,11 @@ urlpatterns = [
|
|||
),
|
||||
re_path(r"^save-list/(?P<list_id>\d+)/?$", views.save_list, name="list-save"),
|
||||
re_path(r"^unsave-list/(?P<list_id>\d+)/?$", views.unsave_list, name="list-unsave"),
|
||||
re_path(
|
||||
r"^list/(?P<list_id>\d+)/embed/(?P<list_key>[0-9a-f]+)?$",
|
||||
views.unsafe_embed_list,
|
||||
name="embed-list",
|
||||
),
|
||||
# User books
|
||||
re_path(rf"{USER_PATH}/books/?$", views.Shelf.as_view(), name="user-shelves"),
|
||||
re_path(
|
||||
|
@ -422,13 +428,29 @@ urlpatterns = [
|
|||
r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover"
|
||||
),
|
||||
re_path(r"^add-description/(?P<book_id>\d+)/?$", views.add_description),
|
||||
re_path(r"^resolve-book/?$", views.resolve_book),
|
||||
re_path(r"^switch-edition/?$", views.switch_edition),
|
||||
re_path(r"^resolve-book/?$", views.resolve_book, name="resolve-book"),
|
||||
re_path(r"^switch-edition/?$", views.switch_edition, name="switch-edition"),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/update/(?P<connector_identifier>[\w\.]+)/?$",
|
||||
views.update_book_from_remote,
|
||||
name="book-update-remote",
|
||||
),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)/update/(?P<connector_identifier>[\w\.]+)/?$",
|
||||
views.update_author_from_remote,
|
||||
name="author-update-remote",
|
||||
),
|
||||
# isbn
|
||||
re_path(r"^isbn/(?P<isbn>\d+)(.json)?/?$", views.Isbn.as_view()),
|
||||
# author
|
||||
re_path(r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view()),
|
||||
re_path(r"^author/(?P<author_id>\d+)/edit/?$", views.EditAuthor.as_view()),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author"
|
||||
),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)/edit/?$",
|
||||
views.EditAuthor.as_view(),
|
||||
name="edit-author",
|
||||
),
|
||||
# reading progress
|
||||
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
|
||||
re_path(r"^delete-readthrough/?$", views.delete_readthrough),
|
||||
|
@ -450,4 +472,9 @@ urlpatterns = [
|
|||
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
|
||||
re_path(r"^accept-follow-request/?$", views.accept_follow_request),
|
||||
re_path(r"^delete-follow-request/?$", views.delete_follow_request),
|
||||
re_path(r"^ostatus_follow/?$", views.remote_follow, name="remote-follow"),
|
||||
re_path(r"^remote_follow/?$", views.remote_follow_page, name="remote-follow-page"),
|
||||
re_path(
|
||||
r"^ostatus_success/?$", views.ostatus_follow_success, name="ostatus-success"
|
||||
),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
@ -19,7 +19,7 @@ def request_isni_data(search_index, search_term, max_records=5):
|
|||
"recordPacking": "xml",
|
||||
"sortKeys": "RLV,pica,0,,",
|
||||
}
|
||||
result = requests.get("http://isni.oclc.org/sru/", params=query_params, timeout=10)
|
||||
result = requests.get("http://isni.oclc.org/sru/", params=query_params, timeout=15)
|
||||
# the OCLC ISNI server asserts the payload is encoded
|
||||
# in latin1, but we know better
|
||||
result.encoding = "utf-8"
|
||||
|
@ -104,12 +104,14 @@ def find_authors_by_name(name_string, description=False):
|
|||
# otherwise just grab the first title listing
|
||||
titles.append(element.find(".//title"))
|
||||
|
||||
if titles is not None:
|
||||
if titles:
|
||||
# some of the "titles" in ISNI are a little ...iffy
|
||||
# '@' is used by ISNI/OCLC to index the starting point ignoring stop words
|
||||
# (e.g. "The @Government of no one")
|
||||
title_elements = [
|
||||
e for e in titles if not e.text.replace("@", "").isnumeric()
|
||||
e
|
||||
for e in titles
|
||||
if hasattr(e, "text") and not e.text.replace("@", "").isnumeric()
|
||||
]
|
||||
if len(title_elements):
|
||||
author.bio = title_elements[0].text.replace("@", "")
|
||||
|
|
|
@ -29,6 +29,7 @@ from .preferences.block import Block, unblock
|
|||
|
||||
# books
|
||||
from .books.books import Book, upload_cover, add_description, resolve_book
|
||||
from .books.books import update_book_from_remote
|
||||
from .books.edit_book import EditBook, ConfirmEditBook
|
||||
from .books.editions import Editions, switch_edition
|
||||
|
||||
|
@ -54,11 +55,18 @@ from .imports.manually_review import (
|
|||
)
|
||||
|
||||
# misc views
|
||||
from .author import Author, EditAuthor
|
||||
from .author import Author, EditAuthor, update_author_from_remote
|
||||
from .directory import Directory
|
||||
from .discover import Discover
|
||||
from .feed import DirectMessage, Feed, Replies, Status
|
||||
from .follow import follow, unfollow
|
||||
from .follow import (
|
||||
follow,
|
||||
unfollow,
|
||||
ostatus_follow_request,
|
||||
ostatus_follow_success,
|
||||
remote_follow,
|
||||
remote_follow_page,
|
||||
)
|
||||
from .follow import accept_follow_request, delete_follow_request
|
||||
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
||||
from .goal import Goal, hide_goal
|
||||
|
@ -76,7 +84,7 @@ from .inbox import Inbox
|
|||
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
||||
from .isbn import Isbn
|
||||
from .list import Lists, SavedLists, List, Curate, UserLists
|
||||
from .list import save_list, unsave_list, delete_list
|
||||
from .list import save_list, unsave_list, delete_list, unsafe_embed_list
|
||||
from .notifications import Notifications
|
||||
from .outbox import Outbox
|
||||
from .reading import create_readthrough, delete_readthrough, delete_progressupdate
|
||||
|
|
|
@ -6,9 +6,11 @@ from django.shortcuts import get_object_or_404, redirect
|
|||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
|
||||
|
@ -73,3 +75,19 @@ class EditAuthor(View):
|
|||
author = form.save()
|
||||
|
||||
return redirect(f"/author/{author.id}")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def update_author_from_remote(request, author_id, connector_identifier):
|
||||
"""load the remote data for this author"""
|
||||
connector = connector_manager.load_connector(
|
||||
get_object_or_404(models.Connector, identifier=connector_identifier)
|
||||
)
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
|
||||
connector.update_author_from_remote(author)
|
||||
|
||||
return redirect("author", author.id)
|
||||
|
|
|
@ -178,3 +178,19 @@ def resolve_book(request):
|
|||
book = connector.get_or_create_book(remote_id)
|
||||
|
||||
return redirect("book", book.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def update_book_from_remote(request, book_id, connector_identifier):
|
||||
"""load the remote data for this book"""
|
||||
connector = connector_manager.load_connector(
|
||||
get_object_or_404(models.Connector, identifier=connector_identifier)
|
||||
)
|
||||
book = get_object_or_404(models.Book.objects.select_subclasses(), id=book_id)
|
||||
|
||||
connector.update_book_from_remote(book)
|
||||
|
||||
return redirect("book", book.id)
|
||||
|
|
|
@ -10,10 +10,11 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
|
||||
from bookwyrm import activitystreams, forms, models
|
||||
from bookwyrm.models.user import FeedFilterChoices
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH, STREAMS
|
||||
from bookwyrm.suggested_users import suggested_users
|
||||
from .helpers import get_user_from_username
|
||||
from .helpers import filter_stream_by_status_type, get_user_from_username
|
||||
from .helpers import is_api_request, is_bookwyrm_request
|
||||
|
||||
|
||||
|
@ -22,7 +23,17 @@ from .helpers import is_api_request, is_bookwyrm_request
|
|||
class Feed(View):
|
||||
"""activity stream"""
|
||||
|
||||
def get(self, request, tab):
|
||||
def post(self, request, tab):
|
||||
"""save feed settings form, with a silent validation fail"""
|
||||
settings_saved = False
|
||||
form = forms.FeedStatusTypesForm(request.POST, instance=request.user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
settings_saved = True
|
||||
|
||||
return self.get(request, tab, settings_saved)
|
||||
|
||||
def get(self, request, tab, settings_saved=False):
|
||||
"""user's homepage with activity feed"""
|
||||
tab = [s for s in STREAMS if s["key"] == tab]
|
||||
tab = tab[0] if tab else STREAMS[0]
|
||||
|
@ -30,7 +41,11 @@ class Feed(View):
|
|||
activities = activitystreams.streams[tab["key"]].get_activity_stream(
|
||||
request.user
|
||||
)
|
||||
paginated = Paginator(activities, PAGE_LENGTH)
|
||||
filtered_activities = filter_stream_by_status_type(
|
||||
activities,
|
||||
allowed_types=request.user.feed_status_types,
|
||||
)
|
||||
paginated = Paginator(filtered_activities, PAGE_LENGTH)
|
||||
|
||||
suggestions = suggested_users.get_suggestions(request.user)
|
||||
|
||||
|
@ -43,6 +58,9 @@ class Feed(View):
|
|||
"tab": tab,
|
||||
"streams": STREAMS,
|
||||
"goal_form": forms.GoalForm(),
|
||||
"feed_status_types_options": FeedFilterChoices,
|
||||
"allowed_status_types": request.user.feed_status_types,
|
||||
"settings_saved": settings_saved,
|
||||
"path": f"/{tab['key']}",
|
||||
},
|
||||
}
|
||||
|
@ -159,12 +177,19 @@ class Status(View):
|
|||
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 = {
|
||||
**feed_page_data(request.user),
|
||||
**{
|
||||
"status": status,
|
||||
"children": children,
|
||||
"ancestors": ancestors,
|
||||
"preview": preview,
|
||||
},
|
||||
}
|
||||
return TemplateResponse(request, "feed/status.html", data)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue