diff --git a/.gitignore b/.gitignore
index 624ce100..e5582694 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
*.swp
**/__pycache__
.local
+/nginx/nginx.conf
# VSCode
/.vscode
diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py
index c032986d..bb2bfb6f 100644
--- a/bookwyrm/connectors/abstract_connector.py
+++ b/bookwyrm/connectors/abstract_connector.py
@@ -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):
diff --git a/bookwyrm/connectors/inventaire.py b/bookwyrm/connectors/inventaire.py
index e9f53856..a9aeb94f 100644
--- a/bookwyrm/connectors/inventaire.py
+++ b/bookwyrm/connectors/inventaire.py
@@ -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"""
diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py
index b8afc7ca..c15277f8 100644
--- a/bookwyrm/connectors/openlibrary.py
+++ b/bookwyrm/connectors/openlibrary.py
@@ -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:
diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py
index 249b9211..7ba7bd97 100644
--- a/bookwyrm/forms.py
+++ b/bookwyrm/forms.py
@@ -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):
diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py
index 6c29ac05..5cc11afd 100644
--- a/bookwyrm/models/author.py
+++ b/bookwyrm/models/author.py
@@ -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}"
diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py
index d97a1b8a..0a551bf2 100644
--- a/bookwyrm/models/book.py
+++ b/bookwyrm/models/book.py
@@ -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"""
diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py
index 32465d6e..a97ae2d5 100644
--- a/bookwyrm/preview_images.py
+++ b/bookwyrm/preview_images.py
@@ -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
)
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index d6c0b5d5..e95ff96e 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -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")
diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css
index 2b6a122f..f385e629 100644
--- a/bookwyrm/static/css/bookwyrm.css
+++ b/bookwyrm/static/css/bookwyrm.css
@@ -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
******************************************************************************/
diff --git a/bookwyrm/static/css/fonts/icomoon.eot b/bookwyrm/static/css/fonts/icomoon.eot
index 8eba8692..7b1f2d9d 100644
Binary files a/bookwyrm/static/css/fonts/icomoon.eot and b/bookwyrm/static/css/fonts/icomoon.eot differ
diff --git a/bookwyrm/static/css/fonts/icomoon.svg b/bookwyrm/static/css/fonts/icomoon.svg
index 82e41329..7dbbe0dc 100644
--- a/bookwyrm/static/css/fonts/icomoon.svg
+++ b/bookwyrm/static/css/fonts/icomoon.svg
@@ -46,4 +46,5 @@
- - {% trans "Wikipedia" %} - -
- {% endif %} + {% if links %} +- - {% trans "View ISNI record" %} - -
- {% endif %} + {% if author.isni %} + + {% endif %} - {% if author.openlibrary_key %} -- - {% trans "View on OpenLibrary" %} - -
- {% endif %} + {% trans "Load data" as button_text %} + {% if author.openlibrary_key %} +- - {% trans "View on Inventaire" %} - -
- {% endif %} + {% if author.inventaire_id %} +{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.aliases.errors id="desc_aliases" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.bio.errors id="desc_bio" %}{{ form.wikipedia_link }}
- {% for error in form.wikipedia_link.errors %} -{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.wikipedia_link.errors id="desc_wikipedia_link" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.born.errors id="desc_born" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.died.errors id="desc_died" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.oepnlibrary_key.errors id="desc_oepnlibrary_key" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.librarything_key.errors id="desc_librarything_key" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %} +{% trans "View on OpenLibrary" %}
++ {% trans "View on OpenLibrary" %} + {% 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 %} +
{% endif %} {% if book.inventaire_id %} -{% trans "View on Inventaire" %}
++ {% trans "View on Inventaire" %} + {% 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 %} +
{% endif %}{{ error | escape }}
- {% endfor %} + + + + {% include 'snippets/form_errors.html' with errors_list=form.title.errors id="desc_title" %}{{ error | escape }}
- {% endfor %} + + + + {% include 'snippets/form_errors.html' with errors_list=form.subtitle.errors id="desc_subtitle" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.description.errors id="desc_description" %}{{ error | escape }}
- {% endfor %} + + + + {% include 'snippets/form_errors.html' with errors_list=form.series.errors id="desc_series" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.series_number.errors id="desc_series_number" %}{{ error | escape }}
- {% endfor %} + + {% trans "Separate multiple values with commas." %} + + + {% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}{{ error | escape }}
- {% endfor %} + + {% trans "Separate multiple values with commas." %} + + + {% include 'snippets/form_errors.html' with errors_list=form.publishers.errors id="desc_publishers" %}{{ error | escape }}
- {% endfor %} + + + + {% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}{{ error | escape }}
- {% endfor %} + + + + {% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.cover.errors id="desc_cover" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.physical_format_detail.errors id="desc_physical_format_detail" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.pages.errors id="desc_pages" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.isbn_13.errors id="desc_isbn_13" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.isbn_10.errors id="desc_isbn_10" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.openlibrary_key.errors id="desc_openlibrary_key" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.oclc_number.errors id="desc_oclc_number" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %}{{ error|escape }}
- {% endfor %} + + + {% include 'snippets/form_errors.html' with errors_list=request_form.email.errors id="desc_request_email" %}{{ error | escape }}
- {% endfor %} + + {% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %}{{ error }}
- {% endfor %} + + {% if errors %} ++ {{ error }} +
+ {% endfor %} +