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