Compare commits
6 Commits
2cc9b436be
...
079cd41ea8
| Author | SHA1 | Date | |
|---|---|---|---|
| 079cd41ea8 | |||
| 99ab7425a5 | |||
| 633d91a11d | |||
| ea47355c00 | |||
| f9e8b69a06 | |||
| 2d3583bbae |
16
.flaskenv
16
.flaskenv
@ -1,13 +1,9 @@
|
|||||||
# Non-critical configuration values
|
# Non-critical configuration values for the runtime environment
|
||||||
# Read automatically if python-dotenv is installed
|
# 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
|
||||||
FLASK_APP = "the_works"
|
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"
|
# environment to run the app in; possible values are "development", "production", "testing"
|
||||||
FLASK_SQLALCHEMY_ECHO = False
|
FLASK_APP_MODE = "development"
|
||||||
FLASK_SQLALCHEMY_RECORD_QUERIES = True
|
|
||||||
|
|
||||||
|
|||||||
37
README.md
37
README.md
@ -16,27 +16,43 @@ the_works also is
|
|||||||
|
|
||||||
## Configuration
|
## 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.
|
### From the environment
|
||||||
|
|
||||||
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_".
|
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.).
|
||||||
|
|
||||||
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.
|
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`.
|
||||||
|
|
||||||
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.
|
### From the_works/config.py
|
||||||
|
|
||||||
|
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
|
## Flask commands
|
||||||
|
|
||||||
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.
|
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))
|
||||||
|
|
||||||
Available commands:
|
Available commands:
|
||||||
|
|
||||||
* `run`: Serve app (don't use for production).
|
* `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)
|
* `shell`: start a shell within the app context (I can i.e. import specific table models and test ORM data structures)
|
||||||
*
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -48,15 +64,12 @@ Available commands:
|
|||||||
Required pip packages
|
Required pip packages
|
||||||
|
|
||||||
* flask
|
* flask
|
||||||
* flask-sqlalchemy
|
|
||||||
* python-dotenv
|
* python-dotenv
|
||||||
|
* flask-sqlalchemy
|
||||||
* Pillow
|
* Pillow
|
||||||
* pytest
|
* pytest
|
||||||
|
|
||||||
Optional pip packages
|
See also `requirements.txt`.
|
||||||
|
|
||||||
* flask-debugtoolbar (optional)
|
|
||||||
* sqlacodegen (optional; only used from the command line during development)
|
|
||||||
|
|
||||||
### CSS and Javascript resources
|
### CSS and Javascript resources
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from the_works import create_app
|
from the_works import create_app
|
||||||
from the_works.database import db as _db
|
from the_works.database import db as _db
|
||||||
from the_works.models import Genre
|
from the_works.models import Genre
|
||||||
|
|
||||||
TEST_DATABASE_URI = "sqlite:///:memory:"
|
# set app mode to testing via environment variable
|
||||||
|
os.environ["FLASK_APP_MODE"] = "testing"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def _app():
|
def _app():
|
||||||
test_config = {
|
_app = create_app()
|
||||||
"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
|
# other setup can go here
|
||||||
context = _app.app_context()
|
context = _app.app_context()
|
||||||
|
|||||||
@ -1,27 +1,29 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
import dotenv # this import is not strictly necessary but it forces pipreqs-to include dotenv when generating `requirements.txt`
|
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.database import init_db
|
||||||
from the_works.models import SIMPLE_MODELS
|
from the_works.models import SIMPLE_MODELS
|
||||||
from the_works.views import home, reihe, titelbild, text, veroeffentlichung, werk, ausgabe
|
from the_works.views import home, reihe, titelbild, text, veroeffentlichung, werk, ausgabe
|
||||||
from the_works.views.simple_view import VIEWS, ViewAll, ViewCreate, ViewUpdate, ViewDelete
|
from the_works.views.simple_view import VIEWS, ViewAll, ViewCreate, ViewUpdate, ViewDelete
|
||||||
|
|
||||||
|
|
||||||
def create_app(config=None):
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# read all config values from environment that are prefixed with "FLASK_"
|
# read config from environment (all values are prefixed with "FLASK_")
|
||||||
app.config.from_prefixed_env()
|
app.config.from_prefixed_env()
|
||||||
|
|
||||||
# some #DEBUG configuration
|
# read config from object(s)
|
||||||
# toolbar = DebugToolbarExtension(app) #DEBUG
|
app.config.from_object(tw_conf.Config)
|
||||||
# app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False #DEBUG
|
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)
|
||||||
# use config from function parameter if present
|
elif 'APP_MODE' in app.config.keys() and isinstance(env := app.config['APP_MODE'], str) and env.lower() == "testing":
|
||||||
if config:
|
app.config.from_object(tw_conf.TestingConfig)
|
||||||
app.config.update(config)
|
else:
|
||||||
|
app.config.from_object(tw_conf.DevelopmentConfig)
|
||||||
|
|
||||||
# some #DEBUG output
|
# some #DEBUG output
|
||||||
print(f"Current Environment: {app.config['ENV'] if 'ENV' in app.config.keys() else 'ENV is not set'}") #DEBUG
|
print(f"Current mode: {app.config['APP_MODE'] if 'APP_MODE' in app.config.keys() else 'not set'}") #DEBUG
|
||||||
|
|
||||||
# initialize database
|
# initialize database
|
||||||
init_db(app)
|
init_db(app)
|
||||||
|
|||||||
22
the_works/config.py
Normal file
22
the_works/config.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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
|
* FILE INPUT FUNCTIONS
|
||||||
*/
|
*/
|
||||||
function initFileinput(id_stub, current_stub, modal_id) {
|
function initFileinput(id_stub, current_stub, modal_id) {
|
||||||
// add input element event handler
|
// add "change" event listener to file input element
|
||||||
document.getElementById(id_stub + "-upload").addEventListener("change", handleFileinputChange);
|
document.getElementById(id_stub + "-upload").addEventListener("change", handleFileinputChange);
|
||||||
// add event handler to modal that shows the current image if the modal was raised by an update action, and hides it otherwise
|
// add "toggle" event listener 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);
|
document.getElementById(modal_id).addEventListener("toggle", handleModalToggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleFileinputChange(event) {
|
function handleFileinputChange(event) {
|
||||||
let id_stub = event.target.id.split("-")[0];
|
let fi = event.target
|
||||||
|
let id_stub = fi.id.split("-")[0];
|
||||||
|
|
||||||
// mark change
|
// mark change
|
||||||
document.getElementById(id_stub + "-haschanged").value += "+";
|
document.getElementById(id_stub + "-haschanged").value += "+";
|
||||||
console.dir(event);
|
|
||||||
|
|
||||||
// no file chosen -> show placeholder
|
// no file chosen -> show placeholder
|
||||||
if ( ! event.target.files.length ) {
|
if ( ! fi.files.length ) {
|
||||||
showOrHideThumbnail(id_stub, "hide");
|
showOrHideThumbnail(id_stub, "hide");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set img src to the uploaded file
|
// set img src to the uploaded file
|
||||||
let f = event.target.files[0];
|
let f = fi.files[0];
|
||||||
let tn = document.getElementById(id_stub + "-thumbnail");
|
let tn = document.getElementById(id_stub + "-thumbnail");
|
||||||
tn.src = URL.createObjectURL(f);
|
tn.src = URL.createObjectURL(f);
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ console.dir(event);
|
|||||||
document.getElementById(id_stub + "-hoehe").innerText = tn.naturalHeight;
|
document.getElementById(id_stub + "-hoehe").innerText = tn.naturalHeight;
|
||||||
// remove image object from memory
|
// remove image object from memory
|
||||||
URL.revokeObjectURL(tn.src);
|
URL.revokeObjectURL(tn.src);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -90,3 +90,16 @@ 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>
|
||||||
<label for="titelbild-upload" role="button">Bild aussuchen</label>
|
<label for="titelbild-upload" role="button">Bild aussuchen</label>
|
||||||
<div class="thumbnail-div">
|
<div class="thumbnail-div">
|
||||||
<a class="thumbnail-a display-none" id="titelbild-bild" href="" title="zum Originalbild">
|
<span class="display-none" id="titelbild-bild">
|
||||||
<img class="thumbnail-img" id="titelbild-thumbnail" alt="Thumbnail" src="" />
|
<img class="thumbnail-img" id="titelbild-thumbnail" alt="Thumbnail" src="" />
|
||||||
</a>
|
</span>
|
||||||
<div class="placeholder" id="titelbild-placeholder">
|
<div class="placeholder" id="titelbild-placeholder">
|
||||||
<svg viewbox="0 0 90 128">
|
<svg viewbox="0 0 90 128">
|
||||||
<use href="#placeholder" />
|
<use href="#placeholder" />
|
||||||
@ -69,13 +69,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="titelbild-info" class="display-none">
|
<div id="titelbild-info" class="display-none">
|
||||||
<small>
|
<small>
|
||||||
<span id="titelbild-dateiname"></span> Bytes<br />
|
<span id="titelbild-dateiname"></span><br />
|
||||||
<span id="titelbild-dateigroesse"></span> Bytes, <span id="titelbild-breite"></span>x<span id="titelbild-hoehe"></span> px
|
<span id="titelbild-dateigroesse"></span> Bytes, <span id="titelbild-breite"></span>x<span id="titelbild-hoehe"></span> px
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="titelbild-controls">
|
<div id="titelbild-controls">
|
||||||
<input type="file" id="titelbild-upload" name="form_Titelbild" aria-label="Titelbild" placeholder="kein Titelbild" accept="image/*"/>
|
<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>max. Dateigröße: 2 MB</small>
|
<small id="titelbild-toolarge" class="display-none">Datei ist zu groß!</small>
|
||||||
|
<small>max. Dateigröße: {{ config['MAX_CONTENT_LENGTH'] }} Bytes</small>
|
||||||
<input type="hidden" id="titelbild-haschanged" name="form_Titelbild_haschanged" value="" />
|
<input type="hidden" id="titelbild-haschanged" name="form_Titelbild_haschanged" value="" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -96,7 +97,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="grid">
|
<footer class="grid">
|
||||||
<button id="form_submit" type="submit" formmethod="post" formaction="{{ url_for('titelbild.create') }}">OK</button>
|
<button id="form_submit" type="submit" onclick="return validate_filesize()" formmethod="post" formaction="{{ url_for('titelbild.create') }}">OK</button>
|
||||||
<button class="secondary" aria-label="close" formmethod="dialog" formnovalidate>Abbrechen</button>
|
<button class="secondary" aria-label="close" formmethod="dialog" formnovalidate>Abbrechen</button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -40,7 +40,8 @@ def thumbnail(id):
|
|||||||
@bp.route("/titelbild/create/", methods=["POST"])
|
@bp.route("/titelbild/create/", methods=["POST"])
|
||||||
def create():
|
def create():
|
||||||
if request.files["form_Titelbild"].filename == "":
|
if request.files["form_Titelbild"].filename == "":
|
||||||
raise TypeError(message="FileStorage object expected")
|
flash("Konnte Operation nicht ausführen (keine Datei ausgewählt)", "error")
|
||||||
|
return redirect(url_for("titelbild.all"), code=303)
|
||||||
blob = request.files["form_Titelbild"].read()
|
blob = request.files["form_Titelbild"].read()
|
||||||
|
|
||||||
with BytesIO(blob) as bytes_io:
|
with BytesIO(blob) as bytes_io:
|
||||||
@ -49,7 +50,7 @@ def create():
|
|||||||
#if db.session.scalar(select(Titelbild).where(Titelbild.sha256 == hashlib.file_digest(bytes_like, "sha256"))):
|
#if db.session.scalar(select(Titelbild).where(Titelbild.sha256 == hashlib.file_digest(bytes_like, "sha256"))):
|
||||||
sha256 = makeshift_digest(bytes_io)
|
sha256 = makeshift_digest(bytes_io)
|
||||||
if db.session.scalar(select(Titelbild).where(Titelbild.sha256 == sha256)):
|
if db.session.scalar(select(Titelbild).where(Titelbild.sha256 == sha256)):
|
||||||
flash("Eintrag ist bereits in Datenbank vorhanden")
|
flash("Eintrag ist bereits in Datenbank vorhanden", "error")
|
||||||
return redirect(url_for("titelbild.all"), code=303)
|
return redirect(url_for("titelbild.all"), code=303)
|
||||||
|
|
||||||
# get Image and thumbnail
|
# get Image and thumbnail
|
||||||
|
|||||||
@ -20,7 +20,7 @@ def all():
|
|||||||
"AltTitel": row.Veroeffentlichung.AltTitel or "",
|
"AltTitel": row.Veroeffentlichung.AltTitel or "",
|
||||||
"AltUntertitel": row.Veroeffentlichung.AltUntertitel or ""
|
"AltUntertitel": row.Veroeffentlichung.AltUntertitel or ""
|
||||||
})
|
})
|
||||||
return render_template("views/veroeffentlichung.html", veroeffentlichungen=veroeffentlichungen, texte=db.session.scalars(select(Text)))
|
return render_template("views/veroeffentlichung.html", veroeffentlichungen=veroeffentlichungen, texte=db.session.scalars(select(Text)), werke=db.session.scalars(select(Werk)))
|
||||||
|
|
||||||
@bp.route("/veroeffentlichung/create/", methods=["POST"])
|
@bp.route("/veroeffentlichung/create/", methods=["POST"])
|
||||||
def create():
|
def create():
|
||||||
|
|||||||
14
tmp.md
14
tmp.md
@ -60,13 +60,13 @@ Sprache, (Genres) Reihe, R.No, (Genres), (Hrsg) IS?Nx3, Preis, Titel
|
|||||||
Stand der neuen DB
|
Stand der neuen DB
|
||||||
|
|
||||||
- Basisdaten sind vorhanden, Ausnahme: Titelbild
|
- Basisdaten sind vorhanden, Ausnahme: Titelbild
|
||||||
- Text ist auf dem alten Stand
|
- wieder neu eingetragen
|
||||||
- es fehlt
|
- Text
|
||||||
- Werk
|
- Werk
|
||||||
- Ogham Stone DL: https://oghamstoneul.wixsite.com/the-ogham-stone/post/the-ogham-stone-2019
|
|
||||||
- Veroeffentlichung
|
- Veroeffentlichung
|
||||||
|
- Titelbild (2-3 fehlen noch: c't Titelbild, Odd Bird Orig., Ogham Stone PDF, …)
|
||||||
|
- es fehlt
|
||||||
- Ausgabe
|
- Ausgabe
|
||||||
- Titelbild
|
|
||||||
- die Python views sind alle upgedated (theoretisch)
|
- die Python views sind alle upgedated (theoretisch)
|
||||||
- zu ändernde Jinja-Templates
|
- zu ändernde Jinja-Templates
|
||||||
- x veroeffentlichung
|
- x veroeffentlichung
|
||||||
@ -74,13 +74,15 @@ Stand der neuen DB
|
|||||||
- x werk_detail
|
- x werk_detail
|
||||||
- ausgabe_detail
|
- ausgabe_detail
|
||||||
- Darstellung von Preis updaten und validaten
|
- 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)
|
- 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
|
- https://en.wikipedia.org/wiki/ISBN, https://en.wikipedia.org/wiki/ISSN
|
||||||
- isbnlib (Python package)
|
- isbnlib (Python package)
|
||||||
- isbnlib-dnb
|
- 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
|
- nice to have: Metadaten zu meinen Werken direkt aus einer externen DB holen
|
||||||
- zB DNB
|
- DNB
|
||||||
- keine Cover o. Klappentexte, Daten sind unsauber (zB Preis)
|
- keine Cover o. Klappentexte, Daten sind unsauber (zB Preis)
|
||||||
- Demo-Abruf-Seite: https://dnb-sru-demo.streamlit.app/
|
- 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?
|
- ist kompliziert; vllt reicht es auch, einmal per Hand alle Werke von Tobias Radloff / Paul Jansen abzurufen und in die DB zu übernehmen?
|
||||||
@ -94,7 +96,7 @@ Stand der neuen DB
|
|||||||
- ISBNdb.com -> kennt 9 Bücher von mir
|
- ISBNdb.com -> kennt 9 Bücher von mir
|
||||||
- kostet, gibt aber free 7-day trial
|
- kostet, gibt aber free 7-day trial
|
||||||
- [Amazon Product Advertising API](https://webservices.amazon.com/paapi5/documentation/)
|
- [Amazon Product Advertising API](https://webservices.amazon.com/paapi5/documentation/)
|
||||||
- kostet nix
|
- können aber nur Händler benutzen
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user