streamlined declarative mapping code, (re)added assoc. proxies, fixed bugs caused by the switch to declarative

This commit is contained in:
eclipse 2025-07-19 00:26:27 +02:00
parent 091c977f03
commit 3dd08fb4c4
3 changed files with 136 additions and 114 deletions

View File

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

View File

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

View File

@ -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/<int:id>")
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()