Compare commits

..

5 Commits

6 changed files with 74 additions and 58 deletions

View File

@@ -158,7 +158,7 @@ def books():
@bp.route("/new", methods=["GET"]) @bp.route("/new", methods=["GET"])
def books_new() -> Response: def books_new() -> Response:
book = Book() book = Book(owner_id=g.user.id)
db.session.add(book) db.session.add(book)
db.session.commit() db.session.commit()
return redirect(url_for(".book", id=book.id), 303) return redirect(url_for(".book", id=book.id), 303)
@@ -177,12 +177,17 @@ def books_import() -> Response:
abort(500, "Error fetching book data") abort(500, "Error fetching book data")
book = Book( book = Book(
owner_id=g.user.id,
title=book_data.title, title=book_data.title,
description=book_data.description, description=book_data.description,
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 +398,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 +480,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 +526,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()

View File

@@ -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:

View File

@@ -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")

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>