diff --git a/README.md b/README.md index 403b620..f9d3ea2 100644 --- a/README.md +++ b/README.md @@ -2,57 +2,81 @@ ![container-critique-logo](container-critique.png) -This is a blog based on Pythons Flask framework. -The blog is intended to be used to review and critique things. +Container Critique is a blog based on Pythons Flask framework. +The blog is intended to be used to review and critique movies, books and similar media. -## Features/To-Dos +## Features - -- [x] Accounts +- [x] User Management + - [x] Registration - [x] Login - [x] Logout - - [x] Register - - [x] User Page -- [ ] Review blog entries - - [x] Writing entries - - [ ] Editing entries - - [x] Deleting entries - - [ ] Better interface for writing, editing, deleting, login -- [x] Infinite-scroll blog page -- [x] Archive page - - [x] Months as headings - - [x] Links to scrolling blog page - - [x] Links to standalone article -- [x] Standalone article page - - [x] Links to scrolling blog page -- [x] Search page + - [x] User profile page + +- [x] Blog Functionality + - [x] Create entries + - [x] Edit entries + - [x] Delete entries + - [x] Standalone article pages + - [x] Links back to main blog page + - [x] Infinite-scroll main page + - [x] Archive page + - [x] Monthly grouping + - [x] Links to main blog page + - [x] Links to standalone articles + +- [x] Search - [x] Full-text search -- [x] RSS feed -- [x] Eye candy - - [x] Star rating + +- [x] Syndication + - [x] RSS feed + +- [x] User Interface + - [x] Responsive navigation + - [x] Header + - [x] Footer - [x] Rich text editor - - [x] CSS for all tags -- [x] Navigation - - [x] Header - - [x] Footer -- [x] Switchable CSS - - [x] CSS dark-theme - - [x] CSS light-theme -- [x] Docker installation -- [x] Logo + - [x] Star ratings + - [x] Styling for common HTML tags + - [x] Theme support + - [x] Dark theme + - [x] Light theme + - [x] Logo + +- [x] Deployment + - [x] Basic Python + - [x] Docker + +## To-Dos + +- [ ] Quality of Life Improvements + - [ ] Improved UI for writing entries + - [ ] Improved UI for editing entries + - [ ] Improved UI for deleting entries + - [ ] Improved UI for login flow ## Usage ## Deployment +### uv (Recommended Over PIP) + +* install [uv](https://github.com/astral-sh/uv?utm_source=chatgpt.com) +* `git clone https://github.com/tiyn/container-critique` +* `cd container-critique` +* install the dependencies with `uv sync` +* edit the `src/config.py` file according to your needs +* run `uv run python src/app.py` +* blog is available on port 5000 + ### PIP/Python -- `git clone https://github.com/tiyn/container-critique` -- `cd container-critique/src` -- edit the `config.py` file according to your needs -- `pip3install -r requirements.txt` - install depenencies -- run `python app.py` -- blog is available on port 5000 +* `git clone https://github.com/tiyn/container-critique` +* `cd container-critique/src` +* edit the `config.py` file according to your needs +* install dependencies with `pip install -r requirements.txt` +* run `python app.py` +* blog is available on port 5000 ### Docker @@ -63,11 +87,11 @@ The `config.py` can be found in the `src` folder. Set the following volumes with the -v tag. -| Volume-Name | Container mount | Description | +| Volume-Name | Container mount | Description | | ------------- | ---------------------- | ---------------------------------- | -| `config-file` | `/blog/config.py` | Config file | -| `data` | `/blog/data` | Directory for data | -| `css` | `/blog/static/css` | (optional) Directory for css files | +| `config-file` | `/blog/config.py` | Config file | +| `data` | `/blog/data` | Directory for data | +| `css` | `/blog/static/css` | (optional) Directory for css files | #### Ports @@ -75,7 +99,7 @@ Set the following ports with the -p tag. | Container-Port | Recommended outside port | Protocol | Description | | -------------- | ------------------------ | -------- | ----------- | -| `5000` | `80` | TCP | HTTP port | +| `5000` | `80` | TCP | HTTP port | #### Example run-command diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..25d5b6a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "container-critique" +version = "1.0.0" +requires-python = ">=3.14" +dependencies = [ + "bleach>=6.3.0", + "flask-ckeditor>=1.0.0", + "flask-login>=0.6.3", + "flask-wtf>=1.3.0", + "whoosh>=2.7.4", +] diff --git a/src/app.py b/src/app.py index c7dcde1..af696b3 100644 --- a/src/app.py +++ b/src/app.py @@ -1,19 +1,17 @@ -from flask import Flask, flash, render_template, redirect, abort, url_for, \ - request -from flask_ckeditor import CKEditor -from flask_login import current_user, login_user, LoginManager, logout_user, \ - login_required -from flask_wtf import CSRFProtect import os + +from flask import (Flask, abort, flash, redirect, render_template, request, url_for) +from flask_ckeditor import CKEditor +from flask_login import (LoginManager, current_user, login_required, login_user, logout_user) +from flask_wtf import CSRFProtect from werkzeug.exceptions import HTTPException import config from content import rating_to_star from database import Database -from forms import LoginForm, RegisterForm, WriteForm, SearchForm +from forms import LoginForm, RegisterForm, SearchForm, WriteForm from search import create_search_index, ft_search - app = Flask(__name__) csrf = CSRFProtect() db = Database() @@ -28,7 +26,7 @@ login.login_view = "login" @login.user_loader def load_user(ident): - """ + """ Returns a user by id. Parameters: @@ -37,68 +35,70 @@ def load_user(ident): Returns: User: user that matches the id, None if none matches the id """ - user = db.get_user_by_id(ident) - if user is not None: - return user - return None + user = db.get_user_by_id(ident) + if user is not None: + return user + return None @app.context_processor def inject_title(): - """ + """ Injects variables to the jinja2 templates. Returns: dict: dictionary of variables to inject. """ - return dict(title=config.TITLE, style=config.STYLE, - description=config.DESCRIPTION, - registration=config.ALLOW_REGISTRATION, r_to_star=rating_to_star) + return dict(title=config.TITLE, + style=config.STYLE, + description=config.DESCRIPTION, + registration=config.ALLOW_REGISTRATION, + r_to_star=rating_to_star) @app.errorhandler(HTTPException) def page_not_found(e): - """ + """ Renders the error pages. Returns: str: html formatted Error page """ - return render_template("error.html", errorcode=str(e.code) + " " + e.name), e.code + return render_template("error.html", errorcode=str(e.code) + " " + e.name), e.code @app.route("/") def index(): - """ + """ Renders the index page. Returns: str: html formatted index page """ - entries = db.get_entries() - entries.reverse() - return render_template("index.html", entries=entries) + entries = db.get_entries() + entries.reverse() + return render_template("index.html", entries=entries) @app.route("/archive") def archive(): - """ + """ Renders the archive page. Returns: str: html formatted archive page """ - entries = db.get_entries() - entries.sort(key=lambda y: y.item.name) - entries.reverse() - entries.sort(key=lambda y: y.item.date) - entries.reverse() - return render_template("archive.html", entries=entries) + entries = db.get_entries() + entries.sort(key=lambda y: y.item.name) + entries.reverse() + entries.sort(key=lambda y: y.item.date) + entries.reverse() + return render_template("archive.html", entries=entries) @app.route("/user/") def user(name): - """ + """ Renders the user page of a specific user. Parameters: @@ -107,19 +107,19 @@ def user(name): Returns: str: html formatted user page """ - entries = db.get_entries_by_username(name) - entries.sort(key=lambda y: y.item.name) - entries.reverse() - entries.sort(key=lambda y: y.item.date) - entries.reverse() - if entries != []: - return render_template("user.html", name=name, entries=entries) - abort(404) + entries = db.get_entries_by_username(name) + entries.sort(key=lambda y: y.item.name) + entries.reverse() + entries.sort(key=lambda y: y.item.date) + entries.reverse() + if entries != []: + return render_template("user.html", name=name, entries=entries) + abort(404) @app.route("/entry/") def entry(ident): - """ + """ Renders the entry page of a specific entry. Parameters: @@ -128,140 +128,171 @@ def entry(ident): Returns: str: html formatted entry page """ - entry = db.get_entry_by_id(ident) - if entry is not None: - return render_template("standalone.html", entry=entry) - abort(404) + entry = db.get_entry_by_id(ident) + if entry is not None: + return render_template("standalone.html", entry=entry) + abort(404) @app.route("/feed") def feed(): - """ + """ Renders the rss feed of a the feed. Returns: str: xml formatted feed """ - entries = db.get_entries() - entries.reverse() - rss_xml = render_template("rss.xml", entries=entries) - return rss_xml + entries = db.get_entries() + entries.reverse() + rss_xml = render_template("rss.xml", entries=entries) + return rss_xml @app.route("/search", methods=["GET", "POST"]) def search(): - """ + """ Renders the search page. Returns: str: html formatted search page. """ - form = SearchForm() - if request.method == "POST": - query_str = request.form["query_str"] - query_res = ft_search(query_str) - return render_template("search.html", form=form, results=query_res), 200 - return render_template("search.html", form=form, content=""), 200 + form = SearchForm() + if request.method == "POST": + query_str = request.form["query_str"] + query_res = ft_search(query_str) + return render_template("search.html", form=form, results=query_res), 200 + return render_template("search.html", form=form, content=""), 200 @app.route("/login", methods=["GET", "POST"]) def login(): - """ + """ Logs the user in. Returns: str: html formatted login page, if login is successful renders the index page. """ - if current_user.is_authenticated: + if current_user.is_authenticated: + return redirect(url_for("index")) + form = LoginForm() + if form.validate_on_submit(): + user = db.get_user_by_name(form.username.data) + print(user) + if user is not None: + if user.check_password(form.password.data): + login_user(user) return redirect(url_for("index")) - form = LoginForm() - if form.validate_on_submit(): - user = db.get_user_by_name(form.username.data) - print(user) - if user is not None: - if user.check_password(form.password.data): - login_user(user) - return redirect(url_for("index")) - flash("Invalid username or password.") - return redirect(url_for("login")) - return render_template("login.html", form=form) + flash("Invalid username or password.") + return redirect(url_for("login")) + return render_template("login.html", form=form) @app.route('/logout') def logout(): - """ + """ Logs out the current user. Returns: str: html formatted index page. """ - logout_user() - return redirect(url_for("index")) + logout_user() + return redirect(url_for("index")) @app.route("/register", methods=["GET", "POST"]) def register(): - """ + """ Registers new users. Returns: str: html formatted registration page, if registration is successful renders the index page. """ - if current_user.is_authenticated or not config.ALLOW_REGISTRATION: + if current_user.is_authenticated or not config.ALLOW_REGISTRATION: + return redirect(url_for("index")) + form = RegisterForm() + if form.validate_on_submit(): + user = db.get_user_by_name(form.username.data) + if user is None: + ident = db.insert_user(form.username.data, form.password.data) + if ident is not None: + user = db.get_user_by_id(ident) + login_user(user) return redirect(url_for("index")) - form = RegisterForm() - if form.validate_on_submit(): - user = db.get_user_by_name(form.username.data) - if user is None: - ident = db.insert_user(form.username.data, form.password.data) - if ident is not None: - user = db.get_user_by_id(ident) - login_user(user) - return redirect(url_for("index")) - flash("An error occured during registration.") - return redirect(url_for("register")) - return render_template("register.html", form=form) + flash("An error occured during registration.") + return redirect(url_for("register")) + return render_template("register.html", form=form) @app.route("/write_entry", methods=["GET", "POST"]) @login_required def write_entry(): - """ + """ Stores newly written entries. Returns: str: html formatted write entry page, if posting of the entry is successful renders the index page. """ - if not current_user.is_authenticated: - return redirect(url_for("index")) - form = WriteForm() - if form.validate_on_submit(): - db.insert_entry(form.name.data, form.date.data, - form.text.data, form.rating.data, current_user.id) - create_search_index() - return redirect(url_for("index")) - return render_template("write.html", form=form) + if not current_user.is_authenticated: + return redirect(url_for("index")) + form = WriteForm() + if form.validate_on_submit(): + db.insert_entry(form.name.data, form.date.data, form.text.data, form.rating.data, + current_user.id) + create_search_index() + return redirect(url_for("index")) + return render_template("write.html", form=form) -@app.route("/delete_entry/", methods=["GET", "POST"]) +@app.route("/delete_entry/", methods=["POST"]) @login_required def delete_entry(ident): - """ + """ Deletes an existing entry. Returns: str: html formatted index entry page. """ - if not current_user.is_authenticated: - return redirect(url_for("index")) - if current_user.id == db.get_entry_by_id(ident).user.id: - db.delete_entry(ident) - create_search_index() + if not current_user.is_authenticated: return redirect(url_for("index")) + if current_user.id == db.get_entry_by_id(ident).user.id: + db.delete_entry(ident) + create_search_index() + return redirect(url_for("index")) + + +@app.route("/edit_entry/", methods=["GET", "POST"]) +@login_required +def edit_entry(ident): + + entry = db.get_entry_by_id(ident) + + if entry is None: + abort(404) + + if current_user.id != entry.user.id: + abort(403) + + form = WriteForm() + + if request.method == "GET": + form.name.data = entry.item.name + form.date.data = entry.item.date + form.text.data = entry.text + form.rating.data = entry.rating + + if form.validate_on_submit(): + + db.update_entry(ident, form.name.data, form.date.data, form.text.data, form.rating.data) + + create_search_index() + + return redirect(url_for("entry", ident=ident)) + + return render_template("write.html", form=form, edit_mode=True) if __name__ == "__main__": - app.run(host="0.0.0.0") + app.run(host="0.0.0.0") diff --git a/src/content.py b/src/content.py index 8507f39..47663ce 100644 --- a/src/content.py +++ b/src/content.py @@ -4,7 +4,7 @@ db = Database() def rating_to_star(rating): - """ + """ Creates a string with stars based on the rating. Parameters: @@ -13,10 +13,10 @@ def rating_to_star(rating): Returns: string: unicode-formatted star-rating string """ - res = u"\u272D"*int(rating/20) - length = len(res) - if rating/20 % 1 >= 0.5: - length += 1 - res += u" \u2BE8 " - res += (u"\u2606" * (5 - length)) - return res + res = u"\u272D" * int(rating / 20) + length = len(res) + if rating / 20 % 1 >= 0.5: + length += 1 + res += u" \u2BE8 " + res += (u"\u2606" * (5 - length)) + return res diff --git a/src/database.py b/src/database.py index 5ccd5e9..564b73a 100644 --- a/src/database.py +++ b/src/database.py @@ -1,11 +1,12 @@ -from datetime import date as dt import os import sqlite3 -from werkzeug.security import generate_password_hash, check_password_hash +from datetime import date as dt + +from werkzeug.security import check_password_hash, generate_password_hash class User(): - """ + """ A class to represent a user. Attributes: @@ -17,16 +18,16 @@ class User(): pass_hash (str): hash of the users password """ - def __init__(self, name, pass_hash=None): - self.name = name - self.id = None - self.is_active = True - self.is_authenticated = True - self.is_anonymous = False - self.pass_hash = pass_hash + def __init__(self, name, pass_hash=None): + self.name = name + self.id = None + self.is_active = True + self.is_authenticated = True + self.is_anonymous = False + self.pass_hash = pass_hash - def set_password(self, password): - """ + def set_password(self, password): + """ Set the password hash of the user from a password. Parameters: @@ -35,10 +36,10 @@ class User(): Returns: None """ - self.pass_hash = generate_password_hash(password) + self.pass_hash = generate_password_hash(password) - def set_id(self, ident): - """ + def set_id(self, ident): + """ Set the id of the user. Parameters: @@ -47,10 +48,10 @@ class User(): Returns: None """ - self.id = ident + self.id = ident - def check_password(self, password): - """ + def check_password(self, password): + """ Check if a given password matches the one of the users by comparing the hashes. @@ -60,20 +61,20 @@ class User(): Returns: bool: True if it matches the users password, False otherwise """ - return check_password_hash(self.pass_hash, password) + return check_password_hash(self.pass_hash, password) - def get_id(self): - """ + def get_id(self): + """ Get the id of the user. Returns: int: id of the user """ - return self.id + return self.id class Item(): - """ + """ A class to represent an item. Attributes: @@ -82,23 +83,23 @@ class Item(): date (str): date the item was created """ - def __init__(self, name, date): - self.name = name - self.date = date - self.id = None + def __init__(self, name, date): + self.name = name + self.date = date + self.id = None - def set_id(self, ident): - """ + def set_id(self, ident): + """ Set the id of the item. Returns: int: id of the item """ - self.id = ident + self.id = ident class Entry(): - """ + """ A class to represent an entry. Attributes: @@ -110,43 +111,43 @@ class Entry(): id (int): id of the item """ - def __init__(self, text, rating, date): - self.text = text - self.rating = rating - self.date = date - self.item = None - self.user = None + def __init__(self, text, rating, date): + self.text = text + self.rating = rating + self.date = date + self.item = None + self.user = None - def set_id(self, ident): - """ + def set_id(self, ident): + """ Set the id of the entry. Parameters: ident(int): id of the entry """ - self.id = ident + self.id = ident - def set_item(self, item): - """ + def set_item(self, item): + """ Set the item of the entry. Parameters: item(Item): item of the entry """ - self.item = item + self.item = item - def set_user(self, user): - """ + def set_user(self, user): + """ Set the user of the entry. Parameters: user(User): user of the entry """ - self.user = user + self.user = user class Database: - """ + """ A class to represent an entry. Attributes: @@ -156,53 +157,53 @@ class Database: DB_DIR(PathLike): path that leads to the directory containing the database """ - def __init__(self): - self.USER_TABLE_FILE = 'USERS' - self.ENTRY_TABLE_FILE = 'ENTRIES' - self.ITEM_TABLE_FILE = 'ITEMS' - self.DB_DIR = os.path.dirname("./data/") - self.setup_db() + def __init__(self): + self.USER_TABLE_FILE = 'USERS' + self.ENTRY_TABLE_FILE = 'ENTRIES' + self.ITEM_TABLE_FILE = 'ITEMS' + self.DB_DIR = os.path.dirname("./data/") + self.setup_db() - def connect(self): - """ + def connect(self): + """ Connect to an existing database instance based on the object attributes. Return: Connection: connection to the database """ - path = os.path.join(self.DB_DIR, "data.db") - return sqlite3.connect(path) + path = os.path.join(self.DB_DIR, "data.db") + return sqlite3.connect(path) - def setup_db(self): - """ + def setup_db(self): + """ Creates a database with the needed tables if it doesn't already exits. """ - db = self.connect() - crs = db.cursor() - query = "CREATE TABLE IF NOT EXISTS " + self.USER_TABLE_FILE + \ - "(id INTEGER PRIMARY KEY AUTOINCREMENT," + \ - "name CHAR(32) NOT NULL UNIQUE," + \ - "password CHAR(32) NOT NULL)" - crs.execute(query) - query = "CREATE TABLE IF NOT EXISTS " + self.ITEM_TABLE_FILE + \ - "(id INTEGER PRIMARY KEY AUTOINCREMENT," + \ - "name CHAR(32) NOT NULL," + \ - "date CHAR(4)," + \ - "UNIQUE(date, name))" - crs.execute(query) - query = "CREATE TABLE IF NOT EXISTS " + self.ENTRY_TABLE_FILE + \ - "(id INTEGER PRIMARY KEY AUTOINCREMENT," + \ - "item_id INTEGER NOT NULL REFERENCES " + self.ITEM_TABLE_FILE + "(id)," + \ - "text TEXT NOT NULL," + \ - "rating INTEGER NOT NULL," +\ - "user_id INTEGER REFERENCES " + self.USER_TABLE_FILE + "(id),"\ - "date CHAR(10) NOT NULL)" - crs.execute(query) - db.commit() + db = self.connect() + crs = db.cursor() + query = "CREATE TABLE IF NOT EXISTS " + self.USER_TABLE_FILE + \ + "(id INTEGER PRIMARY KEY AUTOINCREMENT," + \ + "name CHAR(32) NOT NULL UNIQUE," + \ + "password CHAR(32) NOT NULL)" + crs.execute(query) + query = "CREATE TABLE IF NOT EXISTS " + self.ITEM_TABLE_FILE + \ + "(id INTEGER PRIMARY KEY AUTOINCREMENT," + \ + "name CHAR(32) NOT NULL," + \ + "date CHAR(4)," + \ + "UNIQUE(date, name))" + crs.execute(query) + query = "CREATE TABLE IF NOT EXISTS " + self.ENTRY_TABLE_FILE + \ + "(id INTEGER PRIMARY KEY AUTOINCREMENT," + \ + "item_id INTEGER NOT NULL REFERENCES " + self.ITEM_TABLE_FILE + "(id)," + \ + "text TEXT NOT NULL," + \ + "rating INTEGER NOT NULL," +\ + "user_id INTEGER REFERENCES " + self.USER_TABLE_FILE + "(id),"\ + "date CHAR(10) NOT NULL)" + crs.execute(query) + db.commit() - def insert_user(self, username, password): - """ + def insert_user(self, username, password): + """ Insert a row in the user table. Parameters: @@ -212,20 +213,20 @@ class Database: Returns: int: number of the line the row was added, None if it wasn't successful """ - pass_hash = generate_password_hash(password) - if self.get_user_by_name(username) is None and pass_hash is not None: - db = self.connect() - crs = db.cursor() - query = "INSERT INTO " + self.USER_TABLE_FILE + \ - "(`name`,`password`)" + \ - "VALUES (?, ?) ON CONFLICT DO NOTHING" - crs.execute(query, (username, pass_hash)) - db.commit() - return crs.lastrowid - return None + pass_hash = generate_password_hash(password) + if self.get_user_by_name(username) is None and pass_hash is not None: + db = self.connect() + crs = db.cursor() + query = "INSERT INTO " + self.USER_TABLE_FILE + \ + "(`name`,`password`)" + \ + "VALUES (?, ?) ON CONFLICT DO NOTHING" + crs.execute(query, (username, pass_hash)) + db.commit() + return crs.lastrowid + return None - def insert_entry(self, name, date, text, rating, user_id=None): - """ + def insert_entry(self, name, date, text, rating, user_id=None): + """ Insert a row in the entry table. Parameters: @@ -238,25 +239,25 @@ class Database: Returns: int: number of the line the row was added """ - db = self.connect() - crs = db.cursor() - query = "INSERT OR IGNORE INTO " + self.ITEM_TABLE_FILE + \ - "(`name`,`date`)" + "VALUES (?, ?)" - crs.execute(query, (name, date)) - query = "SELECT id FROM " + self.ITEM_TABLE_FILE + \ - " WHERE name = ? AND date = ?" - crs.execute(query, (name, date)) - item_id = crs.fetchone()[0] - date = dt.today().strftime('%Y-%m-%d') - query = "INSERT INTO " + self.ENTRY_TABLE_FILE + \ - "(`item_id`, `text`, `rating`, `user_id`, `date`)" + \ - "VALUES (?, ?, ?, ?, ?)" - crs.execute(query, (item_id, text, rating, user_id, date)) - db.commit() - return crs.lastrowid + db = self.connect() + crs = db.cursor() + query = "INSERT OR IGNORE INTO " + self.ITEM_TABLE_FILE + \ + "(`name`,`date`)" + "VALUES (?, ?)" + crs.execute(query, (name, date)) + query = "SELECT id FROM " + self.ITEM_TABLE_FILE + \ + " WHERE name = ? AND date = ?" + crs.execute(query, (name, date)) + item_id = crs.fetchone()[0] + date = dt.today().strftime('%Y-%m-%d') + query = "INSERT INTO " + self.ENTRY_TABLE_FILE + \ + "(`item_id`, `text`, `rating`, `user_id`, `date`)" + \ + "VALUES (?, ?, ?, ?, ?)" + crs.execute(query, (item_id, text, rating, user_id, date)) + db.commit() + return crs.lastrowid - def delete_entry(self, ident): - """ + def delete_entry(self, ident): + """ Delete a row from the entry table based on the entrys id. Parameters: @@ -265,31 +266,69 @@ class Database: Returns: int: number of the line the row was removed from """ - db = self.connect() - crs = db.cursor() - query = "DELETE FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?" - crs.execute(query, (ident, )) - db.commit() - return crs.lastrowid + db = self.connect() + crs = db.cursor() + query = "DELETE FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?" + crs.execute(query, (ident,)) + db.commit() + return crs.lastrowid - def get_entries(self): + def update_entry(self, ident, name, item_date, text, rating): + """ + Update an existing entry. + + Parameters: + ident (int): id of the entry + name (str): updated item name + item_date (str): updated item date + text (str): updated review text + rating (int): updated rating """ + + db = self.connect() + crs = db.cursor() + + query = "SELECT item_id FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?" + crs.execute(query, (ident,)) + fetched = crs.fetchone() + + if fetched is None: + return None + + item_id = fetched[0] + + query = "UPDATE " + self.ITEM_TABLE_FILE + \ + " SET name = ?, date = ? WHERE id = ?" + + crs.execute(query, (name, item_date, item_id)) + + query = "UPDATE " + self.ENTRY_TABLE_FILE + \ + " SET text = ?, rating = ? WHERE id = ?" + + crs.execute(query, (text, rating, ident)) + + db.commit() + + return ident + + def get_entries(self): + """ Return all the entries stored in the database. Return: List(Entry): list of entries in database """ - db = self.connect() - crs = db.cursor() - query = "SELECT * FROM " + self.ENTRY_TABLE_FILE - crs.execute(query) - res = [] - for item in crs.fetchall(): - res.append(self.entry_from_db(*item)) - return res + db = self.connect() + crs = db.cursor() + query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + crs.execute(query) + res = [] + for item in crs.fetchall(): + res.append(self.entry_from_db(*item)) + return res - def get_entry_by_id(self, ident): - """ + def get_entry_by_id(self, ident): + """ Return an entry stored in the database based on the entrys id. Parameters: @@ -298,18 +337,18 @@ class Database: Returns: Entry: entry that matched the given id """ - db = self.connect() - crs = db.cursor() - query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?" - crs.execute(query, (ident, )) - fetched = crs.fetchone() - if fetched is None: - return None - else: - return self.entry_from_db(*fetched) + db = self.connect() + crs = db.cursor() + query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?" + crs.execute(query, (ident,)) + fetched = crs.fetchone() + if fetched is None: + return None + else: + return self.entry_from_db(*fetched) - def get_entries_by_username(self, username): - """ + def get_entries_by_username(self, username): + """ Return a entries stored in the database based on the entries name. Parameters: @@ -318,19 +357,19 @@ class Database: Returns: List(Entry): entries that matched the given name """ - db = self.connect() - crs = db.cursor() - query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + \ - " WHERE user_id = (SELECT id FROM " + self.USER_TABLE_FILE + \ - " WHERE name = ?)" - crs.execute(query, (username, )) - res = [] - for item in crs.fetchall(): - res.append(self.entry_from_db(*item)) - return res + db = self.connect() + crs = db.cursor() + query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + \ + " WHERE user_id = (SELECT id FROM " + self.USER_TABLE_FILE + \ + " WHERE name = ?)" + crs.execute(query, (username,)) + res = [] + for item in crs.fetchall(): + res.append(self.entry_from_db(*item)) + return res - def get_item_by_id(self, ident): - """ + def get_item_by_id(self, ident): + """ Return an item stored in the database based on the items id. Parameters: @@ -339,18 +378,18 @@ class Database: Returns: Item: item that matched the given id """ - db = self.connect() - crs = db.cursor() - query = "SELECT * FROM " + self.ITEM_TABLE_FILE + " WHERE id = ?" - crs.execute(query, (ident, )) - fetched = crs.fetchone() - if fetched is None: - return None - else: - return self.item_from_db(*fetched) + db = self.connect() + crs = db.cursor() + query = "SELECT * FROM " + self.ITEM_TABLE_FILE + " WHERE id = ?" + crs.execute(query, (ident,)) + fetched = crs.fetchone() + if fetched is None: + return None + else: + return self.item_from_db(*fetched) - def get_user_by_id(self, ident): - """ + def get_user_by_id(self, ident): + """ Return a user stored in the database based on the users id. Parameters: @@ -359,18 +398,18 @@ class Database: Returns: Item: user that matched the given id """ - db = self.connect() - crs = db.cursor() - query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE id = ?" - crs.execute(query, (ident, )) - fetched = crs.fetchone() - if fetched is None: - return None - else: - return self.user_from_db(*fetched) + db = self.connect() + crs = db.cursor() + query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE id = ?" + crs.execute(query, (ident,)) + fetched = crs.fetchone() + if fetched is None: + return None + else: + return self.user_from_db(*fetched) - def get_user_by_name(self, name): - """ + def get_user_by_name(self, name): + """ Return a user stored in the database based on the user name. Parameters: @@ -379,18 +418,18 @@ class Database: Returns: Entry: user that matched the given name """ - db = self.connect() - crs = db.cursor() - query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE name = ?" - crs.execute(query, (name, )) - fetched = crs.fetchone() - if fetched is None: - return None - else: - return self.user_from_db(*fetched) + db = self.connect() + crs = db.cursor() + query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE name = ?" + crs.execute(query, (name,)) + fetched = crs.fetchone() + if fetched is None: + return None + else: + return self.user_from_db(*fetched) - def user_from_db(self, ident, name, pass_hash): - """ + def user_from_db(self, ident, name, pass_hash): + """ Return a user from given database parameters. Parameters: @@ -401,12 +440,12 @@ class Database: Returns: User: user element with given variables """ - user = User(name, pass_hash) - user.set_id(ident) - return user + user = User(name, pass_hash) + user.set_id(ident) + return user - def item_from_db(self, ident, name, date): - """ + def item_from_db(self, ident, name, date): + """ Return an item from given database parameters. Parameters: @@ -417,12 +456,12 @@ class Database: Returns: Item: entry element with given variables """ - item = Item(name, date) - item.set_id(ident) - return item + item = Item(name, date) + item.set_id(ident) + return item - def entry_from_db(self, ident, item_id, text, rating, user_id, date): - """ + def entry_from_db(self, ident, item_id, text, rating, user_id, date): + """ Return an entry from given database parameters. Parameters: @@ -436,8 +475,8 @@ class Database: Returns: Entry: entry element with given variables """ - entry = Entry(text, rating, date) - entry.set_id(ident) - entry.set_item(self.get_item_by_id(item_id)) - entry.set_user(self.get_user_by_id(user_id)) - return entry + entry = Entry(text, rating, date) + entry.set_id(ident) + entry.set_item(self.get_item_by_id(item_id)) + entry.set_user(self.get_user_by_id(user_id)) + return entry diff --git a/src/forms.py b/src/forms.py index 685ab6e..685359a 100644 --- a/src/forms.py +++ b/src/forms.py @@ -1,61 +1,64 @@ from datetime import date + from flask_ckeditor import CKEditorField from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, SubmitField, StringField +from wtforms import PasswordField, StringField, SubmitField from wtforms.fields import IntegerField -from wtforms.validators import DataRequired, EqualTo, InputRequired, \ - NumberRange, ValidationError, Length +from wtforms.validators import (DataRequired, EqualTo, InputRequired, Length, NumberRange, + ValidationError) class LoginForm(FlaskForm): - """ + """ A Class for the Form that is used while logging in. """ - username = StringField("Username", validators=[DataRequired(), - Length(min=4, max=32)]) - password = PasswordField("Password", validators=[DataRequired(), - Length(min=4, max=32)]) - submit = SubmitField("Sign In") + username = StringField("Username", validators=[DataRequired(), Length(min=4, max=32)]) + password = PasswordField("Password", validators=[DataRequired(), Length(min=4, max=32)]) + submit = SubmitField("Sign In") class RegisterForm(FlaskForm): - """ + """ A Class for the Form that is used while registering. """ - username = StringField("Username", validators=[DataRequired(), - Length(min=4, max=32)]) - password = PasswordField("Password", validators=[DataRequired(), - Length(min=4, max=32)]) - password2 = PasswordField( - "Repeat Password", validators=[DataRequired(), EqualTo("password")]) - submit = SubmitField("Register") + username = StringField("Username", validators=[DataRequired(), Length(min=4, max=32)]) + password = PasswordField("Password", validators=[DataRequired(), Length(min=4, max=32)]) + password2 = PasswordField("Repeat Password", validators=[DataRequired(), EqualTo("password")]) + submit = SubmitField("Register") class SearchForm(FlaskForm): - """ + """ A Class for the Form that is used while searching. """ - query_str = StringField( - "Query", [DataRequired("Please enter the search term")]) - submit = SubmitField("Search") + query_str = StringField("Query", [DataRequired("Please enter the search term")]) + submit = SubmitField("Search") class WriteForm(FlaskForm): - """ + """ A Class for the Form that is used while writing a new entry. """ - name = StringField("Name", validators=[DataRequired(), - Length(min=2, max=64)]) - date = IntegerField("Release Year", default=date.today().year, validators=[ - DataRequired(), NumberRange(min=0, max=date.today().year, - message="Year has to be valid.")]) - text = CKEditorField("Text", validators=[DataRequired()]) - rating = IntegerField("Rating", default=50, validators=[InputRequired( - ), NumberRange(min=0, max=100, message="Number has to be between 0 and 100.")]) - submit = SubmitField("Publish") + name = StringField("Name", validators=[DataRequired(), Length(min=2, max=64)]) + date = IntegerField("Release Year", + default=date.today().year, + validators=[ + DataRequired(), + NumberRange(min=0, max=date.today().year, message="Year has to be valid.") + ]) + text = CKEditorField("Text", validators=[DataRequired()]) + rating = IntegerField("Rating", + default=50, + validators=[ + InputRequired(), + NumberRange(min=0, + max=100, + message="Number has to be between 0 and 100.") + ]) + submit = SubmitField("Publish") - def validate_text(self, text): - """ + def validate_text(self, text): + """ Validate a given input for html level one headers. Parameters: @@ -67,5 +70,5 @@ class WriteForm(FlaskForm): Raises: ValidatenError: if the text contains a first level html tag """ - if "

" in text.data or "

" in text.data: - raise ValidationError("Headings on level 1 are not permitted.") + if "

" in text.data or "

" in text.data: + raise ValidationError("Headings on level 1 are not permitted.") diff --git a/src/requirements.txt b/src/requirements.txt index 0e35447..20b0766 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,7 +1,14 @@ -Flask -Flask_CKEditor -Flask_Login -Flask_WTF -Werkzeug -Whoosh -WTForms +bleach==6.3.0 +blinker==1.9.0 +click==8.3.3 +flask==3.1.3 +flask-ckeditor==1.0.0 +flask-login==0.6.3 +flask-wtf==1.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==3.0.3 +webencodings==0.5.1 +werkzeug==3.1.8 +whoosh==2.7.4 +wtforms==3.2.2 diff --git a/src/search.py b/src/search.py index 8bd0134..89d12d2 100644 --- a/src/search.py +++ b/src/search.py @@ -2,8 +2,8 @@ import os import re from whoosh import scoring +from whoosh.fields import ID, TEXT, Schema from whoosh.index import create_in, open_dir -from whoosh.fields import Schema, TEXT, ID from whoosh.qparser import QueryParser import config @@ -13,7 +13,7 @@ CLEANR = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});') def remove_html_tags(text): - """ + """ Convert a text from html formatted to unformatted. Parameters: @@ -22,31 +22,30 @@ def remove_html_tags(text): Returns: str: text without html tags """ - res = re.sub(CLEANR, '', text) - return res + res = re.sub(CLEANR, '', text) + return res def create_search_index(): - """ + """ Create the index data to search all entries. """ - db = Database() - schema = Schema(title=TEXT(stored=True), - path=ID(stored=True), content=TEXT(stored=True)) - if not os.path.exists(config.INDEX_DIR): - os.mkdir(config.INDEX_DIR) - ix = create_in(config.INDEX_DIR, schema) - writer = ix.writer() - for entry in db.get_entries(): - path = str(entry.id) - text = entry.item.name + " " + entry.item.date + " " + entry.text + \ - " by " + entry.user.name + " " + entry.date - writer.add_document(title=entry.item.name, path=path, content=text) - writer.commit() + db = Database() + schema = Schema(title=TEXT(stored=True), path=ID(stored=True), content=TEXT(stored=True)) + if not os.path.exists(config.INDEX_DIR): + os.mkdir(config.INDEX_DIR) + ix = create_in(config.INDEX_DIR, schema) + writer = ix.writer() + for entry in db.get_entries(): + path = str(entry.id) + text = entry.item.name + " " + entry.item.date + " " + entry.text + \ + " by " + entry.user.name + " " + entry.date + writer.add_document(title=entry.item.name, path=path, content=text) + writer.commit() def ft_search_times(query_str, number): - """ + """ Search for a given term and returns a specific amount of results. Parameters: @@ -56,19 +55,19 @@ def ft_search_times(query_str, number): Returns: List(Entry): list of entries that matched the search """ - ix = open_dir(config.INDEX_DIR) - results = [] - db = Database() - with ix.searcher(weighting=scoring.BM25F) as s: - query = QueryParser("content", ix.schema).parse(query_str) - matches = s.search(query, limit=number) - for match in matches: - results.append(db.get_entry_by_id(match["path"])) - return results + ix = open_dir(config.INDEX_DIR) + results = [] + db = Database() + with ix.searcher(weighting=scoring.BM25F) as s: + query = QueryParser("content", ix.schema).parse(query_str) + matches = s.search(query, limit=number) + for match in matches: + results.append(db.get_entry_by_id(match["path"])) + return results def ft_search(query_str): - """ + """ Search for a given term and show the predefined amount of results. Parameters: @@ -77,4 +76,4 @@ def ft_search(query_str): Returns: List(Entry): list of entries that matched the search """ - return ft_search_times(query_str, config.SEARCH_NUMBER) + return ft_search_times(query_str, config.SEARCH_NUMBER) diff --git a/src/static/css/dark.css b/src/static/css/dark.css index cefc446..3934322 100644 --- a/src/static/css/dark.css +++ b/src/static/css/dark.css @@ -14,12 +14,14 @@ --text1: rgb(220, 120, 0); } -a { +a, +.link-like { color: var(--link0); transition: var(--transtime); } -a:hover { +a:hover, +.link-like:hover { color: var(--link1); } diff --git a/src/static/css/light.css b/src/static/css/light.css index 24b52cd..ffd43ce 100644 --- a/src/static/css/light.css +++ b/src/static/css/light.css @@ -14,12 +14,14 @@ --text1: rgb(0,0,120); } -a { +a, +.link-like { color: var(--link0); transition: var(--transtime); } -a:hover { +a:hover, +.link-like:hover { color: var(--link1); } diff --git a/src/static/css/style.css b/src/static/css/style.css index 04414aa..dce85d3 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -8,15 +8,26 @@ padding: 0; } -a { +a, +.link-like { text-decoration: none; transition: var(--transtime); } -a:hover { +a:hover, +.link-like:hover { cursor: pointer; } +.link-like { + background: none; + border: none; + padding: 0; + margin: 0; + color: inherit; + font: inherit; +} + body { margin: 0; } diff --git a/src/templates/archive.html b/src/templates/archive.html index 4964297..7f124e8 100644 --- a/src/templates/archive.html +++ b/src/templates/archive.html @@ -1,43 +1,43 @@ {% extends "template.html" -%} {% block content -%} -
-
-

Archive


- {% set ns = namespace(prev_item_date="", prev_item_id=None, open_li = False, open_ul = False) -%} - {% for entry in entries -%} - {% if ns.prev_item_id != entry.item.id and ns.prev_item_id != None -%} - - {% set ns.open_li = False -%} - {% endif -%} - {% if entry.item.date != ns.prev_item_date -%} - {% if ns.prev_item_date != "" -%} - - {% set ns.open_ul = False -%} - {% endif -%} -

{{ entry.item.date }}

-
    - {% set ns.open_ul = True -%} - {% endif -%} - {% if ns.prev_item_id == entry.item.id -%} -
    - {% else -%} -
  • - {{ entry.item.name }}
    - {% set ns.open_li = True -%} - {% endif -%} - - {{ entry.date }} {{ r_to_star(entry.rating) }} by {{ entry.user.name }} - - {% set ns.prev_item_date = entry.item.date -%} - {% set ns.prev_item_id = entry.item.id -%} - {% endfor -%} - {% if ns.open_li -%} -
  • - {% endif -%} - {% if ns.open_ul -%} -
- {% endif -%} -
-
+
+
+

Archive


+ {% set ns = namespace(prev_item_date="", prev_item_id=None, open_li = False, open_ul = False) -%} + {% for entry in entries -%} + {% if ns.prev_item_id != entry.item.id and ns.prev_item_id != None -%} + + {% set ns.open_li = False -%} + {% endif -%} + {% if entry.item.date != ns.prev_item_date -%} + {% if ns.prev_item_date != "" -%} + + {% set ns.open_ul = False -%} + {% endif -%} +

{{ entry.item.date }}

+
    + {% set ns.open_ul = True -%} + {% endif -%} + {% if ns.prev_item_id == entry.item.id -%} +
    + {% else -%} +
  • + {{ entry.item.name }}
    + {% set ns.open_li = True -%} + {% endif -%} + + {{ entry.date }} {{ r_to_star(entry.rating) }} by {{ entry.user.name }} + + {% set ns.prev_item_date = entry.item.date -%} + {% set ns.prev_item_id = entry.item.id -%} + {% endfor -%} + {% if ns.open_li -%} +
  • + {% endif -%} + {% if ns.open_ul -%} +
+ {% endif -%} +
+
{% endblock -%} diff --git a/src/templates/error.html b/src/templates/error.html index 7013b17..0ee3638 100644 --- a/src/templates/error.html +++ b/src/templates/error.html @@ -1,10 +1,10 @@ {% extends "template.html" -%} {% block content -%} -
-
- Error
- {{ errorcode }} -
-
+
+
+ Error
+ {{ errorcode }} +
+
{% endblock -%} diff --git a/src/templates/index.html b/src/templates/index.html index f3f32e7..0792482 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -1,27 +1,27 @@ {% extends "template.html" -%} {% block content -%} -
-
-

Blog


- {% for entry in entries -%} -
-

- - {{ entry.item.name }} ({{ entry.item.date }}) {{ r_to_star(entry.rating) }} - -

- - rated {{ entry.rating }}/100 by - - {{ entry.user.name }} - on {{ entry.date }} -
- {% autoescape off -%} - {{ entry.text }} - {% endautoescape -%} -

- {% endfor -%} -
-
+
+
+

Blog


+ {% for entry in entries -%} +
+

+ + {{ entry.item.name }} ({{ entry.item.date }}) {{ r_to_star(entry.rating) }} + +

+ + rated {{ entry.rating }}/100 by + + {{ entry.user.name }} + on {{ entry.date }} +
+ {% autoescape off -%} + {{ entry.text }} + {% endautoescape -%} +

+ {% endfor -%} +
+
{% endblock -%} diff --git a/src/templates/login.html b/src/templates/login.html index e0e44b7..f3c9cd4 100644 --- a/src/templates/login.html +++ b/src/templates/login.html @@ -1,24 +1,24 @@ {% extends "template.html" -%} {% block content -%} -
-
-

Sign In

-
- {{ form.hidden_tag() }} -

- {{ form.username.label }}
- {{ form.username(size=32) }} -

-

- {{ form.password.label }}
- {{ form.password(size=32) }} -

-

{{ form.submit() }}

- {% for mesg in get_flashed_messages() -%} -

{{ mesg }}

- {% endfor -%} -
-
-
+
+
+

Sign In

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }} +

+

{{ form.submit() }}

+ {% for mesg in get_flashed_messages() -%} +

{{ mesg }}

+ {% endfor -%} +
+
+
{% endblock -%} diff --git a/src/templates/register.html b/src/templates/register.html index 0536268..c750338 100644 --- a/src/templates/register.html +++ b/src/templates/register.html @@ -1,37 +1,37 @@ {% extends "template.html" -%} {% block content -%} -
-
-

Register

-
- {{ form.hidden_tag() }} -

- {{ form.username.label }}
- {{ form.username(size=32) }} - {% for error in form.username.errors -%} - [{{ error }}] - {% endfor -%} -

-

- {{ form.password.label }}
- {{ form.password(size=32) }} - {% for error in form.password.errors -%} - [{{ error }}] - {% endfor -%} -

-

- {{ form.password2.label }}
- {{ form.password2(size=32) }} - {% for error in form.password2.errors -%} - [{{ error }}] - {% endfor -%} -

-

{{ form.submit() }}

- {% for mesg in get_flashed_messages() -%} -

{{ mesg }}

- {% endfor -%} -
-
-
+
+
+

Register

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }} + {% for error in form.username.errors -%} + [{{ error }}] + {% endfor -%} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }} + {% for error in form.password.errors -%} + [{{ error }}] + {% endfor -%} +

+

+ {{ form.password2.label }}
+ {{ form.password2(size=32) }} + {% for error in form.password2.errors -%} + [{{ error }}] + {% endfor -%} +

+

{{ form.submit() }}

+ {% for mesg in get_flashed_messages() -%} +

{{ mesg }}

+ {% endfor -%} +
+
+
{% endblock -%} diff --git a/src/templates/rss.xml b/src/templates/rss.xml index bc46e4e..257e816 100644 --- a/src/templates/rss.xml +++ b/src/templates/rss.xml @@ -1,29 +1,30 @@ - - {{ title }} + + {{ title }} {{ description }} en-us - {{ url_for("feed", _external=True) }} - - {% for entry in entries -%} - - - {{ entry.item.name }} ({{ entry.item.date }}) {{ r_to_star(entry.rating) }} by {{ entry.user.name }} - - - {{ url_for("index", _anchor=entry.id, _external=True) }} - - - {{ entry.date }} - - - {% autoescape off -%} - text - {% endautoescape -%} - - - {% endfor -%} - + {{ + url_for("feed", _external=True) }} + {% + for entry in entries -%} + + {{ entry.item.name }} ({{ entry.item.date }}) {{ r_to_star(entry.rating) }} by {{ + entry.user.name }} + + + {{ url_for("index", _anchor=entry.id, _external=True) }} + + + {{ entry.date }} + + + {% autoescape off -%} + text + {% endautoescape -%} + + + {% endfor -%} diff --git a/src/templates/search.html b/src/templates/search.html index 3c3c03a..7e02dc9 100644 --- a/src/templates/search.html +++ b/src/templates/search.html @@ -2,22 +2,23 @@ {% block content %}
-

Search


- +

Search


+
{% endblock -%} diff --git a/src/templates/standalone.html b/src/templates/standalone.html index 2ffa1d8..8ec8608 100644 --- a/src/templates/standalone.html +++ b/src/templates/standalone.html @@ -1,34 +1,39 @@ {% extends "template.html" -%} {% block content -%} -
-
-

- {{ entry.item.name }} ({{ entry.item.date }}) - {{ r_to_star(entry.rating) }} -

- - rated {{ entry.rating }}/100 by - - {{ entry.user.name }} - - on - - {{ entry.date }} - -
- {% if current_user.id == entry.user.id -%} - - [ - - delete entry - - ] -
- {% endif -%} - {% autoescape off -%} - {{ entry.text }} - {% endautoescape -%}
-
-
+
+
+

+ {{ entry.item.name }} ({{ entry.item.date }}) + {{ r_to_star(entry.rating) }} +

+ + rated {{ entry.rating }}/100 by + + {{ entry.user.name }} + + on + + {{ entry.date }} + +
+ {% if current_user.id == entry.user.id -%} + + edit entry + + - +
+ + + +
+ {% endif -%} + {% autoescape off -%} + {{ entry.text }} + {% endautoescape -%}
+
+
{% endblock -%} diff --git a/src/templates/template.html b/src/templates/template.html index 67924ad..b1343e5 100644 --- a/src/templates/template.html +++ b/src/templates/template.html @@ -1,48 +1,51 @@ {% set navigation_bar = [ - (url_for("index"), "index", "Blog"), - (url_for("archive"), "archive", "Archive"), - (url_for("search"), "search", "Search") +(url_for("index"), "index", "Blog"), +(url_for("archive"), "archive", "Archive"), +(url_for("search"), "search", "Search") ] -%} + - {{ title }} - - - - + {{ title }} + + + + + -