Compare commits
4 Commits
b4b931633b
...
2d336cd1da
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d336cd1da | |||
| 18d320422a | |||
| 2e62ab4fb8 | |||
| da41b4be01 |
@@ -182,7 +182,11 @@ def books_import() -> Response:
|
|||||||
isbn=isbn,
|
isbn=isbn,
|
||||||
authors=book_data.authors,
|
authors=book_data.authors,
|
||||||
publisher=book_data.publisher,
|
publisher=book_data.publisher,
|
||||||
first_published=book_data.publishedDate.year,
|
first_published=(
|
||||||
|
book_data.publishedDate.year
|
||||||
|
if isinstance(book_data.publishedDate, date)
|
||||||
|
else book_data.publishedDate
|
||||||
|
),
|
||||||
genres=book_data.categories,
|
genres=book_data.categories,
|
||||||
)
|
)
|
||||||
db.session.add(book)
|
db.session.add(book)
|
||||||
@@ -393,7 +397,7 @@ class BookRequestSchema(BaseModel):
|
|||||||
genres: list[str] = []
|
genres: list[str] = []
|
||||||
publisher: str = ""
|
publisher: str = ""
|
||||||
owner_id: Optional[int] = None
|
owner_id: Optional[int] = None
|
||||||
bought: datetime = Field(default_factory=datetime.now)
|
bought: date = Field(default_factory=datetime.today)
|
||||||
location: str = "billy salon"
|
location: str = "billy salon"
|
||||||
loaned_to: str = ""
|
loaned_to: str = ""
|
||||||
loaned_from: str = ""
|
loaned_from: str = ""
|
||||||
@@ -475,8 +479,8 @@ def readings_new(id: int) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class ReadingRequestSchema(BaseModel):
|
class ReadingRequestSchema(BaseModel):
|
||||||
start_date: datetime = Field(default_factory=datetime.now)
|
start_date: date = Field(default_factory=datetime.today)
|
||||||
end_date: Optional[datetime] = None
|
end_date: Optional[date] = None
|
||||||
finished: bool = False
|
finished: bool = False
|
||||||
dropped: bool = False
|
dropped: bool = False
|
||||||
rating: Optional[int] = None
|
rating: Optional[int] = None
|
||||||
@@ -521,7 +525,7 @@ def reading(book_id: int, reading_id: int) -> str:
|
|||||||
if reading_req.end_date is None and (
|
if reading_req.end_date is None and (
|
||||||
reading_req.finished or reading_req.dropped
|
reading_req.finished or reading_req.dropped
|
||||||
):
|
):
|
||||||
reading_req.end_date = datetime.now()
|
reading_req.end_date = datetime.today()
|
||||||
for key, value in reading_req.model_dump().items():
|
for key, value in reading_req.model_dump().items():
|
||||||
setattr(reading, key, value)
|
setattr(reading, key, value)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, field_validator
|
||||||
|
|
||||||
# {
|
# {
|
||||||
# "title": "Concilio de Sombras (Sombras de Magia 2)",
|
# "title": "Concilio de Sombras (Sombras de Magia 2)",
|
||||||
@@ -50,23 +51,35 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
class GoogleBook(BaseModel):
|
class GoogleBook(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
authors: list[str]
|
authors: list[str] = []
|
||||||
publisher: str
|
publisher: str = ""
|
||||||
publishedDate: date
|
publishedDate: Optional[date | int] = None
|
||||||
description: str
|
description: str = ""
|
||||||
industryIdentifiers: list[dict[str, str]]
|
industryIdentifiers: list[dict[str, str]] = []
|
||||||
pageCount: int
|
pageCount: int = 0
|
||||||
printType: str
|
printType: str = ""
|
||||||
categories: list[str]
|
categories: list[str] = []
|
||||||
maturityRating: str
|
maturityRating: str = ""
|
||||||
allowAnonLogging: bool
|
allowAnonLogging: bool = False
|
||||||
contentVersion: str
|
contentVersion: str = ""
|
||||||
panelizationSummary: dict[str, bool]
|
panelizationSummary: dict[str, bool] = {}
|
||||||
imageLinks: dict[str, str]
|
imageLinks: dict[str, str] = {}
|
||||||
language: str
|
language: str = ""
|
||||||
previewLink: str
|
previewLink: str = ""
|
||||||
infoLink: str
|
infoLink: str = ""
|
||||||
canonicalVolumeLink: str
|
canonicalVolumeLink: str = ""
|
||||||
|
|
||||||
|
# Validate publishedDate when given in YYYY-MM format and convert to date
|
||||||
|
@field_validator("publishedDate", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def validate_published_date(cls, v: Any) -> Any:
|
||||||
|
if isinstance(v, str):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(v, "%Y-%m").date()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
def fetch_google_book_data(isbn: str) -> GoogleBook:
|
def fetch_google_book_data(isbn: str) -> GoogleBook:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Book(db.Model): # type: ignore[name-defined]
|
|||||||
genres: Mapped[list[str]] = mapped_column(JSON, default=list)
|
genres: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||||
publisher: Mapped[str] = mapped_column(default="")
|
publisher: Mapped[str] = mapped_column(default="")
|
||||||
owner_id: Mapped[Optional[int]] = mapped_column(ForeignKey("user.id"))
|
owner_id: Mapped[Optional[int]] = mapped_column(ForeignKey("user.id"))
|
||||||
bought: Mapped[datetime] = mapped_column(default=datetime.now)
|
bought: Mapped[date] = mapped_column(default=datetime.today)
|
||||||
location: Mapped[str] = mapped_column(default="billy salon")
|
location: Mapped[str] = mapped_column(default="billy salon")
|
||||||
loaned_to: Mapped[str] = mapped_column(default="")
|
loaned_to: Mapped[str] = mapped_column(default="")
|
||||||
loaned_from: Mapped[str] = mapped_column(default="")
|
loaned_from: Mapped[str] = mapped_column(default="")
|
||||||
@@ -44,7 +44,7 @@ class Book(db.Model): # type: ignore[name-defined]
|
|||||||
|
|
||||||
class Reading(db.Model): # type: ignore[name-defined]
|
class Reading(db.Model): # type: ignore[name-defined]
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
start_date: Mapped[date] = mapped_column(default=datetime.now)
|
start_date: Mapped[date] = mapped_column(default=datetime.today)
|
||||||
end_date: Mapped[Optional[date]] = mapped_column(default=None)
|
end_date: Mapped[Optional[date]] = mapped_column(default=None)
|
||||||
finished: Mapped[bool] = mapped_column(default=False)
|
finished: Mapped[bool] = mapped_column(default=False)
|
||||||
dropped: Mapped[bool] = mapped_column(default=False)
|
dropped: Mapped[bool] = mapped_column(default=False)
|
||||||
@@ -58,7 +58,7 @@ class Reading(db.Model): # type: ignore[name-defined]
|
|||||||
|
|
||||||
class Wishlist(db.Model): # type: ignore[name-defined]
|
class Wishlist(db.Model): # type: ignore[name-defined]
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
wishlisted: Mapped[date] = mapped_column(default=datetime.now)
|
wishlisted: Mapped[date] = mapped_column(default=datetime.today)
|
||||||
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
|
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
|
||||||
book_id: Mapped[int] = mapped_column(ForeignKey("book.id"))
|
book_id: Mapped[int] = mapped_column(ForeignKey("book.id"))
|
||||||
user: Mapped["User"] = relationship(back_populates="wishes")
|
user: Mapped["User"] = relationship(back_populates="wishes")
|
||||||
|
|||||||
@@ -14,6 +14,29 @@
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static',
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static',
|
||||||
filename='favicon-16x16.png') }}">
|
filename='favicon-16x16.png') }}">
|
||||||
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}">
|
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}">
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='htmx.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='alpine.min.js') }}" defer></script>
|
||||||
|
<script src="{{ url_for('static', filename='tom-select.complete.min.js') }}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
htmx.on('htmx:beforeHistorySave', function () {
|
||||||
|
// find all TomSelect elements
|
||||||
|
document.querySelectorAll('.tomselect')
|
||||||
|
.forEach(elt => elt.tomselect ? elt.tomselect.destroy() : null) // and call destroy() on them
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:beforeSwap', function (evt) {
|
||||||
|
// alert on errors
|
||||||
|
if (evt.detail.xhr.status >= 400) {
|
||||||
|
error_dialog = document.querySelector('#error-alert');
|
||||||
|
error_dialog.querySelector('.modal-title').textContent = 'Error ' + evt.detail.xhr.status;
|
||||||
|
error_dialog.querySelector('.modal-body').innerHTML = evt.detail.xhr.response;
|
||||||
|
error_dialog.showModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body hx-boost="true" hx-push-url="true" hx-target="body">
|
<body hx-boost="true" hx-push-url="true" hx-target="body">
|
||||||
@@ -58,31 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='htmx.min.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='alpine.min.js') }}" defer></script>
|
|
||||||
<script src="{{ url_for('static', filename='tom-select.complete.min.js') }}"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
htmx.on('htmx:beforeHistorySave', function () {
|
|
||||||
// find all TomSelect elements
|
|
||||||
document.querySelectorAll('.tomselect')
|
|
||||||
.forEach(elt => elt.tomselect ? elt.tomselect.destroy() : null) // and call destroy() on them
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('htmx:beforeSwap', function (evt) {
|
|
||||||
// alert on errors
|
|
||||||
if (evt.detail.xhr.status >= 400) {
|
|
||||||
error_dialog = document.querySelector('#error-alert');
|
|
||||||
error_dialog.querySelector('.modal-title').textContent = 'Error ' + evt.detail.xhr.status;
|
|
||||||
error_dialog.querySelector('.modal-body').innerHTML = evt.detail.xhr.response;
|
|
||||||
error_dialog.showModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% block form %}
|
{% block form %}
|
||||||
<form class="row row-cols-1 row-cols-xxl-2" hx-put="/books/{{ book.id }}" hx-trigger="change" hx-swap="none"
|
<form class="row row-cols-1 row-cols-xxl-2" hx-put="/books/{{ book.id }}" hx-trigger="change" hx-push-url="false"
|
||||||
method="post">
|
hx-swap="none" method="post">
|
||||||
{% from "error_feedback.html.j2" import validation_error %}
|
{% from "error_feedback.html.j2" import validation_error %}
|
||||||
{% macro simple_field(field, name, value, type) %}
|
{% macro simple_field(field, name, value, type) %}
|
||||||
<label class="form-label" for="{{ field }}">{{ name }}:</label>
|
<label class="form-label" for="{{ field }}">{{ name }}:</label>
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
|
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<input class="btn btn-primary" type="submit" hx-post="/books/{{ book.id }}" hx-target="body"
|
<input class="btn btn-primary" type="submit" hx-post="/books/{{ book.id }}" hx-target="body"
|
||||||
hx-swap="innerHTML" value="Submit">
|
hx-swap="innerHTML" hx-push-url="true" value="Submit">
|
||||||
<button class="btn btn-danger" hx-delete="/books/{{ book.id }}" hx-target="body" hx-swap="innerHTML"
|
<button class="btn btn-danger" hx-delete="/books/{{ book.id }}" hx-target="body" hx-swap="innerHTML"
|
||||||
hx-confirm="Are you sure?">Delete</button>
|
hx-confirm="Are you sure?" hx-push-url="true">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -35,8 +35,7 @@
|
|||||||
<div x-data="{ofilters: false, ocols: false}">
|
<div x-data="{ofilters: false, ocols: false}">
|
||||||
<div class="btn-toolbar pb-2 justify-content-between">
|
<div class="btn-toolbar pb-2 justify-content-between">
|
||||||
<div class="btn-group pe-2">
|
<div class="btn-group pe-2">
|
||||||
<button class="btn btn-primary border" type="button" hx-get="/books/new"
|
<button class="btn btn-primary border" type="button" hx-get="/books/new">New</button>
|
||||||
hx-target="#search-results">New</button>
|
|
||||||
<button class="btn btn-primary border" type="button"
|
<button class="btn btn-primary border" type="button"
|
||||||
onclick="document.querySelector('#isbn-prompt').showModal()">Import</button>
|
onclick="document.querySelector('#isbn-prompt').showModal()">Import</button>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user