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
VOLUME /blog/data
RUN pip3 install -r requirements.txt
VOLUME /blog/templates/entry

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

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

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

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

@ -1,3 +1,8 @@
from database import Database
db = Database()
def gen_arch_string():
"""
Creates and returns a archive string of every file in ENTRY_DIR.
@ -5,32 +10,100 @@ def gen_arch_string():
Returns:
string: html-formatted archive-string
"""
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():
"""
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:
string: html-formatted index string
"""
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.
If the file is markdown it will convert it.
This functions ensures upscaling for future formats.
Creates a html-string for an entry.
Parameters:
path_ex: path to a file.
ident: ident of an entry.
Returns:
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():

Binary file not shown.

@ -1,14 +1,37 @@
from datetime import date as dt
import os
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:
def __init__(self):
self.TABLE_FILE = 'USERS'
self.DB_DIR = os.path.dirname(".")
self.USER_TABLE_FILE = 'USERS'
self.ENTRY_TABLE_FILE = 'ENTRIES'
self.DB_DIR = os.path.dirname("./data/")
self.setup_db()
def connect(self):
@ -22,41 +45,78 @@ class Database:
"""Creates a database with tables."""
db = self.connect()
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," + \
"name CHAR(32) NOT NULL UNIQUE," + \
"password CHAR(32) NOT NULL)"
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.
"""
if self.check_name(name):
if self.check_user_name(user.name) and user.pass_hash is not None:
db = self.connect()
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"
crs.execute(query, (name, password))
crs.execute(query, (user.name, user.pass_hash))
db.commit()
return True
return False
return crs.lastrowid
return None
def check_name(self, name):
if self.get_by_name(name) is None:
def insert_entry(self, name, date, text, rating, user_id=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 False
def get_by_id(self, ident):
def get_entry_by_id(self, ident):
db = self.connect()
crs = db.cursor()
query = "SELECT * FROM " + self.TABLE_FILE + " WHERE id = ?"
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?"
crs.execute(query, (ident, ))
return crs.fetchone()
def get_by_name(self, name):
def get_user_by_id(self, ident):
db = self.connect()
crs = db.cursor()
query = "SELECT * FROM " + self.TABLE_FILE + " WHERE name = ?"
query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE id = ?"
crs.execute(query, (ident, ))
return crs.fetchone()
def get_user_by_name(self, name):
db = self.connect()
crs = db.cursor()
query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE name = ?"
crs.execute(query, (name, ))
return crs.fetchone()
@ -64,3 +124,9 @@ class Database:
user = User(name, pass_hash)
user.set_id(ident)
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_Login==0.6.2
Flask_WTF==0.14.3
Werkzeug==2.1.2
Werkzeug==2.0.0
WTForms==2.2.1
jinja2==3.0.3

@ -15,7 +15,6 @@
{{ form.password.label }}<br>
{{ form.password(size=32) }}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
{% for mesg in get_flashed_messages() %}
<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('archive') }}">Archive</a>
<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>
<!-- Menu -->
@ -30,7 +25,15 @@
<!-- Content -->
<footer>
<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>
</footer>
</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