src: logging and writing complete

master
tiyn 2 years ago
parent cd09ab9a12
commit 61aa869b93

2
.gitignore vendored

@ -0,0 +1,2 @@
data
data.db

@ -6,6 +6,8 @@ COPY src /blog
WORKDIR /blog WORKDIR /blog
VOLUME /blog/data
RUN pip3 install -r requirements.txt RUN pip3 install -r requirements.txt
VOLUME /blog/templates/entry VOLUME /blog/templates/entry

@ -6,21 +6,21 @@ The blog is intended to be used to review and critique things.
## Features/To-Dos ## Features/To-Dos
- [ ] Accounts - [x] Accounts
- [x] Login - [x] Login
- [x] Logout - [x] Logout
- [ ] Register - [x] Register
- [ ] Review blog entries - [ ] Review blog entries
- [ ] Writing entries - [x] Writing entries
- [ ] Editing entries - [ ] Editing entries
- [ ] Deleting entries - [ ] Deleting entries
- [ ] Infinite-scroll blog page - [ ] Infinite-scroll blog page
- [ ] Archive page - [x] Archive page
- [ ] Months as headings - [x] Months as headings
- [ ] Links to scrolling blog page - [x] Links to scrolling blog page
- [ ] Links to standalone article - [x] Links to standalone article
- [ ] Standalone article page - [x] Standalone article page
- [ ] Links to scrolling blog page - [x] Links to scrolling blog page
- [ ] RSS feed - [ ] RSS feed
- [ ] Eye candy - [ ] Eye candy
- [ ] Star rating - [ ] Star rating
@ -58,9 +58,9 @@ Set the following volumes with the -v tag.
| Volume-Name | Container mount | Description | | Volume-Name | Container mount | Description |
| ------------- | ---------------------- | ---------------------------------- | | ------------- | ---------------------- | ---------------------------------- |
| `config-file` | `/blog/src/config.py` | Config file | | `config-file` | `/blog/config.py` | Config file |
| `css` | `/blog/src/static/css` | (optional) Directory for css files | | `data` | `/blog/data` | Directory for data |
| `html` | `/blog/src/templates` | (optional) Directory for templates | | `css` | `/blog/static/css` | (optional) Directory for css files |
#### Ports #### Ports

@ -6,4 +6,5 @@ docker run --name container-critique \
--restart unless-stopped \ --restart unless-stopped \
-p "5000:5000" \ -p "5000:5000" \
-e FLASK_ENV=development \ -e FLASK_ENV=development \
-v data:/blog/data
-d tiyn/container-critique -d tiyn/container-critique

@ -1,14 +1,21 @@
from flask import Flask, flash, make_response, render_template, request, redirect, abort, url_for from flask import Flask, flash, make_response, render_template, redirect, \
from flask_login import current_user, login_user, LoginManager, logout_user abort, url_for
from flask_login import current_user, login_user, LoginManager, logout_user, \
login_required
from flask_wtf import CSRFProtect from flask_wtf import CSRFProtect
import os
import content as con_gen
import config import config
import content as con_gen
from database import Database, User
from forms import LoginForm, RegisterForm, WriteForm
app = Flask(__name__) app = Flask(__name__)
csrf = CSRFProtect() csrf = CSRFProtect()
app.secret_key = "123534" db = Database()
app.secret_key = os.urandom(32)
csrf.init_app(app) csrf.init_app(app)
login = LoginManager(app) login = LoginManager(app)
@ -18,6 +25,8 @@ TITLE = config.TITLE
STYLE = config.STYLE STYLE = config.STYLE
DESCRIPTION = config.DESCRIPTION DESCRIPTION = config.DESCRIPTION
WEBSITE = config.WEBSITE WEBSITE = config.WEBSITE
REGISTER = config.REGISTER
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
@ -38,9 +47,9 @@ def archive():
return render_template("archive.html", title=TITLE, content_string=content, style=STYLE) return render_template("archive.html", title=TITLE, content_string=content, style=STYLE)
@app.route("/entry/<path>") @app.route("/entry/<ident>")
def entry(path): def entry(ident):
content = con_gen.gen_stand_string(path) content = con_gen.gen_stand_string(ident)
if content != "": if content != "":
return render_template("standalone.html", title=TITLE, content_string=content, style=STYLE) return render_template("standalone.html", title=TITLE, content_string=content, style=STYLE)
abort(404) abort(404)
@ -56,18 +65,14 @@ def feed():
response.headers["Content-Type"] = "application/rss+xml" response.headers["Content-Type"] = "application/rss+xml"
return response return response
@login.user_loader @login.user_loader
def load_user(ident): def load_user(ident):
## TODO: load user from db by id db_user = db.get_user_by_id(ident)
db_user = db.get_by_id(ident)
if db_user is not None: if db_user is not None:
return db.db_to_user(*db_user) return db.db_to_user(*db_user)
return None return None
from login import LoginForm, User
from database import Database
db = Database()
@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
@app.route("/login.html", methods=["GET", "POST"]) @app.route("/login.html", methods=["GET", "POST"])
@ -76,22 +81,59 @@ def login():
return redirect(url_for("index")) return redirect(url_for("index"))
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if form.validate_on_submit():
db_user = db.get_by_name(form.username.data) db_user = db.get_user_by_name(form.username.data)
if db_user is None: if db_user is not None:
flash("Invalid username or password") user = db.db_to_user(*db_user)
return redirect(url_for("login")) if user.check_password(form.password.data):
user = db.db_to_user(*db_user) login_user(user)
if not user.check_password(form.password.data): return redirect(url_for("index"))
flash("Invalid username or password") flash("Invalid username or password.")
return redirect(url_for("login")) return redirect(url_for("login"))
login_user(user, remember=form.remember_me.data)
return redirect(url_for("index"))
return render_template("login.html", title=TITLE, form=form, style=STYLE) return render_template("login.html", title=TITLE, form=form, style=STYLE)
@app.route('/logout') @app.route('/logout')
@app.route('/logout.html')
def logout(): def logout():
logout_user() logout_user()
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route("/register", methods=["GET", "POST"])
@app.route("/register.html", methods=["GET", "POST"])
def register():
if current_user.is_authenticated or not REGISTER:
return redirect(url_for("index"))
form = RegisterForm()
if form.validate_on_submit():
if not REGISTER:
return redirect(url_for("index"))
db_user = db.get_user_by_name(form.username.data)
if db_user is None:
user = User(form.username.data)
user.set_password(form.password.data)
ident = db.insert_user(user)
if ident is not None:
user.set_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", title=TITLE, form=form, style=STYLE)
@app.route("/write", methods=["GET", "POST"])
@app.route("/write.html", methods=["GET", "POST"])
@login_required
def write():
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)
return redirect(url_for("index"))
return render_template("write.html", title=TITLE, form=form, style=STYLE)
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0") app.run(host="0.0.0.0")

@ -9,3 +9,6 @@ WEBSITE = "localhost:5000"
# Theme for the blog: dark, light # Theme for the blog: dark, light
STYLE = "dark" STYLE = "dark"
# Allow new registrations
REGISTER = True

@ -1,3 +1,8 @@
from database import Database
db = Database()
def gen_arch_string(): def gen_arch_string():
""" """
Creates and returns a archive string of every file in ENTRY_DIR. Creates and returns a archive string of every file in ENTRY_DIR.
@ -5,32 +10,100 @@ def gen_arch_string():
Returns: Returns:
string: html-formatted archive-string string: html-formatted archive-string
""" """
return "" content_string = ""
last_year = ""
entries = db.get_entries()
if entries is None:
return ""
entries.sort(key=lambda y: y[2])
for entry in entries:
ident = entry[0]
title = entry[1]
year = entry[2]
rating = entry[4]
if year != last_year:
if last_year != "":
content_string += "</ul>\n"
content_string += "<h2>" + year + "</h2>\n"
content_string += "<ul>\n"
last_year = year
content_string += "<li>"
content_string += "[<a href=\"" + "/index.html#" + str(ident) + \
"\">link</a> - <a href=\"/entry/" + \
str(ident) + "\">standalone</a>] "
content_string += title + \
" (" + str(year) + ") - " + str(rating) + "/100<br>"
content_string += "</li>\n"
return content_string
def gen_index_string(): def gen_index_string():
""" """
Create and returns a string including every file in the ENTRY_DIR as an index. Create and returns a string including every file in the database as an index.
Returns: Returns:
string: html-formatted index string string: html-formatted index string
""" """
return "" content_string = ""
entries = db.get_entries()
if entries is None:
return ""
entries.reverse()
for entry in entries:
ident = entry[0]
title = entry[1]
year = entry[2]
text = entry[3]
rating = entry[4]
username = db.get_user_by_id(entry[5])[1]
reviewed = entry[6]
content_string += "<div class=\"entry\">\n"
content_string += "<h2 id=\"" + \
str(ident) + "\">" + title + " (" + year + ") - " + \
str(rating) + "/100</h2>\n"
content_string += "[<a href=\"" + "/entry/" + \
str(ident) + "\">" + "standalone" + "</a>]<br>\n"
content_string += text
content_string += "<br>"
content_string += "<small>" + \
str(reviewed) + " by " + username + "</small>"
content_string += "</div>"
return content_string
def gen_stand_string(path_ex): def gen_stand_string(ident):
""" """
Creates a html-string for a file. Creates a html-string for an entry.
If the file is markdown it will convert it.
This functions ensures upscaling for future formats.
Parameters: Parameters:
path_ex: path to a file. ident: ident of an entry.
Returns: Returns:
string: html-formatted string string equivalent to the file string: html-formatted string string equivalent to the file
""" """
return "" entry = db.get_entry_by_id(ident)
content_string = ""
if entry is not None:
ident = entry[0]
title = entry[1]
year = entry[2]
text = entry[3]
rating = entry[4]
username = db.get_user_by_id(entry[5])[1]
reviewed = entry[6]
content_string += "<h1>" + title + \
" (" + year + ") - " + str(rating) + "/100 </h1>\n"
content_string += "["
content_string += "<a href=\"" + "/index.html#" + \
str(ident) + "\">" + "link" + "</a>"
content_string += "]<br>\n"
content_string += "<small>" + \
str(reviewed) + " by " + username + "</small>"
content_string += "<br>\n"
content_string += text
content_string += "<br>"
return content_string
def get_rss_string(): def get_rss_string():

Binary file not shown.

@ -1,14 +1,37 @@
from datetime import date as dt
import os import os
import sqlite3 import sqlite3
from werkzeug.security import generate_password_hash, check_password_hash
from login import User class User():
def __init__(self, name, pass_hash=None):
self.name = name
self.id = 0
self.is_active = True
self.is_authenticated = True
self.is_anonymous = False
self.pass_hash = pass_hash
def set_password(self, password):
self.pass_hash = generate_password_hash(password)
def set_id(self, ident):
self.id = ident
def check_password(self, password):
return check_password_hash(self.pass_hash, password)
def get_id(self):
return self.id
class Database: class Database:
def __init__(self): def __init__(self):
self.TABLE_FILE = 'USERS' self.USER_TABLE_FILE = 'USERS'
self.DB_DIR = os.path.dirname(".") self.ENTRY_TABLE_FILE = 'ENTRIES'
self.DB_DIR = os.path.dirname("./data/")
self.setup_db() self.setup_db()
def connect(self): def connect(self):
@ -22,41 +45,78 @@ class Database:
"""Creates a database with tables.""" """Creates a database with tables."""
db = self.connect() db = self.connect()
crs = db.cursor() crs = db.cursor()
query = "CREATE TABLE IF NOT EXISTS " + self.TABLE_FILE + \ query = "CREATE TABLE IF NOT EXISTS " + self.USER_TABLE_FILE + \
"(id INTEGER PRIMARY KEY AUTOINCREMENT," + \ "(id INTEGER PRIMARY KEY AUTOINCREMENT," + \
"name CHAR(32) NOT NULL UNIQUE," + \ "name CHAR(32) NOT NULL UNIQUE," + \
"password CHAR(32) NOT NULL)" "password CHAR(32) NOT NULL)"
crs.execute(query) crs.execute(query)
query = "CREATE TABLE IF NOT EXISTS " + self.ENTRY_TABLE_FILE + \
"(id INTEGER PRIMARY KEY AUTOINCREMENT," + \
"name CHAR(64) NOT NULL," + \
"date CHAR(4) NOT NULL," + \
"text TEXT NOT NULL," + \
"rating INTEGER NOT NULL," +\
"user_id INTEGER," +\
"reviewed CHAR(10) NOT NULL," +\
"FOREIGN KEY(user_id) REFERENCES " + self.USER_TABLE_FILE + "(id))"
crs.execute(query)
db.commit()
def insert_user(self, name, password): def insert_user(self, user):
"""Insert a new user into the database. """Insert a new user into the database.
""" """
if self.check_name(name): if self.check_user_name(user.name) and user.pass_hash is not None:
db = self.connect() db = self.connect()
crs = db.cursor() crs = db.cursor()
query = "INSERT INTO " + self.TABLE_FILE + "(`name`,`password`)" + \ query = "INSERT INTO " + self.USER_TABLE_FILE + "(`name`,`password`)" + \
"VALUES (?, ?) ON CONFLICT DO NOTHING" "VALUES (?, ?) ON CONFLICT DO NOTHING"
crs.execute(query, (name, password)) crs.execute(query, (user.name, user.pass_hash))
db.commit() db.commit()
return True return crs.lastrowid
return False return None
def check_name(self, name): def insert_entry(self, name, date, text, rating, user_id=None):
if self.get_by_name(name) is None: """Insert a new user into the database.
"""
db = self.connect()
crs = db.cursor()
reviewed = dt.today().strftime('%Y-%m-%d')
query = "INSERT INTO " + self.ENTRY_TABLE_FILE + "(`name`,`date`, `text`, `rating`, `user_id`, `reviewed`)" + \
"VALUES (?, ?, ?, ?, ?, ?)"
crs.execute(query, (name, date, text, rating, user_id, reviewed))
db.commit()
return crs.lastrowid
def get_entries(self):
db = self.connect()
crs = db.cursor()
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE
crs.execute(query)
return crs.fetchall()
def check_user_name(self, name):
if self.get_user_by_name(name) is None:
return True return True
return False return False
def get_by_id(self, ident): def get_entry_by_id(self, ident):
db = self.connect()
crs = db.cursor()
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?"
crs.execute(query, (ident, ))
return crs.fetchone()
def get_user_by_id(self, ident):
db = self.connect() db = self.connect()
crs = db.cursor() crs = db.cursor()
query = "SELECT * FROM " + self.TABLE_FILE + " WHERE id = ?" query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE id = ?"
crs.execute(query, (ident, )) crs.execute(query, (ident, ))
return crs.fetchone() return crs.fetchone()
def get_by_name(self, name): def get_user_by_name(self, name):
db = self.connect() db = self.connect()
crs = db.cursor() crs = db.cursor()
query = "SELECT * FROM " + self.TABLE_FILE + " WHERE name = ?" query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE name = ?"
crs.execute(query, (name, )) crs.execute(query, (name, ))
return crs.fetchone() return crs.fetchone()
@ -64,3 +124,9 @@ class Database:
user = User(name, pass_hash) user = User(name, pass_hash)
user.set_id(ident) user.set_id(ident)
return user return user
#db = Database()
#db.insert_entry("name", "2020", "text", 50, 1)
#res = db.get_entries()
#print(res)

@ -0,0 +1,26 @@
from datetime import date
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, TextAreaField
from wtforms.fields.html5 import IntegerField
from wtforms.validators import DataRequired, EqualTo, InputRequired, NumberRange
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Sign In")
class RegisterForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
password2 = PasswordField(
"Repeat Password", validators=[DataRequired(), EqualTo("password")])
submit = SubmitField("Register")
class WriteForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
date = IntegerField("Release Year", default=date.today().year, validators=[DataRequired(), NumberRange(min=0, max=date.today().year, message="Year has to be valid.")])
text = TextAreaField("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")

@ -1,34 +0,0 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired
from werkzeug.security import generate_password_hash, check_password_hash
class User():
def __init__(self, name, pass_hash=None):
self.name = name
self.id = 0
self.is_active = True
self.is_authenticated = True
self.is_anonymous = False
self.pass_hash = pass_hash
def set_password(self, password):
self.pass_hash = generate_password_hash(password)
def set_id(self, ident):
self.id = ident
def check_password(self, password):
return check_password_hash(self.pass_hash, password)
def get_id(self):
return self.id
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
remember_me = BooleanField("Remember Me")
submit = SubmitField("Sign In")

@ -1,5 +1,6 @@
Flask==2.1.2 Flask==2.1.2
Flask_Login==0.6.2 Flask_Login==0.6.2
Flask_WTF==0.14.3 Flask_WTF==0.14.3
Werkzeug==2.1.2 Werkzeug==2.0.0
WTForms==2.2.1 WTForms==2.2.1
jinja2==3.0.3

@ -15,7 +15,6 @@
{{ form.password.label }}<br> {{ form.password.label }}<br>
{{ form.password(size=32) }} {{ form.password(size=32) }}
</p> </p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p> <p>{{ form.submit() }}</p>
{% for mesg in get_flashed_messages() %} {% for mesg in get_flashed_messages() %}
<p>{{ mesg }}</p> <p>{{ mesg }}</p>

@ -0,0 +1,38 @@
{% 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>
{% endblock %}

@ -16,11 +16,6 @@
<a href="{{ url_for('index') }}">Blog</a> <a href="{{ url_for('index') }}">Blog</a>
<a href="{{ url_for('archive') }}">Archive</a> <a href="{{ url_for('archive') }}">Archive</a>
<label for="main-menu-check" class="hide-menu">X</label> <label for="main-menu-check" class="hide-menu">X</label>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div> </div>
</div> </div>
<!-- Menu --> <!-- Menu -->
@ -30,7 +25,15 @@
<!-- Content --> <!-- Content -->
<footer> <footer>
<div class="center"> <div class="center">
Made with <a href="https://github.com/tiyn/container-critique">Container Critique </a>. 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>
-
<a href="{{ url_for('register') }}">Register</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
<a href="{{ url_for('write') }}">Write entry</a>
{% endif %}
</div> </div>
</footer> </footer>
</body> </body>

@ -0,0 +1,45 @@
{% extends "template.html" %}
{% block content %}
<div class="container">
<div class="writing">
<h1>Sign In</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 %}
</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 %}
Loading…
Cancel
Save