Merge branch 'move_to_declarative'
This commit is contained in:
commit
b8edb958d3
13
.flaskenv
Normal file
13
.flaskenv
Normal file
@ -0,0 +1,13 @@
|
||||
# Non-critical configuration values
|
||||
# Read automatically if python-dotenv is installed
|
||||
FLASK_APP = "the_works"
|
||||
FLASK_ENV = "development"
|
||||
FLASK_SECRET_KEY = "f8148ee5d95b0a67122b1cab9993f637a6bf29528f584a9f1575af1a55566748"
|
||||
FLASK_TESTING = False
|
||||
#FLASK_MAX_CONTENT_LENGTH = 1024 * 1024
|
||||
FLASK_DEBUG = True
|
||||
|
||||
FLASK_SQLALCHEMY_DATABASE_URI = "sqlite:///../the_works.sqlite"
|
||||
FLASK_SQLALCHEMY_ECHO = False
|
||||
FLASK_SQLALCHEMY_RECORD_QUERIES = True
|
||||
|
||||
79
README.md
79
README.md
@ -1,32 +1,69 @@
|
||||
# The Works
|
||||
# the_works – a publication management tool for writers
|
||||
|
||||
This software project
|
||||
|
||||
* supports managing texts and publications with multiple languages, pseudonyms, genres, publishers, series, editions, and more
|
||||
* supports storage of cover images
|
||||
* focuses on ease-of-use, safety, and speed
|
||||
* written and field-tested by an actual writer
|
||||
|
||||
the_works also is
|
||||
|
||||
* based on common technologies like Python, SQL, and HTML/JavaScript
|
||||
* underlying frameworks are Flask and SQLAlchemy
|
||||
* includes test coverage (using pytest)
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
The file `.flaskenv` contains the default configuration. Flask reads the file at startup and adds its key-value-pairs to the runtime environment as environment variables. When the Flask app object is being created in `__init__.py`, all environment variables that start with the prefix "FLASK_" get added to the app configuration.
|
||||
|
||||
This is true for any prefixed environment variable, not just the ones from `.flaskenv`. It is therefore possible to set additional config parameters by hand before running the app. Just make sure to prefix the variable name with "FLASK_".
|
||||
|
||||
Configuration values from the runtime environment can be overridden by using Flask's `-e` command line switch to pass a second config file to the app. This file gets processed the same way as `.flaskenv`, which means that all its keys must be prefixed with "FLASK_". These vars take precedence over the default configuration.
|
||||
|
||||
Finally, you can override config settings with Python during the Flask app's instantiation through the factory. To do this, simply pass a dictionary with (unprefixed) key-value-pairs to `create_app()` method as named parameter `config`. Settings passed this way take precedence over those from the default configuration and additional config files.
|
||||
|
||||
|
||||
|
||||
|
||||
## Flask commands
|
||||
|
||||
Execute commands with `python -m flask --app the_works <command>`
|
||||
Execute commands with `python -m flask <command>`. You don't need to specify `--app the_works` as long as the environment variable "FLASK_APP" is set to "the_works"; the default configuration file does this.
|
||||
|
||||
Available commands:
|
||||
|
||||
* `run`: Serve app (don't use for production).
|
||||
<!--* `init-db`: Create empty SQLite database `works.sqlite` in project root. BE CAREFUL: If a database already exists, it will be deleted with everything in it. // 5/25: ich hab die Fkt. wieder rausgenommen, aber ich könnte sie eigentlich prima wieder einbauen … -->
|
||||
* `shell`: start a shell within the app context (I can i.e. import specific table models and test the data structures returned by ORM methods like select())
|
||||
* `shell`: start a shell within the app context (I can i.e. import specific table models and test ORM data structures)
|
||||
*
|
||||
|
||||
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Python Packages
|
||||
|
||||
flask
|
||||
flask-sqlalchemy
|
||||
python-dotenv
|
||||
Pillow
|
||||
pytest
|
||||
flask-debugtoolbar (optional)
|
||||
Required pip packages
|
||||
|
||||
### CSS and Javascript
|
||||
* flask
|
||||
* flask-sqlalchemy
|
||||
* python-dotenv
|
||||
* Pillow
|
||||
* pytest
|
||||
|
||||
PicoCSS (regular version) + SwitchColorMode.js (from Yohn's fork)
|
||||
DataTables.[js|css]
|
||||
Optional pip packages
|
||||
|
||||
* flask-debugtoolbar (optional)
|
||||
* sqlacodegen (optional; only used from the command line during development)
|
||||
|
||||
### CSS and Javascript resources
|
||||
|
||||
* any regular (not classless) stylesheet from [PicoCSS](https://picocss.com) for general styling
|
||||
* `SwitchColorMode.js` from [Yohn's fork of PicoCSS](https://yohn.github.io/PicoCSS/) to enable color mode switching
|
||||
* [DataTables](https://datatables.net/) (JS and CSS components) to enable ordering and filtering result tables
|
||||
* DataTables requires [jQuery](https://jquery.com/), which can either be bundled with DataTables itself or installed as a separate resource
|
||||
|
||||
### Icons
|
||||
|
||||
@ -37,6 +74,17 @@ some icons from heroicons.com
|
||||
|
||||
## Other useful stuff
|
||||
|
||||
### Generate SQLAlchemy code from an existing database
|
||||
|
||||
Right now the_works reflects an existing database in order to infer the underlying data models for SQLAlchemy. Of course, this only works if all the tables already exist in the database. If the_works is run with an empty database (this happens when running tests, for example), the app will create fresh tables in the database. The necessary information about the tables can be generated from an existing database with the help of [sqlacodegen](https://pypi.org/project/sqlacodegen/). Just run this command inside the project's root directory:
|
||||
|
||||
`sqlacodegen --generator tables sqlite:///path/to/good/db.sqlite > ./the_works/tables.py`
|
||||
|
||||
The tool sqlacodegen can also generate Python code declaring the data models directly. This would make the use of reflection obsolete. The command would be:
|
||||
|
||||
`sqlacodegen --generator declarative sqlite:///path/to/good/db.sqlite > outputfile.py`
|
||||
|
||||
|
||||
### Export database schema
|
||||
|
||||
Method 1: `sqlite3 the_works.sqlite .schema > outputfile.sql`
|
||||
@ -48,13 +96,6 @@ Method 2: Open DB in SQLitebrowser and use File -> Export -> Database to SQL fil
|
||||
* overwrite old schema (DROP TABLE, then CREATE TABLE)
|
||||
|
||||
|
||||
### Generate declarative SQLAlchemy code
|
||||
|
||||
Right now the_works reflects an existing database in order to infer the underlying data models for SQLAlchemy. If I wanted to declare the data models directly instead of using reflection, I would need declarative code. This code can be generated from an existing database with the help of [sqlacodegen](https://pypi.org/project/sqlacodegen/):
|
||||
|
||||
`sqlacodegen sqlite:///path/to/db.sqlite > outputfile.py`
|
||||
|
||||
|
||||
### Generate `requirements.txt`
|
||||
|
||||
I use [pipreqs](https://pypi.org/project/pipreqs/) to generate the file `requirements.txt`. The package scans all source files for import statements and uses those to extract all required Pip packages.
|
||||
39
tests/conftest.py
Normal file
39
tests/conftest.py
Normal file
@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
from the_works import create_app
|
||||
from the_works.database import db as _db
|
||||
|
||||
TEST_DATABASE_URI = "sqlite:///:memory:"
|
||||
|
||||
@pytest.fixture()
|
||||
def app():
|
||||
test_config = {
|
||||
"ENV": "Testing",
|
||||
"SQLALCHEMY_DATABASE_URI": TEST_DATABASE_URI,
|
||||
"SECRET_KEY": "This is my very secret key",
|
||||
"TESTING": True
|
||||
}
|
||||
app = create_app(test_config)
|
||||
|
||||
# other setup can go here
|
||||
|
||||
yield app
|
||||
|
||||
# clean up / reset resources here
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db(app):
|
||||
with app.app_context():
|
||||
yield _db
|
||||
_db.drop_all()
|
||||
_db.create_all()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(app):
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def runner(app):
|
||||
return app.test_cli_runner()
|
||||
27
tests/integration/test_int_genre.py
Normal file
27
tests/integration/test_int_genre.py
Normal file
@ -0,0 +1,27 @@
|
||||
from sqlalchemy import select
|
||||
from the_works.database import db
|
||||
from the_works.models import Genre
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
import pytest
|
||||
|
||||
def test_genre_create(client, app):
|
||||
"""Integrated testing of adding a Genre record."""
|
||||
response = client.post("/genre/create", data={"form_Genre": "Test-Genre"}, follow_redirects=True)
|
||||
|
||||
# assert there was exactly 1 redirect
|
||||
assert len(response.history) == 1
|
||||
# assert the redirect led to the correct page
|
||||
assert response.request.path == "/genre/all"
|
||||
assert response.status_code == 200
|
||||
|
||||
# assert record was successfully added to DB
|
||||
with app.app_context():
|
||||
genre = db.session.scalars(select(Genre).where(Genre.Genre == "Test-Genre")).all()
|
||||
assert len(genre) == 1
|
||||
assert isinstance(genre[0], Genre)
|
||||
|
||||
# assert uniqueness of records
|
||||
with pytest.raises(IntegrityError) as excinfo:
|
||||
response = client.post("/genre/create", data={"form_Genre": "Test-Genre"})
|
||||
assert "UNIQUE constraint failed" in str(excinfo.value)
|
||||
|
||||
3
tests/unit/test_home.py
Normal file
3
tests/unit/test_home.py
Normal file
@ -0,0 +1,3 @@
|
||||
def test_request_home_startpage(client):
|
||||
response = client.get("/")
|
||||
assert response.data.startswith(b"<!DOCTYPE html>\n")
|
||||
58
tests/unit/test_unit_genre.py
Normal file
58
tests/unit/test_unit_genre.py
Normal file
@ -0,0 +1,58 @@
|
||||
from the_works.models import Genre
|
||||
import pytest
|
||||
|
||||
|
||||
def test_genre_all(client, db, mocker):
|
||||
"""Test view all() from genre.py."""
|
||||
|
||||
# mock database function
|
||||
# Note: The original method returns an sqlalchemy.engine.Result.ScalarResult, not a list, but the template code
|
||||
# uses the return value in a way that works for both ScalarResult and list
|
||||
mocker.patch("the_works.database.db.session.scalars", return_value=[
|
||||
Genre(ID=4, Genre="bla"),
|
||||
Genre(ID=26, Genre="blubb")
|
||||
])
|
||||
|
||||
# test case: get request
|
||||
response = client.get("/genre")
|
||||
assert response.status_code == 200
|
||||
assert response.data.count(b'<tr id="genre-') == 2
|
||||
|
||||
# test case: post request
|
||||
response = client.post("/genre")
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_genre_create(client, db, mocker):
|
||||
"""Test view create() from genre.py."""
|
||||
|
||||
# mock database function
|
||||
mocker.patch("the_works.database.db.session.add")
|
||||
|
||||
# test a POST request with good data
|
||||
response = client.post("/genre/create", data={"form_Genre": "Testname"}, follow_redirects=True)
|
||||
# exactly 1 redirect
|
||||
assert len(response.history) == 1
|
||||
# redirect to the right page
|
||||
assert response.request.path == "/genre/all"
|
||||
assert response.status_code == 200
|
||||
|
||||
# test a POST request with no form data
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
response = client.post("/genre/create", data={})
|
||||
assert "value can't be empty" in str(excinfo.value)
|
||||
|
||||
# test a POST request with bad form data
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
response = client.post("/genre/create", data={"wrong_key": "Genrename"})
|
||||
assert "value can't be empty" in str(excinfo.value)
|
||||
|
||||
# test a POST request with empty form data
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
response = client.post("/genre/create", data={"form_genre": ""})
|
||||
assert "value can't be empty" in str(excinfo.value)
|
||||
|
||||
# test a GET request
|
||||
response = client.get("/genre/create", query_string={"form_Genre": "GET-Genre"})
|
||||
assert response.status_code == 405
|
||||
|
||||
4
tests/unit/test_unit_home.py
Normal file
4
tests/unit/test_unit_home.py
Normal file
@ -0,0 +1,4 @@
|
||||
def test_home_startpage(client):
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.data.startswith(b"<!DOCTYPE html>\n")
|
||||
@ -1,25 +1,35 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask
|
||||
from the_works.database import init_db
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
def create_app():
|
||||
# this import is not strictly necessary but it forces pipreqs-to include dotenv when generating `requirements.txt`
|
||||
import dotenv
|
||||
|
||||
from the_works.database import init_db
|
||||
from the_works.views import home, text, werk, verlag, sprache, textform, werksform, genre, pseudonym, reihe, herausgeber, veroeffentlichung, titelbild
|
||||
|
||||
#from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
def create_app(config=None):
|
||||
app = Flask(__name__)
|
||||
|
||||
# read config values
|
||||
load_dotenv()
|
||||
# read all config values from environment that are prefixed with "FLASK_"
|
||||
app.config.from_prefixed_env()
|
||||
if os.getenv("SQLALCHEMY_DATABASE_DIALECT") == "sqlite":
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///" + os.path.abspath(app.root_path + "/..") + "/" + os.getenv("SQLALCHEMY_DATABASE_SQLITE_FILENAME")
|
||||
else:
|
||||
exit("no SQLite database URI given; exiting")
|
||||
|
||||
# some #DEBUG configuration
|
||||
# toolbar = DebugToolbarExtension(app) #DEBUG
|
||||
# app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False #DEBUG
|
||||
|
||||
# use config from function parameter if present
|
||||
if config:
|
||||
app.config.update(config)
|
||||
|
||||
# some #DEBUG output
|
||||
print(f"Current Environment: {app.config['ENV'] if 'ENV' in app.config.keys() else 'ENV is not set'}") #DEBUG
|
||||
print(app.config) #DEBUG
|
||||
|
||||
# initialize database
|
||||
init_db(app)
|
||||
|
||||
# register blueprints
|
||||
from the_works.views import home, text, werk, verlag, sprache, textform, werksform, genre, pseudonym, reihe, herausgeber, veroeffentlichung, titelbild
|
||||
app.register_blueprint(genre.bp)
|
||||
app.register_blueprint(herausgeber.bp)
|
||||
app.register_blueprint(home.bp)
|
||||
@ -34,24 +44,15 @@ def create_app():
|
||||
app.register_blueprint(veroeffentlichung.bp)
|
||||
app.register_blueprint(titelbild.bp)
|
||||
|
||||
app.config["MAX_CONTENT_LENGTH"] = 2 * 1024 * 1024
|
||||
|
||||
### DEBUG
|
||||
# toolbar = DebugToolbarExtension(app)
|
||||
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
|
||||
app.config["SQLALCHEMY_ECHO"] = True
|
||||
app.config['SQLALCHEMY_RECORD_QUERIES'] = os.getenv("SQLALCHEMY_RECORD_QUERIES")
|
||||
print(f"Current Environment: " + app.config['ENVIRONMENT'])
|
||||
|
||||
# register helper function
|
||||
app.jinja_env.globals.update(sizeof_fmt=sizeof_fmt)
|
||||
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# helper function to print formatted file size from https://stackoverflow.com/a/1094933
|
||||
def sizeof_fmt(num, suffix="B"):
|
||||
if type(num) == "String":
|
||||
# helper function to print formatted file size; [source](https://stackoverflow.com/a/1094933)
|
||||
def sizeof_fmt(num: int | str, suffix: str = "B") -> str:
|
||||
if isinstance(num, str):
|
||||
num = int(num)
|
||||
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
|
||||
if abs(num) < 1024.0:
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from the_works.models import Base
|
||||
|
||||
db = SQLAlchemy()
|
||||
# Instantiate the SQLAlchemy around the database schema's declarative mapping
|
||||
db = SQLAlchemy(model_class=Base)
|
||||
|
||||
def init_db(app):
|
||||
# initialize the Flask app with the flask_sqlalchemy extension
|
||||
db.init_app(app)
|
||||
|
||||
# create nonexistent tables in DB
|
||||
with app.app_context():
|
||||
db.reflect()
|
||||
db.create_all()
|
||||
|
||||
@ -1,121 +1,279 @@
|
||||
from the_works.database import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
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, validates
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from flask import url_for
|
||||
|
||||
# add method to sqlalchemy.orm.decl_api.Model
|
||||
def _asdict(self):
|
||||
d = {}
|
||||
for col in self.__table__.c:
|
||||
if type(col.type) == db.types.BLOB:
|
||||
d[col.key] = "Blob (NULL)" if self.__getattribute__(col.key) == 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
|
||||
return d
|
||||
db.Model._asdict = _asdict
|
||||
|
||||
# override repr() method from sqlalchemy.orm.decl_api.Model
|
||||
def __repr__(self):
|
||||
return f"{type(self).__name__}({str(self._asdict())})"
|
||||
db.Model.__repr__ = __repr__
|
||||
class Base(DeclarativeBase):
|
||||
def asdict(self) -> dict:
|
||||
d = {}
|
||||
for col in self.__table__.c:
|
||||
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:
|
||||
if isinstance(value := self.__getattribute__(col.key), str) and len(value) > 50:
|
||||
d[col.key] = value[:48] + '...'
|
||||
else:
|
||||
d[col.key] = value
|
||||
return d
|
||||
|
||||
class Text(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Text']
|
||||
reihe = relationship("Reihe", back_populates="text")
|
||||
textform = relationship("Textform", back_populates="text")
|
||||
sprache = relationship("Sprache", back_populates="text")
|
||||
veroeffentlichung = relationship("Veroeffentlichung", back_populates="text")
|
||||
text_genre = relationship("Text_Genre", back_populates="text", cascade="save-update, merge, delete, delete-orphan")
|
||||
genres = association_proxy("text_genre", "Genre")
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}({str(self.asdict())})"
|
||||
|
||||
class Werk(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Werk']
|
||||
reihe = relationship("Reihe", back_populates="werk")
|
||||
verlag = relationship("Verlag", back_populates="werk")
|
||||
werksform = relationship("Werksform", back_populates="werk")
|
||||
veroeffentlichung = relationship("Veroeffentlichung", back_populates="werk")
|
||||
werk_genre = relationship("Werk_Genre", back_populates="werk", cascade="save-update, merge, delete, delete-orphan")
|
||||
# titelbild = relationship("Titelbild", back_populates="werk", cascade="all, delete-orphan", single_parent=True)
|
||||
titelbild = relationship("Titelbild", back_populates="werk")
|
||||
genres = association_proxy("werk_genre", "Genre")
|
||||
werk_herausgeber = relationship("Werk_Herausgeber", back_populates="werk", cascade="save-update, merge, delete, delete-orphan")
|
||||
herausgeber = association_proxy("werk_herausgeber", "Herausgeber")
|
||||
def validate_not_empty(self, value):
|
||||
if not value:
|
||||
raise ValueError("value can't be empty")
|
||||
return value
|
||||
|
||||
class Veroeffentlichung(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Veroeffentlichung']
|
||||
text = relationship("Text", back_populates="veroeffentlichung")
|
||||
werk = relationship("Werk", back_populates="veroeffentlichung")
|
||||
pseudonym = relationship("Pseudonym", back_populates="veroeffentlichung")
|
||||
|
||||
class Reihe(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Reihe']
|
||||
text = relationship("Text", back_populates="reihe")
|
||||
werk = relationship("Werk", back_populates="reihe")
|
||||
verlag = relationship("Verlag", back_populates="reihe")
|
||||
class Genre(Base):
|
||||
__tablename__ = 'Genre'
|
||||
|
||||
class Verlag(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Verlag']
|
||||
werk = relationship("Werk", back_populates="verlag")
|
||||
reihe = relationship("Reihe", back_populates="verlag")
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Genre: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
class Sprache(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Sprache']
|
||||
text = relationship("Text", back_populates="sprache")
|
||||
texte: Mapped[List['Text_Genre']] = relationship(back_populates='genre')
|
||||
werke: Mapped[List['Werk_Genre']] = relationship(back_populates='genre')
|
||||
|
||||
class Textform(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Textform']
|
||||
text = relationship("Text", back_populates="textform")
|
||||
@validates("Genre")
|
||||
def validate_genre(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
class Werksform(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Werksform']
|
||||
werk = relationship("Werk", back_populates="werksform")
|
||||
class Herausgeber(Base):
|
||||
__tablename__ = 'Herausgeber'
|
||||
|
||||
class Genre(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Genre']
|
||||
text_genre = relationship("Text_Genre", back_populates="genre")
|
||||
werk_genre = relationship("Werk_Genre", back_populates="genre")
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Name: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
class Pseudonym(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Pseudonym']
|
||||
veroeffentlichung = relationship("Veroeffentlichung", back_populates="pseudonym")
|
||||
werke: Mapped[List['Werk_Herausgeber']] = relationship(back_populates='herausgeber')
|
||||
|
||||
class Herausgeber(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Herausgeber']
|
||||
werk_herausgeber = relationship("Werk_Herausgeber", back_populates="herausgeber")
|
||||
@validates("Herausgeber")
|
||||
def validate_herausgeber(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
class Titelbild(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Titelbild']
|
||||
werk = relationship("Werk", back_populates="titelbild")
|
||||
|
||||
def _asdict_with_urls(self):
|
||||
tb = self._asdict()
|
||||
class Pseudonym(Base):
|
||||
__tablename__ = 'Pseudonym'
|
||||
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Pseudonym: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='pseudonym')
|
||||
|
||||
@validates("Pseudonym")
|
||||
def validate_pseudonym(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
|
||||
class Sprache(Base):
|
||||
__tablename__ = 'Sprache'
|
||||
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Sprache: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
text: Mapped[List['Text']] = relationship(back_populates='sprache')
|
||||
|
||||
@validates("Sprache")
|
||||
def validate_sprache(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
|
||||
class Textform(Base):
|
||||
__tablename__ = 'Textform'
|
||||
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Textform: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
text: Mapped[List['Text']] = relationship(back_populates='textform')
|
||||
|
||||
@validates("Textform")
|
||||
def validate_textform(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
|
||||
class Titelbild(Base):
|
||||
__tablename__ = 'Titelbild'
|
||||
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Mimetype: Mapped[str]
|
||||
Dateiname: Mapped[str]
|
||||
Dateigroesse: Mapped[int]
|
||||
Breite: Mapped[int]
|
||||
Hoehe: Mapped[int]
|
||||
Bild: Mapped[bytes]
|
||||
Thumbnail: Mapped[bytes]
|
||||
sha256: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
werk: Mapped[List['Werk']] = relationship(back_populates='titelbild')
|
||||
|
||||
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
|
||||
|
||||
class Text_Genre(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Text_Genre']
|
||||
text = relationship("Text", back_populates="text_genre")
|
||||
genre = relationship("Genre", back_populates="text_genre")
|
||||
@validates("sha256")
|
||||
def validate_titelbild(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
def __init__(self, genre: int):
|
||||
self.Genre = genre
|
||||
|
||||
class Werk_Genre(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Werk_Genre']
|
||||
werk = relationship("Werk", back_populates="werk_genre")
|
||||
genre = relationship("Genre", back_populates="werk_genre")
|
||||
class Verlag(Base):
|
||||
__tablename__ = 'Verlag'
|
||||
|
||||
def __init__(self, genre: int):
|
||||
self.Genre = genre
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Verlag: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
class Werk_Herausgeber(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Werk_Herausgeber']
|
||||
werk = relationship("Werk", back_populates="werk_herausgeber")
|
||||
herausgeber = relationship("Herausgeber", back_populates="werk_herausgeber")
|
||||
reihe: Mapped[List['Reihe']] = relationship(back_populates='verlag')
|
||||
werk: Mapped[List['Werk']] = relationship(back_populates='verlag')
|
||||
|
||||
def __init__(self, hrsg: int):
|
||||
self.Herausgeber = hrsg
|
||||
@validates("Verlag")
|
||||
def validate_verlag(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
|
||||
class Werksform(Base):
|
||||
__tablename__ = 'Werksform'
|
||||
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Werksform: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
werk: Mapped[List['Werk']] = relationship(back_populates='werksform')
|
||||
|
||||
@validates("Werksform")
|
||||
def validate_werksform(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
|
||||
class Reihe(Base):
|
||||
__tablename__ = 'Reihe'
|
||||
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Titel: Mapped[str]
|
||||
|
||||
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')
|
||||
|
||||
@validates("Titel")
|
||||
def validate_titel(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
|
||||
class Text(Base):
|
||||
__tablename__ = 'Text'
|
||||
|
||||
# regular columns
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Titel: Mapped[str]
|
||||
Untertitel: Mapped[Optional[str]]
|
||||
|
||||
# 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))
|
||||
|
||||
@validates("Titel")
|
||||
def validate_titel(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
|
||||
class Werk(Base):
|
||||
__tablename__ = 'Werk'
|
||||
|
||||
# regular columns
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
Titel: Mapped[str]
|
||||
Untertitel: Mapped[Optional[str]]
|
||||
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]]
|
||||
Klappentext: Mapped[Optional[str]]
|
||||
Anmerkungen: Mapped[Optional[str]]
|
||||
|
||||
# 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))
|
||||
|
||||
@validates("Titel")
|
||||
def validate_titel(self, key, value):
|
||||
return self.validate_not_empty(value)
|
||||
|
||||
|
||||
class Veroeffentlichung(Base):
|
||||
__tablename__ = 'Veroeffentlichung'
|
||||
|
||||
# regular columns
|
||||
ID: Mapped[int] = mapped_column(primary_key=True)
|
||||
AltTitel: Mapped[Optional[str]]
|
||||
AltUntertitel: Mapped[Optional[str]]
|
||||
|
||||
# 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')
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
121
the_works/old_models.py
Normal file
121
the_works/old_models.py
Normal file
@ -0,0 +1,121 @@
|
||||
from the_works.database import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from flask import url_for
|
||||
import sys
|
||||
|
||||
# add method to sqlalchemy.orm.decl_api.Model
|
||||
def _asdict(self):
|
||||
d = {}
|
||||
for col in self.__table__.c:
|
||||
if type(col.type) == db.types.BLOB:
|
||||
d[col.key] = "Blob (NULL)" if self.__getattribute__(col.key) == 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
|
||||
return d
|
||||
db.Model._asdict = _asdict
|
||||
|
||||
# override repr() method from sqlalchemy.orm.decl_api.Model
|
||||
def __repr__(self):
|
||||
return f"{type(self).__name__}({str(self._asdict())})"
|
||||
db.Model.__repr__ = __repr__
|
||||
|
||||
class Text(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Text']
|
||||
reihe = relationship("Reihe", back_populates="text")
|
||||
textform = relationship("Textform", back_populates="text")
|
||||
sprache = relationship("Sprache", back_populates="text")
|
||||
veroeffentlichung = relationship("Veroeffentlichung", back_populates="text")
|
||||
text_genre = relationship("Text_Genre", back_populates="text", cascade="save-update, merge, delete, delete-orphan")
|
||||
genres = association_proxy("text_genre", "Genre")
|
||||
|
||||
class Werk(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Werk']
|
||||
reihe = relationship("Reihe", back_populates="werk")
|
||||
verlag = relationship("Verlag", back_populates="werk")
|
||||
werksform = relationship("Werksform", back_populates="werk")
|
||||
veroeffentlichung = relationship("Veroeffentlichung", back_populates="werk")
|
||||
werk_genre = relationship("Werk_Genre", back_populates="werk", cascade="save-update, merge, delete, delete-orphan")
|
||||
# titelbild = relationship("Titelbild", back_populates="werk", cascade="all, delete-orphan", single_parent=True)
|
||||
titelbild = relationship("Titelbild", back_populates="werk")
|
||||
genres = association_proxy("werk_genre", "Genre")
|
||||
werk_herausgeber = relationship("Werk_Herausgeber", back_populates="werk", cascade="save-update, merge, delete, delete-orphan")
|
||||
herausgeber = association_proxy("werk_herausgeber", "Herausgeber")
|
||||
|
||||
class Veroeffentlichung(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Veroeffentlichung']
|
||||
text = relationship("Text", back_populates="veroeffentlichung")
|
||||
werk = relationship("Werk", back_populates="veroeffentlichung")
|
||||
pseudonym = relationship("Pseudonym", back_populates="veroeffentlichung")
|
||||
|
||||
class Reihe(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Reihe']
|
||||
text = relationship("Text", back_populates="reihe")
|
||||
werk = relationship("Werk", back_populates="reihe")
|
||||
verlag = relationship("Verlag", back_populates="reihe")
|
||||
|
||||
class Verlag(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Verlag']
|
||||
werk = relationship("Werk", back_populates="verlag")
|
||||
reihe = relationship("Reihe", back_populates="verlag")
|
||||
|
||||
class Sprache(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Sprache']
|
||||
text = relationship("Text", back_populates="sprache")
|
||||
|
||||
class Textform(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Textform']
|
||||
text = relationship("Text", back_populates="textform")
|
||||
|
||||
class Werksform(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Werksform']
|
||||
werk = relationship("Werk", back_populates="werksform")
|
||||
|
||||
class Genre(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Genre']
|
||||
text_genre = relationship("Text_Genre", back_populates="genre")
|
||||
werk_genre = relationship("Werk_Genre", back_populates="genre")
|
||||
|
||||
class Pseudonym(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Pseudonym']
|
||||
veroeffentlichung = relationship("Veroeffentlichung", back_populates="pseudonym")
|
||||
|
||||
class Herausgeber(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Herausgeber']
|
||||
werk_herausgeber = relationship("Werk_Herausgeber", back_populates="herausgeber")
|
||||
|
||||
class Titelbild(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Titelbild']
|
||||
werk = relationship("Werk", back_populates="titelbild")
|
||||
|
||||
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
|
||||
|
||||
class Text_Genre(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Text_Genre']
|
||||
text = relationship("Text", back_populates="text_genre")
|
||||
genre = relationship("Genre", back_populates="text_genre")
|
||||
|
||||
def __init__(self, genre: int):
|
||||
self.Genre = genre
|
||||
|
||||
class Werk_Genre(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Werk_Genre']
|
||||
werk = relationship("Werk", back_populates="werk_genre")
|
||||
genre = relationship("Genre", back_populates="werk_genre")
|
||||
|
||||
def __init__(self, genre: int):
|
||||
self.Genre = genre
|
||||
|
||||
class Werk_Herausgeber(db.Model):
|
||||
__table__ = db.Model.metadata.tables['Werk_Herausgeber']
|
||||
werk = relationship("Werk", back_populates="werk_herausgeber")
|
||||
herausgeber = relationship("Herausgeber", back_populates="werk_herausgeber")
|
||||
|
||||
def __init__(self, hrsg: int):
|
||||
self.Herausgeber = hrsg
|
||||
|
||||
125
the_works/tables.py
Normal file
125
the_works/tables.py
Normal file
@ -0,0 +1,125 @@
|
||||
# this is the verbatim output of `sqlacodegen --generator tables sqlite:///path/to/the_works.sqlite`, run inside project root
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, MetaData, Table, Text
|
||||
|
||||
metadata = MetaData()
|
||||
|
||||
|
||||
t_Genre = Table(
|
||||
'Genre', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Genre', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Herausgeber = Table(
|
||||
'Herausgeber', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Name', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Pseudonym = Table(
|
||||
'Pseudonym', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Pseudonym', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Sprache = Table(
|
||||
'Sprache', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Sprache', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Textform = Table(
|
||||
'Textform', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Textform', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Titelbild = Table(
|
||||
'Titelbild', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Mimetype', Text, nullable=False),
|
||||
Column('Dateiname', Text, nullable=False),
|
||||
Column('Dateigroesse', Integer, nullable=False),
|
||||
Column('Breite', Integer, nullable=False),
|
||||
Column('Hoehe', Integer, nullable=False),
|
||||
Column('Bild', LargeBinary, nullable=False),
|
||||
Column('Thumbnail', LargeBinary, nullable=False),
|
||||
Column('sha256', Text, nullable=False, unique=True)
|
||||
)
|
||||
|
||||
t_Verlag = Table(
|
||||
'Verlag', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Verlag', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Werksform = Table(
|
||||
'Werksform', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Werksform', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Reihe = Table(
|
||||
'Reihe', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Titel', Text, nullable=False),
|
||||
Column('Verlag', ForeignKey('Verlag.ID'))
|
||||
)
|
||||
|
||||
t_Text = Table(
|
||||
'Text', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Titel', Text, nullable=False),
|
||||
Column('Untertitel', Text),
|
||||
Column('Reihe', ForeignKey('Reihe.ID')),
|
||||
Column('Textform', ForeignKey('Textform.ID')),
|
||||
Column('Sprache', ForeignKey('Sprache.ID'))
|
||||
)
|
||||
|
||||
t_Werk = Table(
|
||||
'Werk', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Titel', Text, nullable=False),
|
||||
Column('Untertitel', Text),
|
||||
Column('Werksform', ForeignKey('Werksform.ID')),
|
||||
Column('Verlag', ForeignKey('Verlag.ID')),
|
||||
Column('Reihe', ForeignKey('Reihe.ID')),
|
||||
Column('Reihennummer', Text),
|
||||
Column('Erscheinungsdatum', Text),
|
||||
Column('ISBN_13', Text),
|
||||
Column('ISBN_10', Text),
|
||||
Column('ISSN', Text),
|
||||
Column('Preis', Text),
|
||||
Column('Titelbild', ForeignKey('Titelbild.ID')),
|
||||
Column('Klappentext', Text),
|
||||
Column('Anmerkungen', Text)
|
||||
)
|
||||
|
||||
t_Text_Genre = Table(
|
||||
'Text_Genre', metadata,
|
||||
Column('Text', ForeignKey('Text.ID'), primary_key=True),
|
||||
Column('Genre', ForeignKey('Genre.ID'), primary_key=True)
|
||||
)
|
||||
|
||||
t_Veroeffentlichung = Table(
|
||||
'Veroeffentlichung', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Text', ForeignKey('Text.ID'), nullable=False),
|
||||
Column('Werk', ForeignKey('Werk.ID'), nullable=False),
|
||||
Column('AltTitel', Text),
|
||||
Column('AltUntertitel', Text),
|
||||
Column('Pseudonym', ForeignKey('Pseudonym.ID'), nullable=False)
|
||||
)
|
||||
|
||||
t_Werk_Genre = Table(
|
||||
'Werk_Genre', metadata,
|
||||
Column('Werk', ForeignKey('Werk.ID'), primary_key=True),
|
||||
Column('Genre', ForeignKey('Genre.ID'), primary_key=True)
|
||||
)
|
||||
|
||||
t_Werk_Herausgeber = Table(
|
||||
'Werk_Herausgeber', metadata,
|
||||
Column('Herausgeber', ForeignKey('Herausgeber.ID'), primary_key=True),
|
||||
Column('Werk', ForeignKey('Werk.ID'), primary_key=True)
|
||||
)
|
||||
@ -1,5 +1,5 @@
|
||||
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 Genre
|
||||
|
||||
@ -12,7 +12,7 @@ def all():
|
||||
|
||||
@bp.route("/genre/create", methods=["POST"])
|
||||
def create():
|
||||
db.session.add(Genre(Genre = request.form["form_Genre"]))
|
||||
db.session.add(Genre(Genre = request.form.get("form_Genre", default=None)))
|
||||
db.session.commit()
|
||||
flash("Eintrag erfolgreich hinzugefügt")
|
||||
return redirect(url_for("genre.all"), code=303)
|
||||
@ -20,11 +20,11 @@ def create():
|
||||
@bp.route("/genre/update/<int:id>", methods=["POST"])
|
||||
def update(id):
|
||||
genre = db.session.get(Genre, id)
|
||||
genre.Genre = request.form["form_Genre"]
|
||||
genre.Genre = request.form.get("form_Genre", default=None)
|
||||
db.session.commit()
|
||||
flash("Eintrag erfolgreich geändert")
|
||||
return redirect(url_for("genre.all"), code=303)
|
||||
|
||||
|
||||
@bp.route("/genre/delete/<int:id>")
|
||||
def delete(id):
|
||||
genre = db.session.get(Genre, id)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 Herausgeber
|
||||
|
||||
|
||||
@ -6,12 +6,12 @@ import inspect
|
||||
|
||||
bp = Blueprint("home", __name__)
|
||||
|
||||
# prepare list of DB table classes to be searched by search_all()
|
||||
# prepare list of ORM classes to be searched by search_all()
|
||||
tables = []
|
||||
for name, obj in inspect.getmembers(the_works.models):
|
||||
if "_" not in name and inspect.isclass(obj):
|
||||
if inspect.isclass(obj) and issubclass(obj, the_works.models.Base) and obj.__name__ != "Base":
|
||||
tables.append(obj)
|
||||
#print(tables) #DEBUG
|
||||
print(f"tables is {tables}") #DEBUG
|
||||
|
||||
@bp.route("/")
|
||||
def startpage():
|
||||
@ -40,11 +40,11 @@ def search_all():
|
||||
continue
|
||||
if matchCase:
|
||||
if s in row[0].__getattribute__(column):
|
||||
hits.append(row[0]._asdict())
|
||||
hits.append(row[0].asdict())
|
||||
break
|
||||
else:
|
||||
if s.lower() in row[0].__getattribute__(column).lower():
|
||||
hits.append(row[0]._asdict())
|
||||
hits.append(row[0].asdict())
|
||||
break
|
||||
if hits != []:
|
||||
result[table.__table__.fullname] = hits
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 Pseudonym
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 Reihe, Verlag
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 Sprache
|
||||
|
||||
|
||||
@ -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, 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()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 Textform
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ bp = Blueprint("titelbild", __name__)
|
||||
@bp.route("/titelbild")
|
||||
@bp.route("/titelbild/all")
|
||||
def all():
|
||||
return render_template("views/titelbild.html", titelbilder=map(lambda t: t._asdict_with_urls(), db.session.scalars(select(Titelbild))))
|
||||
return render_template("views/titelbild.html", titelbilder=map(lambda t: t.asdict_with_urls(), db.session.scalars(select(Titelbild))))
|
||||
|
||||
|
||||
@bp.route("/titelbild/image/<int:id>")
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 Verlag
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 Veroeffentlichung, Text, Werk, Werksform, Pseudonym
|
||||
|
||||
|
||||
@ -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, Werk_Genre, Genre, 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()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 Werksform
|
||||
|
||||
|
||||
14
tmp.py
Normal file
14
tmp.py
Normal file
@ -0,0 +1,14 @@
|
||||
from flask import Flask
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://"
|
||||
app.config["SQLALCHEMY_ECHO"] = True
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from the_works.models import Base
|
||||
db = SQLAlchemy(model_class=Base)
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
@ -1,3 +1,5 @@
|
||||
# File contains output of `sqlacodegen --generator declarative sqlite:///path/to/the_works.sqlite`
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, Table, Text
|
||||
125
utils/sqlacodegen_tables.py
Normal file
125
utils/sqlacodegen_tables.py
Normal file
@ -0,0 +1,125 @@
|
||||
# File contains output of `sqlacodegen --generator tables sqlite:///path/to/the_works.sqlite`
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, MetaData, Table, Text
|
||||
|
||||
metadata = MetaData()
|
||||
|
||||
|
||||
t_Genre = Table(
|
||||
'Genre', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Genre', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Herausgeber = Table(
|
||||
'Herausgeber', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Name', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Pseudonym = Table(
|
||||
'Pseudonym', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Pseudonym', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Sprache = Table(
|
||||
'Sprache', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Sprache', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Textform = Table(
|
||||
'Textform', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Textform', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Titelbild = Table(
|
||||
'Titelbild', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Mimetype', Text, nullable=False),
|
||||
Column('Dateiname', Text, nullable=False),
|
||||
Column('Dateigroesse', Integer, nullable=False),
|
||||
Column('Breite', Integer, nullable=False),
|
||||
Column('Hoehe', Integer, nullable=False),
|
||||
Column('Bild', LargeBinary, nullable=False),
|
||||
Column('Thumbnail', LargeBinary, nullable=False),
|
||||
Column('sha256', Text, nullable=False, unique=True)
|
||||
)
|
||||
|
||||
t_Verlag = Table(
|
||||
'Verlag', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Verlag', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Werksform = Table(
|
||||
'Werksform', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Werksform', Text, nullable=False)
|
||||
)
|
||||
|
||||
t_Reihe = Table(
|
||||
'Reihe', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Titel', Text, nullable=False),
|
||||
Column('Verlag', ForeignKey('Verlag.ID'))
|
||||
)
|
||||
|
||||
t_Text = Table(
|
||||
'Text', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Titel', Text, nullable=False),
|
||||
Column('Untertitel', Text),
|
||||
Column('Reihe', ForeignKey('Reihe.ID')),
|
||||
Column('Textform', ForeignKey('Textform.ID')),
|
||||
Column('Sprache', ForeignKey('Sprache.ID'))
|
||||
)
|
||||
|
||||
t_Werk = Table(
|
||||
'Werk', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Titel', Text, nullable=False),
|
||||
Column('Untertitel', Text),
|
||||
Column('Werksform', ForeignKey('Werksform.ID')),
|
||||
Column('Verlag', ForeignKey('Verlag.ID')),
|
||||
Column('Reihe', ForeignKey('Reihe.ID')),
|
||||
Column('Reihennummer', Text),
|
||||
Column('Erscheinungsdatum', Text),
|
||||
Column('ISBN_13', Text),
|
||||
Column('ISBN_10', Text),
|
||||
Column('ISSN', Text),
|
||||
Column('Preis', Text),
|
||||
Column('Titelbild', ForeignKey('Titelbild.ID')),
|
||||
Column('Klappentext', Text),
|
||||
Column('Anmerkungen', Text)
|
||||
)
|
||||
|
||||
t_Text_Genre = Table(
|
||||
'Text_Genre', metadata,
|
||||
Column('Text', ForeignKey('Text.ID'), primary_key=True),
|
||||
Column('Genre', ForeignKey('Genre.ID'), primary_key=True)
|
||||
)
|
||||
|
||||
t_Veroeffentlichung = Table(
|
||||
'Veroeffentlichung', metadata,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('Text', ForeignKey('Text.ID'), nullable=False),
|
||||
Column('Werk', ForeignKey('Werk.ID'), nullable=False),
|
||||
Column('AltTitel', Text),
|
||||
Column('AltUntertitel', Text),
|
||||
Column('Pseudonym', ForeignKey('Pseudonym.ID'), nullable=False)
|
||||
)
|
||||
|
||||
t_Werk_Genre = Table(
|
||||
'Werk_Genre', metadata,
|
||||
Column('Werk', ForeignKey('Werk.ID'), primary_key=True),
|
||||
Column('Genre', ForeignKey('Genre.ID'), primary_key=True)
|
||||
)
|
||||
|
||||
t_Werk_Herausgeber = Table(
|
||||
'Werk_Herausgeber', metadata,
|
||||
Column('Herausgeber', ForeignKey('Herausgeber.ID'), primary_key=True),
|
||||
Column('Werk', ForeignKey('Werk.ID'), primary_key=True)
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user