Merge branch 'main' into list-embed
This commit is contained in:
@ -111,7 +111,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||
return existing.default_edition
|
||||
return existing
|
||||
|
||||
# load the json
|
||||
# load the json data from the remote data source
|
||||
data = self.get_book_data(remote_id)
|
||||
if self.is_work_data(data):
|
||||
try:
|
||||
@ -150,27 +150,37 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||
"""this allows connectors to override the default behavior"""
|
||||
return get_data(remote_id)
|
||||
|
||||
def create_edition_from_data(self, work, edition_data):
|
||||
def create_edition_from_data(self, work, edition_data, instance=None):
|
||||
"""if we already have the work, we're ready"""
|
||||
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
||||
mapped_data["work"] = work.remote_id
|
||||
edition_activity = activitypub.Edition(**mapped_data)
|
||||
edition = edition_activity.to_model(model=models.Edition, overwrite=False)
|
||||
edition.connector = self.connector
|
||||
edition.save()
|
||||
edition = edition_activity.to_model(
|
||||
model=models.Edition, overwrite=False, instance=instance
|
||||
)
|
||||
|
||||
# if we're updating an existing instance, we don't need to load authors
|
||||
if instance:
|
||||
return edition
|
||||
|
||||
if not edition.connector:
|
||||
edition.connector = self.connector
|
||||
edition.save(broadcast=False, update_fields=["connector"])
|
||||
|
||||
for author in self.get_authors_from_data(edition_data):
|
||||
edition.authors.add(author)
|
||||
# use the authors from the work if none are found for the edition
|
||||
if not edition.authors.exists() and work.authors.exists():
|
||||
edition.authors.set(work.authors.all())
|
||||
|
||||
return edition
|
||||
|
||||
def get_or_create_author(self, remote_id):
|
||||
def get_or_create_author(self, remote_id, instance=None):
|
||||
"""load that author"""
|
||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
return existing
|
||||
if not instance:
|
||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
data = self.get_book_data(remote_id)
|
||||
|
||||
@ -181,7 +191,24 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||
return None
|
||||
|
||||
# this will dedupe
|
||||
return activity.to_model(model=models.Author, overwrite=False)
|
||||
return activity.to_model(
|
||||
model=models.Author, overwrite=False, instance=instance
|
||||
)
|
||||
|
||||
def get_remote_id_from_model(self, obj):
|
||||
"""given the data stored, how can we look this up"""
|
||||
return getattr(obj, getattr(self, "generated_remote_link_field"))
|
||||
|
||||
def update_author_from_remote(self, obj):
|
||||
"""load the remote data from this connector and add it to an existing author"""
|
||||
remote_id = self.get_remote_id_from_model(obj)
|
||||
return self.get_or_create_author(remote_id, instance=obj)
|
||||
|
||||
def update_book_from_remote(self, obj):
|
||||
"""load the remote data from this connector and add it to an existing book"""
|
||||
remote_id = self.get_remote_id_from_model(obj)
|
||||
data = self.get_book_data(remote_id)
|
||||
return self.create_edition_from_data(obj.parent_work, data, instance=obj)
|
||||
|
||||
@abstractmethod
|
||||
def is_work_data(self, data):
|
||||
|
@ -11,6 +11,8 @@ from .connector_manager import ConnectorException
|
||||
class Connector(AbstractConnector):
|
||||
"""instantiate a connector for inventaire"""
|
||||
|
||||
generated_remote_link_field = "inventaire_id"
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
@ -210,6 +212,11 @@ class Connector(AbstractConnector):
|
||||
return ""
|
||||
return data.get("extract")
|
||||
|
||||
def get_remote_id_from_model(self, obj):
|
||||
"""use get_remote_id to figure out the link from a model obj"""
|
||||
remote_id_value = obj.inventaire_id
|
||||
return self.get_remote_id(remote_id_value)
|
||||
|
||||
|
||||
def get_language_code(options, code="en"):
|
||||
"""when there are a bunch of translation but we need a single field"""
|
||||
|
@ -12,6 +12,8 @@ from .openlibrary_languages import languages
|
||||
class Connector(AbstractConnector):
|
||||
"""instantiate a connector for OL"""
|
||||
|
||||
generated_remote_link_field = "openlibrary_link"
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
@ -66,6 +68,7 @@ class Connector(AbstractConnector):
|
||||
Mapping("born", remote_field="birth_date"),
|
||||
Mapping("died", remote_field="death_date"),
|
||||
Mapping("bio", formatter=get_description),
|
||||
Mapping("isni", remote_field="remote_ids", formatter=get_isni),
|
||||
]
|
||||
|
||||
def get_book_data(self, remote_id):
|
||||
@ -224,6 +227,13 @@ def get_languages(language_blob):
|
||||
return langs
|
||||
|
||||
|
||||
def get_isni(remote_ids_blob):
|
||||
"""extract the isni from the remote id data for the author"""
|
||||
if not remote_ids_blob or not isinstance(remote_ids_blob, dict):
|
||||
return None
|
||||
return remote_ids_blob.get("isni")
|
||||
|
||||
|
||||
def pick_default_edition(options):
|
||||
"""favor physical copies with covers in english"""
|
||||
if not options:
|
||||
|
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||
from bookwyrm.models.user import FeedFilterChoices
|
||||
|
||||
|
||||
@ -148,6 +149,17 @@ class EditUserForm(CustomForm):
|
||||
"preferred_language",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"avatar": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_avatar"}
|
||||
),
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
||||
"email": forms.EmailInput(attrs={"aria-describedby": "desc_email"}),
|
||||
"discoverable": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_discoverable"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class LimitedEditUserForm(CustomForm):
|
||||
@ -161,6 +173,16 @@ class LimitedEditUserForm(CustomForm):
|
||||
"discoverable",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"avatar": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_avatar"}
|
||||
),
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
||||
"discoverable": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_discoverable"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeleteUserForm(CustomForm):
|
||||
@ -209,6 +231,51 @@ class EditionForm(CustomForm):
|
||||
"connector",
|
||||
"search_vector",
|
||||
]
|
||||
widgets = {
|
||||
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
|
||||
"subtitle": forms.TextInput(attrs={"aria-describedby": "desc_subtitle"}),
|
||||
"description": forms.Textarea(
|
||||
attrs={"aria-describedby": "desc_description"}
|
||||
),
|
||||
"series": forms.TextInput(attrs={"aria-describedby": "desc_series"}),
|
||||
"series_number": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_series_number"}
|
||||
),
|
||||
"languages": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_languages_help desc_languages"}
|
||||
),
|
||||
"publishers": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
||||
),
|
||||
"first_published_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_first_published_date"}
|
||||
),
|
||||
"published_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_published_date"}
|
||||
),
|
||||
"cover": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_cover"}
|
||||
),
|
||||
"physical_format": forms.Select(
|
||||
attrs={"aria-describedby": "desc_physical_format"}
|
||||
),
|
||||
"physical_format_detail": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_physical_format_detail"}
|
||||
),
|
||||
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
|
||||
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
|
||||
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
|
||||
"openlibrary_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_openlibrary_key"}
|
||||
),
|
||||
"inventaire_id": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
||||
),
|
||||
"oclc_number": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_oclc_number"}
|
||||
),
|
||||
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
||||
}
|
||||
|
||||
|
||||
class AuthorForm(CustomForm):
|
||||
@ -226,7 +293,30 @@ class AuthorForm(CustomForm):
|
||||
"inventaire_id",
|
||||
"librarything_key",
|
||||
"goodreads_key",
|
||||
"isni",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"aliases": forms.TextInput(attrs={"aria-describedby": "desc_aliases"}),
|
||||
"bio": forms.Textarea(attrs={"aria-describedby": "desc_bio"}),
|
||||
"wikipedia_link": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_wikipedia_link"}
|
||||
),
|
||||
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
|
||||
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
|
||||
"oepnlibrary_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_oepnlibrary_key"}
|
||||
),
|
||||
"inventaire_id": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
||||
),
|
||||
"librarything_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_librarything_key"}
|
||||
),
|
||||
"goodreads_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_goodreads_key"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ImportForm(forms.Form):
|
||||
@ -301,12 +391,37 @@ class SiteForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.SiteSettings
|
||||
exclude = []
|
||||
widgets = {
|
||||
"instance_short_description": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_instance_short_description"}
|
||||
),
|
||||
"require_confirm_email": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_require_confirm_email"}
|
||||
),
|
||||
"invite_request_text": forms.Textarea(
|
||||
attrs={"aria-describedby": "desc_invite_request_text"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class AnnouncementForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Announcement
|
||||
exclude = ["remote_id"]
|
||||
widgets = {
|
||||
"preview": forms.TextInput(attrs={"aria-describedby": "desc_preview"}),
|
||||
"content": forms.Textarea(attrs={"aria-describedby": "desc_content"}),
|
||||
"event_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_event_date"}
|
||||
),
|
||||
"start_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_start_date"}
|
||||
),
|
||||
"end_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_end_date"}
|
||||
),
|
||||
"active": forms.CheckboxInput(attrs={"aria-describedby": "desc_active"}),
|
||||
}
|
||||
|
||||
|
||||
class ListForm(CustomForm):
|
||||
@ -331,6 +446,9 @@ class EmailBlocklistForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.EmailBlocklist
|
||||
fields = ["domain"]
|
||||
widgets = {
|
||||
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
|
||||
}
|
||||
|
||||
|
||||
class IPBlocklistForm(CustomForm):
|
||||
|
@ -1,4 +1,5 @@
|
||||
""" database schema for info about authors """
|
||||
import re
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.db import models
|
||||
|
||||
@ -33,6 +34,17 @@ class Author(BookDataModel):
|
||||
)
|
||||
bio = fields.HtmlField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def isni_link(self):
|
||||
"""generate the url from the isni id"""
|
||||
clean_isni = re.sub(r"\s", "", self.isni)
|
||||
return f"https://isni.org/isni/{clean_isni}"
|
||||
|
||||
@property
|
||||
def openlibrary_link(self):
|
||||
"""generate the url from the openlibrary id"""
|
||||
return f"https://openlibrary.org/authors/{self.openlibrary_key}"
|
||||
|
||||
def get_remote_id(self):
|
||||
"""editions and works both use "book" instead of model_name"""
|
||||
return f"https://{DOMAIN}/author/{self.id}"
|
||||
|
@ -52,6 +52,16 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||
null=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def openlibrary_link(self):
|
||||
"""generate the url from the openlibrary id"""
|
||||
return f"https://openlibrary.org/books/{self.openlibrary_key}"
|
||||
|
||||
@property
|
||||
def inventaire_link(self):
|
||||
"""generate the url from the inventaire id"""
|
||||
return f"https://inventaire.io/entity/{self.inventaire_id}"
|
||||
|
||||
class Meta:
|
||||
"""can't initialize this model, that wouldn't make sense"""
|
||||
|
||||
|
@ -49,6 +49,28 @@ def get_font(font_name, size=28):
|
||||
return font
|
||||
|
||||
|
||||
def get_wrapped_text(text, font, content_width):
|
||||
"""text wrap length depends on the max width of the content"""
|
||||
|
||||
low = 0
|
||||
high = len(text)
|
||||
|
||||
try:
|
||||
# ideal length is determined via binary search
|
||||
while low < high:
|
||||
mid = math.floor(low + high)
|
||||
wrapped_text = textwrap.fill(text, width=mid)
|
||||
width = font.getsize_multiline(wrapped_text)[0]
|
||||
if width < content_width:
|
||||
low = mid
|
||||
else:
|
||||
high = mid - 1
|
||||
except AttributeError:
|
||||
wrapped_text = text
|
||||
|
||||
return wrapped_text
|
||||
|
||||
|
||||
def generate_texts_layer(texts, content_width):
|
||||
"""Adds text for images"""
|
||||
font_text_zero = get_font("bold", size=20)
|
||||
@ -63,7 +85,8 @@ def generate_texts_layer(texts, content_width):
|
||||
|
||||
if "text_zero" in texts and texts["text_zero"]:
|
||||
# Text one (Book title)
|
||||
text_zero = textwrap.fill(texts["text_zero"], width=72)
|
||||
text_zero = get_wrapped_text(texts["text_zero"], font_text_zero, content_width)
|
||||
|
||||
text_layer_draw.multiline_text(
|
||||
(0, text_y), text_zero, font=font_text_zero, fill=TEXT_COLOR
|
||||
)
|
||||
@ -75,7 +98,8 @@ def generate_texts_layer(texts, content_width):
|
||||
|
||||
if "text_one" in texts and texts["text_one"]:
|
||||
# Text one (Book title)
|
||||
text_one = textwrap.fill(texts["text_one"], width=28)
|
||||
text_one = get_wrapped_text(texts["text_one"], font_text_one, content_width)
|
||||
|
||||
text_layer_draw.multiline_text(
|
||||
(0, text_y), text_one, font=font_text_one, fill=TEXT_COLOR
|
||||
)
|
||||
@ -87,7 +111,8 @@ def generate_texts_layer(texts, content_width):
|
||||
|
||||
if "text_two" in texts and texts["text_two"]:
|
||||
# Text one (Book subtitle)
|
||||
text_two = textwrap.fill(texts["text_two"], width=36)
|
||||
text_two = get_wrapped_text(texts["text_two"], font_text_two, content_width)
|
||||
|
||||
text_layer_draw.multiline_text(
|
||||
(0, text_y), text_two, font=font_text_two, fill=TEXT_COLOR
|
||||
)
|
||||
@ -99,7 +124,10 @@ def generate_texts_layer(texts, content_width):
|
||||
|
||||
if "text_three" in texts and texts["text_three"]:
|
||||
# Text three (Book authors)
|
||||
text_three = textwrap.fill(texts["text_three"], width=36)
|
||||
text_three = get_wrapped_text(
|
||||
texts["text_three"], font_text_three, content_width
|
||||
)
|
||||
|
||||
text_layer_draw.multiline_text(
|
||||
(0, text_y), text_three, font=font_text_three, fill=TEXT_COLOR
|
||||
)
|
||||
|
@ -14,7 +14,7 @@ VERSION = "0.1.0"
|
||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "3eb4edb1"
|
||||
JS_CACHE = "3891b373"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
@ -119,6 +119,34 @@ input[type=file]::file-selector-button:hover {
|
||||
color: #363636;
|
||||
}
|
||||
|
||||
details .dropdown-menu {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
details.dropdown[open] summary.dropdown-trigger::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.detail-pinned-button summary {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.detail-pinned-button form {
|
||||
float: left;
|
||||
width: -webkit-fill-available;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/** Shelving
|
||||
******************************************************************************/
|
||||
|
||||
|
Binary file not shown.
@ -46,4 +46,5 @@
|
||||
<glyph unicode="" glyph-name="star-full" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" />
|
||||
<glyph unicode="" glyph-name="heart" d="M755.188 896c-107.63 0-200.258-87.554-243.164-179-42.938 91.444-135.578 179-243.216 179-148.382 0-268.808-120.44-268.808-268.832 0-301.846 304.5-380.994 512.022-679.418 196.154 296.576 511.978 387.206 511.978 679.418 0 148.392-120.43 268.832-268.812 268.832z" />
|
||||
<glyph unicode="" glyph-name="plus" d="M992 576h-352v352c0 17.672-14.328 32-32 32h-192c-17.672 0-32-14.328-32-32v-352h-352c-17.672 0-32-14.328-32-32v-192c0-17.672 14.328-32 32-32h352v-352c0-17.672 14.328-32 32-32h192c17.672 0 32 14.328 32 32v352h352c17.672 0 32 14.328 32 32v192c0 17.672-14.328 32-32 32z" />
|
||||
<glyph unicode="" glyph-name="download" d="M512-32l480 480h-288v512h-384v-512h-288z" />
|
||||
</font></defs></svg>
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Binary file not shown.
13
bookwyrm/static/css/vendor/icons.css
vendored
13
bookwyrm/static/css/vendor/icons.css
vendored
@ -1,10 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('../fonts/icomoon.eot?36x4a3');
|
||||
src: url('../fonts/icomoon.eot?36x4a3#iefix') format('embedded-opentype'),
|
||||
url('../fonts/icomoon.ttf?36x4a3') format('truetype'),
|
||||
url('../fonts/icomoon.woff?36x4a3') format('woff'),
|
||||
url('../fonts/icomoon.svg?36x4a3#icomoon') format('svg');
|
||||
src: url('../fonts/icomoon.eot?r7jc98');
|
||||
src: url('../fonts/icomoon.eot?r7jc98#iefix') format('embedded-opentype'),
|
||||
url('../fonts/icomoon.ttf?r7jc98') format('truetype'),
|
||||
url('../fonts/icomoon.woff?r7jc98') format('woff'),
|
||||
url('../fonts/icomoon.svg?r7jc98#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
@ -142,3 +142,6 @@
|
||||
.icon-spinner:before {
|
||||
content: "\e97a";
|
||||
}
|
||||
.icon-download:before {
|
||||
content: "\ea36";
|
||||
}
|
||||
|
@ -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 ) {
|
||||
const trigger = event.currentTarget;
|
||||
const input_id = trigger.dataset['duplicate']
|
||||
|
@ -210,10 +210,10 @@ let StatusCache = new class {
|
||||
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
||||
|
||||
// Close menu
|
||||
let menu = button.querySelector(".dropdown-trigger[aria-expanded=true]");
|
||||
let menu = button.querySelector("details[open]");
|
||||
|
||||
if (menu) {
|
||||
menu.click();
|
||||
menu.removeAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ author.local_path }}/edit">
|
||||
<a href="{% url 'edit-author' author.id %}">
|
||||
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
|
||||
<span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
|
||||
</a>
|
||||
@ -23,102 +23,130 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block columns content" itemscope itemtype="https://schema.org/Person">
|
||||
<div class="block columns is-flex-direction-row-reverse" itemscope itemtype="https://schema.org/Person">
|
||||
<meta itemprop="name" content="{{ author.name }}">
|
||||
{% if author.bio %}
|
||||
<div class="column">
|
||||
{% include "snippets/trimmed_text.html" with full=author.bio trim_length=200 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id or author.isni %}
|
||||
{% firstof author.aliases author.born author.died as details %}
|
||||
{% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni as links %}
|
||||
{% if details or links %}
|
||||
<div class="column is-two-fifths">
|
||||
<div class="box py-2">
|
||||
<dl>
|
||||
{% if details %}
|
||||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Author details" %}</h2>
|
||||
<dl class="box">
|
||||
{% if author.aliases %}
|
||||
<div class="is-flex is-flex-wrap-wrap my-1">
|
||||
<div class="is-flex is-flex-wrap-wrap mr-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Aliases:" %}</dt>
|
||||
{% for alias in author.aliases %}
|
||||
<dd itemprop="alternateName" content="{{alias}}">
|
||||
{{alias}}{% if not forloop.last %}, {% endif %}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
<dd>
|
||||
{% include "snippets/trimmed_list.html" with items=author.aliases itemprop="alternateName" %}
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.born %}
|
||||
<div class="is-flex my-1">
|
||||
<div class="is-flex mt-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Born:" %}</dt>
|
||||
<dd itemprop="birthDate">{{ author.born|naturalday }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.died %}
|
||||
<div class="is-flex my-1">
|
||||
<div class="is-flex mt-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Died:" %}</dt>
|
||||
<dd itemprop="deathDate">{{ author.died|naturalday }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if author.wikipedia_link %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
|
||||
{% trans "Wikipedia" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if links %}
|
||||
<section>
|
||||
<h2 class="title is-4">{% trans "External links" %}</h2>
|
||||
<div class="box">
|
||||
{% if author.wikipedia_link %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
|
||||
{% trans "Wikipedia" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.isni %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://isni.org/isni/{{ author.isni|remove_spaces }}" rel="noopener" target="_blank">
|
||||
{% trans "View ISNI record" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if author.isni %}
|
||||
<div class="mt-1">
|
||||
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener" target="_blank">
|
||||
{% trans "View ISNI record" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.openlibrary_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% trans "Load data" as button_text %}
|
||||
{% if author.openlibrary_key %}
|
||||
<div class="mt-1 is-flex">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener">
|
||||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="ol_sync" controls_uid=author.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_ol_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "author/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.inventaire_id %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Inventaire" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if author.inventaire_id %}
|
||||
<div class="mt-1 is-flex">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Inventaire" %}
|
||||
</a>
|
||||
|
||||
{% if author.librarything_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on LibraryThing" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="iv_sync" controls_uid=author.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "author/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.goodreads_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Goodreads" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="column">
|
||||
{% if author.bio %}
|
||||
{{ author.bio|to_markdown|safe }}
|
||||
{% if author.librarything_key %}
|
||||
<div class="mt-1">
|
||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on LibraryThing" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.goodreads_key %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Goodreads" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr aria-hidden="true">
|
||||
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h3>
|
||||
<h2 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h2>
|
||||
<div class="columns is-multiline is-mobile">
|
||||
{% for book in books %}
|
||||
<div class="column is-one-fifth">
|
||||
{% include 'landing/small-book.html' with book=book %}
|
||||
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
|
||||
<div class="is-flex-grow-1">
|
||||
{% include 'landing/small-book.html' with book=book %}
|
||||
</div>
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -34,47 +34,41 @@
|
||||
<div class="field">
|
||||
<label class="label" for="id_name">{% trans "Name:" %}</label>
|
||||
{{ form.name }}
|
||||
{% for error in form.name.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_aliases">{% trans "Aliases:" %}</label>
|
||||
{{ form.aliases }}
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
{% for error in form.aliases.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.aliases.errors id="desc_aliases" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_bio">{% trans "Bio:" %}</label>
|
||||
{{ form.bio }}
|
||||
{% for error in form.bio.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.bio.errors id="desc_bio" %}
|
||||
</div>
|
||||
|
||||
<p class="field"><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
|
||||
{% for error in form.wikipedia_link.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.wikipedia_link.errors id="desc_wikipedia_link" %}
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_born">{% trans "Birth date:" %}</label>
|
||||
<input type="date" name="born" value="{{ form.born.value|date:'Y-m-d' }}" class="input" id="id_born">
|
||||
{% for error in form.born.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.born.errors id="desc_born" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_died">{% trans "Death date:" %}</label>
|
||||
<input type="date" name="died" value="{{ form.died.value|date:'Y-m-d' }}" class="input" id="id_died">
|
||||
{% for error in form.died.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.died.errors id="desc_died" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
@ -82,33 +76,36 @@
|
||||
<div class="field">
|
||||
<label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label>
|
||||
{{ form.openlibrary_key }}
|
||||
{% for error in form.openlibrary_key.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.oepnlibrary_key.errors id="desc_oepnlibrary_key" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label>
|
||||
{{ form.inventaire_id }}
|
||||
{% for error in form.inventaire_id.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label>
|
||||
{{ form.librarything_key }}
|
||||
{% for error in form.librarything_key.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.librarything_key.errors id="desc_librarything_key" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label>
|
||||
{{ form.goodreads_key }}
|
||||
{% for error in form.goodreads_key.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_isni">{% trans "ISNI:" %}</label>
|
||||
{{ form.isni }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.isni.errors id="desc_isni" %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
30
bookwyrm/templates/author/sync_modal.html
Normal file
30
bookwyrm/templates/author/sync_modal.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Load data" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="{{ source }}-update" method="POST" action="{% url 'author-update-remote' author.id source %}">
|
||||
{% csrf_token %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Loading data will connect to <strong>{{ source_name }}</strong> and check for any metadata about this author which aren't present here. Existing metadata will not be overwritten.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">
|
||||
<span>{% trans "Confirm" %}</span>
|
||||
</button>
|
||||
|
||||
{% trans "Cancel" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
@ -90,11 +90,28 @@
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
{% trans "Load data" as button_text %}
|
||||
{% if book.openlibrary_key %}
|
||||
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
|
||||
<p>
|
||||
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="ol_sync" controls_uid=book.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_ol_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "book/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if book.inventaire_id %}
|
||||
<p><a href="https://inventaire.io/entity/{{ book.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a></p>
|
||||
<p>
|
||||
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="iv_sync" controls_uid=book.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "book/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
@ -160,7 +177,7 @@
|
||||
<ul>
|
||||
{% for shelf in user_shelfbooks %}
|
||||
<li class="box">
|
||||
{% blocktrans with path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}<a href="{{ path }}">{{ shelf_name }}</a>{% endblocktrans %}
|
||||
<a href="{{ shelf.shelf.local_path }}">{{ shelf.shelf.name }}</a>
|
||||
<div class="mb-3">
|
||||
{% include 'snippets/shelf_selector.html' with shelf=shelf.shelf class="is-small" readthrough=readthrough %}
|
||||
</div>
|
||||
|
@ -12,106 +12,125 @@
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Metadata" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_title">{% trans "Title:" %}</label>
|
||||
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
|
||||
{% for error in form.title.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_title">
|
||||
{% trans "Title:" %}
|
||||
</label>
|
||||
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title" aria-describedby="desc_title">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.title.errors id="desc_title" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label>
|
||||
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle">
|
||||
{% for error in form.subtitle.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_subtitle">
|
||||
{% trans "Subtitle:" %}
|
||||
</label>
|
||||
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle" aria-describedby="desc_subtitle">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.subtitle.errors id="desc_subtitle" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_description">{% trans "Description:" %}</label>
|
||||
<label class="label" for="id_description">
|
||||
{% trans "Description:" %}
|
||||
</label>
|
||||
{{ form.description }}
|
||||
{% for error in form.description.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.description.errors id="desc_description" %}
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<div class="field">
|
||||
<label class="label" for="id_series">{% trans "Series:" %}</label>
|
||||
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
|
||||
{% for error in form.series.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_series">
|
||||
{% trans "Series:" %}
|
||||
</label>
|
||||
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}" aria-describedby="desc_series">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.series.errors id="desc_series" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="field">
|
||||
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
|
||||
<label class="label" for="id_series_number">
|
||||
{% trans "Series number:" %}
|
||||
</label>
|
||||
{{ form.series_number }}
|
||||
{% for error in form.series_number.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.series_number.errors id="desc_series_number" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_languages">{% trans "Languages:" %}</label>
|
||||
<label class="label" for="id_languages">
|
||||
{% trans "Languages:" %}
|
||||
</label>
|
||||
{{ form.languages }}
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
{% for error in form.languages.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<span class="help" id="desc_languages_help">
|
||||
{% trans "Separate multiple values with commas." %}
|
||||
</span>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Publication" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Publication" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
||||
<label class="label" for="id_publishers">
|
||||
{% trans "Publisher:" %}
|
||||
</label>
|
||||
{{ form.publishers }}
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
{% for error in form.publishers.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<span class="help" id="desc_publishers_help">
|
||||
{% trans "Separate multiple values with commas." %}
|
||||
</span>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.publishers.errors id="desc_publishers" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label>
|
||||
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %}>
|
||||
{% for error in form.first_published_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_first_published_date">
|
||||
{% trans "First published date:" %}
|
||||
</label>
|
||||
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %} aria-describedby="desc_first_published_date">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_published_date">{% trans "Published date:" %}</label>
|
||||
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %}>
|
||||
{% for error in form.published_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_published_date">
|
||||
{% trans "Published date:" %}
|
||||
</label>
|
||||
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %} aria-describedby="desc_published_date">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Authors" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Authors" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
{% if book.authors.exists %}
|
||||
<fieldset>
|
||||
{% for author in book.authors.all %}
|
||||
<div class="is-flex is-justify-content-space-between">
|
||||
<label class="label mb-2">
|
||||
<input type="checkbox" name="remove_authors" value="{{ author.id }}" {% if author.id|stringformat:"i" in remove_authors %}checked{% endif %}>
|
||||
<input type="checkbox" name="remove_authors" value="{{ author.id }}" {% if author.id|stringformat:"i" in remove_authors %}checked{% endif %} aria-describedby="desc_remove_author_{{author.id}}">
|
||||
{% blocktrans with name=author.name %}Remove {{ name }}{% endblocktrans %}
|
||||
</label>
|
||||
<p class="help">
|
||||
<p class="help" id="desc_remove_author_{{author.id}}">
|
||||
<a href="{{ author.local_path }}">{% blocktrans with name=author.name %}Author page for {{ name }}{% endblocktrans %}</a>
|
||||
</p>
|
||||
</div>
|
||||
@ -119,7 +138,9 @@
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<div class="field">
|
||||
<label class="label">{% trans "Add Authors:" %}</label>
|
||||
<label class="label" for="id_add_author">
|
||||
{% trans "Add Authors:" %}
|
||||
</label>
|
||||
{% for author in add_author %}
|
||||
<label class="label is-sr-only" for="id_add_author{% if not forloop.first %}-{{forloop.counter}}{% endif %}">{% trans "Add Author" %}</label>
|
||||
<input class="input" type="text" name="add_author" id="id_add_author{% if not forloop.first %}-{{forloop.counter}}{% endif %}" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}>
|
||||
@ -135,7 +156,9 @@
|
||||
|
||||
<div class="column is-half">
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Cover" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Cover" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
{% if book.cover %}
|
||||
@ -146,108 +169,122 @@
|
||||
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
|
||||
<label class="label" for="id_cover">
|
||||
{% trans "Upload cover:" %}
|
||||
</label>
|
||||
{{ form.cover }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_cover_url">
|
||||
{% trans "Load cover from url:" %}
|
||||
</label>
|
||||
<input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}">
|
||||
<input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}" aria-describedby="desc_cover">
|
||||
</div>
|
||||
{% for error in form.cover.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.cover.errors id="desc_cover" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Physical Properties" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third">
|
||||
<div class="field">
|
||||
<label class="label" for="id_physical_format">{% trans "Format:" %}</label>
|
||||
<label class="label" for="id_physical_format">
|
||||
{% trans "Format:" %}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ form.physical_format }}
|
||||
</div>
|
||||
{% for error in form.physical_format.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label" for="id_physical_format_detail">{% trans "Format details:" %}</label>
|
||||
<label class="label" for="id_physical_format_detail">
|
||||
{% trans "Format details:" %}
|
||||
</label>
|
||||
{{ form.physical_format_detail }}
|
||||
{% for error in form.physical_format_detail.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.physical_format_detail.errors id="desc_physical_format_detail" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_pages">{% trans "Pages:" %}</label>
|
||||
<label class="label" for="id_pages">
|
||||
{% trans "Pages:" %}
|
||||
</label>
|
||||
{{ form.pages }}
|
||||
{% for error in form.pages.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.pages.errors id="desc_pages" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
|
||||
<h2 class="title is-4">
|
||||
{% trans "Book Identifiers" %}
|
||||
</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
|
||||
<label class="label" for="id_isbn_13">
|
||||
{% trans "ISBN 13:" %}
|
||||
</label>
|
||||
{{ form.isbn_13 }}
|
||||
{% for error in form.isbn_13.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.isbn_13.errors id="desc_isbn_13" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_isbn_10">{% trans "ISBN 10:" %}</label>
|
||||
<label class="label" for="id_isbn_10">
|
||||
{% trans "ISBN 10:" %}
|
||||
</label>
|
||||
{{ form.isbn_10 }}
|
||||
{% for error in form.isbn_10.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.isbn_10.errors id="desc_isbn_10" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_openlibrary_key">{% trans "Openlibrary ID:" %}</label>
|
||||
<label class="label" for="id_openlibrary_key">
|
||||
{% trans "Openlibrary ID:" %}
|
||||
</label>
|
||||
{{ form.openlibrary_key }}
|
||||
{% for error in form.openlibrary_key.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.openlibrary_key.errors id="desc_openlibrary_key" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label>
|
||||
<label class="label" for="id_inventaire_id">
|
||||
{% trans "Inventaire ID:" %}
|
||||
</label>
|
||||
{{ form.inventaire_id }}
|
||||
{% for error in form.inventaire_id.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_oclc_number">{% trans "OCLC Number:" %}</label>
|
||||
<label class="label" for="id_oclc_number">
|
||||
{% trans "OCLC Number:" %}
|
||||
</label>
|
||||
{{ form.oclc_number }}
|
||||
{% for error in form.oclc_number.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.oclc_number.errors id="desc_oclc_number" %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_asin">{% trans "ASIN:" %}</label>
|
||||
<label class="label" for="id_asin">
|
||||
{% trans "ASIN:" %}
|
||||
</label>
|
||||
{{ form.asin }}
|
||||
{% for error in form.ASIN.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
30
bookwyrm/templates/book/sync_modal.html
Normal file
30
bookwyrm/templates/book/sync_modal.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Load data" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="{{ source }}-update" method="POST" action="{% url 'book-update-remote' book.id source %}">
|
||||
{% csrf_token %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Loading data will connect to <strong>{{ source_name }}</strong> and check for any metadata about this book which aren't present here. Existing metadata will not be overwritten.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">
|
||||
<span>{% trans "Confirm" %}</span>
|
||||
</button>
|
||||
|
||||
{% trans "Cancel" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
@ -2,25 +2,20 @@
|
||||
{% load utilities %}
|
||||
|
||||
{% with 0|uuid as uuid %}
|
||||
<div
|
||||
<details
|
||||
id="menu_{{ uuid }}"
|
||||
class="
|
||||
dropdown control
|
||||
{% if right %}is-right{% endif %}
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="button dropdown-trigger pulldown-menu {{ class }}"
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-controls="menu_options_{{ uuid }}"
|
||||
data-controls="menu_{{ uuid }}"
|
||||
<summary
|
||||
class="button control dropdown-trigger pulldown-menu {{ class }}"
|
||||
>
|
||||
{% block dropdown-trigger %}{% endblock %}
|
||||
</button>
|
||||
</summary>
|
||||
|
||||
<div class="dropdown-menu">
|
||||
<div class="dropdown-menu control">
|
||||
<ul
|
||||
id="menu_options_{{ uuid }}"
|
||||
class="dropdown-content p-0 is-clipped"
|
||||
@ -29,6 +24,6 @@
|
||||
{% block dropdown-list %}{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
||||
|
@ -6,49 +6,56 @@
|
||||
<h1 class="title">
|
||||
{{ tab.name }}
|
||||
</h1>
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% for stream in streams %}
|
||||
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
||||
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="block is-clipped">
|
||||
<div class="is-pulled-left">
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% for stream in streams %}
|
||||
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
||||
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# feed settings #}
|
||||
<details class="mb-5" {% if settings_saved %}open{% endif %}>
|
||||
<summary>
|
||||
<span class="has-text-weight-bold">
|
||||
{{ _("Feed settings") }}
|
||||
</span>
|
||||
{% if settings_saved %}
|
||||
<span class="tag is-success is-light ml-2">{{ _("Saved!") }}</span>
|
||||
{% endif %}
|
||||
</summary>
|
||||
<form class="level is-align-items-flex-end" method="post" action="/{{ tab.key }}#feed">
|
||||
{% csrf_token %}
|
||||
{# feed settings #}
|
||||
<details class="detail-pinned-button" {% if settings_saved %}open{% endif %}>
|
||||
<summary class="control">
|
||||
<span class="button">
|
||||
<span class="icon icon-dots-three m-0-mobile" aria-hidden="true"></span>
|
||||
<span class="is-sr-only-mobile">{{ _("Feed settings") }}</span>
|
||||
</span>
|
||||
</summary>
|
||||
<form class="notification level is-align-items-flex-end" method="post" action="/{{ tab.key }}#feed">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="level-left">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="label mt-2 mb-1">Status types</label>
|
||||
{% 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 class="level-left">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<span class="is-flex is-align-items-baseline">
|
||||
<label class="label mt-2 mb-1">Status types</label>
|
||||
{% if settings_saved %}
|
||||
<span class="tag is-success is-light ml-2">{{ _("Saved!") }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% for name, value in feed_status_types_options %}
|
||||
<label class="mr-2">
|
||||
<input type="checkbox" name="feed_status_types" value="{{ name }}" {% if name in user.feed_status_types %}checked=""{% endif %}/>
|
||||
{{ value }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right control">
|
||||
<button class="button is-small is-primary is-outlined" type="submit">
|
||||
{{ _("Save settings") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
<div class="level-right control">
|
||||
<button class="button is-small is-primary is-outlined" type="submit">
|
||||
{{ _("Save settings") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{# announcements and system messages #}
|
||||
{% if not activities.number > 1 %}
|
||||
|
@ -14,16 +14,14 @@
|
||||
<div class="block">
|
||||
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
||||
<input type="text" name="name" maxlength="100" class="input" id="id_name" placeholder="{{ user.localname }}" value="{% if request.user.name %}{{ request.user.name }}{% endif %}">
|
||||
{% for error in form.name.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||
</div>
|
||||
<div class="block">
|
||||
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
||||
<textarea name="summary" cols="None" rows="None" class="textarea" id="id_summary" placeholder="{% trans 'A little bit about you' %}">{% if request.user.summary %}{{ request.user.summary }}{% endif %}</textarea>
|
||||
{% for error in form.summary.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -31,9 +29,8 @@
|
||||
<div class="block">
|
||||
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
|
||||
{{ form.avatar }}
|
||||
{% for error in form.avatar.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.avatar.errors id="desc_avatar" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -65,10 +65,9 @@
|
||||
{% csrf_token %}
|
||||
<div class="block">
|
||||
<label for="id_request_email" class="label">{% trans "Email address:" %}</label>
|
||||
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
|
||||
{% for error in request_form.email.errors %}
|
||||
<p class="help is-danger">{{ error|escape }}</p>
|
||||
{% endfor %}
|
||||
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email" aria-describedby="desc_request_email">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=request_form.email.errors id="desc_request_email" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
|
@ -26,11 +26,10 @@
|
||||
<div class="field">
|
||||
<label class="label" for="id_password_confirm">{% trans "Password:" %}</label>
|
||||
<div class="control">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm" aria-describedby="desc_password">
|
||||
</div>
|
||||
{% for error in login_form.password.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
|
@ -8,21 +8,33 @@
|
||||
<div class="column">
|
||||
<div class="block">
|
||||
<h1 class="title">{% trans "Reset Password" %}</h1>
|
||||
{% for error in errors %}
|
||||
<p class="is-danger">{{ error }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% if errors %}
|
||||
<div id="form_errors">
|
||||
{% for error in errors %}
|
||||
<p class="is-danger">
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<form name="password-reset" method="post" action="/password-reset/{{ code }}">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_new_password">{% trans "Password:" %}</label>
|
||||
<label class="label" for="id_new_password">
|
||||
{% trans "Password:" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_new_password">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_new_password" aria-describedby="form_errors">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_confirm_password">{% trans "Confirm password:" %}</label>
|
||||
<label class="label" for="id_confirm_password">
|
||||
{% trans "Confirm password:" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
|
||||
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password" aria-describedby="form_errors">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
|
@ -267,5 +267,6 @@
|
||||
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -7,8 +7,8 @@
|
||||
<div class="column is-one-quarter">
|
||||
<div class="card is-stretchable">
|
||||
<header class="card-header">
|
||||
<h4 class="card-header-title">
|
||||
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||
<h4 class="card-header-title is-clipped">
|
||||
<a href="{{ list.local_path }}" class="is-clipped">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||
</h4>
|
||||
{% if request.user.is_authenticated and request.user|saved:list %}
|
||||
<div class="card-header-icon">
|
||||
|
70
bookwyrm/templates/ostatus/error.html
Normal file
70
bookwyrm/templates/ostatus/error.html
Normal file
@ -0,0 +1,70 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
{% if error == 'invalid_username' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> is not a valid username{% endblocktrans %}.</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again' %}.</p>
|
||||
</div>
|
||||
{% elif error == 'user_not_found' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> could not be found or <code>{{ remote_domain }}</code> does not support identity discovery{% endblocktrans %}.</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again' %}.</p>
|
||||
</div>
|
||||
{% elif error == 'not_supported' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> was found but <code>{{ remote_domain }}</code> does not support 'remote follow'{% endblocktrans %}.</p>
|
||||
<p>{% blocktrans %}Try searching for <strong>{{ user }}</strong> on <code>{{ remote_domain }}</code> instead{% endblocktrans %}.</p>
|
||||
</div>
|
||||
{% elif not request.user.is_authenticated %}
|
||||
<div class="navbar-item">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<form name="login" method="post" action="{% url 'login' %}?next={{ request.path }}?acct={{ user.remote_id }}">
|
||||
{% csrf_token %}
|
||||
<div class="columns is-variable is-1">
|
||||
<div class="column">
|
||||
<label class="is-sr-only" for="id_localname">{% trans "Username:" %}</label>
|
||||
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname" placeholder="{% trans 'username' %}">
|
||||
</div>
|
||||
<div class="column">
|
||||
<label class="is-sr-only" for="id_password">{% trans "Password:" %}</label>
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
|
||||
<p class="help"><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif error == 'ostatus_subscribe' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}Something went wrong trying to follow <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again.' %}</p>
|
||||
</div>
|
||||
{% elif error == 'is_blocked' %}
|
||||
<div class="notification is-danger has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You have blocked <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'has_blocked' %}
|
||||
<div class="notification is-danger has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> has blocked you{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'already_following' %}
|
||||
<div class="notification is-success has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You are already following <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'already_requested' %}
|
||||
<div class="notification is-success has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You have already requested to follow <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="block is-pulled-right">
|
||||
<button type="button" class="button" onclick="closeWindow()">Close window</button>
|
||||
</div>
|
||||
{% endblock %}
|
46
bookwyrm/templates/ostatus/remote_follow.html
Normal file
46
bookwyrm/templates/ostatus/remote_follow.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block heading %}
|
||||
{% blocktrans with username=user.localname sitename=site.name %}Follow {{ username }} on the fediverse{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<p>{% blocktrans with username=user.display_name %}Follow {{ username }} from another Fediverse account like BookWyrm, Mastodon, or Pleroma.{% endblocktrans %}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<section class="card-content content">
|
||||
<form name="remote-follow" method="post" action="{% url 'remote-follow' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ user.id }}">
|
||||
<label class="label" for="remote_user">{% trans 'User handle to follow from:' %}</label>
|
||||
<input class="input" type="text" name="remote_user" id="remote_user" placeholder="user@example.social" required>
|
||||
<button class="button mt-1 is-primary" type="submit">{% trans 'Follow!' %}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
15
bookwyrm/templates/ostatus/remote_follow_button.html
Normal file
15
bookwyrm/templates/ostatus/remote_follow_button.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% load i18n %}
|
||||
{% if request.user == user %}
|
||||
{% else %}
|
||||
|
||||
<div class="field mb-0">
|
||||
<div class="control">
|
||||
<a class="button is-small is-link" href="{% url 'remote-follow-page' %}?user={{ user.username }}" target="_blank" rel="noopener noreferrer" onclick="BookWyrm.displayPopUp(`{% url 'remote-follow-page' %}?user={{ user.username }}`, `remoteFollow`); return false;" aria-describedby="remote_follow_warning">
|
||||
{% blocktrans with username=user.localname %}Follow on Fediverse{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
<p id="remote_follow_warning" class="mt-1 is-size-7 has-text-weight-light">
|
||||
{% trans 'This link opens in a pop-up window' %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
63
bookwyrm/templates/ostatus/subscribe.html
Normal file
63
bookwyrm/templates/ostatus/subscribe.html
Normal file
@ -0,0 +1,63 @@
|
||||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}
|
||||
{% if not request.user.is_authenticated %}
|
||||
{% blocktrans with sitename=site.name %}Log in to {{ sitename }}{% endblocktrans %}
|
||||
{% elif error %}
|
||||
{% blocktrans with sitename=site.name %}Error following from {{ sitename }}{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with sitename=site.name %}Follow from {{ sitename }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% if error %}
|
||||
{% trans 'Uh oh...' %}
|
||||
{% elif not request.user.is_authenticated %}
|
||||
{% trans "Let's log in first..." %}
|
||||
{% else %}
|
||||
{% blocktrans with sitename=site.name %}Follow from {{ sitename }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if error or not request.user.is_authenticated %}
|
||||
{% include 'ostatus/error.html' with error=error user=user account=account %}
|
||||
{% else %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans 'Locked account' %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
<form name="follow" method="post" action="{% url 'follow' %}/?next={% url 'ostatus-success' %}?following={{ user.username }}">
|
||||
{% csrf_token %}
|
||||
<input name="user" value="{{ user.username }}" hidden>
|
||||
<button class="button is-link" type="submit">{% blocktrans with username=user.display_name %}Follow {{ username }}{% endblocktrans %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if user.summary %}
|
||||
{{ user.summary|to_markdown|safe|truncatechars_html:120 }}
|
||||
{% else %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
35
bookwyrm/templates/ostatus/success.html
Normal file
35
bookwyrm/templates/ostatus/success.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="notification is-success">
|
||||
<span class="icon icon-check m-0-mobile" aria-hidden="true"></span>
|
||||
<span>{% blocktrans with display_name=user.display_name %}You are now following {{ display_name }}!{% endblocktrans %}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block is-pulled-right">
|
||||
<button type="button" class="button" onclick="closeWindow()">Close window</button>
|
||||
</div>
|
||||
{% endblock %}
|
41
bookwyrm/templates/ostatus/template.html
Normal file
41
bookwyrm/templates/ostatus/template.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% load layout %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{% get_lang %}">
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="{% static 'css/vendor/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/vendor/icons.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bookwyrm.css' %}">
|
||||
<script>
|
||||
function closeWindow() {
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<img class="image logo navbar-item" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static 'images/logo-small.png' %}{% endif %}" alt="Home page">
|
||||
<h2 class="navbar-item subtitle">{% block heading %}{% endblock %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="section is-flex-grow-1 columns is-centered">
|
||||
<div class="block column is-one-third">
|
||||
{% block content%}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var csrf_token = '{{ csrf_token }}';
|
||||
</script>
|
||||
<script src="{% static 'js/bookwyrm.js' %}?v={{ js_cache }}"></script>
|
||||
</body>
|
||||
</html>
|
@ -18,10 +18,9 @@
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">{% trans "Confirm password:" %}</label>
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required>
|
||||
{% for error in form.password.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
||||
</form>
|
||||
|
@ -33,31 +33,27 @@
|
||||
{% endif %}
|
||||
<div class="column">
|
||||
{{ form.avatar }}
|
||||
{% for error in form.avatar.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.avatar.errors id="desc_avatar" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
||||
{{ form.name }}
|
||||
{% for error in form.name.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
||||
{{ form.summary }}
|
||||
{% for error in form.summary.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_email">{% trans "Email address:" %}</label>
|
||||
{{ form.email }}
|
||||
{% for error in form.email.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.email.errors id="desc_email" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -69,19 +65,23 @@
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_show_goal">
|
||||
{% trans "Show reading goal prompt in feed:" %}
|
||||
{{ form.show_goal }}
|
||||
{% trans "Show reading goal prompt in feed" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_show_suggested_users">
|
||||
{% trans "Show suggested users:" %}
|
||||
{{ form.show_suggested_users }}
|
||||
{% trans "Show suggested users" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_discoverable">
|
||||
{% trans "Show this account in suggested users:" %}
|
||||
{{ form.discoverable }}
|
||||
{% trans "Show this account in suggested users" %}
|
||||
</label>
|
||||
{% url 'directory' as path %}
|
||||
<p class="help">
|
||||
<p class="help" id="desc_discoverable">
|
||||
{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
@ -107,8 +107,8 @@
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_manually_approves_followers">
|
||||
{% trans "Manually approve followers:" %}
|
||||
{{ form.manually_approves_followers }}
|
||||
{% trans "Manually approve followers" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -13,60 +13,68 @@
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
<p>
|
||||
<label class="label" for="id_preview">{% trans "Preview:" %}</label>
|
||||
<label class="label" for="id_preview">
|
||||
{% trans "Preview:" %}
|
||||
</label>
|
||||
{{ form.preview }}
|
||||
{% for error in form.preview.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.preview.errors id="desc_preview" %}
|
||||
</p>
|
||||
<p>
|
||||
<label class="label" for="id_content">{% trans "Content:" %}</label>
|
||||
<label class="label" for="id_content">
|
||||
{% trans "Content:" %}
|
||||
</label>
|
||||
{{ form.content }}
|
||||
{% for error in form.content.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.content.errors id="desc_content" %}
|
||||
</p>
|
||||
<p>
|
||||
<label class="label" for="id_event_date">{% trans "Event date:" %}</label>
|
||||
<label class="label" for="id_event_date">
|
||||
{% trans "Event date:" %}
|
||||
</label>
|
||||
<input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
|
||||
{% for error in form.event_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.event_date.errors id="desc_event_date" %}
|
||||
</p>
|
||||
<hr aria-hidden="true">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p>
|
||||
<label class="label" for="id_start_date">{% trans "Start date:" %}</label>
|
||||
<label class="label" for="id_start_date">
|
||||
{% trans "Start date:" %}
|
||||
</label>
|
||||
<input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
|
||||
{% for error in form.start_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.start_date.errors id="desc_start_date" %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>
|
||||
<label class="label" for="id_end_date">{% trans "End date:" %}</label>
|
||||
<label class="label" for="id_end_date">
|
||||
{% trans "End date:" %}
|
||||
</label>
|
||||
<input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
|
||||
{% for error in form.end_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.end_date.errors id="desc_end_date" %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<p>
|
||||
<label class="label" for="id_active">{% trans "Active:" %}</label>
|
||||
<label class="label" for="id_active">
|
||||
{% trans "Active:" %}
|
||||
</label>
|
||||
{{ form.active }}
|
||||
{% for error in form.active.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.active.errors id="desc_active" %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -17,10 +17,8 @@
|
||||
{{ form.domain }}
|
||||
</div>
|
||||
</div>
|
||||
{% for error in form.domain.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.domain.errors id="desc_domain" %}
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
|
||||
|
@ -27,11 +27,12 @@
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label" for="id_server_name">{% trans "Instance:" %}</label>
|
||||
<input type="text" name="server_name" maxlength="255" class="input" required id="id_server_name" value="{{ form.server_name.value|default:'' }}" placeholder="domain.com">
|
||||
{% for error in form.server_name.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_server_name">
|
||||
{% trans "Instance:" %}
|
||||
</label>
|
||||
<input type="text" name="server_name" maxlength="255" class="input" required id="id_server_name" value="{{ form.server_name.value|default:'' }}" placeholder="domain.com" aria-describedby="desc_server_name">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.server_name.errors id="desc_server_name" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
@ -49,29 +50,37 @@
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label" for="id_application_type">{% trans "Software:" %}</label>
|
||||
<input type="text" name="application_type" maxlength="255" class="input" id="id_application_type" value="{{ form.application_type.value|default:'' }}">
|
||||
{% for error in form.application_type.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_application_type">
|
||||
{% trans "Software:" %}
|
||||
</label>
|
||||
<input type="text" name="application_type" maxlength="255" class="input" id="id_application_type" value="{{ form.application_type.value|default:'' }}" aria-describedby="desc_application_type">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.application_type.errors id="desc_application_type" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label" for="id_application_version">{% trans "Version:" %}</label>
|
||||
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}">
|
||||
{% for error in form.application_version.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<label class="label" for="id_application_version">
|
||||
{% trans "Version:" %}
|
||||
</label>
|
||||
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}" aria-describedby="desc_application_version">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.application_version.errors id="desc_application_version" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_notes">{% trans "Notes:" %}</label>
|
||||
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea>
|
||||
<label class="label" for="id_notes">
|
||||
{% trans "Notes:" %}
|
||||
</label>
|
||||
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">
|
||||
{{ form.notes.value|default:'' }}
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -20,16 +20,16 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24">
|
||||
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24" aria-describedby="desc_address">
|
||||
</div>
|
||||
|
||||
{% for error in form.address.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.address.errors id="desc_address" %}
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -33,8 +33,8 @@
|
||||
{{ site_form.instance_description }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label>
|
||||
<p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support HTML or Markdown." %}</p>
|
||||
<label class="label mb-0" for="id_instance_short_description">{% trans "Short description:" %}</label>
|
||||
<p class="help" id="desc_instance_short_description">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support HTML or Markdown." %}</p>
|
||||
{{ site_form.instance_short_description }}
|
||||
</div>
|
||||
<div class="field">
|
||||
@ -114,7 +114,7 @@
|
||||
{{ site_form.require_confirm_email }}
|
||||
{% trans "Require users to confirm email address" %}
|
||||
</label>
|
||||
<p class="help">{% trans "(Recommended if registration is open)" %}</p>
|
||||
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||
@ -123,9 +123,8 @@
|
||||
<div class="field">
|
||||
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
||||
{{ site_form.invite_request_text }}
|
||||
{% for error in site_form.invite_request_text.errors %}
|
||||
<p class="help is-danger">{{ error|escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -15,10 +15,9 @@
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">{% trans "Your password:" %}</label>
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required>
|
||||
{% for error in form.password.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
||||
</form>
|
||||
|
@ -50,18 +50,23 @@
|
||||
{% endif %}
|
||||
{% with group=user.groups.first %}
|
||||
<div class="select">
|
||||
<select name="groups" id="id_user_group">
|
||||
<select name="groups" id="id_user_group" aria-describedby="desc_user_group">
|
||||
{% for value, name in group_form.fields.groups.choices %}
|
||||
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>{{ name|title }}</option>
|
||||
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>
|
||||
{{ name|title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
<option value="" {% if not group %}selected{% endif %}>User</option>
|
||||
<option value="" {% if not group %}selected{% endif %}>
|
||||
User
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
{% for error in group_form.groups.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
|
||||
{% endwith %}
|
||||
<button class="button">{% trans "Save" %}</button>
|
||||
<button class="button">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
9
bookwyrm/templates/snippets/form_errors.html
Normal file
9
bookwyrm/templates/snippets/form_errors.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% if errors_list %}
|
||||
<div id="{{ id }}">
|
||||
{% for error in errors_list %}
|
||||
<p class="help is-danger">
|
||||
{{ error | escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
@ -3,32 +3,31 @@
|
||||
<div class="field">
|
||||
<label class="label" for="id_localname_register">{% trans "Username:" %}</label>
|
||||
<div class="control">
|
||||
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname_register" value="{% if register_form.localname.value %}{{ register_form.localname.value }}{% endif %}">
|
||||
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname_register" value="{% if register_form.localname.value %}{{ register_form.localname.value }}{% endif %}" aria-describedby="desc_localname_register">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=register_form.localname.errors id="desc_localname_register" %}
|
||||
</div>
|
||||
{% for error in register_form.localname.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
|
||||
<div class="control">
|
||||
<input type="email" name="email" maxlength="254" class="input" id="id_email_register" value="{% if register_form.email.value %}{{ register_form.email.value }}{% endif %}" required>
|
||||
{% for error in register_form.email.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<input type="email" name="email" maxlength="254" class="input" id="id_email_register" value="{% if register_form.email.value %}{{ register_form.email.value }}{% endif %}" required aria-describedby="desc_email_register">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=register_form.email.errors id="desc_email_register" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_password_register">{% trans "Password:" %}</label>
|
||||
<div class="control">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_register">
|
||||
{% for error in register_form.password.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_register" aria-describedby="desc_password_register">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=register_form.password.errors id="desc_password_register" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">{% trans "Sign Up" %}</button>
|
||||
<button class="button is-primary" type="submit">
|
||||
{% trans "Sign Up" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -86,11 +86,11 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book controls_text="want_to_read" controls_uid=uuid move_from=current.id refresh=True %}
|
||||
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book controls_text="want_to_read" controls_uid=uuid move_from=current.id refresh=True class="" %}
|
||||
|
||||
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book controls_text="start_reading" controls_uid=uuid move_from=current.id refresh=True %}
|
||||
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book controls_text="start_reading" controls_uid=uuid move_from=current.id refresh=True class="" %}
|
||||
|
||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid move_from=current.id readthrough=readthrough refresh=True %}
|
||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid move_from=current.id readthrough=readthrough refresh=True class="" %}
|
||||
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
34
bookwyrm/templates/snippets/trimmed_list.html
Normal file
34
bookwyrm/templates/snippets/trimmed_list.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% spaceless %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% firstof limit 3 as limit %}
|
||||
{% with subtraction_value='-'|add:limit %}
|
||||
{% with remainder_count=items|length|add:subtraction_value %}
|
||||
{% with remainder_count_display=remainder_count|intcomma %}
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
{% for item in items|slice:limit %}
|
||||
<span
|
||||
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
||||
>{{ item }}</span>{% if not forloop.last %}, {% elif remainder_count > 0 %}, {% blocktrans trimmed count counter=remainder_count %}
|
||||
and {{ remainder_count_display }} other
|
||||
{% plural %}
|
||||
and {{ remainder_count_display }} others
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</summary>
|
||||
|
||||
{% for item in items|slice:"3:" %}
|
||||
<span
|
||||
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
||||
>{{ item }}</span>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</details>
|
||||
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
@ -39,6 +39,9 @@
|
||||
{% if not is_self and request.user.is_authenticated %}
|
||||
{% include 'snippets/follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
{% if not is_self %}
|
||||
{% include 'ostatus/remote_follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_self and user.follower_requests.all %}
|
||||
<div class="follow-requests">
|
||||
|
@ -5,7 +5,6 @@ from uuid import uuid4
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.templatetags.static import static
|
||||
|
||||
|
||||
@ -98,10 +97,3 @@ def get_isni(existing, author, autoescape=True):
|
||||
f'<input type="text" name="isni-for-{author.id}" value="{isni}" hidden>'
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@register.filter(name="remove_spaces")
|
||||
@stringfilter
|
||||
def remove_spaces(arg):
|
||||
"""Removes spaces from argument passed in"""
|
||||
return re.sub(r"\s", "", str(arg))
|
||||
|
@ -40,6 +40,8 @@ class AbstractConnector(TestCase):
|
||||
class TestConnector(abstract_connector.AbstractConnector):
|
||||
"""nothing added here"""
|
||||
|
||||
generated_remote_link_field = "openlibrary_link"
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
return search_result
|
||||
|
||||
@ -87,9 +89,7 @@ class AbstractConnector(TestCase):
|
||||
def test_get_or_create_book_existing(self):
|
||||
"""find an existing book by remote/origin id"""
|
||||
self.assertEqual(models.Book.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id)
|
||||
)
|
||||
self.assertEqual(self.book.remote_id, f"https://{DOMAIN}/book/{self.book.id}")
|
||||
self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
|
||||
|
||||
# dedupe by origin id
|
||||
@ -99,7 +99,7 @@ class AbstractConnector(TestCase):
|
||||
|
||||
# dedupe by remote id
|
||||
result = self.connector.get_or_create_book(
|
||||
"https://%s/book/%d" % (DOMAIN, self.book.id)
|
||||
f"https://{DOMAIN}/book/{self.book.id}"
|
||||
)
|
||||
self.assertEqual(models.Book.objects.count(), 1)
|
||||
self.assertEqual(result, self.book)
|
||||
@ -119,7 +119,8 @@ class AbstractConnector(TestCase):
|
||||
@responses.activate
|
||||
def test_get_or_create_author(self):
|
||||
"""load an author"""
|
||||
self.connector.author_mappings = [ # pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
]
|
||||
@ -139,3 +140,26 @@ class AbstractConnector(TestCase):
|
||||
author = models.Author.objects.create(name="Test Author")
|
||||
result = self.connector.get_or_create_author(author.remote_id)
|
||||
self.assertEqual(author, result)
|
||||
|
||||
@responses.activate
|
||||
def test_update_author_from_remote(self):
|
||||
"""trigger the function that looks up the remote data"""
|
||||
author = models.Author.objects.create(name="Test", openlibrary_key="OL123A")
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
Mapping("isni"),
|
||||
]
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://openlibrary.org/authors/OL123A",
|
||||
json={"id": "https://www.example.com/author", "name": "Beep", "isni": "hi"},
|
||||
)
|
||||
|
||||
self.connector.update_author_from_remote(author)
|
||||
|
||||
author.refresh_from_db()
|
||||
self.assertEqual(author.name, "Test")
|
||||
self.assertEqual(author.isni, "hi")
|
||||
|
@ -306,3 +306,11 @@ class Inventaire(TestCase):
|
||||
|
||||
extract = self.connector.get_description({"enwiki": "test_path"})
|
||||
self.assertEqual(extract, "hi hi")
|
||||
|
||||
def test_remote_id_from_model(self):
|
||||
"""figure out a url from an id"""
|
||||
obj = models.Author.objects.create(name="hello", inventaire_id="123")
|
||||
self.assertEqual(
|
||||
self.connector.get_remote_id_from_model(obj),
|
||||
"https://inventaire.io?action=by-uris&uris=123",
|
||||
)
|
||||
|
@ -98,6 +98,9 @@ class Openlibrary(TestCase):
|
||||
"type": "/type/datetime",
|
||||
"value": "2008-08-31 10:09:33.413686",
|
||||
},
|
||||
"remote_ids": {
|
||||
"isni": "000111",
|
||||
},
|
||||
"key": "/authors/OL453734A",
|
||||
"type": {"key": "/type/author"},
|
||||
"id": 1259965,
|
||||
@ -110,6 +113,7 @@ class Openlibrary(TestCase):
|
||||
self.assertIsInstance(result, models.Author)
|
||||
self.assertEqual(result.name, "George Elliott")
|
||||
self.assertEqual(result.openlibrary_key, "OL453734A")
|
||||
self.assertEqual(result.isni, "000111")
|
||||
|
||||
def test_get_cover_url(self):
|
||||
"""formats a url that should contain the cover image"""
|
||||
|
@ -209,6 +209,28 @@ class BookViews(TestCase):
|
||||
self.assertEqual(self.book.description, "new description hi")
|
||||
self.assertEqual(self.book.last_edited_by, self.local_user)
|
||||
|
||||
def test_update_book_from_remote(self):
|
||||
"""call out to sync with remote connector"""
|
||||
models.Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://openlibrary.org",
|
||||
books_url="https://openlibrary.org",
|
||||
covers_url="https://covers.openlibrary.org",
|
||||
search_url="https://openlibrary.org/search?q=",
|
||||
isbn_search_url="https://openlibrary.org/isbn",
|
||||
)
|
||||
self.local_user.groups.add(self.group)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.connectors.openlibrary.Connector.update_book_from_remote"
|
||||
) as mock:
|
||||
views.update_book_from_remote(request, self.book.id, "openlibrary.org")
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
||||
|
||||
def _setup_cover_url():
|
||||
"""creates cover url mock"""
|
||||
|
@ -148,3 +148,26 @@ class AuthorViews(TestCase):
|
||||
self.assertEqual(author.name, "Test Author")
|
||||
validate_html(resp.render())
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_update_author_from_remote(self):
|
||||
"""call out to sync with remote connector"""
|
||||
author = models.Author.objects.create(name="Test Author")
|
||||
models.Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://openlibrary.org",
|
||||
books_url="https://openlibrary.org",
|
||||
covers_url="https://covers.openlibrary.org",
|
||||
search_url="https://openlibrary.org/search?q=",
|
||||
isbn_search_url="https://openlibrary.org/isbn",
|
||||
)
|
||||
self.local_user.groups.add(self.group)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.connectors.openlibrary.Connector.update_author_from_remote"
|
||||
) as mock:
|
||||
views.update_author_from_remote(request, author.id, "openlibrary.org")
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
@ -4,10 +4,12 @@ from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
|
||||
@ -16,6 +18,7 @@ class FollowViews(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
models.SiteSettings.objects.create()
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
@ -174,3 +177,43 @@ class FollowViews(TestCase):
|
||||
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
||||
# follow relationship should not exist
|
||||
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)
|
||||
|
||||
def test_ostatus_follow_request(self, _):
|
||||
"""check ostatus subscribe template loads"""
|
||||
request = self.factory.get(
|
||||
"", {"acct": "https%3A%2F%2Fexample.com%2Fusers%2Frat"}
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.ostatus_follow_request(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_remote_follow_page(self, _):
|
||||
"""check remote follow page loads"""
|
||||
request = self.factory.get("", {"acct": "mouse@local.com"})
|
||||
request.user = self.remote_user
|
||||
result = views.remote_follow_page(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_ostatus_follow_success(self, _):
|
||||
"""check remote follow success page loads"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.remote_user
|
||||
request.following = "mouse@local.com"
|
||||
result = views.ostatus_follow_success(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_remote_follow(self, _):
|
||||
"""check follow from remote page loads"""
|
||||
request = self.factory.post("", {"user": self.remote_user.id})
|
||||
request.user = self.remote_user
|
||||
request.remote_user = "mouse@local.com"
|
||||
result = views.remote_follow(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
@ -44,6 +44,7 @@ urlpatterns = [
|
||||
re_path(r"^api/v1/instance/?$", views.instance_info),
|
||||
re_path(r"^api/v1/instance/peers/?$", views.peers),
|
||||
re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"),
|
||||
re_path(r"^ostatus_subscribe/?$", views.ostatus_follow_request),
|
||||
# polling updates
|
||||
re_path("^api/updates/notifications/?$", views.get_notification_count),
|
||||
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
|
||||
@ -427,13 +428,29 @@ urlpatterns = [
|
||||
r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover"
|
||||
),
|
||||
re_path(r"^add-description/(?P<book_id>\d+)/?$", views.add_description),
|
||||
re_path(r"^resolve-book/?$", views.resolve_book),
|
||||
re_path(r"^switch-edition/?$", views.switch_edition),
|
||||
re_path(r"^resolve-book/?$", views.resolve_book, name="resolve-book"),
|
||||
re_path(r"^switch-edition/?$", views.switch_edition, name="switch-edition"),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/update/(?P<connector_identifier>[\w\.]+)/?$",
|
||||
views.update_book_from_remote,
|
||||
name="book-update-remote",
|
||||
),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)/update/(?P<connector_identifier>[\w\.]+)/?$",
|
||||
views.update_author_from_remote,
|
||||
name="author-update-remote",
|
||||
),
|
||||
# isbn
|
||||
re_path(r"^isbn/(?P<isbn>\d+)(.json)?/?$", views.Isbn.as_view()),
|
||||
# author
|
||||
re_path(r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view()),
|
||||
re_path(r"^author/(?P<author_id>\d+)/edit/?$", views.EditAuthor.as_view()),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author"
|
||||
),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)/edit/?$",
|
||||
views.EditAuthor.as_view(),
|
||||
name="edit-author",
|
||||
),
|
||||
# reading progress
|
||||
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
|
||||
re_path(r"^delete-readthrough/?$", views.delete_readthrough),
|
||||
@ -455,4 +472,9 @@ urlpatterns = [
|
||||
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
|
||||
re_path(r"^accept-follow-request/?$", views.accept_follow_request),
|
||||
re_path(r"^delete-follow-request/?$", views.delete_follow_request),
|
||||
re_path(r"^ostatus_follow/?$", views.remote_follow, name="remote-follow"),
|
||||
re_path(r"^remote_follow/?$", views.remote_follow_page, name="remote-follow-page"),
|
||||
re_path(
|
||||
r"^ostatus_success/?$", views.ostatus_follow_success, name="ostatus-success"
|
||||
),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
@ -104,12 +104,14 @@ def find_authors_by_name(name_string, description=False):
|
||||
# otherwise just grab the first title listing
|
||||
titles.append(element.find(".//title"))
|
||||
|
||||
if titles is not None:
|
||||
if titles:
|
||||
# some of the "titles" in ISNI are a little ...iffy
|
||||
# '@' is used by ISNI/OCLC to index the starting point ignoring stop words
|
||||
# (e.g. "The @Government of no one")
|
||||
title_elements = [
|
||||
e for e in titles if not e.text.replace("@", "").isnumeric()
|
||||
e
|
||||
for e in titles
|
||||
if hasattr(e, "text") and not e.text.replace("@", "").isnumeric()
|
||||
]
|
||||
if len(title_elements):
|
||||
author.bio = title_elements[0].text.replace("@", "")
|
||||
|
@ -29,6 +29,7 @@ from .preferences.block import Block, unblock
|
||||
|
||||
# books
|
||||
from .books.books import Book, upload_cover, add_description, resolve_book
|
||||
from .books.books import update_book_from_remote
|
||||
from .books.edit_book import EditBook, ConfirmEditBook
|
||||
from .books.editions import Editions, switch_edition
|
||||
|
||||
@ -54,11 +55,18 @@ from .imports.manually_review import (
|
||||
)
|
||||
|
||||
# misc views
|
||||
from .author import Author, EditAuthor
|
||||
from .author import Author, EditAuthor, update_author_from_remote
|
||||
from .directory import Directory
|
||||
from .discover import Discover
|
||||
from .feed import DirectMessage, Feed, Replies, Status
|
||||
from .follow import follow, unfollow
|
||||
from .follow import (
|
||||
follow,
|
||||
unfollow,
|
||||
ostatus_follow_request,
|
||||
ostatus_follow_success,
|
||||
remote_follow,
|
||||
remote_follow_page,
|
||||
)
|
||||
from .follow import accept_follow_request, delete_follow_request
|
||||
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
||||
from .goal import Goal, hide_goal
|
||||
|
@ -6,9 +6,11 @@ from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
|
||||
@ -73,3 +75,19 @@ class EditAuthor(View):
|
||||
author = form.save()
|
||||
|
||||
return redirect(f"/author/{author.id}")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def update_author_from_remote(request, author_id, connector_identifier):
|
||||
"""load the remote data for this author"""
|
||||
connector = connector_manager.load_connector(
|
||||
get_object_or_404(models.Connector, identifier=connector_identifier)
|
||||
)
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
|
||||
connector.update_author_from_remote(author)
|
||||
|
||||
return redirect("author", author.id)
|
||||
|
@ -178,3 +178,19 @@ def resolve_book(request):
|
||||
book = connector.get_or_create_book(remote_id)
|
||||
|
||||
return redirect("book", book.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def update_book_from_remote(request, book_id, connector_identifier):
|
||||
"""load the remote data for this book"""
|
||||
connector = connector_manager.load_connector(
|
||||
get_object_or_404(models.Connector, identifier=connector_identifier)
|
||||
)
|
||||
book = get_object_or_404(models.Book.objects.select_subclasses(), id=book_id)
|
||||
|
||||
connector.update_book_from_remote(book)
|
||||
|
||||
return redirect("book", book.id)
|
||||
|
@ -1,11 +1,19 @@
|
||||
""" views for actions you can take in the application """
|
||||
import urllib.parse
|
||||
import re
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
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
|
||||
@ -23,6 +31,9 @@ def follow(request):
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
if request.GET.get("next"):
|
||||
return redirect(request.GET.get("next", "/"))
|
||||
|
||||
return redirect(to_follow.local_path)
|
||||
|
||||
|
||||
@ -84,3 +95,91 @@ def delete_follow_request(request):
|
||||
|
||||
follow_request.delete()
|
||||
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
|
||||
|
||||
|
||||
# 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):
|
||||
"""helper function to resolve a localname or a username to a user"""
|
||||
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
|
||||
if not query:
|
||||
return None
|
||||
|
||||
if query[0] == "@":
|
||||
query = query[1:]
|
||||
|
||||
try:
|
||||
domain = query.split("@")[1]
|
||||
except IndexError:
|
||||
@ -86,6 +91,35 @@ def handle_remote_webfinger(query):
|
||||
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):
|
||||
"""look up a book in the db and return an edition"""
|
||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||
|
@ -30,7 +30,11 @@ def webfinger(request):
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": user.remote_id,
|
||||
}
|
||||
},
|
||||
{
|
||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||
"template": f"https://{DOMAIN}/ostatus_subscribe?acct={{uri}}",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
Reference in New Issue
Block a user