From 61aa869b93d303eb4fb0f3657951639e09f683e5 Mon Sep 17 00:00:00 2001 From: tiyn Date: Sat, 30 Jul 2022 23:56:17 +0200 Subject: [PATCH] src: logging and writing complete --- .gitignore | 2 + Dockerfile | 2 + README.md | 24 ++++----- rebuild.sh | 1 + src/app.py | 88 +++++++++++++++++++++++--------- src/config.py | 3 ++ src/content.py | 91 +++++++++++++++++++++++++++++---- src/data.db | Bin 16384 -> 0 bytes src/data/.gitkeep | 0 src/database.py | 98 ++++++++++++++++++++++++++++++------ src/forms.py | 26 ++++++++++ src/login.py | 34 ------------- src/requirements.txt | 3 +- src/templates/login.html | 1 - src/templates/register.html | 38 ++++++++++++++ src/templates/template.html | 15 +++--- src/templates/write.html | 45 +++++++++++++++++ 17 files changed, 369 insertions(+), 102 deletions(-) create mode 100644 .gitignore delete mode 100644 src/data.db create mode 100644 src/data/.gitkeep create mode 100644 src/forms.py delete mode 100644 src/login.py create mode 100644 src/templates/register.html create mode 100644 src/templates/write.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4119916 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +data +data.db diff --git a/Dockerfile b/Dockerfile index 3f2c87f..befc2f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ COPY src /blog WORKDIR /blog +VOLUME /blog/data + RUN pip3 install -r requirements.txt VOLUME /blog/templates/entry diff --git a/README.md b/README.md index 482789c..b41a5a3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/rebuild.sh b/rebuild.sh index 816ac62..24c20a1 100755 --- a/rebuild.sh +++ b/rebuild.sh @@ -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 diff --git a/src/app.py b/src/app.py index ab3ad17..2c3bf94 100644 --- a/src/app.py +++ b/src/app.py @@ -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/") -def entry(path): - content = con_gen.gen_stand_string(path) +@app.route("/entry/") +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")) - 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) - return redirect(url_for("index")) + db_user = db.get_user_by_name(form.username.data) + if db_user is not None: + user = db.db_to_user(*db_user) + 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") diff --git a/src/config.py b/src/config.py index b21bbab..3821a0c 100644 --- a/src/config.py +++ b/src/config.py @@ -9,3 +9,6 @@ WEBSITE = "localhost:5000" # Theme for the blog: dark, light STYLE = "dark" + +# Allow new registrations +REGISTER = True diff --git a/src/content.py b/src/content.py index 458c6d2..4842c5a 100644 --- a/src/content.py +++ b/src/content.py @@ -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 """ - 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 += "\n" + content_string += "

" + year + "

\n" + content_string += "
    \n" + last_year = year + content_string += "
  • " + content_string += "[link - standalone] " + content_string += title + \ + " (" + str(year) + ") - " + str(rating) + "/100
    " + content_string += "
  • \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 """ - 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 += "
    \n" + content_string += "

    " + title + " (" + year + ") - " + \ + str(rating) + "/100

    \n" + content_string += "[" + "standalone" + "]
    \n" + content_string += text + content_string += "
    " + content_string += "" + \ + str(reviewed) + " by " + username + "" + content_string += "
    " + 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 += "

    " + title + \ + " (" + year + ") - " + str(rating) + "/100

    \n" + content_string += "[" + content_string += "" + "link" + "" + content_string += "]
    \n" + content_string += "" + \ + str(reviewed) + " by " + username + "" + content_string += "
    \n" + content_string += text + content_string += "
    " + return content_string def get_rss_string(): diff --git a/src/data.db b/src/data.db deleted file mode 100644 index ae25c7d0ea7d641b7a2f97ae2d5144fad2403457..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI(&u-H&90zcF8DpI^!Y%CP!w#)hp){$(IthtvvY;YOw=_BQRJn-*)z-hs1lK+Q zkH))j8RCeWu>(aM;V^xFvg0_hE&FqGtPHM#NvT*qUrlAnT=JYyO5QL=2szMsSLcVI z`ewWS@a}Kr0eLxkacKS^#t8*pPcqgGWARo*>@w>1{AkGDiy`xqxZe$R`<@8n=4NPi@y?IxuG3&)KW1SP z1S|==SBYpYWKn#Yud+W|G#Wd5)@zHB$t+W!e@$54mh-AUs%9{1RfVCU_cz#wI#crt z(Fp+o2tWV=5P$##AOHafKmY;|fWZF{SkfotXg!?DRjFo+@pYCvXT?Y9aPQ3VY#ql_ z?yZ((|9TO4ANs179H*XgQ%5YUg9s>e$>n z&9hEsXStVg>1+=9srjAggn$49AOHafKmY;|fB*y_009U<;86+GjHBB9SBCrZ|2tyd kJ*ov_F%WOX+mPoaQ^c>n+a diff --git a/src/data/.gitkeep b/src/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/database.py b/src/database.py index 6ef3a3a..cded5fa 100644 --- a/src/database.py +++ b/src/database.py @@ -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.ENTRY_TABLE_FILE + " WHERE id = ?" + crs.execute(query, (ident, )) + return crs.fetchone() + + def get_user_by_id(self, ident): db = self.connect() crs = db.cursor() - query = "SELECT * FROM " + self.TABLE_FILE + " WHERE id = ?" + query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE id = ?" crs.execute(query, (ident, )) return crs.fetchone() - def get_by_name(self, name): + def get_user_by_name(self, name): db = self.connect() crs = db.cursor() - query = "SELECT * FROM " + self.TABLE_FILE + " WHERE name = ?" + 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) diff --git a/src/forms.py b/src/forms.py new file mode 100644 index 0000000..d551894 --- /dev/null +++ b/src/forms.py @@ -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") diff --git a/src/login.py b/src/login.py deleted file mode 100644 index c7220c5..0000000 --- a/src/login.py +++ /dev/null @@ -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") diff --git a/src/requirements.txt b/src/requirements.txt index 120c115..b311835 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -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 diff --git a/src/templates/login.html b/src/templates/login.html index 0c1f14c..5358fc6 100644 --- a/src/templates/login.html +++ b/src/templates/login.html @@ -15,7 +15,6 @@ {{ form.password.label }}
    {{ form.password(size=32) }}

    -

    {{ form.remember_me() }} {{ form.remember_me.label }}

    {{ form.submit() }}

    {% for mesg in get_flashed_messages() %}

    {{ mesg }}

    diff --git a/src/templates/register.html b/src/templates/register.html new file mode 100644 index 0000000..e864fbe --- /dev/null +++ b/src/templates/register.html @@ -0,0 +1,38 @@ +{% extends "template.html" %} + +{% block content %} + +
    +
    +

    Register

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

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

    +

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

    +

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

    +

    {{ form.submit() }}

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

    {{ mesg }}

    + {% endfor %} +
    +
    +
    +{% endblock %} diff --git a/src/templates/template.html b/src/templates/template.html index d900ed4..e32cb37 100644 --- a/src/templates/template.html +++ b/src/templates/template.html @@ -16,11 +16,6 @@ Blog Archive - {% if current_user.is_anonymous %} - Login - {% else %} - Logout - {% endif %} @@ -30,7 +25,15 @@ diff --git a/src/templates/write.html b/src/templates/write.html new file mode 100644 index 0000000..7864627 --- /dev/null +++ b/src/templates/write.html @@ -0,0 +1,45 @@ +{% extends "template.html" %} + +{% block content %} + +
    +
    +

    Sign In

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

    + {{ form.name.label }}
    + {{ form.name(size=64) }} + {% for error in form.name.errors %} + [{{ error }}] + {% endfor %} +

    +

    + {{ form.date.label }}
    + {{ form.date }} + {% for error in form.date.errors %} + [{{ error }}] + {% endfor %} +

    +

    + {{ form.text.label }}
    + {{ form.text }} + {% for error in form.text.errors %} + [{{ error }}] + {% endfor %} +

    +

    + {{ form.rating.label }}
    + {{ form.rating }} + {% for error in form.rating.errors %} + [{{ error }}] + {% endfor %} +

    +

    {{ form.submit() }}

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

    {{ mesg }}

    + {% endfor %} +
    +
    +
    +{% endblock %}