Init project
This commit is contained in:
parent
45748f5d2f
commit
d13dff5146
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -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
|
||||||
11
.idea/HabitTracker.iml
generated
Normal file
11
.idea/HabitTracker.iml
generated
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
17
.idea/dataSources.xml
generated
Normal file
17
.idea/dataSources.xml
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="db" uuid="293bf84a-a146-451e-bdde-9f7806476a67">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:C:\Users\alter\Documents\Schule\Informatik\HabitTracker\db\db.sqlite</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
<libraries>
|
||||||
|
<library>
|
||||||
|
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar</url>
|
||||||
|
</library>
|
||||||
|
</libraries>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/misc.xml
generated
Normal file
9
.idea/misc.xml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.10 (HabitTracker) (2)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="Python 3.10 (HabitTracker) (2)" project-jdk-type="Python SDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/HabitTracker.iml" filepath="$PROJECT_DIR$/.idea/HabitTracker.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/sqldialects.xml
generated
Normal file
7
.idea/sqldialects.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/db/migrations/1705052036_create_user_table.sql" dialect="SQLite" />
|
||||||
|
<file url="PROJECT" dialect="SQLite" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
79
app.py
Normal file
79
app.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from flask import Flask, render_template, redirect, url_for, request
|
||||||
|
from flask_login import login_required, LoginManager
|
||||||
|
|
||||||
|
from models.User import User
|
||||||
|
|
||||||
|
# Create a new Flask instance
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Create a new route
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
# return 'Hello World'
|
||||||
|
return render_template('index.html', title='Home', utc_dt=datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S"))
|
||||||
|
|
||||||
|
@app.route('/test')
|
||||||
|
@login_required
|
||||||
|
def secret():
|
||||||
|
return 'Pssst!'
|
||||||
|
|
||||||
|
@app.route('/login')
|
||||||
|
def login():
|
||||||
|
return render_template('auth/login.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/signup')
|
||||||
|
def signup():
|
||||||
|
return render_template('auth/signup.html', errors={})
|
||||||
|
|
||||||
|
@app.route('/signup', methods=['POST'])
|
||||||
|
def signup_post():
|
||||||
|
email = request.form.get('email')
|
||||||
|
name = request.form.get('name')
|
||||||
|
password = request.form.get('password')
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
errors = {}
|
||||||
|
if not email:
|
||||||
|
errors['email'] = 'Email is required.'
|
||||||
|
if not name:
|
||||||
|
errors['name'] = 'Name is required.'
|
||||||
|
if not password:
|
||||||
|
errors['password'] = 'Password is required.'
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'auth/signup.html',
|
||||||
|
email=email,
|
||||||
|
name=name,
|
||||||
|
password=password,
|
||||||
|
errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save user to database. Maybe log the user in directly.
|
||||||
|
|
||||||
|
# Redirect to login page
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
# Log out functionality
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
if __name__ == '__main__':
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.login_view = 'login'
|
||||||
|
login_manager.init_app(app)
|
||||||
|
app.secret_key = 'PSSSSSHHHT!'
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return User.get(user_id)
|
||||||
|
|
||||||
|
app.run(port=5000, debug=True)
|
||||||
28
db/SQLiteClient.py
Normal file
28
db/SQLiteClient.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import hashlib
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
def con3():
|
||||||
|
conn = sqlite3.connect('db/db.sqlite')
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(name, email, password):
|
||||||
|
password = hashlib.md5(password.encode()).hexdigest()
|
||||||
|
query = f"INSERT INTO users (name, email, password) VALUES ({name}, {email}, {password})"
|
||||||
|
conn = con3()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(query)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return cursor.lastrowid
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(id):
|
||||||
|
query = f"SELECT * FROM users WHERE id = {id}"
|
||||||
|
conn = con3()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(query)
|
||||||
|
user = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return user
|
||||||
30
db/create_migration.py
Normal file
30
db/create_migration.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def is_snake_case(s):
|
||||||
|
if not s:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return s.islower() and all(char.isalpha() or char == '_' for char in s)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migration_name = ""
|
||||||
|
while not is_snake_case(migration_name):
|
||||||
|
if migration_name:
|
||||||
|
print(f"{migration_name} ist nicht im Camcel-Case")
|
||||||
|
migration_name = input('Gebe einen Namen für die Migration im snake_case an: ')
|
||||||
|
|
||||||
|
timestamp = datetime.datetime.now().timestamp()
|
||||||
|
migrations_path = os.path.join(os.path.dirname(__file__), 'migrations')
|
||||||
|
print(migrations_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.isdir(migrations_path):
|
||||||
|
raise FileNotFoundError
|
||||||
|
|
||||||
|
file = open(os.path.join(os.path.dirname(__file__), 'migrations', str(int(timestamp)) + '_' + migration_name + '.sql'), 'w')
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("migrations/ Directive could not be found within the db directory, contained in the project main directory.")
|
||||||
BIN
db/db.sqlite
Normal file
BIN
db/db.sqlite
Normal file
Binary file not shown.
63
db/migrate.py
Normal file
63
db/migrate.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
from sqlite3 import Connection
|
||||||
|
|
||||||
|
CREATE_MIGRATIONS_TABLE = """
|
||||||
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
file TEXT,
|
||||||
|
created_at TEXT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = sqlite3.connect("db.sqlite")
|
||||||
|
|
||||||
|
# Create migration table, if not exists yet
|
||||||
|
conn.execute(CREATE_MIGRATIONS_TABLE)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def create_migration(conn: Connection, migration: str):
|
||||||
|
conn.execute(f"""
|
||||||
|
INSERT INTO migrations (file, created_at) VALUES ("{migration}", "{datetime.datetime.now()}");
|
||||||
|
""")
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns True, if the migration has already been migrates once.
|
||||||
|
"""
|
||||||
|
def check_migration(conn: Connection, migration: str):
|
||||||
|
res = conn.cursor().execute(f"""
|
||||||
|
SELECT EXISTS(SELECT file FROM migrations WHERE file = "{migration}");
|
||||||
|
""")
|
||||||
|
return res.fetchone()[0] == 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Init DB with migrations table
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
migrations_path = os.path.join(os.path.dirname(__file__), 'migrations')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.isdir(migrations_path):
|
||||||
|
raise FileNotFoundError
|
||||||
|
|
||||||
|
conn = sqlite3.connect("db.sqlite")
|
||||||
|
for file in os.listdir(migrations_path):
|
||||||
|
if not file.endswith('.sql'):
|
||||||
|
continue
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), 'migrations', file)) as migration:
|
||||||
|
migration_name = Path(file).stem
|
||||||
|
|
||||||
|
if check_migration(conn, migration_name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Migrating {migration_name}...")
|
||||||
|
create_migration(conn, migration_name)
|
||||||
|
conn.execute(migration.read())
|
||||||
|
print(f"Migrated {migration_name}\n")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("migrations/ Directive could not be found within the db directory, contained in the project main directory.")
|
||||||
9
db/migrations/1705052036_create_user_table.sql
Normal file
9
db/migrations/1705052036_create_user_table.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
18
models/User.py
Normal file
18
models/User.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from db.SQLiteClient import create_user, get_user
|
||||||
|
|
||||||
|
|
||||||
|
class User:
|
||||||
|
def __init__(self, id, name, email):
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.email = email
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(name, email, password):
|
||||||
|
id = create_user(name, email, password)
|
||||||
|
return User(id, name, email)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get(id):
|
||||||
|
user = get_user(id)
|
||||||
|
return User(user[0], user[1], user[2]) if user else None
|
||||||
28
templates/auth/login.html
Normal file
28
templates/auth/login.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'layouts/main.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Login</h5>
|
||||||
|
<div class="card-body column">
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="email" class="col-sm-2 col-form-label">Email</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="email" class="form-control" id="email" placeholder="email@example.com">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="password" class="col-sm-2 col-form-label">Passwort</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="email" class="form-control" id="password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<button type="submit" class="btn btn-primary mb-3">Einloggen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
33
templates/auth/signup.html
Normal file
33
templates/auth/signup.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'layouts/main.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="column">
|
||||||
|
<h3>Registrieren</h3>
|
||||||
|
<form method="POST" action="/signup">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email-Adresse</label>
|
||||||
|
<input type="email" class="form-control {% if errors.get('email') %} is-invalid {% endif %}" id="email" name="email" placeholder="name@example.com" value="{{ email }}">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ errors.get('email', '') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control {% if errors.get('name') %} is-invalid {% endif %}" id="name" name="name" placeholder="Max" value="{{ name }}">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ errors.get('name', '') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Passwort</label>
|
||||||
|
<input type="password" class="form-control {% if errors.get('password') %} is-invalid {% endif %}" id="password" name="password" value="{{ password }}">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ errors.get('password', '') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button type="submit" class="btn btn-primary mb-3">Registrieren</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
17
templates/index.html
Normal file
17
templates/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'layouts/main.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Hello World!</h1>
|
||||||
|
<h3>{{ utc_dt }}</h3>
|
||||||
|
|
||||||
|
<button id="test">
|
||||||
|
Drück nicht!
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('test').addEventListener('click', function() {
|
||||||
|
alert('Du hast mich gedrückt!');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
57
templates/layouts/main.html
Normal file
57
templates/layouts/main.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" >
|
||||||
|
<title>{{ title }} - HabitTracker</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('index') }}">HabitTracker</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if title == 'Home' %} active {% endif %}" aria-current="page" href="{{ url_for('index') }}">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Dropdown
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item me-2">
|
||||||
|
<a class="btn btn-primary" aria-current="page" href="{{ url_for('login') }}">Login</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="btn btn-outline-secondary" aria-current="page" href="{{ url_for('signup') }}">Signup</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="container mt-3">
|
||||||
|
{% block content %} {% endblock %}
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous"></script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user