Peter Oertel пре 1 година
комит
f51754c84e
6 измењених фајлова са 327 додато и 0 уклоњено
  1. 2 0
      .gitignore
  2. 170 0
      main.py
  3. 14 0
      static/custom.css
  4. 52 0
      templates/base.html
  5. 64 0
      templates/home.html
  6. 25 0
      templates/login.html

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/venv
+.env

+ 170 - 0
main.py

@@ -0,0 +1,170 @@
+from email.policy import default
+import os
+import re
+import bcrypt
+import dotenv
+from flask import (
+    Flask,
+    flash,
+    redirect,
+    render_template,
+    request,
+    session,
+    url_for,
+)
+from flask_sqlalchemy import SQLAlchemy
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.sql import func
+
+
+dotenv.load_dotenv()
+app = Flask(__name__)
+app.secret_key = os.getenv("SECRET_KEY", "whataloadofnonsense")
+
+
+class Base(DeclarativeBase):
+    pass
+
+
+app.config["SQLALCHEMY_DATABASE_URI"] = (
+    "mysql://root:root@localhost:3306/topperstasks"
+)
+db = SQLAlchemy(model_class=Base)
+db.init_app(app)
+
+
+class User(db.Model):
+    __tablename__ = "users"
+    id = db.Column(db.Integer, primary_key=True)
+    username = db.Column(db.String, unique=True, nullable=False)
+    passhash = db.Column(db.String, nullable=False)
+    is_admin = db.Column(db.Boolean, default=False)
+
+    tasks = db.relationship("Task", back_populates="user")
+    task_assignments = db.relationship("Task_Assignment", back_populates="user")
+
+
+class Task(db.Model):
+    __tablename__ = "tasks"
+    id = db.Column(db.Integer, primary_key=True)
+    created_datetime = db.Column(db.String, nullable=False, default=func.now())
+    tasktext = db.Column(db.String, nullable=False)
+    created_by = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
+    due = db.Column(db.String)
+    completed = db.Column(db.Boolean, default=False)
+    deleted = db.Column(db.Boolean, default=False)
+
+    user = db.relationship(User, back_populates="tasks")
+    task_assignments = db.relationship("Task_Assignment", back_populates="task")
+
+
+class Task_Assignment(db.Model):
+    __tablename__ = "task_assignments"
+    id = db.Column(db.Integer, primary_key=True)
+    user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
+    task_id = db.Column(db.Integer, db.ForeignKey(Task.id), nullable=False)
+
+    user = db.relationship(User, back_populates="task_assignments")
+    task = db.relationship(Task, back_populates="task_assignments")
+
+
[email protected]("/")
+def home():
+    if not session.get("userid"):
+        return redirect(url_for("login"))
+
+    tasks = db.session.execute(
+        db.select(Task).where(Task.deleted == False)
+    ).scalars()
+
+    return render_template("home.html", tasks=tasks)
+
+
[email protected]("/login", methods=["GET", "POST"])
+def login():
+    if request.method == "POST":
+        try:
+            username = request.form.get("user")
+            password = request.form.get("password")
+
+            user: User = db.session.execute(
+                db.select(User).where(User.username == username)
+            ).scalar_one_or_none()
+
+            if not user:
+                flash("Failed to login", category="warning")
+                return redirect(url_for("login"))
+
+            if not user.passhash:
+                salt = bcrypt.gensalt()
+                user.passhash = bcrypt.hashpw(password.encode(), salt)
+                db.session.commit()
+                flash("Password set succesfully!", category="success")
+                return redirect(url_for("login"))
+
+            if bcrypt.checkpw(password.encode(), user.passhash.encode()):
+                session["userid"] = user.id
+                return redirect(url_for("home"))
+            else:
+                flash("Failed to login", category="warning")
+                return redirect(url_for("login"))
+        except:
+            flash("Critical Error, contact Peter pls", category="error")
+            return redirect(url_for("login"))
+
+    return render_template("login.html")
+
+
[email protected]("/addtask", methods=["POST"])
+def addtask():
+    try:
+        tasktext = request.form.get("task-text")
+        user_id = session.get("userid")
+
+        if not tasktext:
+            flash("Invalid task text", category="warning")
+            return redirect(url_for("home"))
+
+        if not user_id:
+            return redirect(url_for("login"))
+
+        newtask = Task(tasktext=tasktext, created_by=user_id)
+        db.session.add(newtask)
+        db.session.commit()
+    except:
+        flash("Critical Error, contact Peter pls", category="error")
+        return redirect(url_for("home"))
+
+    return redirect(url_for("home"))
+
+
[email protected]("/updatetask", methods=["POST"])
+def updatetask():
+    if "task-id" not in request.form:
+        flash("Failed to find task", category="warning")
+        return redirect(url_for("home"))
+
+    task = db.session.execute(
+        db.select(Task).where(Task.id == request.form.get("task-id"))
+    ).scalar_one_or_none()
+
+    if not task:
+        flash("Failed to find task", category="warning")
+        return redirect(url_for("home"))
+
+    if "due" in request.form:
+        if not task.due:
+            flash("Invalid due date", category="warning")
+            return redirect(url_for("home"))
+
+        datetime_str = request.form.get("due")
+        datetime_str = datetime_str.replace("T", " ")
+        datetime_str += ":00"
+        task.due = datetime_str
+
+        db.session.commit()
+        return redirect(url_for("home"))
+
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", debug=True)

+ 14 - 0
static/custom.css

@@ -0,0 +1,14 @@
+.max-wack {
+    width: 100%;
+}
+
+.task-card {
+    margin-bottom: 1rem;
+    background-color: var(--bs-secondary-bg);
+    border-radius: 1rem;
+    padding: 1rem;
+}
+
+.task-toggler {
+    float: right;
+}

+ 52 - 0
templates/base.html

@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html lang="en" data-bs-theme="dark">
+    <head>
+        <title>
+            {% block title %}
+            Toppers Tasks
+            {% endblock %}
+        </title>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+        <link
+            href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
+            rel="stylesheet">
+        <link rel="stylesheet" href="/static/custom.css">
+        <link rel="stylesheet"
+            href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
+    </head>
+    <body>
+        <nav class="navbar navbar-expand-lg bg-body-teritary">
+            <div class="container-fluid">
+                <a class="navbar-brand" href="#">Toppers Tasks</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 active" aria-current="page"
+                                href="/">Home</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" href="/account">Account</a>
+                        </li>
+                        <li class="nav-item"><a href="/admin"
+                                class="nav-link">Admin</a></li>
+                    </ul>
+                </div>
+            </div>
+        </nav>
+        <div class="container">
+            {% block content %}
+            {% endblock %}
+        </div>
+        <script
+            src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
+    </body>
+</html>

+ 64 - 0
templates/home.html

@@ -0,0 +1,64 @@
+{% extends "base.html" %}
+{% block title %}
+Toppers Tasks
+{% endblock %}
+{% block content %}
+{% for category, message in get_flashed_messages(with_categories=True) %}
+<div class="alert alert-{{ category }}" role="alert">
+    {{ message }}
+</div>
+{% endfor %}
+<form action="/addtask" method="post"
+    class="d-flex flex-row mb-3">
+    <input class="form-control form-control-lg me-3" type="text"
+        name="task-text"
+        id="task-text"
+        placeholder="New Task">
+    <button class="btn btn-primary" type="submit">
+        <i class="bi-plus-circle"></i>
+    </button>
+</form>
+<div class="row">
+    {% for task in tasks %}
+    <div class="task-card" id="task-{{task.id}}">
+        <a class="task-toggler" href="#task-details-{{task.id}}"
+            data-bs-toggle="collapse">
+            <i class="bi-list"></i>
+        </a>
+        <div class="task-text">{{task.tasktext}}</div>
+        <div id="task-details-{{task.id}}" class="collapse task-details mt-3">
+            <table class="table table-hover">
+                <tbody>
+                    <tr>
+                        <td>Created By</td>
+                        <td>{{ task.user.username }}</td>
+                    </tr>
+                    <tr>
+                        <td>Created Time</td>
+                        <td>{{ task.created_datetime }}</td>
+                    </tr>
+                    <tr>
+                        <td>Due By</td>
+                        <td>{% if task.due %}{{ task.due }}
+                            {% else %}
+                            <form action="/updatetask"
+                                method="post">
+                                <input type="text" id="task-id"
+                                    name="task-id"
+                                    value="{{task.id}}" hidden>
+                                <input type="datetime-local" name="due"
+                                    id="due">
+                                <button class="btn btn-primary"
+                                    type="submit">Update</button>
+                            </form>
+                            {% endif %}
+                        </td>
+                    </tr>
+
+                </tbody>
+            </table>
+        </div>
+    </div>
+    {% endfor %}
+</div>
+{% endblock %}

+ 25 - 0
templates/login.html

@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% block title %}
+Toppers Tasks | Login
+{% endblock %}
+{% block content %}
+{% for category, message in get_flashed_messages(with_categories=True) %}
+<div class="alert alert-{{ category }}" role="alert">
+    {{ message }}
+</div>
+{% endfor %}
+<form action="/login" method="post">
+    <div class="mb-3">
+        <label for="user" class="form-label">Username or Email</label>
+        <input type="text" name="user" id="user" class="form-control">
+    </div>
+    <div class="mb-3">
+        <label for="password" class="form-label">Password</label>
+        <input type="password" name="password" id="password"
+            class="form-control">
+    </div>
+    <div class="mb-3">
+        <button type="submit" class="btn btn-primary max-wack">Login</button>
+    </div>
+</form>
+{% endblock %}