Compare commits
No commits in common. "079cd41ea8eb19a02eea6037cf3d1f613f3515db" and "2cc9b436bed189f6dffe472495c7d0abe7c0ee65" have entirely different histories.
079cd41ea8
...
2cc9b436be
16
.flaskenv
16
.flaskenv
@ -1,9 +1,13 @@
|
||||
# Non-critical configuration values for the runtime environment
|
||||
# File is read automatically if python-dotenv is installed
|
||||
|
||||
# set this to your app's name if you want to omit the flask option '--app=<app_name>' on the command line
|
||||
# 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
|
||||
|
||||
# environment to run the app in; possible values are "development", "production", "testing"
|
||||
FLASK_APP_MODE = "development"
|
||||
FLASK_SQLALCHEMY_DATABASE_URI = "sqlite:///../the_works.sqlite"
|
||||
FLASK_SQLALCHEMY_ECHO = False
|
||||
FLASK_SQLALCHEMY_RECORD_QUERIES = True
|
||||
|
||||
|
||||
37
README.md
37
README.md
@ -16,43 +16,27 @@ the_works also is
|
||||
|
||||
## Configuration
|
||||
|
||||
### From the environment
|
||||
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.
|
||||
|
||||
When the_works is started, the app first reads all environment variables prefixed with "FLASK_" (e.g. "FLASK_APP"). Any variables in the file `.flaskenv` will be added to the environment beforehand ("python-dotenv" must be installed). This can be used to determine the mode to run the app in (development, production etc.).
|
||||
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_".
|
||||
|
||||
Note that only those environment variables get added to the_works' configuration that begin with "FLASK_". Ths is true whether they were added on the command line, defined in`.flaskenv`, or even added by using Python's `os.environ`.
|
||||
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.
|
||||
|
||||
### From the_works/config.py
|
||||
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.
|
||||
|
||||
The main configuration happens inside the file `the_works/config.py`. All static variables defined in the class "Config" will be added as key-value-pairs to the_works' configuration dict.
|
||||
|
||||
In addition to "Config", the_works will then read all values from either "DevelopmentConfig", "ProductionConfig", or "TestingConfig". These classes are all subclasses of "Config". the_works determines which subclass to use by reading the Flask configuration setting "APP_MODE". If "APP_MODE" is one of either "development", "production", or "testing", the corresponding subclass will be used. If "APP_MODE" is set to a different value or not at all, "DevelopentConfig" will be used as default.
|
||||
|
||||
Note that if a value is defined in both "Config" and one of its subclasses, the value from the subclass will supersede the one from the base class.
|
||||
|
||||
### Settings for the_works
|
||||
|
||||
The following settings are specific to the_works:
|
||||
|
||||
* `APP_MODE = development | production | testing` – detemines witch configuration to use in addition to the base config; default is development
|
||||
|
||||
### Useful Flask settings
|
||||
|
||||
* `APP = <app_name>` – set this bevore running flask on the command line if you want to omit `--app the_works`
|
||||
* either `SQLALCHEMY_DATABASE_URI` or `SQLALCHEMY_DATABASE_URI` must be set or flask-sqlalchemy will throw an error; for URI syntax see the [SQLAlchemy docs](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)
|
||||
* see the list of builtin configuration values in the [Flask docs](https://flask.palletsprojects.com/en/stable/config/#builtin-configuration-values)
|
||||
|
||||
|
||||
## Flask commands
|
||||
|
||||
Execute commands with `python -m flask --app the_works <command>`. You can omit `--app` by setting the environment variable "FLASK_APP" to "the_works" (see [here](#useful-flask-settings))
|
||||
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 ORM data structures)
|
||||
|
||||
|
||||
*
|
||||
|
||||
|
||||
|
||||
@ -64,12 +48,15 @@ Available commands:
|
||||
Required pip packages
|
||||
|
||||
* flask
|
||||
* python-dotenv
|
||||
* flask-sqlalchemy
|
||||
* python-dotenv
|
||||
* Pillow
|
||||
* pytest
|
||||
|
||||
See also `requirements.txt`.
|
||||
Optional pip packages
|
||||
|
||||
* flask-debugtoolbar (optional)
|
||||
* sqlacodegen (optional; only used from the command line during development)
|
||||
|
||||
### CSS and Javascript resources
|
||||
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import os
|
||||
import pytest
|
||||
from the_works import create_app
|
||||
from the_works.database import db as _db
|
||||
from the_works.models import Genre
|
||||
|
||||
# set app mode to testing via environment variable
|
||||
os.environ["FLASK_APP_MODE"] = "testing"
|
||||
|
||||
TEST_DATABASE_URI = "sqlite:///:memory:"
|
||||
|
||||
@pytest.fixture()
|
||||
def _app():
|
||||
_app = create_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
|
||||
context = _app.app_context()
|
||||
|
||||
@ -1,29 +1,27 @@
|
||||
from flask import Flask
|
||||
import dotenv # this import is not strictly necessary but it forces pipreqs-to include dotenv when generating `requirements.txt`
|
||||
import the_works.config as tw_conf
|
||||
from the_works.database import init_db
|
||||
from the_works.models import SIMPLE_MODELS
|
||||
from the_works.views import home, reihe, titelbild, text, veroeffentlichung, werk, ausgabe
|
||||
from the_works.views.simple_view import VIEWS, ViewAll, ViewCreate, ViewUpdate, ViewDelete
|
||||
|
||||
|
||||
def create_app():
|
||||
def create_app(config=None):
|
||||
app = Flask(__name__)
|
||||
|
||||
# read config from environment (all values are prefixed with "FLASK_")
|
||||
# read all config values from environment that are prefixed with "FLASK_"
|
||||
app.config.from_prefixed_env()
|
||||
|
||||
# read config from object(s)
|
||||
app.config.from_object(tw_conf.Config)
|
||||
if 'APP_MODE' in app.config.keys() and isinstance(env := app.config['APP_MODE'], str) and env.lower() == "production":
|
||||
app.config.from_object(tw_conf.ProductionConfig)
|
||||
elif 'APP_MODE' in app.config.keys() and isinstance(env := app.config['APP_MODE'], str) and env.lower() == "testing":
|
||||
app.config.from_object(tw_conf.TestingConfig)
|
||||
else:
|
||||
app.config.from_object(tw_conf.DevelopmentConfig)
|
||||
# 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 mode: {app.config['APP_MODE'] if 'APP_MODE' in app.config.keys() else 'not set'}") #DEBUG
|
||||
print(f"Current Environment: {app.config['ENV'] if 'ENV' in app.config.keys() else 'ENV is not set'}") #DEBUG
|
||||
|
||||
# initialize database
|
||||
init_db(app)
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
class Config(object):
|
||||
MAX_CONTENT_LENGTH = 2 * 1024 * 1024
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///../the_works.sqlite"
|
||||
SECRET_KEY = "f8148ee5d95b0a67122b1cab9993f637a6bf29528f584a9f1575af1a55566748"
|
||||
SQLALCHEMY_ECHO = False
|
||||
SQLALCHEMY_RECORD_QUERIES = True
|
||||
DEBUG = True
|
||||
|
||||
class ProductionConfig(Config):
|
||||
pass
|
||||
#SQLALCHEMY_DATABASE_URI =
|
||||
#SECRET_KEY = "differentsecretkey0123456789"
|
||||
#SQLALCHEMY_RECORD_QUERIES = False
|
||||
|
||||
class TestingConfig(Config):
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
||||
SECRET_KEY = "This is my very secret key"
|
||||
DEBUG = True
|
||||
TESTING = True
|
||||
|
||||
@ -2,28 +2,28 @@
|
||||
* FILE INPUT FUNCTIONS
|
||||
*/
|
||||
function initFileinput(id_stub, current_stub, modal_id) {
|
||||
// add "change" event listener to file input element
|
||||
// add input element event handler
|
||||
document.getElementById(id_stub + "-upload").addEventListener("change", handleFileinputChange);
|
||||
// add "toggle" event listener to modal that shows the current image if the modal was raised by an update action, and hides it otherwise
|
||||
// add event handler to modal that shows the current image if the modal was raised by an update action, and hides it otherwise
|
||||
document.getElementById(modal_id).addEventListener("toggle", handleModalToggle);
|
||||
}
|
||||
|
||||
|
||||
function handleFileinputChange(event) {
|
||||
let fi = event.target
|
||||
let id_stub = fi.id.split("-")[0];
|
||||
let id_stub = event.target.id.split("-")[0];
|
||||
|
||||
// mark change
|
||||
document.getElementById(id_stub + "-haschanged").value += "+";
|
||||
console.dir(event);
|
||||
|
||||
// no file chosen -> show placeholder
|
||||
if ( ! fi.files.length ) {
|
||||
if ( ! event.target.files.length ) {
|
||||
showOrHideThumbnail(id_stub, "hide");
|
||||
return true;
|
||||
}
|
||||
|
||||
// set img src to the uploaded file
|
||||
let f = fi.files[0];
|
||||
let f = event.target.files[0];
|
||||
let tn = document.getElementById(id_stub + "-thumbnail");
|
||||
tn.src = URL.createObjectURL(f);
|
||||
|
||||
@ -39,7 +39,7 @@ function handleFileinputChange(event) {
|
||||
document.getElementById(id_stub + "-hoehe").innerText = tn.naturalHeight;
|
||||
// remove image object from memory
|
||||
URL.revokeObjectURL(tn.src);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -90,16 +90,3 @@ function showOrHideThumbnail(id_stub, mode) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function validate_filesize(input_id="titelbild-upload") {
|
||||
let fi = document.getElementById(input_id);
|
||||
console.dir(fi);
|
||||
fi.setCustomValidity("");
|
||||
fi.setAttribute("aria-invalid", "false");
|
||||
|
||||
if ( fi.files[0].size > fi.dataset.maxfilesize ) {
|
||||
fi.setCustomValidity("Datei ist zu groß, bitte kleinere auswählen.");
|
||||
fi.setAttribute("aria-invalid", "true");
|
||||
fi.reportValidity();
|
||||
}
|
||||
}
|
||||
@ -58,9 +58,9 @@
|
||||
</label>
|
||||
<label for="titelbild-upload" role="button">Bild aussuchen</label>
|
||||
<div class="thumbnail-div">
|
||||
<span class="display-none" id="titelbild-bild">
|
||||
<a class="thumbnail-a display-none" id="titelbild-bild" href="" title="zum Originalbild">
|
||||
<img class="thumbnail-img" id="titelbild-thumbnail" alt="Thumbnail" src="" />
|
||||
</span>
|
||||
</a>
|
||||
<div class="placeholder" id="titelbild-placeholder">
|
||||
<svg viewbox="0 0 90 128">
|
||||
<use href="#placeholder" />
|
||||
@ -69,14 +69,13 @@
|
||||
</div>
|
||||
<div id="titelbild-info" class="display-none">
|
||||
<small>
|
||||
<span id="titelbild-dateiname"></span><br />
|
||||
<span id="titelbild-dateiname"></span> Bytes<br />
|
||||
<span id="titelbild-dateigroesse"></span> Bytes, <span id="titelbild-breite"></span>x<span id="titelbild-hoehe"></span> px
|
||||
</small>
|
||||
</div>
|
||||
<div id="titelbild-controls">
|
||||
<input type="file" id="titelbild-upload" name="form_Titelbild" aria-label="Titelbild" placeholder="kein Titelbild" accept="image/*" data-maxfilesize="{{ config['MAX_CONTENT_LENGTH'] }}" required />
|
||||
<small id="titelbild-toolarge" class="display-none">Datei ist zu groß!</small>
|
||||
<small>max. Dateigröße: {{ config['MAX_CONTENT_LENGTH'] }} Bytes</small>
|
||||
<input type="file" id="titelbild-upload" name="form_Titelbild" aria-label="Titelbild" placeholder="kein Titelbild" accept="image/*"/>
|
||||
<small>max. Dateigröße: 2 MB</small>
|
||||
<input type="hidden" id="titelbild-haschanged" name="form_Titelbild_haschanged" value="" />
|
||||
</div>
|
||||
</section>
|
||||
@ -97,7 +96,7 @@
|
||||
</section>
|
||||
|
||||
<footer class="grid">
|
||||
<button id="form_submit" type="submit" onclick="return validate_filesize()" formmethod="post" formaction="{{ url_for('titelbild.create') }}">OK</button>
|
||||
<button id="form_submit" type="submit" formmethod="post" formaction="{{ url_for('titelbild.create') }}">OK</button>
|
||||
<button class="secondary" aria-label="close" formmethod="dialog" formnovalidate>Abbrechen</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@ -40,8 +40,7 @@ def thumbnail(id):
|
||||
@bp.route("/titelbild/create/", methods=["POST"])
|
||||
def create():
|
||||
if request.files["form_Titelbild"].filename == "":
|
||||
flash("Konnte Operation nicht ausführen (keine Datei ausgewählt)", "error")
|
||||
return redirect(url_for("titelbild.all"), code=303)
|
||||
raise TypeError(message="FileStorage object expected")
|
||||
blob = request.files["form_Titelbild"].read()
|
||||
|
||||
with BytesIO(blob) as bytes_io:
|
||||
@ -50,7 +49,7 @@ def create():
|
||||
#if db.session.scalar(select(Titelbild).where(Titelbild.sha256 == hashlib.file_digest(bytes_like, "sha256"))):
|
||||
sha256 = makeshift_digest(bytes_io)
|
||||
if db.session.scalar(select(Titelbild).where(Titelbild.sha256 == sha256)):
|
||||
flash("Eintrag ist bereits in Datenbank vorhanden", "error")
|
||||
flash("Eintrag ist bereits in Datenbank vorhanden")
|
||||
return redirect(url_for("titelbild.all"), code=303)
|
||||
|
||||
# get Image and thumbnail
|
||||
|
||||
@ -20,7 +20,7 @@ def all():
|
||||
"AltTitel": row.Veroeffentlichung.AltTitel or "",
|
||||
"AltUntertitel": row.Veroeffentlichung.AltUntertitel or ""
|
||||
})
|
||||
return render_template("views/veroeffentlichung.html", veroeffentlichungen=veroeffentlichungen, texte=db.session.scalars(select(Text)), werke=db.session.scalars(select(Werk)))
|
||||
return render_template("views/veroeffentlichung.html", veroeffentlichungen=veroeffentlichungen, texte=db.session.scalars(select(Text)))
|
||||
|
||||
@bp.route("/veroeffentlichung/create/", methods=["POST"])
|
||||
def create():
|
||||
|
||||
16
tmp.md
16
tmp.md
@ -60,13 +60,13 @@ Sprache, (Genres) Reihe, R.No, (Genres), (Hrsg) IS?Nx3, Preis, Titel
|
||||
Stand der neuen DB
|
||||
|
||||
- Basisdaten sind vorhanden, Ausnahme: Titelbild
|
||||
- wieder neu eingetragen
|
||||
- Text
|
||||
- Werk
|
||||
- Veroeffentlichung
|
||||
- Titelbild (2-3 fehlen noch: c't Titelbild, Odd Bird Orig., Ogham Stone PDF, …)
|
||||
- Text ist auf dem alten Stand
|
||||
- es fehlt
|
||||
- Werk
|
||||
- Ogham Stone DL: https://oghamstoneul.wixsite.com/the-ogham-stone/post/the-ogham-stone-2019
|
||||
- Veroeffentlichung
|
||||
- Ausgabe
|
||||
- Titelbild
|
||||
- die Python views sind alle upgedated (theoretisch)
|
||||
- zu ändernde Jinja-Templates
|
||||
- x veroeffentlichung
|
||||
@ -74,15 +74,13 @@ Stand der neuen DB
|
||||
- x werk_detail
|
||||
- ausgabe_detail
|
||||
- Darstellung von Preis updaten und validaten
|
||||
- wie kann ich feststellen, welche Texte und Werke *nicht* in der Tabelle Veroeffentlichung referenziert werden?
|
||||
|
||||
- nice to have: Darstellung und Validation von IS*N verbessern (auf Basis der jew. Spezifikation)
|
||||
- https://en.wikipedia.org/wiki/ISBN, https://en.wikipedia.org/wiki/ISSN
|
||||
- isbnlib (Python package)
|
||||
- isbnlib-dnb
|
||||
- titelbild.html: validate file size before submit to avoid status code 413 -> see https://stackoverflow.com/a/8667695
|
||||
- nice to have: Metadaten zu meinen Werken direkt aus einer externen DB holen
|
||||
- DNB
|
||||
- zB DNB
|
||||
- keine Cover o. Klappentexte, Daten sind unsauber (zB Preis)
|
||||
- Demo-Abruf-Seite: https://dnb-sru-demo.streamlit.app/
|
||||
- ist kompliziert; vllt reicht es auch, einmal per Hand alle Werke von Tobias Radloff / Paul Jansen abzurufen und in die DB zu übernehmen?
|
||||
@ -96,7 +94,7 @@ Stand der neuen DB
|
||||
- ISBNdb.com -> kennt 9 Bücher von mir
|
||||
- kostet, gibt aber free 7-day trial
|
||||
- [Amazon Product Advertising API](https://webservices.amazon.com/paapi5/documentation/)
|
||||
- können aber nur Händler benutzen
|
||||
- kostet nix
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
works.oldschema.sqlite
Normal file
BIN
works.oldschema.sqlite
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user