Compare commits

..

No commits in common. "master" and "fe_init" have entirely different histories.

31 changed files with 80 additions and 637 deletions

5
.gitignore vendored
View File

@ -1,3 +1,4 @@
# Created by https://www.toptal.com/developers/gitignore/api/python,flask,vim,visualstudiocode # Created by https://www.toptal.com/developers/gitignore/api/python,flask,vim,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=python,flask,vim,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=python,flask,vim,visualstudiocode
*.db *.db
@ -23,7 +24,7 @@ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
#lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
@ -233,5 +234,3 @@ tags
# End of https://www.toptal.com/developers/gitignore/api/python,flask,vim,visualstudiocode # End of https://www.toptal.com/developers/gitignore/api/python,flask,vim,visualstudiocode
# deb files
*.deb

View File

@ -219,7 +219,7 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
taskmanager flaskapp
Copyright (C) 2023 Decentrala Copyright (C) 2023 Decentrala
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

View File

@ -1,19 +0,0 @@
VENV = venv
PYTHON = $(VENV)/bin/python3
PIP = $(VENV)/bin/pip
run: $(VENV)/bin/activate
$(PYTHON) init_db.py
$(PYTHON) run.py
$(VENV)/bin/activate: requirements.txt
python3 -m venv $(VENV)
$(PIP) install -r requirements.txt
clean:
rm -rf __pycache__
rm -rf $(VENV)
rm -r taskmanager/__pycache__/
rm instance/taskmanager.db

View File

@ -1,39 +1,4 @@
# Task Manager # flaskapp
Interactive TODO list web application Web app
# Development Setup
Install python and pip on local machine
```bash
pip install virtualenv
python -m venv venv #/path/to/new/virtual/environment
source venv/bin/activate #activate virtual env
pip install -r requirments.txt
python3 ./init_db.py #initialize database
python3 ./run.py #run project
```
# On database changes
Delete file `/instance/taskmanager.db`
And reinit the db
```shell
python3 ./init_db.py
```
# Build app
```bash
cd build-deb/
make
```
# Install app
```bash
apt install ./build-deb/taskmanager.deb
```

View File

@ -7,12 +7,8 @@ man: man/taskmanager.1.md
deb: man ../requirments.txt ../run.py ../taskmanager ../LICENSE deb: man ../requirments.txt ../run.py ../taskmanager ../LICENSE
cp -r ../taskmanager/* taskmanager/var/taskmanager/taskmanager/ cp -r ../taskmanager/* taskmanager/var/taskmanager/taskmanager/
cp ../run.py taskmanager/var/taskmanager/ cp ../run.py taskmanager/var/taskmanager/
cp ../init_db.py taskmanager/var/taskmanager/
cp ../LICENSE taskmanager/var/taskmanager/ cp ../LICENSE taskmanager/var/taskmanager/
chmod -w taskmanager/DEBIAN/*
chmod +w taskmanager/DEBIAN/control
dpkg-deb --build taskmanager dpkg-deb --build taskmanager
chmod +w taskmanager/DEBIAN/*
clean: clean:
rm -f taskmanager.deb rm -f taskmanager.deb
rm -f man/taskmanager.1 rm -f man/taskmanager.1

View File

@ -1,15 +1,15 @@
% TASKMANAGER(1) taskmanager 1.0.0 % FLASKAPP(1) taskmanager 1.0.0
% Decentrala % Decentrala
% Jun 2023 % Jun 2023
# NAME # NAME
taskmanager - Interactive TODO list Web app taskmanager - Web app
# SYNOPSIS # SYNOPSIS
**python3 run.py** **python3 run.py**
# DESCRIPTION # DESCRIPTION
Interactive TODO list Web app Web app
# AUTHORS # AUTHORS
Decentrala Decentrala

View File

@ -7,5 +7,5 @@ Installed-Size: 2000
Depends: gunicorn, python3-flask-sqlalchemy Depends: gunicorn, python3-flask-sqlalchemy
Homepage: https://gitea.dmz.rs/Decentrala/taskmanager Homepage: https://gitea.dmz.rs/Decentrala/taskmanager
Maintainer: Decentrala <dmz@dmz.rs> Maintainer: Decentrala <dmz@dmz.rs>
Description: Interactive TODO list Web app Description: Web app
Version: 1.0.10 Version: 1.0.0

View File

@ -1,12 +1,3 @@
#!/bin/sh #!/bin/sh
/usr/bin/systemctl enable taskmanager.service /usr/bin/systemctl enable taskmanager.service
/var/taskmanager/init_db.py
/sbin/service taskmanager start /sbin/service taskmanager start
if [ -f /tmp/oldtaskmanagerconfig.ini ] ; then
cp /tmp/oldtaskmanagerconfig.ini /var/taskmanager/taskmanager/config.ini
rm /tmp/oldtaskmanagerconfig.ini
fi
if [ -f /tmp/oldtaskmanager.db ] ; then
cp /tmp/oldtaskmanager.db /var/taskmanager/instance/taskmanager.db
rm /tmp/oldtaskmanager.db
fi

View File

@ -1,6 +0,0 @@
if [ -f /var/taskmanager/taskmanager/config.ini ] ; then
cp /var/taskmanager/taskmanager/config.ini /tmp/oldtaskmanagerconfig.ini
fi
if [ -f /var/taskmanager/instance/taskmanager.db ] ; then
cp /var/taskmanager/instance/taskmanager.db /tmp/oldtaskmanager.db
fi

View File

@ -1,9 +1,3 @@
#!/bin/sh #!/bin/sh
/sbin/service taskmanager stop /sbin/service taskmanager stop
/usr/bin/systemctl disable taskmanager.service /usr/bin/systemdctl disable taskmanager.service
if [ -f /var/taskmanager/taskmanager/config.ini ] ; then
cp /var/taskmanager/taskmanager/config.ini /tmp/oldtaskmanagerconfig.ini
fi
if [ -f /var/taskmanager/instance/taskmanager.db ] ; then
cp /var/taskmanager/instance/taskmanager.db /tmp/oldtaskmanager.db
fi

View File

@ -1,12 +0,0 @@
[Unit]
Description=Gunicorn taskmanager service
Documentation=man:gunicorn(1)
After=network.target nss-lookup.target
[Service]
WorkingDirectory=/var/taskmanager/
ExecStart=/usr/bin/gunicorn --workers 3 --bind 0.0.0.0:80 run:app
[Install]
WantedBy=multi-user.target

View File

@ -1,9 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from taskmanager import db, app from flaskapp import db
print('[i] Trying to create databse...') print('[i] Trying to create databse...')
try: try:
with app.app_context():
db.create_all() db.create_all()
print('[+] Success you can proceed with deployment!') print('[+] Success you can proceed with deployment!')
except: except:

3
run.py
View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3 from flaskapp import app
from taskmanager import app
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=False) app.run(debug=False)

View File

@ -1,2 +0,0 @@
[credentials]
ADMINPASS = defaultpassword

View File

@ -1,17 +0,0 @@
from taskmanager.models import *
def gettaskusers(taskid):
users = list()
userids = list()
try:
taskusers = TaskUser.query.filter_by(taskid = taskid).all()
except:
taskusers = list()
for taskuser in taskusers:
userids.append(taskuser.userid)
for userid in userids:
users.append(User.query.get(userid))
return users

View File

@ -1,19 +1,7 @@
from taskmanager import db from taskmanager import db
class Task(db.Model): class Table(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False) variable1 = db.Column(db.Integer, nullable=False)
desc = db.Column(db.String, nullable=True) variable2 = db.Column(db.Integer, nullable=False)
creatorid = db.Column(db.Integer, nullable=True)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, nullable=False)
contact = db.Column(db.String, nullable=False)
password = db.Column(db.String, nullable=True)
class TaskUser(db.Model):
id = db.Column(db.Integer, primary_key=True)
taskid = db.Column(db.Integer, nullable=False)
userid = db.Column(db.Integer, nullable=False)

View File

@ -1,184 +1,34 @@
import configparser
import os
from flask import render_template, request, redirect from flask import render_template, request, redirect
from taskmanager import app, db from taskmanager import app, db
from taskmanager.functions import * from taskmanager.functions import *
from taskmanager.models import * from taskmanager.models import *
PROJECT_PATH = os.path.abspath(os.path.dirname(__file__))
CONFIG_PATH = os.path.join(PROJECT_PATH, "config.ini")
config = configparser.ConfigParser()
config.read(CONFIG_PATH)
ADMINPASS = config.get('credentials', 'ADMINPASS')
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def index(): def index():
tasks = Task.query.all()
return render_template('pages/index.html', tasks = tasks)
@app.route('/addtask', methods=['GET','POST'])
def addtask():
if request.method == 'GET': if request.method == 'GET':
return render_template('pages/addtask.html') localvariable1 = "Placeholder1"
elif request.method == 'POST': localvariable1 = "Placeholder2"
taskname = request.form['taskname'] try:
taskdesc = request.form['taskdesc'] return render_template('index.html', pagevariable1 = localvariable1, pagevariable2 = localvariable2 )
username = request.form['username'] except:
# Input sanitation return 'Error retriving page'
# Task name
if not taskname.isprintable() or ("<" in taskname and ">" in taskname):
return render_template('pages/response.html', response = "Task name has to be made only of letters or numbers.")
if len(taskname) < 1 or len(taskname) > 40:
return render_template('pages/response.html', response = "Task name lenght invalid, only smaller then 40 charachters allowed")
# Username
if username == "":
creatorid = None
else: else:
try: return 'HTTP request method not recogniezed'
creatorid = User.query.filter_by(username = username).first().id
except:
return render_template('pages/response.html', response = 'No user with this username. Please register')
if creatorid is None:
return render_template('pages/response.html', response = 'No user with this username. Please register.')
# Task descripton @app.route('/submit', methods=['POST', 'GET'])
if taskdesc != '': def submit():
if not taskdesc.isprintable() or ("<" in taskdesc and ">" in taskdesc):
return render_template('pages/response.html', response = "Task description has to be made of printable characters.")
if len(taskdesc) > 2000:
return render_template('pages/response.html', response = "Task description lenght invalid, only smaller then 2000 charachters allowed")
sqladdtask = Task(name = taskname, desc = taskdesc, creatorid = creatorid)
try:
db.session.add(sqladdtask)
db.session.commit()
return render_template('pages/response.html', response = 'Task added')
except:
return render_template('pages/response.html', response = 'Adding task failed')
@app.route('/register', methods=['POST', 'GET'])
def register():
if request.method == 'GET': if request.method == 'GET':
return render_template('pages/register.html') return render_template('submit.html')
elif request.method == 'POST': elif request.method == 'POST':
username = request.form['username'] userinput1 = request.form['forminput1']
contact = request.form['contact'] userinput2 = request.form['forminput2']
password = request.form['password'] sqlrow = Table(variable1 = int(userinput1), variable2 = int(userinput2))
# Username
if not username.isalnum():
return render_template('pages/response.html', response = "Username has to be made only of letters or numbers.")
if len(username) < 1 or len(username) > 40:
return render_template('pages/response.html', response = "Username lenght invalid, only smaller then 40 charachters allowed")
# Contact
if contact != '':
if not contact.isprintable() or ("<" in contact and ">" in contact):
return render_template('pages/response.html', response = "Contact information has to be made of printable characters.")
if len(contact) > 100:
return render_template('pages/response.html', response = "Contact lenght invalid, only smaller then 100 charachters allowed")
# Password
if password != '':
if not password.isprintable():
return render_template('pages/response.html', response = "Password has to be made of printable characters.")
if len(password) > 500:
return render_template('pages/response.html', response = "Password lenght invalid, only smaller then 500 charachters allowed")
sqladduser = User(username = username, contact = contact, password = password)
try: try:
db.session.add(sqladduser) db.session.add(sqlrow)
db.session.commit() db.session.commit()
return render_template('pages/response.html', response = 'User added') return 'Row added'
except: except:
return render_template('pages/response.html', response = 'Adding user failed') return 'Adding row to table failed'
else: else:
return render_template('pages/response.html', response = 'HTTP request method not recogniezed') return 'HTTP request method not recogniezed'
@app.route('/projects/<int:task_id>', methods=['GET','POST'])
def project(task_id:int):
try:
task = Task.query.get(task_id)
except:
return render_template('pages/response.html', response = 'Task not found, bad URL')
if task is None:
return render_template('pages/response.html', response = 'Task not found, bad URL')
users = gettaskusers(task_id)
if request.method == 'GET':
return render_template("pages/project.html", task = task, users = users)
elif request.method == 'POST':
# Assigning user to task
username = request.form['username']
for user in users:
if username == user.username:
return render_template('pages/response.html', response = 'User already added to task')
try:
userid = User.query.filter_by(username = username).first().id
except:
return render_template('pages/response.html', response = 'User not found, please register.')
if userid is None:
return render_template('pages/response.html', response = 'User not found, please register.')
sqladduser = TaskUser(userid = userid, taskid = task_id)
try:
db.session.add(sqladduser)
db.session.commit()
return render_template('pages/response.html', response = 'User added')
except:
return render_template('pages/response.html', response = 'Adding user failed')
@app.route('/projects/<int:task_id>/del', methods=['GET','POST'])
def deltask(task_id:int):
try:
task = Task.query.get(task_id)
except:
return render_template('pages/response.html', response = 'Task not found, bad URL')
if task is None:
return render_template('pages/response.html', response = 'Task not found, bad URL')
try:
taskusers = TaskUser.query.filter_by(taskid = task_id).all()
except:
taskusers = None
creatorid = task.creatorid
if request.method == 'GET':
if creatorid is None:
try:
db.session.delete(task)
db.session.commit()
except:
return render_template('pages/response.html', response = 'Deleting task failed')
try:
if taskusers != None:
for taskuser in taskusers:
db.session.delete(taskuser)
db.session.commit()
except:
return render_template('pages/response.html', response = 'Deleting user assignment to task failed')
return render_template('pages/response.html', response = 'Task deleted')
else:
return render_template('pages/deltask.html', task = task)
if request.method == 'POST':
password = request.form['password']
if len(password) < 1 or len(password) > 500:
return render_template('pages/response.html', response = "Password lenght invalid, only smaller then 500 charachters allowed")
# Check password
if password != ADMINPASS and password != User.query.get(creatorid).password:
return render_template('pages/response.html', response = 'Wrong password')
# Delete task
try:
db.session.delete(task)
db.session.commit()
except:
return render_template('pages/response.html', response = 'Deleting task failed')
try:
if taskusers != None:
for taskuser in taskusers:
db.session.delete(taskuser)
db.session.commit()
except:
return render_template('pages/response.html', response = 'Deleting user assignment to task failed')
return render_template('pages/response.html', response = 'Task deleted')

View File

@ -1,27 +1,16 @@
:root { :root {
--border-radus: 1rem; --border-radus: 1rem;
--background: #383840; --background: #000000;
--header-background: #212121; --header-background: #FFFFFF;
--header-height: 3rem; --header-height: 3rem;
--input-bar-height: 3rem; --input-bar-height: 3rem;
--white: #FFF;
--primary: #d2d2d2;
--font-primary: #d2d2d2;
color: #FFF;
} }
* { body{
margin: 0;
padding: 0;
}
a, a:visited {
color: var(--font-primary);
}
body {
background: var(--background); background: var(--background);
font-family: sans-serif; font-family: sans-serif;
margin: 0;
padding: 0;
} }
main { main {
@ -30,7 +19,6 @@ main {
align-self: flex-start; align-self: flex-start;
gap: 1rem; gap: 1rem;
margin: 0 auto; margin: 0 auto;
padding: 30px 0;
height: calc(100vh - var(--header-height) - var(--input-bar-height)); height: calc(100vh - var(--header-height) - var(--input-bar-height));
box-sizing: border-box; box-sizing: border-box;
} }
@ -47,12 +35,6 @@ header {
box-sizing: border-box; box-sizing: border-box;
} }
header ul {
display: flex;
list-style-type: none;
gap:15px
}
footer { footer {
width: 100vw; width: 100vw;
height: var(--input-bar-height); height: var(--input-bar-height);
@ -64,120 +46,3 @@ footer {
box-sizing: border-box; box-sizing: border-box;
gap: 1rem; gap: 1rem;
} }
/* global classes */
.container {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 20px 30px;
}
.page h1 {
margin-bottom: 15px;
}
.btn-wrap {
display: flex;
align-items: center;
justify-content: center;
padding: 15px 12px;
}
.btn {
display: inline-block;
padding: 5px 20px;
border: 1px solid var(--primary);
color: var(--primary);
background-color: transparent;
border-radius: 5px;
}
.btn:hover {
background-color: rgba(0,0,0,0.3);
cursor: pointer;
}
.btn a {
text-decoration: none;
}
.label {
text-transform: uppercase;
letter-spacing: 2px;
font-size: 14px;
font-weight: 600;
margin-bottom: 11px;
display: inline-block;
user-select: none;
}
.underline {
border-bottom: 1px solid var(--font-primary);
padding-bottom: 2px;
margin-top:20px;
}
.section-tasks > ul {
margin-left: 30px;
}
/* page index*/
.page-index .btn{
margin-bottom: 20px;
}
.task-wrap {
border: 1px solid var(--primary);
padding: 20px 40px 40px 40px;
border-radius: 5px;
box-shadow: inset 0 0 9px 4px rgba(255,255,255, 0.1);
}
.task {
display: flex;
border-bottom: 1px solid white;
}
.task a:hover {
background-color: rgba(255,255,255,0.1);
}
.task a {
display: flex;
gap: 15px;
width: 100%;
padding: 20px;
text-decoration: none;
}
/* Page projects */
.page-project h1 {
text-transform: uppercase;
}
.user-info-wrap{
margin-bottom: 10px;
}
/* form */
.form-wrap {
max-width: 400px;
}
.form-input {
display: flex;
flex-direction: column;
padding: 10px 0;
}
.form-input input {
height: 25px;
border-radius: 5px;
outline: none;
}

View File

@ -1,2 +0,0 @@
<footer>
</footer>

View File

@ -1,8 +0,0 @@
<header>
<nav class="container">
<ul>
<li class="current"><a href="/">Home</a></li>
<li><a href="/register">Register</a></li>
</ul>
</nav>
</header>

View File

@ -1,31 +0,0 @@
{% extends "layouts/base.html" %}
{% block content %}
<header>
<nav class="container">
<ul>
<li class="current"><a href="/">Home</a></li>
<li><a href="/register">Register</a></li>
</ul>
</nav>
</header>
<main class="container page page-index">
<section>
<div class="btn">
<a href="/addtask">Add new task</a>
</div>
<div class="tasks-wrap">
<h1>Tasks</h1>
{% for task in tasks %}
<div class="task">
<a href="/projects/{{task.id}}">
<div>{{task.id}}.</div>
<div>{{task.name}}</div>
</a>
</div>
{% endfor %}
</div>
</section>
</main>
<footer>
</footer>
{% endblock content %}

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Decentrala</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header>
</header>
<main>
<section>
{% for task in tasks %}
<li>{{task}}</li>
{% endfor %}
</section>
</main>
<footer>
</footer>
</body>
</html>

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/static/style.css" />
<title>Task manager</title>
</head>
<body>
{% include "includes/header.html" %}
{% block content %}
{% endblock content %}
{% include "includes/footer.html" %}
</body>
</html>

View File

@ -1,25 +0,0 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-addtask">
<h1>Create new task</h1>
<div class="form-wrap">
<form action="/addtask" method="POST">
<div class="form-input">
<label for="taskname" class="label">Task name:</label>
<input type="text" name="taskname" id="taskname" required />
</div>
<div class="form-input">
<label for="taskdesc" class="label">Description:</label>
<input type="text" name="taskdesc" id="taskdesc" placeholder="optional"/>
</div>
<div class="form-input">
<label for="username" class="label">Username:</label>
<input type="text" name="username" id="username" placeholder="optional"/>
</div>
<div class="btn-wrap">
<button class="btn">Submit</button>
</div>
</div>
</form>
</main>
{% endblock content %}

View File

@ -1,19 +0,0 @@
{% extends "layouts/base.html" %}
{% block content %}
<body>
<main class="container page page-addtask">
<h1>Create new task</h1>
<div class="form-wrap">
<form action="/projects/{{task.id}}/del" method="POST">
<p> Task creator's password <p>
<div class="form-input">
<label for="password" class="label">password:</label>
<input type="password" name="password" id="password" required />
</div>
<div class="btn-wrap">
<button class="btn">DELETE</button>
</div>
</div>
</form>
</main>
{% endblock content %}

View File

@ -1,21 +0,0 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-index">
<section>
<div class="btn">
<a href="/addtask">Add new task</a>
</div>
<div class="tasks-wrap">
<h1>Tasks</h1>
{% for task in tasks %}
<div class="task">
<a href="/projects/{{task.id}}">
<div>{{task.id}}.</div>
<div>{{task.name}}</div>
</a>
</div>
{% endfor %}
</div>
</section>
</main>
{% endblock content %}

View File

@ -1,36 +0,0 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-project">
<section >
<h1>{{task.name}}</h1>
<label class="label underline">Description</label>
<p>{{task.desc}}</p>
</section>
<section class="section-task">
<div>
<label class="label underline">Users added to this task</label>
{% for user in users %}
<div class="user-info-wrap">
<div><b>Username:</b> {{user.username}}</div>
<div><b>Contact info:</b> {{user.contact}}</div>
</div>
{% endfor %}
</div>
<div>
<label class="label underline"> Add person to task</label>
<div class="form-wrap">
<form action="/projects/{{task.id}}" method="POST">
<div class="form-input">
<label for="username" class="label">Username:</label>
<input type="text" name="username" id="username" required />
</div>
<div class="btn-wrap">
<button class="btn">Submit</button>
</div>
</form>
<p><a href="/projects/{{task.id}}/del">DELETE TASK</a><p>
</div>
</div>
</section>
</main>
{% endblock content %}

View File

@ -1,24 +0,0 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-register">
<div class="form-wrap">
<form action="/register" method="POST">
<div class="form-input">
<label for="username">Username:</label>
<input type="text" name="username" id="username" required />
</div>
<div class="form-input">
<label for="contact">Contact:</label>
<input type="text" name="contact" id="contact" required />
</div>
<div class="form-input">
<label for="password">Password:</label>
<input type="password" name="password" placeholder="optional" id="password"/>
</div>
<div class="btn-wrap">
<button class="btn">Submit</button>
</div>
</form>
</div>
</main>
{% endblock content %}

View File

@ -1,8 +0,0 @@
{% extends "layouts/base.html" %}
{% block content %}
<main class="container page page-project">
<section >
<p>{{response}}<p>
</section>
</main>
{% endblock content %}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Submit</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<main>
<form action="/submit" method="POST">
<label for="forminput1">forminput1</label>
<input type="text" name="forminput1" id="forminput1" required>
<label for="forminput2">forminput2</label>
<input type="text" name="forminput2" id="forminput2" required>
<button> Submit </button>
</form>
</main>
</body>
</html>