forked from mirrors/bookwyrm
Merge branch 'main' into list-embed
This commit is contained in:
commit
5b8c1bde89
85 changed files with 5443 additions and 2384 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@
|
||||||
*.swp
|
*.swp
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
.local
|
.local
|
||||||
|
/nginx/nginx.conf
|
||||||
|
|
||||||
# VSCode
|
# VSCode
|
||||||
/.vscode
|
/.vscode
|
||||||
|
|
|
@ -111,7 +111,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
return existing.default_edition
|
return existing.default_edition
|
||||||
return existing
|
return existing
|
||||||
|
|
||||||
# load the json
|
# load the json data from the remote data source
|
||||||
data = self.get_book_data(remote_id)
|
data = self.get_book_data(remote_id)
|
||||||
if self.is_work_data(data):
|
if self.is_work_data(data):
|
||||||
try:
|
try:
|
||||||
|
@ -150,27 +150,37 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
"""this allows connectors to override the default behavior"""
|
"""this allows connectors to override the default behavior"""
|
||||||
return get_data(remote_id)
|
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"""
|
"""if we already have the work, we're ready"""
|
||||||
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
||||||
mapped_data["work"] = work.remote_id
|
mapped_data["work"] = work.remote_id
|
||||||
edition_activity = activitypub.Edition(**mapped_data)
|
edition_activity = activitypub.Edition(**mapped_data)
|
||||||
edition = edition_activity.to_model(model=models.Edition, overwrite=False)
|
edition = edition_activity.to_model(
|
||||||
edition.connector = self.connector
|
model=models.Edition, overwrite=False, instance=instance
|
||||||
edition.save()
|
)
|
||||||
|
|
||||||
|
# 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):
|
for author in self.get_authors_from_data(edition_data):
|
||||||
edition.authors.add(author)
|
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():
|
if not edition.authors.exists() and work.authors.exists():
|
||||||
edition.authors.set(work.authors.all())
|
edition.authors.set(work.authors.all())
|
||||||
|
|
||||||
return edition
|
return edition
|
||||||
|
|
||||||
def get_or_create_author(self, remote_id):
|
def get_or_create_author(self, remote_id, instance=None):
|
||||||
"""load that author"""
|
"""load that author"""
|
||||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
if not instance:
|
||||||
if existing:
|
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||||
return existing
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
data = self.get_book_data(remote_id)
|
data = self.get_book_data(remote_id)
|
||||||
|
|
||||||
|
@ -181,7 +191,24 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# this will dedupe
|
# 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
|
@abstractmethod
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
|
|
|
@ -11,6 +11,8 @@ from .connector_manager import ConnectorException
|
||||||
class Connector(AbstractConnector):
|
class Connector(AbstractConnector):
|
||||||
"""instantiate a connector for inventaire"""
|
"""instantiate a connector for inventaire"""
|
||||||
|
|
||||||
|
generated_remote_link_field = "inventaire_id"
|
||||||
|
|
||||||
def __init__(self, identifier):
|
def __init__(self, identifier):
|
||||||
super().__init__(identifier)
|
super().__init__(identifier)
|
||||||
|
|
||||||
|
@ -210,6 +212,11 @@ class Connector(AbstractConnector):
|
||||||
return ""
|
return ""
|
||||||
return data.get("extract")
|
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"):
|
def get_language_code(options, code="en"):
|
||||||
"""when there are a bunch of translation but we need a single field"""
|
"""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):
|
class Connector(AbstractConnector):
|
||||||
"""instantiate a connector for OL"""
|
"""instantiate a connector for OL"""
|
||||||
|
|
||||||
|
generated_remote_link_field = "openlibrary_link"
|
||||||
|
|
||||||
def __init__(self, identifier):
|
def __init__(self, identifier):
|
||||||
super().__init__(identifier)
|
super().__init__(identifier)
|
||||||
|
|
||||||
|
@ -66,6 +68,7 @@ class Connector(AbstractConnector):
|
||||||
Mapping("born", remote_field="birth_date"),
|
Mapping("born", remote_field="birth_date"),
|
||||||
Mapping("died", remote_field="death_date"),
|
Mapping("died", remote_field="death_date"),
|
||||||
Mapping("bio", formatter=get_description),
|
Mapping("bio", formatter=get_description),
|
||||||
|
Mapping("isni", remote_field="remote_ids", formatter=get_isni),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_book_data(self, remote_id):
|
def get_book_data(self, remote_id):
|
||||||
|
@ -224,6 +227,13 @@ def get_languages(language_blob):
|
||||||
return langs
|
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):
|
def pick_default_edition(options):
|
||||||
"""favor physical copies with covers in english"""
|
"""favor physical copies with covers in english"""
|
||||||
if not options:
|
if not options:
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||||
from bookwyrm.models.user import FeedFilterChoices
|
from bookwyrm.models.user import FeedFilterChoices
|
||||||
|
|
||||||
|
|
||||||
|
@ -148,6 +149,17 @@ class EditUserForm(CustomForm):
|
||||||
"preferred_language",
|
"preferred_language",
|
||||||
]
|
]
|
||||||
help_texts = {f: None for f in fields}
|
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):
|
class LimitedEditUserForm(CustomForm):
|
||||||
|
@ -161,6 +173,16 @@ class LimitedEditUserForm(CustomForm):
|
||||||
"discoverable",
|
"discoverable",
|
||||||
]
|
]
|
||||||
help_texts = {f: None for f in fields}
|
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):
|
class DeleteUserForm(CustomForm):
|
||||||
|
@ -209,6 +231,51 @@ class EditionForm(CustomForm):
|
||||||
"connector",
|
"connector",
|
||||||
"search_vector",
|
"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):
|
class AuthorForm(CustomForm):
|
||||||
|
@ -226,7 +293,30 @@ class AuthorForm(CustomForm):
|
||||||
"inventaire_id",
|
"inventaire_id",
|
||||||
"librarything_key",
|
"librarything_key",
|
||||||
"goodreads_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):
|
class ImportForm(forms.Form):
|
||||||
|
@ -301,12 +391,37 @@ class SiteForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.SiteSettings
|
model = models.SiteSettings
|
||||||
exclude = []
|
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 AnnouncementForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Announcement
|
model = models.Announcement
|
||||||
exclude = ["remote_id"]
|
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):
|
class ListForm(CustomForm):
|
||||||
|
@ -331,6 +446,9 @@ class EmailBlocklistForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.EmailBlocklist
|
model = models.EmailBlocklist
|
||||||
fields = ["domain"]
|
fields = ["domain"]
|
||||||
|
widgets = {
|
||||||
|
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class IPBlocklistForm(CustomForm):
|
class IPBlocklistForm(CustomForm):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" database schema for info about authors """
|
""" database schema for info about authors """
|
||||||
|
import re
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
@ -33,6 +34,17 @@ class Author(BookDataModel):
|
||||||
)
|
)
|
||||||
bio = fields.HtmlField(null=True, blank=True)
|
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):
|
def get_remote_id(self):
|
||||||
"""editions and works both use "book" instead of model_name"""
|
"""editions and works both use "book" instead of model_name"""
|
||||||
return f"https://{DOMAIN}/author/{self.id}"
|
return f"https://{DOMAIN}/author/{self.id}"
|
||||||
|
|
|
@ -52,6 +52,16 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||||
null=True,
|
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:
|
class Meta:
|
||||||
"""can't initialize this model, that wouldn't make sense"""
|
"""can't initialize this model, that wouldn't make sense"""
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,28 @@ def get_font(font_name, size=28):
|
||||||
return font
|
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):
|
def generate_texts_layer(texts, content_width):
|
||||||
"""Adds text for images"""
|
"""Adds text for images"""
|
||||||
font_text_zero = get_font("bold", size=20)
|
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"]:
|
if "text_zero" in texts and texts["text_zero"]:
|
||||||
# Text one (Book title)
|
# 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(
|
text_layer_draw.multiline_text(
|
||||||
(0, text_y), text_zero, font=font_text_zero, fill=TEXT_COLOR
|
(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"]:
|
if "text_one" in texts and texts["text_one"]:
|
||||||
# Text one (Book title)
|
# 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(
|
text_layer_draw.multiline_text(
|
||||||
(0, text_y), text_one, font=font_text_one, fill=TEXT_COLOR
|
(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"]:
|
if "text_two" in texts and texts["text_two"]:
|
||||||
# Text one (Book subtitle)
|
# 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(
|
text_layer_draw.multiline_text(
|
||||||
(0, text_y), text_two, font=font_text_two, fill=TEXT_COLOR
|
(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"]:
|
if "text_three" in texts and texts["text_three"]:
|
||||||
# Text three (Book authors)
|
# 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(
|
text_layer_draw.multiline_text(
|
||||||
(0, text_y), text_three, font=font_text_three, fill=TEXT_COLOR
|
(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)
|
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||||
|
|
||||||
JS_CACHE = "3eb4edb1"
|
JS_CACHE = "3891b373"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
|
|
@ -119,6 +119,34 @@ input[type=file]::file-selector-button:hover {
|
||||||
color: #363636;
|
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
|
/** 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="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="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="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>
|
</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-face {
|
||||||
font-family: 'icomoon';
|
font-family: 'icomoon';
|
||||||
src: url('../fonts/icomoon.eot?36x4a3');
|
src: url('../fonts/icomoon.eot?r7jc98');
|
||||||
src: url('../fonts/icomoon.eot?36x4a3#iefix') format('embedded-opentype'),
|
src: url('../fonts/icomoon.eot?r7jc98#iefix') format('embedded-opentype'),
|
||||||
url('../fonts/icomoon.ttf?36x4a3') format('truetype'),
|
url('../fonts/icomoon.ttf?r7jc98') format('truetype'),
|
||||||
url('../fonts/icomoon.woff?36x4a3') format('woff'),
|
url('../fonts/icomoon.woff?r7jc98') format('woff'),
|
||||||
url('../fonts/icomoon.svg?36x4a3#icomoon') format('svg');
|
url('../fonts/icomoon.svg?r7jc98#icomoon') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: block;
|
font-display: block;
|
||||||
|
@ -142,3 +142,6 @@
|
||||||
.icon-spinner:before {
|
.icon-spinner:before {
|
||||||
content: "\e97a";
|
content: "\e97a";
|
||||||
}
|
}
|
||||||
|
.icon-download:before {
|
||||||
|
content: "\ea36";
|
||||||
|
}
|
||||||
|
|
|
@ -414,6 +414,21 @@ let BookWyrm = new class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ) {
|
duplicateInput (event ) {
|
||||||
const trigger = event.currentTarget;
|
const trigger = event.currentTarget;
|
||||||
const input_id = trigger.dataset['duplicate']
|
const input_id = trigger.dataset['duplicate']
|
||||||
|
|
|
@ -210,10 +210,10 @@ let StatusCache = new class {
|
||||||
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
||||||
|
|
||||||
// Close menu
|
// Close menu
|
||||||
let menu = button.querySelector(".dropdown-trigger[aria-expanded=true]");
|
let menu = button.querySelector("details[open]");
|
||||||
|
|
||||||
if (menu) {
|
if (menu) {
|
||||||
menu.click();
|
menu.removeAttribute("open");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||||
<div class="column is-narrow">
|
<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="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
|
||||||
<span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
|
<span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -23,102 +23,130 @@
|
||||||
</div>
|
</div>
|
||||||
</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 }}">
|
<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="column is-two-fifths">
|
||||||
<div class="box py-2">
|
{% if details %}
|
||||||
<dl>
|
<section class="block content">
|
||||||
|
<h2 class="title is-4">{% trans "Author details" %}</h2>
|
||||||
|
<dl class="box">
|
||||||
{% if author.aliases %}
|
{% 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>
|
<dt class="has-text-weight-bold mr-1">{% trans "Aliases:" %}</dt>
|
||||||
{% for alias in author.aliases %}
|
<dd>
|
||||||
<dd itemprop="alternateName" content="{{alias}}">
|
{% include "snippets/trimmed_list.html" with items=author.aliases itemprop="alternateName" %}
|
||||||
{{alias}}{% if not forloop.last %}, {% endif %}
|
</dd>
|
||||||
</dd>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if author.born %}
|
{% 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>
|
<dt class="has-text-weight-bold mr-1">{% trans "Born:" %}</dt>
|
||||||
<dd itemprop="birthDate">{{ author.born|naturalday }}</dd>
|
<dd itemprop="birthDate">{{ author.born|naturalday }}</dd>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if author.died %}
|
{% 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>
|
<dt class="has-text-weight-bold mr-1">{% trans "Died:" %}</dt>
|
||||||
<dd itemprop="deathDate">{{ author.died|naturalday }}</dd>
|
<dd itemprop="deathDate">{{ author.died|naturalday }}</dd>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if author.wikipedia_link %}
|
{% if links %}
|
||||||
<p class="my-1">
|
<section>
|
||||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
|
<h2 class="title is-4">{% trans "External links" %}</h2>
|
||||||
{% trans "Wikipedia" %}
|
<div class="box">
|
||||||
</a>
|
{% if author.wikipedia_link %}
|
||||||
</p>
|
<div>
|
||||||
{% endif %}
|
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
|
||||||
|
{% trans "Wikipedia" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if author.isni %}
|
{% if author.isni %}
|
||||||
<p class="my-1">
|
<div class="mt-1">
|
||||||
<a itemprop="sameAs" href="https://isni.org/isni/{{ author.isni|remove_spaces }}" rel="noopener" target="_blank">
|
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener" target="_blank">
|
||||||
{% trans "View ISNI record" %}
|
{% trans "View ISNI record" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if author.openlibrary_key %}
|
{% trans "Load data" as button_text %}
|
||||||
<p class="my-1">
|
{% if author.openlibrary_key %}
|
||||||
<a itemprop="sameAs" href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener">
|
<div class="mt-1 is-flex">
|
||||||
{% trans "View on OpenLibrary" %}
|
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener">
|
||||||
</a>
|
{% trans "View on OpenLibrary" %}
|
||||||
</p>
|
</a>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if author.inventaire_id %}
|
||||||
<p class="my-1">
|
<div class="mt-1 is-flex">
|
||||||
<a itemprop="sameAs" href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener">
|
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener">
|
||||||
{% trans "View on Inventaire" %}
|
{% trans "View on Inventaire" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if author.librarything_key %}
|
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||||
<p class="my-1">
|
{% with controls_text="iv_sync" controls_uid=author.id %}
|
||||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
|
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
||||||
{% trans "View on LibraryThing" %}
|
{% include "author/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
||||||
</a>
|
{% endwith %}
|
||||||
</p>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if author.goodreads_key %}
|
{% if author.librarything_key %}
|
||||||
<p class="my-1">
|
<div class="mt-1">
|
||||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
|
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
|
||||||
{% trans "View on Goodreads" %}
|
{% trans "View on LibraryThing" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
</div>
|
{% if author.goodreads_key %}
|
||||||
{% endif %}
|
<div>
|
||||||
<div class="column">
|
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
|
||||||
{% if author.bio %}
|
{% trans "View on Goodreads" %}
|
||||||
{{ author.bio|to_markdown|safe }}
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr aria-hidden="true">
|
||||||
|
|
||||||
<div class="block">
|
<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">
|
<div class="columns is-multiline is-mobile">
|
||||||
{% for book in books %}
|
{% for book in books %}
|
||||||
<div class="column is-one-fifth">
|
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
|
||||||
{% include 'landing/small-book.html' with book=book %}
|
<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 %}
|
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -34,47 +34,41 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_name">{% trans "Name:" %}</label>
|
<label class="label" for="id_name">{% trans "Name:" %}</label>
|
||||||
{{ form.name }}
|
{{ form.name }}
|
||||||
{% for error in form.name.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_aliases">{% trans "Aliases:" %}</label>
|
<label class="label" for="id_aliases">{% trans "Aliases:" %}</label>
|
||||||
{{ form.aliases }}
|
{{ form.aliases }}
|
||||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||||
{% for error in form.aliases.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.aliases.errors id="desc_aliases" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_bio">{% trans "Bio:" %}</label>
|
<label class="label" for="id_bio">{% trans "Bio:" %}</label>
|
||||||
{{ form.bio }}
|
{{ form.bio }}
|
||||||
{% for error in form.bio.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.bio.errors id="desc_bio" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="field"><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
|
<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>
|
{% include 'snippets/form_errors.html' with errors_list=form.wikipedia_link.errors id="desc_wikipedia_link" %}
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_born">{% trans "Birth date:" %}</label>
|
<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">
|
<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>
|
{% include 'snippets/form_errors.html' with errors_list=form.born.errors id="desc_born" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_died">{% trans "Death date:" %}</label>
|
<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">
|
<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>
|
{% include 'snippets/form_errors.html' with errors_list=form.died.errors id="desc_died" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
@ -82,33 +76,36 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label>
|
<label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label>
|
||||||
{{ form.openlibrary_key }}
|
{{ form.openlibrary_key }}
|
||||||
{% for error in form.openlibrary_key.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.oepnlibrary_key.errors id="desc_oepnlibrary_key" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<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 }}
|
{{ form.inventaire_id }}
|
||||||
{% for error in form.inventaire_id.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label>
|
<label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label>
|
||||||
{{ form.librarything_key }}
|
{{ form.librarything_key }}
|
||||||
{% for error in form.librarything_key.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.librarything_key.errors id="desc_librarything_key" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label>
|
<label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label>
|
||||||
{{ form.goodreads_key }}
|
{{ form.goodreads_key }}
|
||||||
{% for error in form.goodreads_key.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %}
|
||||||
{% endfor %}
|
</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>
|
||||||
|
|
||||||
</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>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
{% trans "Load data" as button_text %}
|
||||||
{% if book.openlibrary_key %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if book.inventaire_id %}
|
{% 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 %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -160,7 +177,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
{% for shelf in user_shelfbooks %}
|
{% for shelf in user_shelfbooks %}
|
||||||
<li class="box">
|
<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">
|
<div class="mb-3">
|
||||||
{% include 'snippets/shelf_selector.html' with shelf=shelf.shelf class="is-small" readthrough=readthrough %}
|
{% include 'snippets/shelf_selector.html' with shelf=shelf.shelf class="is-small" readthrough=readthrough %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,106 +12,125 @@
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
<h2 class="title is-4">
|
||||||
|
{% trans "Metadata" %}
|
||||||
|
</h2>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_title">{% trans "Title:" %}</label>
|
<label class="label" for="id_title">
|
||||||
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
|
{% trans "Title:" %}
|
||||||
{% for error in form.title.errors %}
|
</label>
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title" aria-describedby="desc_title">
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.title.errors id="desc_title" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label>
|
<label class="label" for="id_subtitle">
|
||||||
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle">
|
{% trans "Subtitle:" %}
|
||||||
{% for error in form.subtitle.errors %}
|
</label>
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle" aria-describedby="desc_subtitle">
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.subtitle.errors id="desc_subtitle" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_description">{% trans "Description:" %}</label>
|
<label class="label" for="id_description">
|
||||||
|
{% trans "Description:" %}
|
||||||
|
</label>
|
||||||
{{ form.description }}
|
{{ form.description }}
|
||||||
{% for error in form.description.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.description.errors id="desc_description" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-two-thirds">
|
<div class="column is-two-thirds">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_series">{% trans "Series:" %}</label>
|
<label class="label" for="id_series">
|
||||||
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
|
{% trans "Series:" %}
|
||||||
{% for error in form.series.errors %}
|
</label>
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}" aria-describedby="desc_series">
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.series.errors id="desc_series" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-one-third">
|
<div class="column is-one-third">
|
||||||
<div class="field">
|
<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 }}
|
{{ form.series_number }}
|
||||||
{% for error in form.series_number.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.series_number.errors id="desc_series_number" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_languages">{% trans "Languages:" %}</label>
|
<label class="label" for="id_languages">
|
||||||
|
{% trans "Languages:" %}
|
||||||
|
</label>
|
||||||
{{ form.languages }}
|
{{ form.languages }}
|
||||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
<span class="help" id="desc_languages_help">
|
||||||
{% for error in form.languages.errors %}
|
{% trans "Separate multiple values with commas." %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
</span>
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Publication" %}</h2>
|
<h2 class="title is-4">
|
||||||
|
{% trans "Publication" %}
|
||||||
|
</h2>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
<label class="label" for="id_publishers">
|
||||||
|
{% trans "Publisher:" %}
|
||||||
|
</label>
|
||||||
{{ form.publishers }}
|
{{ form.publishers }}
|
||||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
<span class="help" id="desc_publishers_help">
|
||||||
{% for error in form.publishers.errors %}
|
{% trans "Separate multiple values with commas." %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
</span>
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.publishers.errors id="desc_publishers" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label>
|
<label class="label" for="id_first_published_date">
|
||||||
<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 %}>
|
{% trans "First published date:" %}
|
||||||
{% for error in form.first_published_date.errors %}
|
</label>
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<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">
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_published_date">{% trans "Published date:" %}</label>
|
<label class="label" for="id_published_date">
|
||||||
<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 %}>
|
{% trans "Published date:" %}
|
||||||
{% for error in form.published_date.errors %}
|
</label>
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<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">
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Authors" %}</h2>
|
<h2 class="title is-4">
|
||||||
|
{% trans "Authors" %}
|
||||||
|
</h2>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
{% if book.authors.exists %}
|
{% if book.authors.exists %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% for author in book.authors.all %}
|
{% for author in book.authors.all %}
|
||||||
<div class="is-flex is-justify-content-space-between">
|
<div class="is-flex is-justify-content-space-between">
|
||||||
<label class="label mb-2">
|
<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 %}
|
{% blocktrans with name=author.name %}Remove {{ name }}{% endblocktrans %}
|
||||||
</label>
|
</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>
|
<a href="{{ author.local_path }}">{% blocktrans with name=author.name %}Author page for {{ name }}{% endblocktrans %}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -119,7 +138,9 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="field">
|
<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 %}
|
{% 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>
|
<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 %}>
|
<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">
|
<div class="column is-half">
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Cover" %}</h2>
|
<h2 class="title is-4">
|
||||||
|
{% trans "Cover" %}
|
||||||
|
</h2>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
{% if book.cover %}
|
{% if book.cover %}
|
||||||
|
@ -146,108 +169,122 @@
|
||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="field">
|
<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 }}
|
{{ form.cover }}
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_cover_url">
|
<label class="label" for="id_cover_url">
|
||||||
{% trans "Load cover from url:" %}
|
{% trans "Load cover from url:" %}
|
||||||
</label>
|
</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>
|
</div>
|
||||||
{% for error in form.cover.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.cover.errors id="desc_cover" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<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="box">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-one-third">
|
<div class="column is-one-third">
|
||||||
<div class="field">
|
<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">
|
<div class="select">
|
||||||
{{ form.physical_format }}
|
{{ form.physical_format }}
|
||||||
</div>
|
</div>
|
||||||
{% for error in form.physical_format.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="field">
|
<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 }}
|
{{ form.physical_format_detail }}
|
||||||
{% for error in form.physical_format_detail.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.physical_format_detail.errors id="desc_physical_format_detail" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_pages">{% trans "Pages:" %}</label>
|
<label class="label" for="id_pages">
|
||||||
|
{% trans "Pages:" %}
|
||||||
|
</label>
|
||||||
{{ form.pages }}
|
{{ form.pages }}
|
||||||
{% for error in form.pages.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.pages.errors id="desc_pages" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<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="box">
|
||||||
<div class="field">
|
<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 }}
|
{{ form.isbn_13 }}
|
||||||
{% for error in form.isbn_13.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.isbn_13.errors id="desc_isbn_13" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<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 }}
|
{{ form.isbn_10 }}
|
||||||
{% for error in form.isbn_10.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.isbn_10.errors id="desc_isbn_10" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<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 }}
|
{{ form.openlibrary_key }}
|
||||||
{% for error in form.openlibrary_key.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.openlibrary_key.errors id="desc_openlibrary_key" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<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 }}
|
{{ form.inventaire_id }}
|
||||||
{% for error in form.inventaire_id.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<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 }}
|
{{ form.oclc_number }}
|
||||||
{% for error in form.oclc_number.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.oclc_number.errors id="desc_oclc_number" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_asin">{% trans "ASIN:" %}</label>
|
<label class="label" for="id_asin">
|
||||||
|
{% trans "ASIN:" %}
|
||||||
|
</label>
|
||||||
{{ form.asin }}
|
{{ form.asin }}
|
||||||
{% for error in form.ASIN.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% with 0|uuid as uuid %}
|
{% with 0|uuid as uuid %}
|
||||||
<div
|
<details
|
||||||
id="menu_{{ uuid }}"
|
id="menu_{{ uuid }}"
|
||||||
class="
|
class="
|
||||||
dropdown control
|
dropdown control
|
||||||
{% if right %}is-right{% endif %}
|
{% if right %}is-right{% endif %}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<button
|
<summary
|
||||||
class="button dropdown-trigger pulldown-menu {{ class }}"
|
class="button control dropdown-trigger pulldown-menu {{ class }}"
|
||||||
type="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-controls="menu_options_{{ uuid }}"
|
|
||||||
data-controls="menu_{{ uuid }}"
|
|
||||||
>
|
>
|
||||||
{% block dropdown-trigger %}{% endblock %}
|
{% block dropdown-trigger %}{% endblock %}
|
||||||
</button>
|
</summary>
|
||||||
|
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu control">
|
||||||
<ul
|
<ul
|
||||||
id="menu_options_{{ uuid }}"
|
id="menu_options_{{ uuid }}"
|
||||||
class="dropdown-content p-0 is-clipped"
|
class="dropdown-content p-0 is-clipped"
|
||||||
|
@ -29,6 +24,6 @@
|
||||||
{% block dropdown-list %}{% endblock %}
|
{% block dropdown-list %}{% endblock %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
|
|
|
@ -6,49 +6,56 @@
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
{{ tab.name }}
|
{{ tab.name }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="tabs">
|
<div class="block is-clipped">
|
||||||
<ul>
|
<div class="is-pulled-left">
|
||||||
{% for stream in streams %}
|
<div class="tabs">
|
||||||
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
<ul>
|
||||||
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
{% for stream in streams %}
|
||||||
</li>
|
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
||||||
{% endfor %}
|
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# feed settings #}
|
{# feed settings #}
|
||||||
<details class="mb-5" {% if settings_saved %}open{% endif %}>
|
<details class="detail-pinned-button" {% if settings_saved %}open{% endif %}>
|
||||||
<summary>
|
<summary class="control">
|
||||||
<span class="has-text-weight-bold">
|
<span class="button">
|
||||||
{{ _("Feed settings") }}
|
<span class="icon icon-dots-three m-0-mobile" aria-hidden="true"></span>
|
||||||
</span>
|
<span class="is-sr-only-mobile">{{ _("Feed settings") }}</span>
|
||||||
{% if settings_saved %}
|
</span>
|
||||||
<span class="tag is-success is-light ml-2">{{ _("Saved!") }}</span>
|
</summary>
|
||||||
{% endif %}
|
<form class="notification level is-align-items-flex-end" method="post" action="/{{ tab.key }}#feed">
|
||||||
</summary>
|
{% csrf_token %}
|
||||||
<form class="level is-align-items-flex-end" method="post" action="/{{ tab.key }}#feed">
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="label mt-2 mb-1">Status types</label>
|
<span class="is-flex is-align-items-baseline">
|
||||||
{% for name, value in feed_status_types_options %}
|
<label class="label mt-2 mb-1">Status types</label>
|
||||||
<label class="mr-2">
|
{% if settings_saved %}
|
||||||
<input type="checkbox" name="feed_status_types" value="{{ name }}" {% if name in user.feed_status_types %}checked=""{% endif %}/>
|
<span class="tag is-success is-light ml-2">{{ _("Saved!") }}</span>
|
||||||
{{ value }}
|
{% endif %}
|
||||||
</label>
|
</span>
|
||||||
{% endfor %}
|
{% 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>
|
</div>
|
||||||
</div>
|
<div class="level-right control">
|
||||||
<div class="level-right control">
|
<button class="button is-small is-primary is-outlined" type="submit">
|
||||||
<button class="button is-small is-primary is-outlined" type="submit">
|
{{ _("Save settings") }}
|
||||||
{{ _("Save settings") }}
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</details>
|
||||||
</details>
|
</div>
|
||||||
|
|
||||||
{# announcements and system messages #}
|
{# announcements and system messages #}
|
||||||
{% if not activities.number > 1 %}
|
{% if not activities.number > 1 %}
|
||||||
|
|
|
@ -14,16 +14,14 @@
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
<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 %}">
|
<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>
|
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
<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>
|
<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>
|
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -31,9 +29,8 @@
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
|
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
|
||||||
{{ form.avatar }}
|
{{ form.avatar }}
|
||||||
{% for error in form.avatar.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.avatar.errors id="desc_avatar" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -65,10 +65,9 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label for="id_request_email" class="label">{% trans "Email address:" %}</label>
|
<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">
|
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email" aria-describedby="desc_request_email">
|
||||||
{% for error in request_form.email.errors %}
|
|
||||||
<p class="help is-danger">{{ error|escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=request_form.email.errors id="desc_request_email" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -26,11 +26,10 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_password_confirm">{% trans "Password:" %}</label>
|
<label class="label" for="id_password_confirm">{% trans "Password:" %}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm">
|
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm" aria-describedby="desc_password">
|
||||||
</div>
|
</div>
|
||||||
{% for error in login_form.password.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
|
@ -8,21 +8,33 @@
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h1 class="title">{% trans "Reset Password" %}</h1>
|
<h1 class="title">{% trans "Reset Password" %}</h1>
|
||||||
{% for error in errors %}
|
|
||||||
<p class="is-danger">{{ error }}</p>
|
{% if errors %}
|
||||||
{% endfor %}
|
<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 }}">
|
<form name="password-reset" method="post" action="/password-reset/{{ code }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="field">
|
<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">
|
<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>
|
</div>
|
||||||
<div class="field">
|
<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">
|
<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>
|
</div>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
|
|
|
@ -267,5 +267,6 @@
|
||||||
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
<div class="column is-one-quarter">
|
<div class="column is-one-quarter">
|
||||||
<div class="card is-stretchable">
|
<div class="card is-stretchable">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<h4 class="card-header-title">
|
<h4 class="card-header-title is-clipped">
|
||||||
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
<a href="{{ list.local_path }}" class="is-clipped">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||||
</h4>
|
</h4>
|
||||||
{% if request.user.is_authenticated and request.user|saved:list %}
|
{% if request.user.is_authenticated and request.user|saved:list %}
|
||||||
<div class="card-header-icon">
|
<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 %}
|
{% csrf_token %}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_password">{% trans "Confirm password:" %}</label>
|
<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>
|
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||||
{% for error in form.password.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -33,31 +33,27 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="column">
|
<div class="column">
|
||||||
{{ form.avatar }}
|
{{ form.avatar }}
|
||||||
{% for error in form.avatar.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.avatar.errors id="desc_avatar" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
||||||
{{ form.name }}
|
{{ form.name }}
|
||||||
{% for error in form.name.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
||||||
{{ form.summary }}
|
{{ form.summary }}
|
||||||
{% for error in form.summary.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_email">{% trans "Email address:" %}</label>
|
<label class="label" for="id_email">{% trans "Email address:" %}</label>
|
||||||
{{ form.email }}
|
{{ form.email }}
|
||||||
{% for error in form.email.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.email.errors id="desc_email" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -69,19 +65,23 @@
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="checkbox label" for="id_show_goal">
|
<label class="checkbox label" for="id_show_goal">
|
||||||
{% trans "Show reading goal prompt in feed:" %}
|
|
||||||
{{ form.show_goal }}
|
{{ form.show_goal }}
|
||||||
|
{% trans "Show reading goal prompt in feed" %}
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
<label class="checkbox label" for="id_show_suggested_users">
|
<label class="checkbox label" for="id_show_suggested_users">
|
||||||
{% trans "Show suggested users:" %}
|
|
||||||
{{ form.show_suggested_users }}
|
{{ form.show_suggested_users }}
|
||||||
|
{% trans "Show suggested users" %}
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
<label class="checkbox label" for="id_discoverable">
|
<label class="checkbox label" for="id_discoverable">
|
||||||
{% trans "Show this account in suggested users:" %}
|
|
||||||
{{ form.discoverable }}
|
{{ form.discoverable }}
|
||||||
|
{% trans "Show this account in suggested users" %}
|
||||||
</label>
|
</label>
|
||||||
{% url 'directory' as path %}
|
{% 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 %}
|
{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,8 +107,8 @@
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="checkbox label" for="id_manually_approves_followers">
|
<label class="checkbox label" for="id_manually_approves_followers">
|
||||||
{% trans "Manually approve followers:" %}
|
|
||||||
{{ form.manually_approves_followers }}
|
{{ form.manually_approves_followers }}
|
||||||
|
{% trans "Manually approve followers" %}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
|
@ -13,60 +13,68 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_preview">{% trans "Preview:" %}</label>
|
<label class="label" for="id_preview">
|
||||||
|
{% trans "Preview:" %}
|
||||||
|
</label>
|
||||||
{{ form.preview }}
|
{{ form.preview }}
|
||||||
{% for error in form.preview.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.preview.errors id="desc_preview" %}
|
||||||
{% endfor %}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_content">{% trans "Content:" %}</label>
|
<label class="label" for="id_content">
|
||||||
|
{% trans "Content:" %}
|
||||||
|
</label>
|
||||||
{{ form.content }}
|
{{ form.content }}
|
||||||
{% for error in form.content.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.content.errors id="desc_content" %}
|
||||||
{% endfor %}
|
|
||||||
</p>
|
</p>
|
||||||
<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">
|
<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>
|
{% include 'snippets/form_errors.html' with errors_list=form.event_date.errors id="desc_event_date" %}
|
||||||
{% endfor %}
|
|
||||||
</p>
|
</p>
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<p>
|
<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">
|
<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>
|
{% include 'snippets/form_errors.html' with errors_list=form.start_date.errors id="desc_start_date" %}
|
||||||
{% endfor %}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<p>
|
<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' }}">
|
<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>
|
{% include 'snippets/form_errors.html' with errors_list=form.end_date.errors id="desc_end_date" %}
|
||||||
{% endfor %}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_active">{% trans "Active:" %}</label>
|
<label class="label" for="id_active">
|
||||||
|
{% trans "Active:" %}
|
||||||
|
</label>
|
||||||
{{ form.active }}
|
{{ form.active }}
|
||||||
{% for error in form.active.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.active.errors id="desc_active" %}
|
||||||
{% endfor %}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -17,10 +17,8 @@
|
||||||
{{ form.domain }}
|
{{ form.domain }}
|
||||||
</div>
|
</div>
|
||||||
</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="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
|
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
|
||||||
|
|
|
@ -27,11 +27,12 @@
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_server_name">{% trans "Instance:" %}</label>
|
<label class="label" for="id_server_name">
|
||||||
<input type="text" name="server_name" maxlength="255" class="input" required id="id_server_name" value="{{ form.server_name.value|default:'' }}" placeholder="domain.com">
|
{% trans "Instance:" %}
|
||||||
{% for error in form.server_name.errors %}
|
</label>
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<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">
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.server_name.errors id="desc_server_name" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
|
@ -49,29 +50,37 @@
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_application_type">{% trans "Software:" %}</label>
|
<label class="label" for="id_application_type">
|
||||||
<input type="text" name="application_type" maxlength="255" class="input" id="id_application_type" value="{{ form.application_type.value|default:'' }}">
|
{% trans "Software:" %}
|
||||||
{% for error in form.application_type.errors %}
|
</label>
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<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">
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.application_type.errors id="desc_application_type" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_application_version">{% trans "Version:" %}</label>
|
<label class="label" for="id_application_version">
|
||||||
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}">
|
{% trans "Version:" %}
|
||||||
{% for error in form.application_version.errors %}
|
</label>
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<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">
|
||||||
{% endfor %}
|
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.application_version.errors id="desc_application_version" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_notes">{% trans "Notes:" %}</label>
|
<label class="label" for="id_notes">
|
||||||
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea>
|
{% trans "Notes:" %}
|
||||||
|
</label>
|
||||||
|
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">
|
||||||
|
{{ form.notes.value|default:'' }}
|
||||||
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
<button type="submit" class="button is-primary">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -20,16 +20,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<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>
|
</div>
|
||||||
|
|
||||||
{% for error in form.address.errors %}
|
{% include 'snippets/form_errors.html' with errors_list=form.address.errors id="desc_address" %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -33,8 +33,8 @@
|
||||||
{{ site_form.instance_description }}
|
{{ site_form.instance_description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label>
|
<label class="label mb-0" for="id_instance_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>
|
<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 }}
|
{{ site_form.instance_short_description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -114,7 +114,7 @@
|
||||||
{{ site_form.require_confirm_email }}
|
{{ site_form.require_confirm_email }}
|
||||||
{% trans "Require users to confirm email address" %}
|
{% trans "Require users to confirm email address" %}
|
||||||
</label>
|
</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>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||||
|
@ -123,9 +123,8 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
||||||
{{ site_form.invite_request_text }}
|
{{ site_form.invite_request_text }}
|
||||||
{% for error in site_form.invite_request_text.errors %}
|
|
||||||
<p class="help is-danger">{{ error|escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -15,10 +15,9 @@
|
||||||
</p>
|
</p>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_password">{% trans "Your password:" %}</label>
|
<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>
|
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||||
{% for error in form.password.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -50,18 +50,23 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% with group=user.groups.first %}
|
{% with group=user.groups.first %}
|
||||||
<div class="select">
|
<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 %}
|
{% 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 %}
|
{% endfor %}
|
||||||
<option value="" {% if not group %}selected{% endif %}>User</option>
|
<option value="" {% if not group %}selected{% endif %}>
|
||||||
|
User
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{% for error in group_form.groups.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
|
||||||
{% endfor %}
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<button class="button">{% trans "Save" %}</button>
|
<button class="button">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
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">
|
<div class="field">
|
||||||
<label class="label" for="id_localname_register">{% trans "Username:" %}</label>
|
<label class="label" for="id_localname_register">{% trans "Username:" %}</label>
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
{% for error in register_form.localname.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
|
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
|
||||||
<div class="control">
|
<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>
|
<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">
|
||||||
{% for error in register_form.email.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=register_form.email.errors id="desc_email_register" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_password_register">{% trans "Password:" %}</label>
|
<label class="label" for="id_password_register">{% trans "Password:" %}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_register">
|
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_register" aria-describedby="desc_password_register">
|
||||||
{% for error in register_form.password.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
{% include 'snippets/form_errors.html' with errors_list=register_form.password.errors id="desc_password_register" %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -86,11 +86,11 @@
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% 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 %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% 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 %}
|
{% if not is_self and request.user.is_authenticated %}
|
||||||
{% include 'snippets/follow_button.html' with user=user %}
|
{% include 'snippets/follow_button.html' with user=user %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not is_self %}
|
||||||
|
{% include 'ostatus/remote_follow_button.html' with user=user %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if is_self and user.follower_requests.all %}
|
{% if is_self and user.follower_requests.all %}
|
||||||
<div class="follow-requests">
|
<div class="follow-requests">
|
||||||
|
|
|
@ -5,7 +5,6 @@ from uuid import uuid4
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.template.defaultfilters import stringfilter
|
|
||||||
from django.templatetags.static import static
|
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>'
|
f'<input type="text" name="isni-for-{author.id}" value="{isni}" hidden>'
|
||||||
)
|
)
|
||||||
return ""
|
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):
|
class TestConnector(abstract_connector.AbstractConnector):
|
||||||
"""nothing added here"""
|
"""nothing added here"""
|
||||||
|
|
||||||
|
generated_remote_link_field = "openlibrary_link"
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
return search_result
|
return search_result
|
||||||
|
|
||||||
|
@ -87,9 +89,7 @@ class AbstractConnector(TestCase):
|
||||||
def test_get_or_create_book_existing(self):
|
def test_get_or_create_book_existing(self):
|
||||||
"""find an existing book by remote/origin id"""
|
"""find an existing book by remote/origin id"""
|
||||||
self.assertEqual(models.Book.objects.count(), 1)
|
self.assertEqual(models.Book.objects.count(), 1)
|
||||||
self.assertEqual(
|
self.assertEqual(self.book.remote_id, f"https://{DOMAIN}/book/{self.book.id}")
|
||||||
self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id)
|
|
||||||
)
|
|
||||||
self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
|
self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
|
||||||
|
|
||||||
# dedupe by origin id
|
# dedupe by origin id
|
||||||
|
@ -99,7 +99,7 @@ class AbstractConnector(TestCase):
|
||||||
|
|
||||||
# dedupe by remote id
|
# dedupe by remote id
|
||||||
result = self.connector.get_or_create_book(
|
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(models.Book.objects.count(), 1)
|
||||||
self.assertEqual(result, self.book)
|
self.assertEqual(result, self.book)
|
||||||
|
@ -119,7 +119,8 @@ class AbstractConnector(TestCase):
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_get_or_create_author(self):
|
def test_get_or_create_author(self):
|
||||||
"""load an author"""
|
"""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("id"),
|
||||||
Mapping("name"),
|
Mapping("name"),
|
||||||
]
|
]
|
||||||
|
@ -139,3 +140,26 @@ class AbstractConnector(TestCase):
|
||||||
author = models.Author.objects.create(name="Test Author")
|
author = models.Author.objects.create(name="Test Author")
|
||||||
result = self.connector.get_or_create_author(author.remote_id)
|
result = self.connector.get_or_create_author(author.remote_id)
|
||||||
self.assertEqual(author, result)
|
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"})
|
extract = self.connector.get_description({"enwiki": "test_path"})
|
||||||
self.assertEqual(extract, "hi hi")
|
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",
|
"type": "/type/datetime",
|
||||||
"value": "2008-08-31 10:09:33.413686",
|
"value": "2008-08-31 10:09:33.413686",
|
||||||
},
|
},
|
||||||
|
"remote_ids": {
|
||||||
|
"isni": "000111",
|
||||||
|
},
|
||||||
"key": "/authors/OL453734A",
|
"key": "/authors/OL453734A",
|
||||||
"type": {"key": "/type/author"},
|
"type": {"key": "/type/author"},
|
||||||
"id": 1259965,
|
"id": 1259965,
|
||||||
|
@ -110,6 +113,7 @@ class Openlibrary(TestCase):
|
||||||
self.assertIsInstance(result, models.Author)
|
self.assertIsInstance(result, models.Author)
|
||||||
self.assertEqual(result.name, "George Elliott")
|
self.assertEqual(result.name, "George Elliott")
|
||||||
self.assertEqual(result.openlibrary_key, "OL453734A")
|
self.assertEqual(result.openlibrary_key, "OL453734A")
|
||||||
|
self.assertEqual(result.isni, "000111")
|
||||||
|
|
||||||
def test_get_cover_url(self):
|
def test_get_cover_url(self):
|
||||||
"""formats a url that should contain the cover image"""
|
"""formats a url that should contain the cover image"""
|
||||||
|
|
|
@ -209,6 +209,28 @@ class BookViews(TestCase):
|
||||||
self.assertEqual(self.book.description, "new description hi")
|
self.assertEqual(self.book.description, "new description hi")
|
||||||
self.assertEqual(self.book.last_edited_by, self.local_user)
|
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 _setup_cover_url():
|
def _setup_cover_url():
|
||||||
"""creates cover url mock"""
|
"""creates cover url mock"""
|
||||||
|
|
|
@ -148,3 +148,26 @@ class AuthorViews(TestCase):
|
||||||
self.assertEqual(author.name, "Test Author")
|
self.assertEqual(author.name, "Test Author")
|
||||||
validate_html(resp.render())
|
validate_html(resp.render())
|
||||||
self.assertEqual(resp.status_code, 200)
|
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.auth.models import Group, Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.tests.validate_html import validate_html
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
|
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
|
||||||
|
@ -16,6 +18,7 @@ class FollowViews(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""we need basic test data and mocks"""
|
"""we need basic test data and mocks"""
|
||||||
|
models.SiteSettings.objects.create()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||||
|
@ -174,3 +177,43 @@ class FollowViews(TestCase):
|
||||||
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
||||||
# follow relationship should not exist
|
# follow relationship should not exist
|
||||||
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)
|
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)
|
||||||
|
|
|
@ -44,6 +44,7 @@ urlpatterns = [
|
||||||
re_path(r"^api/v1/instance/?$", views.instance_info),
|
re_path(r"^api/v1/instance/?$", views.instance_info),
|
||||||
re_path(r"^api/v1/instance/peers/?$", views.peers),
|
re_path(r"^api/v1/instance/peers/?$", views.peers),
|
||||||
re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"),
|
re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"),
|
||||||
|
re_path(r"^ostatus_subscribe/?$", views.ostatus_follow_request),
|
||||||
# polling updates
|
# polling updates
|
||||||
re_path("^api/updates/notifications/?$", views.get_notification_count),
|
re_path("^api/updates/notifications/?$", views.get_notification_count),
|
||||||
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
|
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
|
||||||
|
@ -427,13 +428,29 @@ urlpatterns = [
|
||||||
r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover"
|
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"^add-description/(?P<book_id>\d+)/?$", views.add_description),
|
||||||
re_path(r"^resolve-book/?$", views.resolve_book),
|
re_path(r"^resolve-book/?$", views.resolve_book, name="resolve-book"),
|
||||||
re_path(r"^switch-edition/?$", views.switch_edition),
|
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
|
# isbn
|
||||||
re_path(r"^isbn/(?P<isbn>\d+)(.json)?/?$", views.Isbn.as_view()),
|
re_path(r"^isbn/(?P<isbn>\d+)(.json)?/?$", views.Isbn.as_view()),
|
||||||
# author
|
# author
|
||||||
re_path(r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view()),
|
re_path(
|
||||||
re_path(r"^author/(?P<author_id>\d+)/edit/?$", views.EditAuthor.as_view()),
|
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
|
# reading progress
|
||||||
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
|
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
|
||||||
re_path(r"^delete-readthrough/?$", views.delete_readthrough),
|
re_path(r"^delete-readthrough/?$", views.delete_readthrough),
|
||||||
|
@ -455,4 +472,9 @@ urlpatterns = [
|
||||||
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
|
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
|
||||||
re_path(r"^accept-follow-request/?$", views.accept_follow_request),
|
re_path(r"^accept-follow-request/?$", views.accept_follow_request),
|
||||||
re_path(r"^delete-follow-request/?$", views.delete_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)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
|
@ -104,12 +104,14 @@ def find_authors_by_name(name_string, description=False):
|
||||||
# otherwise just grab the first title listing
|
# otherwise just grab the first title listing
|
||||||
titles.append(element.find(".//title"))
|
titles.append(element.find(".//title"))
|
||||||
|
|
||||||
if titles is not None:
|
if titles:
|
||||||
# some of the "titles" in ISNI are a little ...iffy
|
# some of the "titles" in ISNI are a little ...iffy
|
||||||
# '@' is used by ISNI/OCLC to index the starting point ignoring stop words
|
# '@' is used by ISNI/OCLC to index the starting point ignoring stop words
|
||||||
# (e.g. "The @Government of no one")
|
# (e.g. "The @Government of no one")
|
||||||
title_elements = [
|
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):
|
if len(title_elements):
|
||||||
author.bio = title_elements[0].text.replace("@", "")
|
author.bio = title_elements[0].text.replace("@", "")
|
||||||
|
|
|
@ -29,6 +29,7 @@ from .preferences.block import Block, unblock
|
||||||
|
|
||||||
# books
|
# books
|
||||||
from .books.books import Book, upload_cover, add_description, resolve_book
|
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.edit_book import EditBook, ConfirmEditBook
|
||||||
from .books.editions import Editions, switch_edition
|
from .books.editions import Editions, switch_edition
|
||||||
|
|
||||||
|
@ -54,11 +55,18 @@ from .imports.manually_review import (
|
||||||
)
|
)
|
||||||
|
|
||||||
# misc views
|
# misc views
|
||||||
from .author import Author, EditAuthor
|
from .author import Author, EditAuthor, update_author_from_remote
|
||||||
from .directory import Directory
|
from .directory import Directory
|
||||||
from .discover import Discover
|
from .discover import Discover
|
||||||
from .feed import DirectMessage, Feed, Replies, Status
|
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 .follow import accept_follow_request, delete_follow_request
|
||||||
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
||||||
from .goal import Goal, hide_goal
|
from .goal import Goal, hide_goal
|
||||||
|
|
|
@ -6,9 +6,11 @@ from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.connectors import connector_manager
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
from bookwyrm.views.helpers import is_api_request
|
from bookwyrm.views.helpers import is_api_request
|
||||||
|
|
||||||
|
@ -73,3 +75,19 @@ class EditAuthor(View):
|
||||||
author = form.save()
|
author = form.save()
|
||||||
|
|
||||||
return redirect(f"/author/{author.id}")
|
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)
|
book = connector.get_or_create_book(remote_id)
|
||||||
|
|
||||||
return redirect("book", book.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)
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
""" views for actions you can take in the application """
|
""" views for actions you can take in the application """
|
||||||
|
import urllib.parse
|
||||||
|
import re
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from .helpers import get_user_from_username
|
from .helpers import (
|
||||||
|
get_user_from_username,
|
||||||
|
handle_remote_webfinger,
|
||||||
|
subscribe_remote_webfinger,
|
||||||
|
WebFingerError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -23,6 +31,9 @@ def follow(request):
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if request.GET.get("next"):
|
||||||
|
return redirect(request.GET.get("next", "/"))
|
||||||
|
|
||||||
return redirect(to_follow.local_path)
|
return redirect(to_follow.local_path)
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,3 +95,91 @@ def delete_follow_request(request):
|
||||||
|
|
||||||
follow_request.delete()
|
follow_request.delete()
|
||||||
return redirect(f"/user/{request.user.localname}")
|
return redirect(f"/user/{request.user.localname}")
|
||||||
|
|
||||||
|
|
||||||
|
def ostatus_follow_request(request):
|
||||||
|
"""prepare an outgoing remote follow request"""
|
||||||
|
uri = urllib.parse.unquote(request.GET.get("acct"))
|
||||||
|
username_parts = re.search(
|
||||||
|
r"(?:^http(?:s?):\/\/)([\w\-\.]*)(?:.)*(?:(?:\/)([\w]*))", uri
|
||||||
|
)
|
||||||
|
account = f"{username_parts[2]}@{username_parts[1]}"
|
||||||
|
user = handle_remote_webfinger(account)
|
||||||
|
error = None
|
||||||
|
|
||||||
|
if user is None or user == "":
|
||||||
|
error = "ostatus_subscribe"
|
||||||
|
|
||||||
|
# don't do these checks for AnonymousUser before they sign in
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
|
||||||
|
# you have blocked them so you probably don't want to follow
|
||||||
|
if hasattr(request.user, "blocks") and user in request.user.blocks.all():
|
||||||
|
error = "is_blocked"
|
||||||
|
# they have blocked you
|
||||||
|
if hasattr(user, "blocks") and request.user in user.blocks.all():
|
||||||
|
error = "has_blocked"
|
||||||
|
# you're already following them
|
||||||
|
if hasattr(user, "followers") and request.user in user.followers.all():
|
||||||
|
error = "already_following"
|
||||||
|
# you're not following yet but you already asked
|
||||||
|
if (
|
||||||
|
hasattr(user, "follower_requests")
|
||||||
|
and request.user in user.follower_requests.all()
|
||||||
|
):
|
||||||
|
error = "already_requested"
|
||||||
|
|
||||||
|
data = {"account": account, "user": user, "error": error}
|
||||||
|
|
||||||
|
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def ostatus_follow_success(request):
|
||||||
|
"""display success message for remote follow"""
|
||||||
|
user = get_user_from_username(request.user, request.GET.get("following"))
|
||||||
|
data = {"account": user.name, "user": user, "error": None}
|
||||||
|
return TemplateResponse(request, "ostatus/success.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
def remote_follow_page(request):
|
||||||
|
"""display remote follow page"""
|
||||||
|
user = get_user_from_username(request.user, request.GET.get("user"))
|
||||||
|
data = {"user": user}
|
||||||
|
return TemplateResponse(request, "ostatus/remote_follow.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def remote_follow(request):
|
||||||
|
"""direct user to follow from remote account using ostatus subscribe protocol"""
|
||||||
|
remote_user = request.POST.get("remote_user")
|
||||||
|
try:
|
||||||
|
if remote_user[0] == "@":
|
||||||
|
remote_user = remote_user[1:]
|
||||||
|
remote_domain = remote_user.split("@")[1]
|
||||||
|
except (TypeError, IndexError):
|
||||||
|
remote_domain = None
|
||||||
|
|
||||||
|
wf_response = subscribe_remote_webfinger(remote_user)
|
||||||
|
user = get_object_or_404(models.User, id=request.POST.get("user"))
|
||||||
|
|
||||||
|
if wf_response is None:
|
||||||
|
data = {
|
||||||
|
"account": remote_user,
|
||||||
|
"user": user,
|
||||||
|
"error": "not_supported",
|
||||||
|
"remote_domain": remote_domain,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||||
|
|
||||||
|
if isinstance(wf_response, WebFingerError):
|
||||||
|
data = {
|
||||||
|
"account": remote_user,
|
||||||
|
"user": user,
|
||||||
|
"error": str(wf_response),
|
||||||
|
"remote_domain": remote_domain,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||||
|
|
||||||
|
url = wf_response.replace("{uri}", urllib.parse.quote(user.remote_id))
|
||||||
|
return redirect(url)
|
||||||
|
|
|
@ -16,6 +16,13 @@ from bookwyrm.status import create_generated_note
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unnecessary-pass
|
||||||
|
class WebFingerError(Exception):
|
||||||
|
"""empty error class for problems finding user information with webfinger"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_user_from_username(viewer, username):
|
def get_user_from_username(viewer, username):
|
||||||
"""helper function to resolve a localname or a username to a user"""
|
"""helper function to resolve a localname or a username to a user"""
|
||||||
if viewer.is_authenticated and viewer.localname == username:
|
if viewer.is_authenticated and viewer.localname == username:
|
||||||
|
@ -57,10 +64,8 @@ def handle_remote_webfinger(query):
|
||||||
# usernames could be @user@domain or user@domain
|
# usernames could be @user@domain or user@domain
|
||||||
if not query:
|
if not query:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if query[0] == "@":
|
if query[0] == "@":
|
||||||
query = query[1:]
|
query = query[1:]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
domain = query.split("@")[1]
|
domain = query.split("@")[1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
@ -86,6 +91,35 @@ def handle_remote_webfinger(query):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def subscribe_remote_webfinger(query):
|
||||||
|
"""get subscribe template from other servers"""
|
||||||
|
template = None
|
||||||
|
# usernames could be @user@domain or user@domain
|
||||||
|
if not query:
|
||||||
|
return WebFingerError("invalid_username")
|
||||||
|
|
||||||
|
if query[0] == "@":
|
||||||
|
query = query[1:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
domain = query.split("@")[1]
|
||||||
|
except IndexError:
|
||||||
|
return WebFingerError("invalid_username")
|
||||||
|
|
||||||
|
url = f"https://{domain}/.well-known/webfinger?resource=acct:{query}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = get_data(url)
|
||||||
|
except (ConnectorException, HTTPError):
|
||||||
|
return WebFingerError("user_not_found")
|
||||||
|
|
||||||
|
for link in data.get("links"):
|
||||||
|
if link.get("rel") == "http://ostatus.org/schema/1.0/subscribe":
|
||||||
|
template = link["template"]
|
||||||
|
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
def get_edition(book_id):
|
def get_edition(book_id):
|
||||||
"""look up a book in the db and return an edition"""
|
"""look up a book in the db and return an edition"""
|
||||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||||
|
|
|
@ -30,7 +30,11 @@ def webfinger(request):
|
||||||
"rel": "self",
|
"rel": "self",
|
||||||
"type": "application/activity+json",
|
"type": "application/activity+json",
|
||||||
"href": user.remote_id,
|
"href": user.remote_id,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
"template": f"https://{DOMAIN}/ostatus_subscribe?acct={{uri}}",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
29
complete_bwdev.sh
Normal file
29
complete_bwdev.sh
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#/usr/bin/env bash
|
||||||
|
# for zsh, run:
|
||||||
|
# autoload bashcompinit
|
||||||
|
# bashcompinit
|
||||||
|
complete -W "up
|
||||||
|
service_ports_web
|
||||||
|
initdb
|
||||||
|
resetdb
|
||||||
|
makemigrations
|
||||||
|
migrate
|
||||||
|
bash
|
||||||
|
shell
|
||||||
|
dbshell
|
||||||
|
restart_celery
|
||||||
|
pytest
|
||||||
|
collectstatic
|
||||||
|
makemessages
|
||||||
|
compilemessages
|
||||||
|
update_locales
|
||||||
|
build
|
||||||
|
clean
|
||||||
|
black
|
||||||
|
populate_streams
|
||||||
|
populate_suggestions
|
||||||
|
generate_thumbnails
|
||||||
|
generate_preview_images
|
||||||
|
copy_media_to_s3
|
||||||
|
set_cors_to_s3
|
||||||
|
runweb" -o bashdefault -o default bw-dev
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -17,7 +17,8 @@ server {
|
||||||
|
|
||||||
# # redirect http to https
|
# # redirect http to https
|
||||||
# return 301 https://your-domain.com$request_uri;
|
# return 301 https://your-domain.com$request_uri;
|
||||||
# }
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# server {
|
# server {
|
||||||
# listen [::]:443 ssl http2;
|
# listen [::]:443 ssl http2;
|
||||||
|
@ -54,7 +55,7 @@ server {
|
||||||
# location /static/ {
|
# location /static/ {
|
||||||
# alias /app/static/;
|
# alias /app/static/;
|
||||||
# }
|
# }
|
||||||
}
|
# }
|
||||||
|
|
||||||
# Reverse-Proxy server
|
# Reverse-Proxy server
|
||||||
# server {
|
# server {
|
||||||
|
|
Loading…
Reference in a new issue