Initial commit
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="letter_data.db" uuid="53ed27a5-d5a3-4e0e-8f9c-ee4b0e04ddbb">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/letter_data.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
|
||||
<serverData>
|
||||
<paths name="mosers@localhost:4222 agent">
|
||||
<serverdata>
|
||||
<mappings>
|
||||
<mapping local="$PROJECT_DIR$" web="/" />
|
||||
</mappings>
|
||||
</serverdata>
|
||||
</paths>
|
||||
<paths name="mosers@localhost:4222 agent (2)">
|
||||
<serverdata>
|
||||
<mappings>
|
||||
<mapping local="$PROJECT_DIR$" web="/" />
|
||||
</mappings>
|
||||
</serverdata>
|
||||
</paths>
|
||||
</serverData>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,34 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="6">
|
||||
<item index="0" class="java.lang.String" itemvalue="rich-pixels" />
|
||||
<item index="1" class="java.lang.String" itemvalue="matplotlib" />
|
||||
<item index="2" class="java.lang.String" itemvalue="pillow" />
|
||||
<item index="3" class="java.lang.String" itemvalue="rich" />
|
||||
<item index="4" class="java.lang.String" itemvalue="numpy" />
|
||||
<item index="5" class="java.lang.String" itemvalue="textual" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N806" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="QodanaSanity" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
|
||||
</project>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/tharanor-letter.iml" filepath="$PROJECT_DIR$/.idea/tharanor-letter.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/tharanor-letter.py" dialect="GenericSQL" />
|
||||
<file url="PROJECT" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WebResourcesPaths">
|
||||
<contentEntries>
|
||||
<entry url="file://$PROJECT_DIR$">
|
||||
<entryData>
|
||||
<resourceRoots>
|
||||
<path value="file://$PROJECT_DIR$/static" />
|
||||
</resourceRoots>
|
||||
</entryData>
|
||||
</entry>
|
||||
</contentEntries>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,25 @@
|
||||
import sqlite3
|
||||
|
||||
DB_PATH = 'letter_data.db'
|
||||
|
||||
|
||||
def init_db():
|
||||
with sqlite3.connect(DB_PATH) as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS sender (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT,
|
||||
address TEXT,
|
||||
logo TEXT,
|
||||
signature TEXT)''')
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS state (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT)''')
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS recipient (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT,
|
||||
address TEXT,
|
||||
state_id INTEGER,
|
||||
FOREIGN KEY(state_id) REFERENCES state(id))''')
|
||||
|
||||
init_db()
|
@ -0,0 +1,179 @@
|
||||
/* CSS for Paged.js interface – v0.4 */
|
||||
|
||||
/* Change the look */
|
||||
:root {
|
||||
--color-background: whitesmoke;
|
||||
--color-pageSheet: #cfcfcf;
|
||||
--color-pageBox: violet;
|
||||
--color-paper: white;
|
||||
--color-marginBox: transparent;
|
||||
--pagedjs-crop-color: black;
|
||||
--pagedjs-crop-shadow: white;
|
||||
--pagedjs-crop-stroke: 1px;
|
||||
}
|
||||
|
||||
/* To define how the book look on the screen: */
|
||||
@media screen, pagedjs-ignore {
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.pagedjs_pages {
|
||||
display: flex;
|
||||
width: calc(var(--pagedjs-width) * 2);
|
||||
flex: 0;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pagedjs_page {
|
||||
background-color: var(--color-paper);
|
||||
box-shadow: 0 0 0 1px var(--color-pageSheet);
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
margin-top: 10mm;
|
||||
}
|
||||
|
||||
.pagedjs_first_page {
|
||||
margin-left: var(--pagedjs-width);
|
||||
}
|
||||
|
||||
.pagedjs_page:last-of-type {
|
||||
margin-bottom: 10mm;
|
||||
}
|
||||
|
||||
.pagedjs_pagebox{
|
||||
box-shadow: 0 0 0 1px var(--color-pageBox);
|
||||
}
|
||||
|
||||
.pagedjs_left_page{
|
||||
z-index: 20;
|
||||
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width))!important;
|
||||
}
|
||||
|
||||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-middle{
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.pagedjs_right_page{
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
left: calc(var(--pagedjs-bleed-left)*-1);
|
||||
}
|
||||
|
||||
/* show the margin-box */
|
||||
|
||||
.pagedjs_margin-top-left-corner-holder,
|
||||
.pagedjs_margin-top,
|
||||
.pagedjs_margin-top-left,
|
||||
.pagedjs_margin-top-center,
|
||||
.pagedjs_margin-top-right,
|
||||
.pagedjs_margin-top-right-corner-holder,
|
||||
.pagedjs_margin-bottom-left-corner-holder,
|
||||
.pagedjs_margin-bottom,
|
||||
.pagedjs_margin-bottom-left,
|
||||
.pagedjs_margin-bottom-center,
|
||||
.pagedjs_margin-bottom-right,
|
||||
.pagedjs_margin-bottom-right-corner-holder,
|
||||
.pagedjs_margin-right,
|
||||
.pagedjs_margin-right-top,
|
||||
.pagedjs_margin-right-middle,
|
||||
.pagedjs_margin-right-bottom,
|
||||
.pagedjs_margin-left,
|
||||
.pagedjs_margin-left-top,
|
||||
.pagedjs_margin-left-middle,
|
||||
.pagedjs_margin-left-bottom {
|
||||
box-shadow: 0 0 0 1px inset var(--color-marginBox);
|
||||
}
|
||||
|
||||
/* uncomment this part for recto/verso book : ------------------------------------ */
|
||||
|
||||
|
||||
.pagedjs_pages {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagedjs_first_page {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.pagedjs_page {
|
||||
margin: 0 auto;
|
||||
margin-top: 10mm;
|
||||
}
|
||||
|
||||
.pagedjs_left_page{
|
||||
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width) + var(--pagedjs-bleed-left))!important;
|
||||
}
|
||||
|
||||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop{
|
||||
border-color: var(--pagedjs-crop-color);
|
||||
}
|
||||
|
||||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-middle{
|
||||
width: var(--pagedjs-cross-size)!important;
|
||||
}
|
||||
|
||||
.pagedjs_right_page{
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
/* uncomment this par to see the baseline : -------------------------------------------*/
|
||||
|
||||
|
||||
/* .pagedjs_pagebox {
|
||||
--pagedjs-baseline: 22px;
|
||||
--pagedjs-baseline-position: 5px;
|
||||
--pagedjs-baseline-color: cyan;
|
||||
background: linear-gradient(transparent 0%, transparent calc(var(--pagedjs-baseline) - 1px), var(--pagedjs-baseline-color) calc(var(--pagedjs-baseline) - 1px), var(--pagedjs-baseline-color) var(--pagedjs-baseline)), transparent;
|
||||
background-size: 100% var(--pagedjs-baseline);
|
||||
background-repeat: repeat-y;
|
||||
background-position-y: var(--pagedjs-baseline-position);
|
||||
} */
|
||||
|
||||
|
||||
/*--------------------------------------------------------------------------------------*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Marks (to delete when merge in paged.js) */
|
||||
|
||||
.pagedjs_marks-crop{
|
||||
z-index: 999999999999;
|
||||
|
||||
}
|
||||
|
||||
.pagedjs_bleed-top .pagedjs_marks-crop,
|
||||
.pagedjs_bleed-bottom .pagedjs_marks-crop{
|
||||
box-shadow: 1px 0px 0px 0px var(--pagedjs-crop-shadow);
|
||||
}
|
||||
|
||||
.pagedjs_bleed-top .pagedjs_marks-crop:last-child,
|
||||
.pagedjs_bleed-bottom .pagedjs_marks-crop:last-child{
|
||||
box-shadow: -1px 0px 0px 0px var(--pagedjs-crop-shadow);
|
||||
}
|
||||
|
||||
.pagedjs_bleed-left .pagedjs_marks-crop,
|
||||
.pagedjs_bleed-right .pagedjs_marks-crop{
|
||||
box-shadow: 0px 1px 0px 0px var(--pagedjs-crop-shadow);
|
||||
}
|
||||
|
||||
.pagedjs_bleed-left .pagedjs_marks-crop:last-child,
|
||||
.pagedjs_bleed-right .pagedjs_marks-crop:last-child{
|
||||
box-shadow: 0px -1px 0px 0px var(--pagedjs-crop-shadow);
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#textInput {
|
||||
width: calc(100% - 20px);
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: max-content;
|
||||
grid-row-gap: 5px;
|
||||
grid-column-gap: 1em;
|
||||
align-items: center;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
select, button, textarea {
|
||||
padding: 5px 10px;
|
||||
background-color: transparent;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
select, button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.mini {
|
||||
font-family: serif;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 1.5em;
|
||||
padding-left: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal img, .modal .modal-edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal.edit img {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.modal.edit .modal-edit {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal.edit .modal-add {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 1em;
|
||||
max-width: 400px;
|
||||
margin: 10% auto;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.modal-content input, .modal-content select, .modal-content button, .modal-content textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 450px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#textInput {
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: max-content max-content;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 660px) {
|
||||
body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#textInput {
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
@font-face {
|
||||
src: url("/static/fonts/jorvik-informal.regular.TTF");
|
||||
font-family: Jorvik;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
src: url("/static/fonts/Celtica-Bold.ttf");
|
||||
font-family: Celtica;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
body, button {
|
||||
font-family: Jorvik, fantasy;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm 20mm 30mm 20mm;
|
||||
background: no-repeat center/100% url("/static/uploads/bg.jpg");
|
||||
}
|
||||
|
||||
div.siegel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p.kuralie, p.content {
|
||||
initial-letter: 2;
|
||||
}
|
||||
|
||||
p.content {
|
||||
white-space: pre-line;
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
text-align-last: left;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
p.kuralie::first-letter, p.content::first-letter {
|
||||
font-size: 200%;
|
||||
padding: 4px 2px;
|
||||
margin-right: 4px;
|
||||
float: left;
|
||||
font-family: Celtica, fantasy;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.pagedjs_page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: var(--pagedjs-width-right);
|
||||
max-height: var(--pagedjs-height-right);
|
||||
}
|
||||
}
|
Nachher Breite: | Höhe: | Größe: 254 KiB |
Nachher Breite: | Höhe: | Größe: 521 KiB |
Nachher Breite: | Höhe: | Größe: 468 KiB |
Nachher Breite: | Höhe: | Größe: 42 KiB |
Nachher Breite: | Höhe: | Größe: 7.7 KiB |
Nachher Breite: | Höhe: | Größe: 20 KiB |
Nachher Breite: | Höhe: | Größe: 13 KiB |
@ -0,0 +1,228 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Schreiberling</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🪶</text></svg>">
|
||||
<link href="/static/css/tharanor.form.css" type="text/css" rel="stylesheet">
|
||||
<link href="/static/css/tharanor.letter.css" type="text/css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Schreiberling des Rates der Gilden</h1>
|
||||
|
||||
<form action="/generate" method="post">
|
||||
<div class="form-grid">
|
||||
<label for="senderSelect">Verfasser:</label>
|
||||
<span>
|
||||
<select name="sender" id="senderSelect">
|
||||
{% for s in senders %}
|
||||
<option value="{{ s.id }}">{{ s.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" class="mini add" onclick="openModalAdd('senderModal')">✚</button>
|
||||
<button type="button" class="mini edit" onclick="openModalEdit('sender')">✎</button>
|
||||
</span>
|
||||
|
||||
<label for="stateSelect">Reich:</label>
|
||||
<span>
|
||||
<select id="stateSelect">
|
||||
<option value="all">-- Alle --</option>
|
||||
{% for s in states %}
|
||||
<option value="{{ s.id }}">{{ s.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" class="mini add" onclick="openModalAdd('stateModal')">✚</button>
|
||||
<button type="button" class="mini edit" onclick="openModalEdit('state')">✎</button>
|
||||
</span>
|
||||
|
||||
<label for="recipientSelect">Empfänger:</label>
|
||||
<span>
|
||||
<select name="recipient" id="recipientSelect">
|
||||
{% for r in recipients %}
|
||||
<option value="{{ r.id }}">{{ r.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" class="mini add" onclick="openModalAdd('recipientModal')">✚</button>
|
||||
<button type="button" class="mini edit" onclick="openModalEdit('recipient')">✎</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label for="textInput">Brieftext:</label><br>
|
||||
<textarea name="text" id="textInput"></textarea><br><br>
|
||||
|
||||
<button type="submit">Brief in Auftrag geben</button>
|
||||
</form>
|
||||
|
||||
<!-- Sender Modal -->
|
||||
<div id="senderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 class="modal-add">Neuen Verfasser anlegen</h3>
|
||||
<h3 class="modal-edit">Verfasser editieren</h3>
|
||||
<form id="senderForm">
|
||||
<input type="hidden" name="id">
|
||||
<label>
|
||||
Name:<br>
|
||||
<input name="name" placeholder="Name" required>
|
||||
</label>
|
||||
<label>
|
||||
Kuralie:<br>
|
||||
<textarea name="kuralie" placeholder="Kuralie" rows="4"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
Siegel:<br>
|
||||
<img id="siegelPreview" src="" height="100" alt="siegel"/>
|
||||
<input type="file" name="siegel" required>
|
||||
</label>
|
||||
<label>
|
||||
Signatur:<br>
|
||||
<img id="signaturPreview" src="" width="300" alt="signatur"/>
|
||||
<input type="file" name="signatur" required>
|
||||
</label>
|
||||
<button type="submit">Speichern</button>
|
||||
<button type="button" onclick="closeModal('senderModal')">Abbrechen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- State Modal -->
|
||||
<div id="stateModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 class="modal-add">Neues Reich anlegen</h3>
|
||||
<h3 class="modal-edit">Reich editieren</h3>
|
||||
<form id="stateForm">
|
||||
<input type="hidden" name="id">
|
||||
<label>
|
||||
Name:<br>
|
||||
<input name="name" placeholder="Name" required>
|
||||
</label>
|
||||
<button type="submit">Speichern</button>
|
||||
<button type="button" onclick="closeModal('stateModal')">Abbrechen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Modal -->
|
||||
<div id="recipientModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 class="modal-add">Neuen Korrespondent anlegen</h3>
|
||||
<h3 class="modal-edit">Korrespondent editieren</h3>
|
||||
<form id="recipientForm">
|
||||
<input type="hidden" name="id">
|
||||
<label>
|
||||
Name:<br>
|
||||
<input name="name" placeholder="Name" required>
|
||||
</label>
|
||||
<label>
|
||||
Kuralie:<br>
|
||||
<textarea name="kuralie" placeholder="Kuralie" rows="4"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
Reich:<br>
|
||||
<select name="state_id">
|
||||
<option value="">-- Kein Reich --</option>
|
||||
{% for state in states %}
|
||||
<option value="{{ state.id }}">{{ state.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Speichern</button>
|
||||
<button type="button" onclick="closeModal('recipientModal')">Abbrechen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
document.querySelectorAll(".modal-content form").forEach(f => f.addEventListener('submit', modalEvent));
|
||||
|
||||
function openModalAdd(id) {
|
||||
document.getElementById(id).classList.add('active');
|
||||
}
|
||||
|
||||
function openModalEdit(type) {
|
||||
const select = document.getElementById(`${type}Select`);
|
||||
const selected_id = select.value;
|
||||
|
||||
fetch(`/api/${type}s/${selected_id}`)
|
||||
.then(res => res.json())
|
||||
.then(x => {
|
||||
const form = document.getElementById(`${type}Form`);
|
||||
Object.entries(x)
|
||||
.filter(([k]) => !["siegel", "signatur"].includes(k))
|
||||
.forEach(([k, v]) => form[k].value = v);
|
||||
if (type === "sender") {
|
||||
document.getElementById('siegelPreview').src = `/static/uploads/${x.siegel}`;
|
||||
document.getElementById('signaturPreview').src = `/static/uploads/${x.signatur}`;
|
||||
}
|
||||
document.getElementById(`${type}Modal`).classList.add('active', "edit");
|
||||
});
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).getElementsByTagName("form")[0].reset()
|
||||
document.getElementById(id).classList.remove('active', "edit");
|
||||
}
|
||||
|
||||
function modalEvent(e) {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const modal = e.target.parentNode.parentNode;
|
||||
const type = modal.id.replace("Modal", "");
|
||||
const is_edit = modal.classList.contains("edit");
|
||||
const id = data.get("id");
|
||||
data.delete("id");
|
||||
|
||||
fetch(is_edit ? `/api/${type}s/${id}` : `/api/${type}s`, {
|
||||
method: is_edit ? 'PUT' : 'POST',
|
||||
body: data
|
||||
}).then(res => res.json()).then(() => {
|
||||
closeModal(`${type}Modal`);
|
||||
refreshDropdown(type);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('stateSelect').addEventListener('change', function () {
|
||||
const stateId = this.value;
|
||||
|
||||
fetch(`/api/recipients?state_id=${stateId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const recipientSelect = document.getElementById('recipientSelect');
|
||||
recipientSelect.innerHTML = '';
|
||||
data.forEach(r => {
|
||||
const option = document.createElement('option');
|
||||
option.value = r.id;
|
||||
option.textContent = r.name;
|
||||
recipientSelect.appendChild(option);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function refreshDropdown(type) {
|
||||
fetch(`/api/${type}s`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const select = document.getElementById(`${type}Select`);
|
||||
const current = select.value;
|
||||
select.innerHTML = (type === "state") ? '<option value="all">-- Alle --</option>' : '';
|
||||
let modalSelect;
|
||||
if (type === 'state') {
|
||||
modalSelect = document.querySelector('#recipientModal select[name="state_id"]');
|
||||
modalSelect.innerHTML = '<option value="">-- Kein Reich --</option>';
|
||||
}
|
||||
data.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.textContent = s.name;
|
||||
select.appendChild(opt);
|
||||
if (type === 'state') {
|
||||
modalSelect.appendChild(opt.cloneNode(true));
|
||||
}
|
||||
});
|
||||
select.value = current;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<title>{{ recipient.state_name }} | {{ recipient.name }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="/static/css/interface.css" rel="stylesheet" type="text/css">
|
||||
<link href="/static/css/tharanor.letter.css" rel="stylesheet" type="text/css">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🪶</text></svg>">
|
||||
<script src="/static/js/paged.polyfill.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="siegel">
|
||||
<img class="siegel" src="{{ url_for('static', filename='uploads/' + sender.siegel) }}" alt="Siegel" height="120">
|
||||
</div>
|
||||
<p class="kuralie">{{ sender.kuralie }}</p>
|
||||
<p class="kuralie">{{ recipient.kuralie }}<br>
|
||||
<br>
|
||||
<p class="content">{{ text | e }}</p>
|
||||
<div class="signatur">
|
||||
<img class="signatur" src="{{ url_for('static', filename='uploads/' + sender.signatur) }}" alt="Signatur" width="300">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,156 @@
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from werkzeug.utils import secure_filename
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['UPLOAD_FOLDER'] = Path('./static/uploads')
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
|
||||
DB_PATH = 'letter_data.db'
|
||||
|
||||
|
||||
def query_db(query, args=(), one=False):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
cur.execute(query, args)
|
||||
rv = cur.fetchall()
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
senders = query_db('SELECT * FROM sender')
|
||||
recipients = query_db('SELECT * FROM recipient')
|
||||
states = query_db('SELECT * FROM state')
|
||||
return render_template('index.html', senders=senders, recipients=recipients, states=states)
|
||||
|
||||
|
||||
@app.route('/generate', methods=['POST'])
|
||||
def generate():
|
||||
sender_id = request.form['sender']
|
||||
recipient_id = request.form['recipient']
|
||||
text = request.form['text']
|
||||
sender = query_db('SELECT * FROM sender WHERE id = ?', [sender_id], one=True)
|
||||
recipient = query_db('SELECT recipient.*, state.name as state_name FROM recipient LEFT JOIN state ON recipient.state_id = state.id WHERE recipient.id = ?', [recipient_id], one=True)
|
||||
return render_template('letter.html', sender=sender, recipient=recipient, text=text)
|
||||
|
||||
|
||||
@app.route('/api/senders', methods=['POST'])
|
||||
def api_add_sender():
|
||||
name = request.form['name']
|
||||
kuralie = request.form['kuralie']
|
||||
siegel = request.files['logo']
|
||||
signatur = request.files['signature']
|
||||
|
||||
siegel_filename = secure_filename(siegel.filename)
|
||||
siegel.save(app.config['UPLOAD_FOLDER'] / siegel)
|
||||
|
||||
sig_filename = secure_filename(signatur.filename)
|
||||
signatur.save(app.config['UPLOAD_FOLDER'] / sig_filename)
|
||||
|
||||
query_db('INSERT INTO sender (name, kuralie, siegel, signatur) VALUES (?, ?, ?, ?)',
|
||||
[name, kuralie, siegel_filename, sig_filename])
|
||||
return jsonify(success=True)
|
||||
|
||||
|
||||
@app.route('/api/recipients', methods=['POST'])
|
||||
def api_add_recipient():
|
||||
name = request.form['name']
|
||||
kuralie = request.form['kuralie']
|
||||
state_id = request.form['state_id'] or None
|
||||
query_db('INSERT INTO recipient (name, kuralie, state_id) VALUES (?, ?, ?)', [name, kuralie, state_id])
|
||||
return jsonify(success=True)
|
||||
|
||||
|
||||
@app.route('/api/states', methods=['POST'])
|
||||
def api_add_state():
|
||||
name = request.form['name']
|
||||
query_db('INSERT INTO state (name) VALUES (?)', [name])
|
||||
return jsonify(success=True)
|
||||
|
||||
|
||||
@app.route('/api/senders', methods=['GET'])
|
||||
def api_get_senders():
|
||||
senders = query_db('SELECT id, name FROM sender')
|
||||
return jsonify([dict(row) for row in senders])
|
||||
|
||||
|
||||
@app.route('/api/senders/<int:sender_id>', methods=['GET'])
|
||||
def api_get_sender(sender_id):
|
||||
sender = query_db('SELECT * FROM sender WHERE id = ?', [sender_id], one=True)
|
||||
return jsonify(dict(sender))
|
||||
|
||||
|
||||
@app.route('/api/recipients', methods=['GET'])
|
||||
def api_get_recipients():
|
||||
state_id = request.args.get('state_id')
|
||||
if state_id and state_id != 'all':
|
||||
recipients = query_db('SELECT id, name FROM recipient WHERE state_id = ?', [state_id])
|
||||
else:
|
||||
recipients = query_db('SELECT id, name FROM recipient')
|
||||
return jsonify([dict(row) for row in recipients])
|
||||
|
||||
|
||||
@app.route('/api/recipients/<int:recipient_id>', methods=['GET'])
|
||||
def api_get_recipient(recipient_id):
|
||||
recipient = query_db('SELECT * FROM recipient WHERE id = ?', [recipient_id], one=True)
|
||||
return jsonify(dict(recipient))
|
||||
|
||||
|
||||
@app.route('/api/states', methods=['GET'])
|
||||
def api_get_states():
|
||||
states = query_db('SELECT id, name FROM state')
|
||||
return jsonify([dict(row) for row in states])
|
||||
|
||||
|
||||
@app.route('/api/states/<int:state_id>', methods=['GET'])
|
||||
def api_get_state(state_id):
|
||||
state = query_db('SELECT * FROM state WHERE id = ?', [state_id], one=True)
|
||||
return jsonify(dict(state))
|
||||
|
||||
|
||||
@app.route('/api/senders/<int:sender_id>', methods=['PUT'])
|
||||
def api_edit_sender(sender_id):
|
||||
name = request.form['name']
|
||||
kuralie = request.form['kuralie']
|
||||
siegel = request.files['siegel']
|
||||
signatur = request.files['signatur']
|
||||
|
||||
siegel_filename = secure_filename(siegel.filename) if siegel else None
|
||||
sig_filename = secure_filename(signatur.filename) if signatur else None
|
||||
|
||||
if siegel:
|
||||
siegel.save(app.config['UPLOAD_FOLDER'] / siegel_filename)
|
||||
if signatur:
|
||||
signatur.save(app.config['UPLOAD_FOLDER'] / sig_filename)
|
||||
|
||||
query_db('UPDATE sender SET name = ?, kuralie = ?, siegel = ?, signatur = ? WHERE id = ?',
|
||||
[name, kuralie, siegel_filename, sig_filename, sender_id])
|
||||
return jsonify(success=True)
|
||||
|
||||
|
||||
@app.route('/api/recipients/<int:recipient_id>', methods=['PUT'])
|
||||
def api_edit_recipient(recipient_id):
|
||||
name = request.form['name']
|
||||
kuralie = request.form['kuralie']
|
||||
state_id = request.form['state_id']
|
||||
|
||||
query_db('UPDATE recipient SET name = ?, kuralie = ?, state_id = ? WHERE id = ?',
|
||||
[name, kuralie, state_id, recipient_id])
|
||||
return jsonify(success=True)
|
||||
|
||||
|
||||
@app.route('/api/states/<int:state_id>', methods=['PUT'])
|
||||
def api_edit_state(state_id):
|
||||
name = request.form['name']
|
||||
query_db('UPDATE state SET name = ? WHERE id = ?', [name, state_id])
|
||||
return jsonify(success=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|