diff --git a/the_works/models.py b/the_works/models.py index b914839..27d6de5 100644 --- a/the_works/models.py +++ b/the_works/models.py @@ -1,27 +1,28 @@ -# code is based on output from `sqlacodegen --generator declarative sqlite:///path/to/the_works.sqlite` +# code is built upon output from sqlacodegen -from typing import List, Optional - -from sqlalchemy import Column, ForeignKey, Table, types -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship - -from flask import url_for import sys +from typing import List, Optional +from sqlalchemy import ForeignKey, types +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship +from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy +from flask import url_for class Base(DeclarativeBase): - def _asdict(self): + def asdict(self) -> dict: d = {} for col in self.__table__.c: - if type(col.type) == types.BLOB: - d[col.key] = "Blob (NULL)" if self.__getattribute__(col.key) == None else f"Blob ({sys.getsizeof(self.__getattribute__(col.key))} Bytes)" + if isinstance(col.type, types.BLOB): + d[col.key] = "Blob (NULL)" if self.__getattribute__(col.key) is None else f"Blob ({sys.getsizeof(self.__getattribute__(col.key))} Bytes)" else: - value = str(self.__getattribute__(col.key)) - d[col.key] = value[:50] + '...' if len(value) > 50 else value + if isinstance(value := self.__getattribute__(col.key), str) and len(value) > 50: + d[col.key] = value[:48] + '...' + else: + d[col.key] = value return d - def __repr__(self): - return f"{type(self).__name__}({str(self._asdict())})" + def __repr__(self) -> str: + return f"{type(self).__name__}({str(self.asdict())})" class Genre(Base): @@ -30,8 +31,8 @@ class Genre(Base): ID: Mapped[int] = mapped_column(primary_key=True) Genre: Mapped[str] - text: Mapped[List['Text']] = relationship('Text', secondary='Text_Genre', back_populates='genre') - werk: Mapped[List['Werk']] = relationship('Werk', secondary='Werk_Genre', back_populates='genre') + texte: Mapped[List['Text_Genre']] = relationship(back_populates='genre') + werke: Mapped[List['Werk_Genre']] = relationship(back_populates='genre') class Herausgeber(Base): @@ -40,7 +41,7 @@ class Herausgeber(Base): ID: Mapped[int] = mapped_column(primary_key=True) Name: Mapped[str] - werk: Mapped[List['Werk']] = relationship('Werk', secondary='Werk_Herausgeber', back_populates='herausgeber') + werke: Mapped[List['Werk_Herausgeber']] = relationship(back_populates='herausgeber') class Pseudonym(Base): @@ -49,7 +50,7 @@ class Pseudonym(Base): ID: Mapped[int] = mapped_column(primary_key=True) Pseudonym: Mapped[str] - veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship('Veroeffentlichung', back_populates='pseudonym') + veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='pseudonym') class Sprache(Base): @@ -58,7 +59,7 @@ class Sprache(Base): ID: Mapped[int] = mapped_column(primary_key=True) Sprache: Mapped[str] - text: Mapped[List['Text']] = relationship('Text', back_populates='sprache') + text: Mapped[List['Text']] = relationship(back_populates='sprache') class Textform(Base): @@ -67,7 +68,7 @@ class Textform(Base): ID: Mapped[int] = mapped_column(primary_key=True) Textform: Mapped[str] - text: Mapped[List['Text']] = relationship('Text', back_populates='textform') + text: Mapped[List['Text']] = relationship(back_populates='textform') class Titelbild(Base): @@ -83,10 +84,10 @@ class Titelbild(Base): Thumbnail: Mapped[bytes] sha256: Mapped[str] = mapped_column(unique=True) - werk: Mapped[List['Werk']] = relationship('Werk', back_populates='titelbild') + werk: Mapped[List['Werk']] = relationship(back_populates='titelbild') - def _asdict_with_urls(self): - tb = self._asdict() + def asdict_with_urls(self): + tb = self.asdict() tb["Bild"] = url_for("titelbild.image", id=self.ID) tb["Thumbnail"] = url_for("titelbild.thumbnail", id=self.ID) return tb @@ -98,8 +99,8 @@ class Verlag(Base): ID: Mapped[int] = mapped_column(primary_key=True) Verlag: Mapped[str] - reihe: Mapped[List['Reihe']] = relationship('Reihe', back_populates='verlag') - werk: Mapped[List['Werk']] = relationship('Werk', back_populates='verlag') + reihe: Mapped[List['Reihe']] = relationship(back_populates='verlag') + werk: Mapped[List['Werk']] = relationship(back_populates='verlag') class Werksform(Base): @@ -108,7 +109,7 @@ class Werksform(Base): ID: Mapped[int] = mapped_column(primary_key=True) Werksform: Mapped[str] - werk: Mapped[List['Werk']] = relationship('Werk', back_populates='werksform') + werk: Mapped[List['Werk']] = relationship(back_populates='werksform') class Reihe(Base): @@ -116,92 +117,117 @@ class Reihe(Base): ID: Mapped[int] = mapped_column(primary_key=True) Titel: Mapped[str] - Verlag: Mapped[Optional[str]] = mapped_column('Verlag', ForeignKey('Verlag.ID')) - verlag: Mapped[Optional['Verlag']] = relationship('Verlag', back_populates='reihe') - text: Mapped[List['Text']] = relationship('Text', back_populates='reihe') - werk: Mapped[List['Werk']] = relationship('Werk', back_populates='reihe') + Verlag: Mapped[Optional[str]] = mapped_column(ForeignKey('Verlag.ID')) + verlag: Mapped['Verlag'] = relationship(back_populates='reihe') + + text: Mapped[List['Text']] = relationship(back_populates='reihe') + werk: Mapped[List['Werk']] = relationship(back_populates='reihe') class Text(Base): __tablename__ = 'Text' + # regular columns ID: Mapped[int] = mapped_column(primary_key=True) Titel: Mapped[str] Untertitel: Mapped[Optional[str]] - Reihe: Mapped[Optional[int]] = mapped_column('Reihe', ForeignKey('Reihe.ID')) - Textform: Mapped[Optional[int]] = mapped_column('Textform', ForeignKey('Textform.ID')) - Sprache: Mapped[Optional[int]] = mapped_column('Sprache', ForeignKey('Sprache.ID')) - genre: Mapped[List['Genre']] = relationship('Genre', secondary='Text_Genre', back_populates='text') - reihe: Mapped[Optional['Reihe']] = relationship('Reihe', back_populates='text') - sprache: Mapped[Optional['Sprache']] = relationship('Sprache', back_populates='text') - textform: Mapped[Optional['Textform']] = relationship('Textform', back_populates='text') - veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship('Veroeffentlichung', back_populates='text') + # many-to-one + Reihe: Mapped[Optional[int]] = mapped_column(ForeignKey('Reihe.ID')) + reihe: Mapped[Optional["Reihe"]] = relationship(back_populates="text") + Sprache: Mapped[int] = mapped_column(ForeignKey('Sprache.ID')) + sprache: Mapped['Sprache'] = relationship(back_populates='text') + Textform: Mapped[int] = mapped_column(ForeignKey('Textform.ID')) + textform: Mapped['Textform'] = relationship(back_populates='text') + + # one-to-many + veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='text') + + # many-to-many + genres: Mapped[List['Text_Genre']] = relationship(back_populates='text', cascade="all, delete-orphan") + genre_ids: AssociationProxy[List["Genre"]] = association_proxy("genres", "Genre", creator=lambda genre_id: Text_Genre(Genre=genre_id)) class Werk(Base): __tablename__ = 'Werk' + # regular columns ID: Mapped[int] = mapped_column(primary_key=True) Titel: Mapped[str] Untertitel: Mapped[Optional[str]] - Werksform: Mapped[Optional[int]] = mapped_column('Werksform', ForeignKey('Werksform.ID')) - Verlag: Mapped[Optional[int]] = mapped_column('Verlag', ForeignKey('Verlag.ID')) - Reihe: Mapped[Optional[int]] = mapped_column('Reihe', ForeignKey('Reihe.ID')) Reihennummer: Mapped[Optional[str]] Erscheinungsdatum: Mapped[Optional[str]] ISBN_13: Mapped[Optional[str]] ISBN_10: Mapped[Optional[str]] ISSN: Mapped[Optional[str]] Preis: Mapped[Optional[str]] - Titelbild: Mapped[Optional[int]] = mapped_column('Titelbild', ForeignKey('Titelbild.ID')) Klappentext: Mapped[Optional[str]] Anmerkungen: Mapped[Optional[str]] - genre: Mapped[List['Genre']] = relationship('Genre', secondary='Werk_Genre', back_populates='werk') - herausgeber: Mapped[List['Herausgeber']] = relationship('Herausgeber', secondary='Werk_Herausgeber', back_populates='werk') - reihe: Mapped[Optional['Reihe']] = relationship('Reihe', back_populates='werk') - titelbild: Mapped[Optional['Titelbild']] = relationship('Titelbild', back_populates='werk') - verlag: Mapped[Optional['Verlag']] = relationship('Verlag', back_populates='werk') - werksform: Mapped[Optional['Werksform']] = relationship('Werksform', back_populates='werk') - veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship('Veroeffentlichung', back_populates='werk') + # many-to-one + Reihe: Mapped[Optional[int]] = mapped_column(ForeignKey('Reihe.ID')) + reihe: Mapped[Optional['Reihe']] = relationship(back_populates='werk') + Titelbild: Mapped[Optional[int]] = mapped_column(ForeignKey('Titelbild.ID')) + titelbild: Mapped[Optional['Titelbild']] = relationship(back_populates='werk') + Verlag: Mapped[Optional[int]] = mapped_column(ForeignKey('Verlag.ID')) + verlag: Mapped[Optional['Verlag']] = relationship(back_populates='werk') + Werksform: Mapped[int] = mapped_column(ForeignKey('Werksform.ID')) + werksform: Mapped['Werksform'] = relationship(back_populates='werk') + + # one-to-many + veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='werk') + + # many-to-many + genres: Mapped[List['Werk_Genre']] = relationship(back_populates='werk', cascade='all, delete-orphan') + genre_ids: AssociationProxy[List["Genre"]] = association_proxy("genres", "Genre", creator=lambda genre_id: Werk_Genre(Genre=genre_id)) + herausgeber: Mapped[List['Werk_Herausgeber']] = relationship(back_populates='werk', cascade="all, delete-orphan") + herausgeber_ids: AssociationProxy[List['Herausgeber']] = association_proxy("herausgeber", "Herausgeber", creator=lambda hrsg_id: Werk_Herausgeber(Herausgeber=hrsg_id)) class Veroeffentlichung(Base): __tablename__ = 'Veroeffentlichung' + # regular columns ID: Mapped[int] = mapped_column(primary_key=True) - Text: Mapped[int] = mapped_column('Text', ForeignKey('Text.ID')) - Werk: Mapped[int] = mapped_column('Werk', ForeignKey('Werk.ID')) - Pseudonym: Mapped[int] = mapped_column('Pseudonym', ForeignKey('Pseudonym.ID')) AltTitel: Mapped[Optional[str]] AltUntertitel: Mapped[Optional[str]] - pseudonym: Mapped['Pseudonym'] = relationship('Pseudonym', back_populates='veroeffentlichung') - text: Mapped['Text'] = relationship('Text', back_populates='veroeffentlichung') - werk: Mapped['Werk'] = relationship('Werk', back_populates='veroeffentlichung') + # many-to-one + Pseudonym: Mapped[int] = mapped_column(ForeignKey('Pseudonym.ID')) + pseudonym: Mapped['Pseudonym'] = relationship(back_populates='veroeffentlichung') + Text: Mapped[int] = mapped_column(ForeignKey('Text.ID')) + text: Mapped['Text'] = relationship(back_populates='veroeffentlichung') + Werk: Mapped[int] = mapped_column(ForeignKey('Werk.ID')) + werk: Mapped['Werk'] = relationship(back_populates='veroeffentlichung') -t_Text_Genre = Table( - 'Text_Genre', - Base.metadata, - Column('Text', ForeignKey('Text.ID'), primary_key=True), - Column('Genre', ForeignKey('Genre.ID'), primary_key=True) -) +class Text_Genre(Base): + __tablename__ = 'Text_Genre' + + Text: Mapped[int] = mapped_column(ForeignKey("Text.ID"), primary_key=True) + Genre: Mapped[int] = mapped_column(ForeignKey("Genre.ID"), primary_key=True) + + text: Mapped['Text'] = relationship(back_populates="genres") + genre: Mapped['Genre'] = relationship(back_populates="texte") -t_Werk_Genre = Table( - 'Werk_Genre', - Base.metadata, - Column('Werk', ForeignKey('Werk.ID'), primary_key=True), - Column('Genre', ForeignKey('Genre.ID'), primary_key=True) -) +class Werk_Genre(Base): + __tablename__ = 'Werk_Genre' + + Werk: Mapped[int] = mapped_column(ForeignKey('Werk.ID'), primary_key=True) + Genre: Mapped[int] = mapped_column(ForeignKey("Genre.ID"), primary_key=True) + + werk: Mapped['Werk'] = relationship(back_populates="genres") + genre: Mapped['Genre'] = relationship(back_populates="werke") -t_Werk_Herausgeber = Table( - 'Werk_Herausgeber', - Base.metadata, - Column('Herausgeber', ForeignKey('Herausgeber.ID'), primary_key=True), - Column('Werk', ForeignKey('Werk.ID'), primary_key=True) -) +class Werk_Herausgeber(Base): + __tablename__ = 'Werk_Herausgeber' + + Werk: Mapped[int] = mapped_column(ForeignKey('Werk.ID'), primary_key=True) + Herausgeber: Mapped[int] = mapped_column(ForeignKey("Herausgeber.ID"), primary_key=True) + + werk: Mapped['Werk'] = relationship(back_populates="herausgeber") + herausgeber: Mapped['Herausgeber'] = relationship(back_populates="werke") + diff --git a/the_works/views/text.py b/the_works/views/text.py index 54f1437..c36e5c2 100644 --- a/the_works/views/text.py +++ b/the_works/views/text.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, redirect, flash, url_for -from sqlalchemy import select, insert, update, delete +from sqlalchemy import select from the_works.database import db -from the_works.models import Text, Reihe, Sprache, Textform, t_Text_Genre, Genre +from the_works.models import Text, Reihe, Sprache, Textform, Genre bp = Blueprint("text", __name__) @@ -23,8 +23,7 @@ def all(): "tf_id": row.Text.Textform, "Sprache": row.Sprache.Sprache, "s_id": row.Text.Sprache, - "Genre_list": [tg.genre.Genre for tg in row.Text.text_genre], - "g_id_list": row.Text.genres + "Genre_list": [tg.genre.Genre for tg in row.Text.genres] }) return render_template("views/text.html", texte=texte) @@ -35,15 +34,11 @@ def read(id): return render_template("views/text_detail.html", text={"ID": 0}, reihen=db.session.scalars(select(Reihe)), textformen=db.session.scalars(select(Textform)), sprachen=db.session.scalars(select(Sprache)), genres=db.session.scalars(select(Genre))) # all other ids -> update existing entry t = db.session.get(Text, id) - text = { - "ID": t.ID, - "Titel": t.Titel, - "Untertitel": t.Untertitel or "", - "Reihe": t.Reihe or "", - "Textform": t.Textform, - "Sprache": t.Sprache, - "Genres": t.genres - } + if not t: + raise ValueError(f"Text with ID {id} not found") + text = t.asdict() + text["Genres"] = t.genre_ids + return render_template("views/text_detail.html", text=text, reihen=db.session.scalars(select(Reihe)), textformen=db.session.scalars(select(Textform)), sprachen=db.session.scalars(select(Sprache)), genres=db.session.scalars(select(Genre))) @bp.route("/text/create", methods=["POST"]) @@ -56,7 +51,7 @@ def create(): Sprache = request.form["form_Sprache"] ) for g in request.form.getlist("form_Genres"): - text.genres.append(g) + text.genre_ids.append(int(g)) db.session.add(text) db.session.commit() flash("Eintrag erfolgreich hinzugefügt") @@ -75,11 +70,11 @@ def update(id): text.Sprache = request.form["form_Sprache"] # update genre list by removing genres not in form selection and adding selected ones not currently in list - form_set = set(map(lambda g: int(g), request.form.getlist("form_Genre"))) - for g in set(text.genres) - form_set: - text.genres.remove(g) - for g in form_set - set(text.genres): - text.genres.append(g) + form_set = set(map(int, request.form.getlist("form_Genre"))) + for g in set(text.genre_ids) - form_set: + text.genre_ids.remove(g) + for g in form_set - set(text.genre_ids): + text.genre_ids.append(g) # commit changes db.session.commit() diff --git a/the_works/views/werk.py b/the_works/views/werk.py index 2b48b85..fb151db 100644 --- a/the_works/views/werk.py +++ b/the_works/views/werk.py @@ -1,8 +1,7 @@ from flask import Blueprint, render_template, request, redirect, flash, url_for -from sqlalchemy import select, insert, update, delete +from sqlalchemy import select from the_works.database import db -from the_works.models import Werk, Reihe, Verlag, Werksform, t_Werk_Genre, Genre, t_Werk_Herausgeber, Herausgeber, Titelbild -from the_works.views import titelbild as tb +from the_works.models import Werk, Reihe, Verlag, Werksform, Genre, Herausgeber, Titelbild bp = Blueprint("werk", __name__) @@ -29,11 +28,11 @@ def all(): "ISSN": row.Werk.ISSN or "", "Preis": row.Werk.Preis or "", # "Titelbild": url_for("titelbild.thumbnail", id=row.Werk.Titelbild) if row.Werk.Titelbild else "", - "Titelbild": db.session.get(Titelbild, row.Werk.Titelbild)._asdict_with_urls() if row.Werk.Titelbild else "", + "Titelbild": db.session.get(Titelbild, row.Werk.Titelbild).asdict_with_urls() if row.Werk.Titelbild else "", "Klappentext": row.Werk.Klappentext or "", "Anmerkungen": row.Werk.Anmerkungen or "", - "Herausgeber_list": [wh.herausgeber.Name for wh in row.Werk.werk_herausgeber], - "Genre_list": [wg.genre.Genre for wg in row.Werk.werk_genre], + "Herausgeber_list": [wh.herausgeber.Name for wh in row.Werk.herausgeber], + "Genre_list": [wg.genre.Genre for wg in row.Werk.genres], }) return render_template("views/werk.html", werke=werke) @@ -41,17 +40,19 @@ def all(): @bp.route("/werk/read/") def read(id): # prepare Titelbilder as dict including URLs for thumbnail and full pic - titelbilder = map(lambda t: t._asdict_with_urls(), db.session.scalars(select(Titelbild))) + titelbilder = map(lambda t: t.asdict_with_urls(), db.session.scalars(select(Titelbild))) # id of zero -> return empty data if id == 0: return render_template("views/werk_detail.html", werk={"ID": 0, "Erscheinungsdatum": ""}, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), titelbilder=titelbilder) # all other ids -> read existing entry from DB and return as dict - werk = db.session.get(Werk, id) - if not werk: + w = db.session.get(Werk, id) + if not w: raise ValueError(f"Werk with ID {id} not found") - werk = werk._asdict() + werk = w.asdict() + werk["Genres"] = w.genre_ids + werk["Herausgeber"] = w.herausgeber_ids return render_template("views/werk_detail.html", werk=werk, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), titelbilder=titelbilder) @@ -75,9 +76,9 @@ def create(): Anmerkungen = request.form["form_Anmerkungen"] or None ) for g in request.form.getlist("form_Genre"): - werk.genres.append(g) + werk.genre_ids.append(g) for h in request.form.getlist("form_Herausgeber"): - werk.herausgeber.append(h) + werk.herausgeber_ids.append(h) db.session.add(werk) db.session.commit() flash("Eintrag erfolgreich hinzugefügt") @@ -106,18 +107,18 @@ def update(id): werk.Anmerkungen = request.form["form_Anmerkungen"] or None # update associated values: Genre - form_set = set(map(lambda g: int(g), request.form.getlist("form_Genre"))) - for g in set(werk.genres) - form_set: - werk.genres.remove(g) - for g in form_set - set(werk.genres): - werk.genres.append(g) + form_set = set(map(int, request.form.getlist("form_Genre"))) + for g in set(werk.genre_ids) - form_set: + werk.genre_ids.remove(g) + for g in form_set - set(werk.genre_ids): + werk.genre_ids.append(g) # update associated values: Herausgeber - form_set = set(map(lambda h: int(h), request.form.getlist("form_Herausgeber"))) - for h in set(werk.herausgeber) - form_set: - werk.herausgeber.remove(h) - for h in form_set - set(werk.herausgeber): - werk.herausgeber.append(h) + form_set = set(map(int, request.form.getlist("form_Herausgeber"))) + for h in set(werk.herausgeber_ids) - form_set: + werk.herausgeber_ids.remove(h) + for h in form_set - set(werk.herausgeber_ids): + werk.herausgeber_ids.append(h) # commit changes db.session.commit()