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"])
def books_new() -> Response:
book = Book()
book = Book(owner_id=g.user.id)
db.session.add(book)
db.session.commit()
return redirect(url_for(".book", id=book.id), 303)
@@ -177,12 +177,17 @@ def books_import() -> Response:
abort(500, "Error fetching book data")
book = Book(
owner_id=g.user.id,
title=book_data.title,
description=book_data.description,
isbn=isbn,
authors=book_data.authors,
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,
)
db.session.add(book)
@@ -393,7 +398,7 @@ class BookRequestSchema(BaseModel):
genres: list[str] = []
publisher: str = ""
owner_id: Optional[int] = None
bought: datetime = Field(default_factory=datetime.now)
bought: date = Field(default_factory=datetime.today)
location: str = "billy salon"
loaned_to: str = ""
loaned_from: str = ""
@@ -475,8 +480,8 @@ def readings_new(id: int) -> str:
class ReadingRequestSchema(BaseModel):
start_date: datetime = Field(default_factory=datetime.now)
end_date: Optional[datetime] = None
start_date: date = Field(default_factory=datetime.today)
end_date: Optional[date] = None
finished: bool = False
dropped: bool = False
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 (
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():
setattr(reading, key, value)
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
from pydantic import BaseModel
from pydantic import BaseModel, field_validator
# {
# "title": "Concilio de Sombras (Sombras de Magia 2)",
@@ -50,23 +51,35 @@ from pydantic import BaseModel
class GoogleBook(BaseModel):
title: str
authors: list[str]
publisher: str
publishedDate: date
description: str
industryIdentifiers: list[dict[str, str]]
pageCount: int
printType: str
categories: list[str]
maturityRating: str
allowAnonLogging: bool
contentVersion: str
panelizationSummary: dict[str, bool]
imageLinks: dict[str, str]
language: str
previewLink: str
infoLink: str
canonicalVolumeLink: str
authors: list[str] = []
publisher: str = ""
publishedDate: Optional[date | int] = None
description: str = ""
industryIdentifiers: list[dict[str, str]] = []
pageCount: int = 0
printType: str = ""
categories: list[str] = []
maturityRating: str = ""
allowAnonLogging: bool = False
contentVersion: str = ""
panelizationSummary: dict[str, bool] = {}
imageLinks: dict[str, str] = {}
language: str = ""
previewLink: str = ""
infoLink: 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:

View File

@@ -29,7 +29,7 @@ class Book(db.Model): # type: ignore[name-defined]
genres: Mapped[list[str]] = mapped_column(JSON, default=list)
publisher: Mapped[str] = mapped_column(default="")
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")
loaned_to: 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]
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)
finished: 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]
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"))
book_id: Mapped[int] = mapped_column(ForeignKey("book.id"))
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',
filename='favicon-16x16.png') }}">
<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>
<body hx-boost="true" hx-push-url="true" hx-target="body">
@@ -58,31 +81,7 @@
</div>
</div>
</dialog>
</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>
</html>

View File

@@ -8,8 +8,8 @@
<div>
{% block form %}
<form class="row row-cols-1 row-cols-xxl-2" hx-put="/books/{{ book.id }}" hx-trigger="change" hx-swap="none"
method="post">
<form class="row row-cols-1 row-cols-xxl-2" hx-put="/books/{{ book.id }}" hx-trigger="change" hx-push-url="false"
hx-swap="none" method="post">
{% from "error_feedback.html.j2" import validation_error %}
{% macro simple_field(field, name, value, type) %}
<label class="form-label" for="{{ field }}">{{ name }}:</label>
@@ -94,9 +94,9 @@
<div class="pt-2">
<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"
hx-confirm="Are you sure?">Delete</button>
hx-confirm="Are you sure?" hx-push-url="true">Delete</button>
</div>
</div>
</form>

View File

@@ -35,8 +35,7 @@
<div x-data="{ofilters: false, ocols: false}">
<div class="btn-toolbar pb-2 justify-content-between">
<div class="btn-group pe-2">
<button class="btn btn-primary border" type="button" hx-get="/books/new"
hx-target="#search-results">New</button>
<button class="btn btn-primary border" type="button" hx-get="/books/new">New</button>
<button class="btn btn-primary border" type="button"
onclick="document.querySelector('#isbn-prompt').showModal()">Import</button>