mirror of
https://github.com/tiyn/container-critique.git
synced 2026-05-23 00:51:35 +02:00
1.0.0: Edit functionality added
This commit is contained in:
110
README.md
110
README.md
@@ -2,57 +2,81 @@
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
|
||||
11
pyproject.toml
Normal file
11
pyproject.toml
Normal file
@@ -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",
|
||||
]
|
||||
239
src/app.py
239
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/<name>")
|
||||
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/<ident>")
|
||||
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/<ident>", methods=["GET", "POST"])
|
||||
@app.route("/delete_entry/<ident>", 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/<ident>", 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")
|
||||
|
||||
@@ -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
|
||||
|
||||
443
src/database.py
443
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
|
||||
|
||||
73
src/forms.py
73
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 "<h1>" in text.data or "</h1>" in text.data:
|
||||
raise ValidationError("Headings on level 1 are not permitted.")
|
||||
if "<h1>" in text.data or "</h1>" in text.data:
|
||||
raise ValidationError("Headings on level 1 are not permitted.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
{% extends "template.html" -%}
|
||||
|
||||
{% block content -%}
|
||||
<div class="container">
|
||||
<div class="archive">
|
||||
<h1>Archive</h1><br>
|
||||
{% 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 -%}
|
||||
</li>
|
||||
{% set ns.open_li = False -%}
|
||||
{% endif -%}
|
||||
{% if entry.item.date != ns.prev_item_date -%}
|
||||
{% if ns.prev_item_date != "" -%}
|
||||
</ul>
|
||||
{% set ns.open_ul = False -%}
|
||||
{% endif -%}
|
||||
<h2> {{ entry.item.date }} </h2>
|
||||
<ul>
|
||||
{% set ns.open_ul = True -%}
|
||||
{% endif -%}
|
||||
{% if ns.prev_item_id == entry.item.id -%}
|
||||
<br>
|
||||
{% else -%}
|
||||
<li>
|
||||
{{ entry.item.name }}<br>
|
||||
{% set ns.open_li = True -%}
|
||||
{% endif -%}
|
||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||
{{ entry.date }} {{ r_to_star(entry.rating) }} by {{ entry.user.name }}
|
||||
</a>
|
||||
{% set ns.prev_item_date = entry.item.date -%}
|
||||
{% set ns.prev_item_id = entry.item.id -%}
|
||||
{% endfor -%}
|
||||
{% if ns.open_li -%}
|
||||
</li>
|
||||
{% endif -%}
|
||||
{% if ns.open_ul -%}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="archive">
|
||||
<h1>Archive</h1><br>
|
||||
{% 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 -%}
|
||||
</li>
|
||||
{% set ns.open_li = False -%}
|
||||
{% endif -%}
|
||||
{% if entry.item.date != ns.prev_item_date -%}
|
||||
{% if ns.prev_item_date != "" -%}
|
||||
</ul>
|
||||
{% set ns.open_ul = False -%}
|
||||
{% endif -%}
|
||||
<h2> {{ entry.item.date }} </h2>
|
||||
<ul>
|
||||
{% set ns.open_ul = True -%}
|
||||
{% endif -%}
|
||||
{% if ns.prev_item_id == entry.item.id -%}
|
||||
<br>
|
||||
{% else -%}
|
||||
<li>
|
||||
{{ entry.item.name }}<br>
|
||||
{% set ns.open_li = True -%}
|
||||
{% endif -%}
|
||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||
{{ entry.date }} {{ r_to_star(entry.rating) }} by {{ entry.user.name }}
|
||||
</a>
|
||||
{% set ns.prev_item_date = entry.item.date -%}
|
||||
{% set ns.prev_item_id = entry.item.id -%}
|
||||
{% endfor -%}
|
||||
{% if ns.open_li -%}
|
||||
</li>
|
||||
{% endif -%}
|
||||
{% if ns.open_ul -%}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{% extends "template.html" -%}
|
||||
|
||||
{% block content -%}
|
||||
<div class="container">
|
||||
<div class="important">
|
||||
Error<br>
|
||||
<span>{{ errorcode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="important">
|
||||
Error<br>
|
||||
<span>{{ errorcode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
{% extends "template.html" -%}
|
||||
|
||||
{% block content -%}
|
||||
<div class="container">
|
||||
<div class="blog">
|
||||
<h1>Blog</h1><br>
|
||||
{% for entry in entries -%}
|
||||
<div class="entry">
|
||||
<h1 id="{{ entry.id }}">
|
||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||
{{ entry.item.name }} ({{ entry.item.date }}) {{ r_to_star(entry.rating) }}
|
||||
</a>
|
||||
</h1>
|
||||
<small>
|
||||
rated {{ entry.rating }}/100 by
|
||||
<a href="{{ url_for('user', name=entry.user.name) }}">
|
||||
{{ entry.user.name }}
|
||||
</a> on {{ entry.date }}
|
||||
</small><br>
|
||||
{% autoescape off -%}
|
||||
{{ entry.text }}
|
||||
{% endautoescape -%}
|
||||
</div><br>
|
||||
{% endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="blog">
|
||||
<h1>Blog</h1><br>
|
||||
{% for entry in entries -%}
|
||||
<div class="entry">
|
||||
<h1 id="{{ entry.id }}">
|
||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||
{{ entry.item.name }} ({{ entry.item.date }}) {{ r_to_star(entry.rating) }}
|
||||
</a>
|
||||
</h1>
|
||||
<small>
|
||||
rated {{ entry.rating }}/100 by
|
||||
<a href="{{ url_for('user', name=entry.user.name) }}">
|
||||
{{ entry.user.name }}
|
||||
</a> on {{ entry.date }}
|
||||
</small><br>
|
||||
{% autoescape off -%}
|
||||
{{ entry.text }}
|
||||
{% endautoescape -%}
|
||||
</div><br>
|
||||
{% endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{% extends "template.html" -%}
|
||||
|
||||
{% block content -%}
|
||||
<div class="container">
|
||||
<div class="logging">
|
||||
<h1>Sign In</h1>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password.label }}<br>
|
||||
{{ form.password(size=32) }}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
{% for mesg in get_flashed_messages() -%}
|
||||
<p>{{ mesg }}</p>
|
||||
{% endfor -%}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="logging">
|
||||
<h1>Sign In</h1>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password.label }}<br>
|
||||
{{ form.password(size=32) }}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
{% for mesg in get_flashed_messages() -%}
|
||||
<p>{{ mesg }}</p>
|
||||
{% endfor -%}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
{% extends "template.html" -%}
|
||||
|
||||
{% block content -%}
|
||||
<div class="container">
|
||||
<div class="logging">
|
||||
<h1>Register</h1>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}
|
||||
{% for error in form.username.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password.label }}<br>
|
||||
{{ form.password(size=32) }}
|
||||
{% for error in form.password.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password2.label }}<br>
|
||||
{{ form.password2(size=32) }}
|
||||
{% for error in form.password2.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
{% for mesg in get_flashed_messages() -%}
|
||||
<p>{{ mesg }}</p>
|
||||
{% endfor -%}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="logging">
|
||||
<h1>Register</h1>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}
|
||||
{% for error in form.username.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password.label }}<br>
|
||||
{{ form.password(size=32) }}
|
||||
{% for error in form.password.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password2.label }}<br>
|
||||
{{ form.password2(size=32) }}
|
||||
{% for error in form.password2.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
{% for mesg in get_flashed_messages() -%}
|
||||
<p>{{ mesg }}</p>
|
||||
{% endfor -%}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
|
||||
<channel>
|
||||
<title>{{ title }}</title>
|
||||
<channel>
|
||||
<title>{{ title }}</title>
|
||||
<description>{{ description }}</description>
|
||||
<language>en-us</language>
|
||||
<link>{{ url_for("feed", _external=True) }}</link>
|
||||
<atom:link href="{{ url_for('feed', _external=True) }}" rel="self" type="application/rss+xml" />
|
||||
{% for entry in entries -%}
|
||||
<item>
|
||||
<title>
|
||||
{{ entry.item.name }} ({{ entry.item.date }}) {{ r_to_star(entry.rating) }} by {{ entry.user.name }}
|
||||
</title>
|
||||
<guid>
|
||||
{{ url_for("index", _anchor=entry.id, _external=True) }}
|
||||
</guid>
|
||||
<pubDate>
|
||||
{{ entry.date }}
|
||||
</pubDate>
|
||||
<description>
|
||||
{% autoescape off -%}
|
||||
text
|
||||
{% endautoescape -%}
|
||||
</description>
|
||||
</item>
|
||||
{% endfor -%}
|
||||
</channel>
|
||||
<link>{{
|
||||
url_for("feed", _external=True) }}</link>
|
||||
<atom:link
|
||||
href="{{ url_for('feed', _external=True) }}" rel="self" type="application/rss+xml" /> {%
|
||||
for entry in entries -%} <item>
|
||||
<title>
|
||||
{{ entry.item.name }} ({{ entry.item.date }}) {{ r_to_star(entry.rating) }} by {{
|
||||
entry.user.name }}
|
||||
</title>
|
||||
<guid>
|
||||
{{ url_for("index", _anchor=entry.id, _external=True) }}
|
||||
</guid>
|
||||
<pubDate>
|
||||
{{ entry.date }}
|
||||
</pubDate>
|
||||
<description>
|
||||
{% autoescape off -%}
|
||||
text
|
||||
{% endautoescape -%}
|
||||
</description>
|
||||
</item>
|
||||
{% endfor -%} </channel>
|
||||
</rss>
|
||||
|
||||
@@ -2,22 +2,23 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Search</h1><br>
|
||||
<div class="search">
|
||||
<form action="{{ url_for('search') }}" method=post>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ form.query_str }}
|
||||
{{ form.submit }}
|
||||
</form>
|
||||
<ul>
|
||||
{% for entry in results -%}
|
||||
<li>
|
||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||
{{ entry.date }} {{ r_to_star(entry.rating) }} {{ entry.item.name }} ({{ entry.item.date }}) by {{ entry.user.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
</div>
|
||||
<h1>Search</h1><br>
|
||||
<div class="search">
|
||||
<form action="{{ url_for('search') }}" method=post>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ form.query_str }}
|
||||
{{ form.submit }}
|
||||
</form>
|
||||
<ul>
|
||||
{% for entry in results -%}
|
||||
<li>
|
||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||
{{ entry.date }} {{ r_to_star(entry.rating) }} {{ entry.item.name }} ({{ entry.item.date }}) by {{
|
||||
entry.user.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
{% extends "template.html" -%}
|
||||
|
||||
{% block content -%}
|
||||
<div class="container">
|
||||
<div class="standalone">
|
||||
<h1>
|
||||
{{ entry.item.name }} ({{ entry.item.date }})
|
||||
{{ r_to_star(entry.rating) }}
|
||||
</h1>
|
||||
<small>
|
||||
rated {{ entry.rating }}/100 by
|
||||
<a href="{{ url_for('user', name=entry.user.name) }}">
|
||||
{{ entry.user.name }}
|
||||
</a>
|
||||
on
|
||||
<a href="{{ url_for('index', _anchor='{0:d}'.format(entry.id)) }}">
|
||||
{{ entry.date }}
|
||||
</a>
|
||||
</small><br>
|
||||
{% if current_user.id == entry.user.id -%}
|
||||
<small>
|
||||
[
|
||||
<a href="{{ url_for('delete_entry', ident='{0:d}'.format(entry.id)) }}">
|
||||
delete entry
|
||||
</a>
|
||||
]
|
||||
</small><br>
|
||||
{% endif -%}
|
||||
{% autoescape off -%}
|
||||
{{ entry.text }}
|
||||
{% endautoescape -%}<br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="standalone">
|
||||
<h1>
|
||||
{{ entry.item.name }} ({{ entry.item.date }})
|
||||
{{ r_to_star(entry.rating) }}
|
||||
</h1>
|
||||
<small>
|
||||
rated {{ entry.rating }}/100 by
|
||||
<a href="{{ url_for('user', name=entry.user.name) }}">
|
||||
{{ entry.user.name }}
|
||||
</a>
|
||||
on
|
||||
<a href="{{ url_for('index', _anchor='{0:d}'.format(entry.id)) }}">
|
||||
{{ entry.date }}
|
||||
</a>
|
||||
</small><br>
|
||||
{% if current_user.id == entry.user.id -%}
|
||||
<a class="link-like" href="{{ url_for('edit_entry', ident='{0:d}'.format(entry.id)) }}">
|
||||
edit entry
|
||||
</a>
|
||||
-
|
||||
<form method="post" action="{{ url_for('delete_entry', ident='{0:d}'.format(entry.id)) }}" style="display:inline;"
|
||||
onsubmit="return confirm('Delete entry?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<button type="submit" class="link-like">
|
||||
delete entry
|
||||
</button>
|
||||
</form>
|
||||
{% endif -%}
|
||||
{% autoescape off -%}
|
||||
{{ entry.text }}
|
||||
{% endautoescape -%}<br>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
@@ -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")
|
||||
] -%}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width" initial-scale=1.0>
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='graphics/logo.png') }}">
|
||||
<link href="{{ url_for('static', filename='css/' + style + '.css') }}" rel="stylesheet" type="text/css">
|
||||
<title>{{ title }}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width" initial-scale=1.0>
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='graphics/logo.png') }}">
|
||||
<link href="{{ url_for('static', filename='css/' + style + '.css') }}" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main-menu-dropdown">
|
||||
<!-- <img class="logo" src="/static/images/logo.png"> -->
|
||||
<a href="{{ url_for('index') }}">
|
||||
<img class="logo" src="{{ url_for('static', filename='graphics/logo.png') }}">
|
||||
<span>{{ title }}</span>
|
||||
</a>
|
||||
<input type="checkbox" id="main-menu-check">
|
||||
<label for="main-menu-check" class="show-menu">☰</label>
|
||||
<div class="main-menu">
|
||||
{% for href, id, caption in navigation_bar -%}
|
||||
<a href="{{ href|e }}">{{ caption|e }}</a>
|
||||
{% endfor -%}
|
||||
<label for="main-menu-check" class="hide-menu">X</label>
|
||||
</div>
|
||||
<div class="main-menu-dropdown">
|
||||
<!-- <img class="logo" src="/static/images/logo.png"> -->
|
||||
<a href="{{ url_for('index') }}">
|
||||
<img class="logo" src="{{ url_for('static', filename='graphics/logo.png') }}">
|
||||
<span>{{ title }}</span>
|
||||
</a>
|
||||
<input type="checkbox" id="main-menu-check">
|
||||
<label for="main-menu-check" class="show-menu">☰</label>
|
||||
<div class="main-menu">
|
||||
{% for href, id, caption in navigation_bar -%}
|
||||
<a href="{{ href|e }}">{{ caption|e }}</a>
|
||||
{% endfor -%}
|
||||
<label for="main-menu-check" class="hide-menu">X</label>
|
||||
</div>
|
||||
{% block content -%}
|
||||
{% endblock -%}
|
||||
<footer>
|
||||
Made with <a href="https://github.com/tiyn/container-critique">Container Critique </a>.<br>
|
||||
{% if current_user.is_anonymous -%}
|
||||
<a href="{{ url_for('login') }}">Login</a>
|
||||
{% if registration -%}
|
||||
-
|
||||
<a href="{{ url_for('register') }}">Register</a>
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
<a href="{{ url_for('logout') }}">Logout</a> -
|
||||
<a href="{{ url_for('write_entry') }}">Write entry</a>
|
||||
{% endif -%}
|
||||
</footer>
|
||||
</div>
|
||||
{% block content -%}
|
||||
{% endblock -%}
|
||||
<footer>
|
||||
Made with <a href="https://github.com/tiyn/container-critique">Container Critique </a>.<br>
|
||||
{% if current_user.is_anonymous -%}
|
||||
<a href="{{ url_for('login') }}">Login</a>
|
||||
{% if registration -%}
|
||||
-
|
||||
<a href="{{ url_for('register') }}">Register</a>
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
<a href="{{ url_for('logout') }}">Logout</a> -
|
||||
<a href="{{ url_for('write_entry') }}">Write entry</a>
|
||||
{% endif -%}
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -2,42 +2,42 @@
|
||||
|
||||
{% block content -%}
|
||||
<div class="container">
|
||||
<div class="archive">
|
||||
<h1>User: {{ name }}</h1><br>
|
||||
{% 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 -%}
|
||||
</li>
|
||||
{% set ns.open_li = False -%}
|
||||
{% endif -%}
|
||||
{% if entry.item.date != ns.prev_item_date -%}
|
||||
{% if ns.prev_item_date != "" -%}
|
||||
</ul>
|
||||
{% set ns.open_ul = False -%}
|
||||
{% endif -%}
|
||||
<h2> {{ entry.item.date }} </h2>
|
||||
<ul>
|
||||
{% set ns.open_ul = True -%}
|
||||
{% endif -%}
|
||||
{% if ns.prev_item_id == entry.item.id -%}
|
||||
<br>
|
||||
{% else -%}
|
||||
<li>
|
||||
{{ entry.item.name }}<br>
|
||||
{% set ns.open_li = True -%}
|
||||
{% endif -%}
|
||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||
{{ entry.date }} {{ r_to_star(entry.rating) }}
|
||||
</a>
|
||||
{% set ns.prev_item_date = entry.item.date -%}
|
||||
{% set ns.prev_item_id = entry.item.id -%}
|
||||
{% endfor -%}
|
||||
{% if ns.open_li -%}
|
||||
</li>
|
||||
{% endif -%}
|
||||
{% if ns.open_ul -%}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="archive">
|
||||
<h1>User: {{ name }}</h1><br>
|
||||
{% 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 -%}
|
||||
</li>
|
||||
{% set ns.open_li = False -%}
|
||||
{% endif -%}
|
||||
{% if entry.item.date != ns.prev_item_date -%}
|
||||
{% if ns.prev_item_date != "" -%}
|
||||
</ul>
|
||||
{% set ns.open_ul = False -%}
|
||||
{% endif -%}
|
||||
<h2> {{ entry.item.date }} </h2>
|
||||
<ul>
|
||||
{% set ns.open_ul = True -%}
|
||||
{% endif -%}
|
||||
{% if ns.prev_item_id == entry.item.id -%}
|
||||
<br>
|
||||
{% else -%}
|
||||
<li>
|
||||
{{ entry.item.name }}<br>
|
||||
{% set ns.open_li = True -%}
|
||||
{% endif -%}
|
||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||
{{ entry.date }} {{ r_to_star(entry.rating) }}
|
||||
</a>
|
||||
{% set ns.prev_item_date = entry.item.date -%}
|
||||
{% set ns.prev_item_id = entry.item.id -%}
|
||||
{% endfor -%}
|
||||
{% if ns.open_li -%}
|
||||
</li>
|
||||
{% endif -%}
|
||||
{% if ns.open_ul -%}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
@@ -2,46 +2,46 @@
|
||||
|
||||
{% block content -%}
|
||||
<div class="container">
|
||||
<div class="writing">
|
||||
<h1>Write Critique</h1>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.name.label }}<br>
|
||||
{{ form.name(size=64) }}
|
||||
{% for error in form.name.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.date.label }}<br>
|
||||
{{ form.date }}
|
||||
{% for error in form.date.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>
|
||||
<div class="writing">
|
||||
<h1>Write Critique</h1>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.name.label }}<br>
|
||||
{{ form.name(size=64) }}
|
||||
{% for error in form.name.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.date.label }}<br>
|
||||
{{ form.date }}
|
||||
{% for error in form.date.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
{{ form.text.label }}<br>
|
||||
{{ form.text }}
|
||||
{% for error in form.text.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
{{ ckeditor.load() }}
|
||||
{{ ckeditor.config(name="text") }}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.rating.label }}<br>
|
||||
{{ form.rating }}
|
||||
{% for error in form.rating.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
{% for mesg in get_flashed_messages() -%}
|
||||
<p>{{ mesg }}</p>
|
||||
{% endfor -%}
|
||||
</form>
|
||||
</div>
|
||||
{{ form.text.label }}<br>
|
||||
{{ form.text }}
|
||||
{% for error in form.text.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
{{ ckeditor.load() }}
|
||||
{{ ckeditor.config(name="text") }}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.rating.label }}<br>
|
||||
{{ form.rating }}
|
||||
{% for error in form.rating.errors -%}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor -%}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
{% for mesg in get_flashed_messages() -%}
|
||||
<p>{{ mesg }}</p>
|
||||
{% endfor -%}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
|
||||
215
uv.lock
generated
Normal file
215
uv.lock
generated
Normal file
@@ -0,0 +1,215 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "bleach"
|
||||
version = "6.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "container-critique"
|
||||
version = "1.0.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "bleach" },
|
||||
{ name = "flask-ckeditor" },
|
||||
{ name = "flask-login" },
|
||||
{ name = "flask-wtf" },
|
||||
{ name = "whoosh" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "bleach", specifier = ">=6.3.0" },
|
||||
{ name = "flask-ckeditor", specifier = ">=1.0.0" },
|
||||
{ name = "flask-login", specifier = ">=0.6.3" },
|
||||
{ name = "flask-wtf", specifier = ">=1.3.0" },
|
||||
{ name = "whoosh", specifier = ">=2.7.4" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
{ name = "click" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markupsafe" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-ckeditor"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/c9/d7559169bfc76c29054e1a4a4f1c49c034c1ba931181e48bb6af7ed87e7a/flask_ckeditor-1.0.0.tar.gz", hash = "sha256:e1737ca180ea0d46d53226f888f4786589f2e8ed810694aaff2aa68dfad15a98", size = 3387711, upload-time = "2024-07-04T15:42:45.23Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/80/961516d05ffaf17c32aa595d30cdf7287a218d2ff60f212a09de512ffb19/Flask_CKEditor-1.0.0-py2.py3-none-any.whl", hash = "sha256:1a4aa871b510100df7bb8401b71cdcddfe1fd6d860649bc3760d0d43df485d72", size = 4581046, upload-time = "2024-07-04T15:42:42.851Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-login"
|
||||
version = "0.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834, upload-time = "2023-10-30T14:53:21.151Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303, upload-time = "2023-10-30T14:53:19.636Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-wtf"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "wtforms" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/f1/605a56d4ea217b307f3e6f4d663e0351253d85d841edc93ba559f0648e19/flask_wtf-1.3.0.tar.gz", hash = "sha256:61d5dabc50c3df885c297dcbd80810443a5d632106c8a69cab8ce740f0cdd7cc", size = 50414, upload-time = "2026-04-23T07:41:55.096Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/d2/97adf2ec7af95522573e6dd5493ee84792d0fbfb2def010c4a581b8d6e5e/flask_wtf-1.3.0-py3-none-any.whl", hash = "sha256:dc5e3a4ce97f75c47bf6c1c72ad2c3b7bdf579a2ed13aebcc5d3d81fe2571160", size = 13959, upload-time = "2026-04-23T07:41:53.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webencodings"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoosh"
|
||||
version = "2.7.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", hash = "sha256:7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83", size = 968741, upload-time = "2016-04-04T01:19:32.327Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/19/24d0f1f454a2c1eb689ca28d2f178db81e5024f42d82729a4ff6771155cf/Whoosh-2.7.4-py2.py3-none-any.whl", hash = "sha256:aa39c3c3426e3fd107dcb4bde64ca1e276a65a889d9085a6e4b54ba82420a852", size = 468790, upload-time = "2016-04-04T01:19:40.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wtforms"
|
||||
version = "3.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/91/ed9b517da898e3fb747566aa3c12a734bd64ea7449a0d25ec74ce8f8b8eb/wtforms-3.2.2.tar.gz", hash = "sha256:7b00c73f8670f35d4edb0293dcd81b980528bee72fd662b182aaba27ae570b93", size = 139583, upload-time = "2026-05-03T05:53:44.147Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/76/bb225c8300f3a0ba28e01df51419c6c9574a297c43d71b29048e03b65deb/wtforms-3.2.2-py3-none-any.whl", hash = "sha256:72b90d5d921bd3119252069cf0301e9c13915f9e52792652bc91c5dda4b79e56", size = 158656, upload-time = "2026-05-03T05:53:46.072Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user