diff --git a/.gitignore b/.gitignore index 2c2d98b..b44effb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ .idea __pycache__/ venv -db/labertasche.db-shm -db/labertasche.db-wal +db/*.db +db/*.db-shm +db/*.db-wal output /output/ -*.sql *.old diff --git a/__implementation_example/data/blog/article-3.json b/__implementation_example/data/blog/article-3.json new file mode 100644 index 0000000..ec0d26b --- /dev/null +++ b/__implementation_example/data/blog/article-3.json @@ -0,0 +1 @@ +{"comments": [{"comment_id": 9, "email": "commenter9@", "content": "9 This is a test comment and has no actual value. Please test all methods on this.", "created_on": "2020-12-16 23:37:00", "replied_to": null, "gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f"}], "replies": [{"comment_id": 10, "email": "commenter10@", "content": "10 This is a reply to the previous comment and has no actual value. Please test all methods on this.", "created_on": "2020-12-16 23:37:00", "replied_to": 9, "gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f"}]} \ No newline at end of file diff --git a/__implementation_example/data/blog/stramine.json b/__implementation_example/data/blog/stramine.json index 92117fd..b074820 100644 --- a/__implementation_example/data/blog/stramine.json +++ b/__implementation_example/data/blog/stramine.json @@ -1,22 +1 @@ -{ - "comments": [ - { - "comment_id": 1, - "email": "commenter1@", - "content": "This is an example comment with over 40 characters.", - "created_on": "2020-12-04 12:23:14", - "replied_to": null, - "gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f" - } - ], - "replies": [ - { - "comment_id": 2, - "email": "commenter2@", - "content": "This is an example reply, to test if this works.", - "created_on": "2020-12-04 12:24:19", - "replied_to": 1, - "gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f" - } - ] -} \ No newline at end of file +{"comments": [], "replies": [{"comment_id": 2, "email": "commenter2@", "content": "2 This is a reply to the previous comment and has no actual value. Please test all methods on this.", "created_on": "2020-12-16 23:37:00", "replied_to": 1, "gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f"}]} \ No newline at end of file diff --git a/db/example-data.sql b/db/example-data.sql new file mode 100644 index 0000000..5bc1505 --- /dev/null +++ b/db/example-data.sql @@ -0,0 +1,108 @@ +/********************************************************************************** + * _author : Domeniko Gentner + * _mail : code@tuxstash.de + * _repo : https://git.tuxstash.de/gothseidank/labertasche + * _license : This project is under MIT License + ********************************************************************************** + * + * This script generates sample data for the example implementation. + * Feed it into the automatically created database with either DBBeaver + * or the sqlite command line tool. + * + * Please note: Labertasche must have run once to create the database! + * + ********************************************************************************** + */ + +/* delete old data */ +DELETE FROM t_comments; +DELETE FROM t_projects; +DELETE FROM t_email; +DELETE FROM t_comments; +DELETE FROM t_location; + +/* Create example projects */ +INSERT INTO t_projects (id_project, name) + VALUES + (1, 'default'), + (2, 'example.com'), + (3, 'tuxstash.de'), + (4, 'beispiel.de'), + (5, 'labertasche.tuxstash.de') +; + +/* Create existing locations for each project */ +INSERT INTO t_location (id_location, location, project_id) + VALUES + (1, '/blog/stramine/', 1), + (2, '/blog/readme/', 1), + (3, '/blog/article-1/', 2), + (4, '/blog/article-2/', 2), + (5, '/blog/article-3/', 3), + (6, '/blog/article-4/', 3), + (7, '/blog/article-5/', 4), + (8, '/blog/article-6/', 4), + (9, '/blog/article-7/', 5), + (10, '/blog/article-8/', 5) +; + +/* Create some emails that are blocked and allowed */ +INSERT INTO t_email (id_email, email, is_allowed, is_blocked, project_id) + VALUES + (1, "commenter1@example.com", true, false, 1), + (2, "commenter2@example.com", false, true, 1), + (3, "commenter3@example.com", true, false, 2), + (4, "commenter4@example.com", false, true, 2), + (5, "commenter5@example.com", true, false, 3), + (6, "commenter6@example.com", false, true, 3), + (7, "commenter7@example.com", true, false, 4), + (8, "commenter8@example.com", false, true, 4), + (9, "commenter9@example.com", true, false, 5), + (10, "commenter10@example.com", false, true, 5) +; + + +/* Create some comments */ +INSERT INTO t_comments (comments_id, location_id, email, content, created_on, is_published, is_spam, spam_score, replied_to, confirmation, deletion, gravatar, project_id) + VALUES + (1, 1, 'commenter1@example.com', '1 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 1), + (2, 1, 'commenter2@example.com', '2 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 1, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 1), + (3, 2, 'commenter3@example.com', '3 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 1), + (4, 2, 'commenter4@example.com', '4 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 3, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 1), + + (5, 3, 'commenter5@example.com', '5 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 2), + (6, 3, 'commenter6@example.com', '6 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 5, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 2), + (7, 4, 'commenter7@example.com', '7 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 2), + (8, 4, 'commenter8@example.com', '8 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 7, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 2), + + (9, 5, 'commenter9@example.com', '9 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', false, true, 0.09, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 3), + (10, 5, 'commenter10@example.com', '10 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 9, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 3), + (11, 6, 'commenter11@example.com', '11 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.09, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 3), + (12, 6, 'commenter12@example.com', '12 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 11, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 3), + + (13, 7, 'commenter13@example.com', '13 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 4), + (14, 7, 'commenter14@example.com', '14 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 13, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 4), + (15, 8, 'commenter15@example.com', '15 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 4), + (16, 8, 'commenter16@example.com', '16 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 16, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 4), + + (17, 9, 'commenter17@example.com', '17 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 5), + (18, 9, 'commenter18@example.com', '18 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 18, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 5), + (19, 10, 'commenter19@example.com', '19 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 5), + (20, 10, 'commenter20@example.com', '20 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 19, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 5) +; + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/labertasche.yaml b/labertasche.yaml index d539705..d2d7959 100644 --- a/labertasche.yaml +++ b/labertasche.yaml @@ -8,19 +8,32 @@ system: web_url: "http://dev.localhost:1314/" # Url where the comment system is served blog_url: "http://dev.localhost:1313/" # Url of your website - cookie-domain: "dev.localhost" # Url where the comment system is served + cookie_domain: "dev.localhost" # hostname where the comment system is served database_uri: "sqlite:///db/labertasche.db" # Database URI. See documentation. Default is sqlite. secret: "6Gxvb52bIJCm2vfDsmWKzShKp1omrzVG" # CHANGE ME! THIS IS IMPORTANT! output: "./__implementation_example/data/" # Base path for the output json debug: false # Leave this as is, this is for development. send_otp_to_publish: true # Disables confirmation w/ OTP via mail + cookie_secure: false + +projects: + - default: + web_url: "http://dev.localhost:1314/" + blog_url: "http://dev.localhost:1313/" + output: "./__implementation_example/data/" + send_otp_to_publish: true + + - example.com: + web_url: "http://comments.example.com/" + blog_url: "http://blog.example.com/" + output: "./example/data/" + send_otp_to_publish: true gravatar: cache: true # Enable caching of gravatar images static_dir: "./__implementation_example/static/images/gravatar/" # Where to store cached images, must exist! size: 256 # only applies if images are cached, # otherwise use ?s=size at the end of the gravatar url - dashboard: username: "admin" # CHANGE ME! password: "admin" # CHANGE ME! diff --git a/labertasche/blueprints/__init__.py b/labertasche/blueprints/__init__.py index 6dd3415..4d25aed 100644 --- a/labertasche/blueprints/__init__.py +++ b/labertasche/blueprints/__init__.py @@ -9,4 +9,4 @@ from .bp_comments import bp_comments from .bp_login import bp_login from .bp_dashboard import bp_dashboard - +from .bp_jsconnector import bp_jsconnector diff --git a/labertasche/blueprints/bp_comments.py b/labertasche/blueprints/bp_comments/__init__.py similarity index 93% rename from labertasche/blueprints/bp_comments.py rename to labertasche/blueprints/bp_comments/__init__.py index bd5a557..a24bd0b 100644 --- a/labertasche/blueprints/bp_comments.py +++ b/labertasche/blueprints/bp_comments/__init__.py @@ -48,8 +48,8 @@ def check_and_insert_new_comment(): # Validate json and check length again if not is_valid_json(new_comment) or \ - len(new_comment['content']) < 40 or \ - len(new_comment['email']) < 5: + len(new_comment['content']) < 40 or \ + len(new_comment['email']) < 5: print("too short", file=stderr) return make_response(jsonify(status='post-invalid-json'), 400) @@ -108,8 +108,8 @@ def check_and_insert_new_comment(): is_spam = False # Look for location - loc_query = db.session.query(TLocation)\ - .filter(TLocation.location == new_comment['location']) + loc_query = db.session.query(TLocation) \ + .filter(TLocation.location == new_comment['location']) if loc_query.first(): # Location exists, set existing location id @@ -131,6 +131,7 @@ def check_and_insert_new_comment(): new_comment.pop("location") # insert comment + # noinspection PyBroadException try: new_comment.update({"is_published": False}) new_comment.update({"created_on": default_timestamp()}) @@ -154,11 +155,7 @@ def check_and_insert_new_comment(): print(f"Duplicate from {request.environ['REMOTE_ADDR']}, error is:\n{e}", file=stderr) return make_response(jsonify(status="post-duplicate"), 400) - except Exception as e: # must be at bottom - # mail(f"check_and_insert_new_comment has thrown an error: {e}", ) - print("---------------------------------------------") - print(e, file=stderr) - print("---------------------------------------------") + except Exception: # must be at bottom return make_response(jsonify(status="post-internal-server-error"), 400) export_location(t_comment.location_id) diff --git a/labertasche/blueprints/bp_dashboard.py b/labertasche/blueprints/bp_dashboard.py deleted file mode 100644 index 66875ba..0000000 --- a/labertasche/blueprints/bp_dashboard.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# /********************************************************************************** -# * _author : Domeniko Gentner -# * _mail : code@tuxstash.de -# * _repo : https://git.tuxstash.de/gothseidank/labertasche -# * _license : This project is under MIT License -# *********************************************************************************/ -from flask import Blueprint, render_template, request, redirect, make_response, jsonify -from flask_login import login_required -from labertasche.database import labertasche_db as db -from labertasche.models import TLocation, TComments, TEmail, TProjects -from labertasche.helper import dates_of_the_week, export_location, get_id_from_project_name -from sqlalchemy import func -import re - -# Blueprint -bp_dashboard = Blueprint("bp_dashboard", __name__, url_prefix='/dashboard') - - -@bp_dashboard.route("/") -@login_required -def dashboard_index(): - t_projects = db.session.query(TProjects).all() - projects = list() - for each in t_projects: - comments = db.session.query(TComments).filter(TComments.project_id == each.id_project) \ - .filter(TComments.is_published == True) \ - .filter(TComments.is_spam == False).count() - unpub_comments = db.session.query(TComments).filter(TComments.project_id == each.id_project) \ - .filter(TComments.is_spam == False) \ - .filter(TComments.is_published == False).count() - spam = db.session.query(TComments).filter(TComments.project_id == each.id_project) \ - .filter(TComments.is_spam == True).count() - - projects.append(dict({ - "id_project": each.id_project, - "name": each.name, - "total_comments": comments, - "total_spam": spam, - "total_unpublished": unpub_comments - })) - return render_template('project_overview.html', projects=projects) - - -@bp_dashboard.route("/project/new", methods=['POST']) -@login_required -def dashboard_new_project(): - return make_response(jsonify(status='too-short'), 200) - - -@bp_dashboard.route('//') -@login_required -def dashboard_project_index(project: str): - proj_id = get_id_from_project_name(project) - dates = dates_of_the_week() - spam = list() - published = list() - unpublished = list() - for each in dates: - spam_comments = db.session.query(TComments).filter(TComments.project_id == proj_id)\ - .filter(func.DATE(TComments.created_on) == each.date())\ - .filter(TComments.is_spam == True).all() - - pub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \ - .filter(TComments.is_spam == False)\ - .filter(TComments.is_published == True).all() - - unpub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \ - .filter(TComments.is_spam == False)\ - .filter(TComments.is_published == False).all() - - published.append(len(pub_comments)) - spam.append(len(spam_comments)) - unpublished.append(len(unpub_comments)) - - return render_template('dashboard.html', dates=dates, spam=spam, published=published, unpublished=unpublished) - - -@bp_dashboard.route('/review-spam/', methods=["POST", "GET"]) -@bp_dashboard.route('/review-spam/', methods=["POST", "GET"]) -@login_required -def dashboard_review_spam(location_id=None): - all_locations = db.session.query(TLocation).all() - - # Check post - if request.method == "POST": - location_id = request.form.get('selected_location') - - # no parameters found - if location_id is None: - return render_template("review-spam.html", locations=all_locations, selected=location_id) - - try: - if int(location_id) >= 1: - spam_comments = db.session.query(TComments).filter(TComments.location_id == location_id)\ - .filter(TComments.is_spam == True) - return render_template("review-spam.html", locations=all_locations, selected=location_id, - spam_comments=spam_comments) - except ValueError: - pass - - export_location(location_id) - return render_template("review-spam.html", locations=all_locations, selected=location_id) - - -@bp_dashboard.route('/manage-comments/', methods=["POST", "GET"]) -@bp_dashboard.route('/manage-comments/', methods=["POST", "GET"]) -@login_required -def dashboard_manage_regular_comments(location_id=None): - all_locations = db.session.query(TLocation).all() - - # Check post - if request.method == "POST": - location_id = request.form.get('selected_location') - - # no parameters found - if location_id is None: - return render_template("manage-comments.html", locations=all_locations, selected=location_id) - - try: - if int(location_id) >= 1: - spam_comments = db.session.query(TComments).filter(TComments.location_id == location_id) \ - .filter(TComments.is_spam == False) - return render_template("manage-comments.html", locations=all_locations, selected=location_id, - spam_comments=spam_comments) - except ValueError: - pass - - export_location(location_id) - return render_template("manage-comments.html", locations=all_locations, selected=location_id) - - -@bp_dashboard.route('/manage-mail/') -@login_required -def dashboard_allow_email(): - addresses = db.session.query(TEmail).all() - return render_template("manage_mail_addresses.html", addresses=addresses) - - -@bp_dashboard.route('/toggle-mail-allowed/') -@login_required -def dashboard_allow_email_toggle(id_email): - address = db.session.query(TEmail).filter(TEmail.id_email == id_email).first() - if address: - setattr(address, "is_allowed", (not address.is_allowed)) - setattr(address, "is_blocked", (not address.is_blocked)) - db.session.commit() - return redirect(request.referrer) - - -@bp_dashboard.route('/reset-mail-reputation/') -@login_required -def dashboard_reset_mail_reputation(id_email): - db.session.query(TEmail).filter(TEmail.id_email == id_email).delete() - db.session.commit() - return redirect(request.referrer) - - -@bp_dashboard.route('/delete-comment//', methods=['GET']) -@login_required -def dashboard_review_spam_delete_comment(location_id, comment_id): - comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() - db.session.delete(comment) - db.session.commit() - - # Remove after last slash, to keep the location but get rid of the comment id - url = re.match("^(.*[/])", request.referrer)[0] - export_location(location_id) - return redirect(f"{url}/{location_id}") - - -@bp_dashboard.route('/allow-comment//', methods=['GET']) -@login_required -def dashboard_review_spam_allow_comment(comment_id, location_id): - comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() - if comment: - setattr(comment, 'is_published', True) - setattr(comment, 'is_spam', False) - db.session.commit() - - url = re.match("^(.*[/])", request.referrer)[0] - export_location(location_id) - return redirect(f"{url}/{location_id}") - - -@bp_dashboard.route('/block-mail//', methods=["GET"]) -@login_required -def dashboard_review_spam_block_mail(location_id, comment_id): - comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() - if comment: - mail = db.session.query(TEmail).filter(TEmail.email == comment.email).first() - if mail: - setattr(mail, 'is_allowed', False) - setattr(mail, 'is_blocked', True) - else: - new_mail = { - "email": comment.first().email, - "is_allowed": False, - "is_blocked": True - } - db.session.add(TEmail(**new_mail)) - - # Delete all comments made by this mail address - db.session.query(TComments).filter(TComments.email == comment.email).delete() - db.session.commit() - - url = re.match("^(.*[/])", request.referrer)[0] - export_location(location_id) - return redirect(f"{url}/{location_id}") - - -@bp_dashboard.route('/allow-user//', methods=["GET"]) -@login_required -def dashboard_review_spam_allow_user(location_id, comment_id): - comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() - if comment: - mail = db.session.query(TEmail).filter(TEmail.email == comment.email).first() - if mail: - setattr(mail, 'is_allowed', True) - setattr(mail, 'is_blocked', False) - else: - new_mail = { - "email": comment.email, - "is_allowed": True, - "is_blocked": False - } - db.session.add(TEmail(**new_mail)) - - # Allow all comments made by this mail address - all_comments = db.session.query(TComments).filter(TComments.email == comment.email).all() - if all_comments: - for comment in all_comments: - setattr(comment, 'is_published', True) - setattr(comment, 'is_spam', False) - - db.session.commit() - url = re.match("^(.*[/])", request.referrer)[0] - export_location(location_id) - return redirect(f"{url}/{location_id}") diff --git a/labertasche/blueprints/bp_dashboard/__init__.py b/labertasche/blueprints/bp_dashboard/__init__.py new file mode 100644 index 0000000..15a5b83 --- /dev/null +++ b/labertasche/blueprints/bp_dashboard/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from flask import Blueprint + +# Blueprint +bp_dashboard = Blueprint("bp_dashboard", __name__, url_prefix='/dashboard') + +# Files with routes +from .projects import dashboard_project_list +from .mail import dashboard_manage_mail +from .spam import dashboard_review_spam +from .comments import dashboard_manage_regular_comments diff --git a/labertasche/blueprints/bp_dashboard/comments.py b/labertasche/blueprints/bp_dashboard/comments.py new file mode 100644 index 0000000..e5cabce --- /dev/null +++ b/labertasche/blueprints/bp_dashboard/comments.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_dashboard +from flask import render_template, request, redirect, url_for +from flask_login import login_required +from labertasche.database import labertasche_db as db +from labertasche.models import TLocation, TComments +from labertasche.helper import export_location, get_id_from_project_name + + +@bp_dashboard.route('/manage-comments/', methods=["GET"]) +@login_required +def dashboard_manage_regular_comments(project: str): + location_id = 0 + proj_id = get_id_from_project_name(project) + all_locations = db.session.query(TLocation).filter(TLocation.project_id == proj_id).all() + + # Project does not exist, error code is used by Javascript, not Flask + if proj_id == -1: + return redirect(url_for("bp_dashboard.dashboard_project_list", error=404)) + + if request.args.get('location'): + location_id = request.args.get('location') + + # no parameters found + if location_id is None: + return render_template("manage-comments.html", locations=all_locations, + selected=location_id, title="Manage Comments", + action="comments") + + try: + if int(location_id) >= 1: + spam_comments = db.session.query(TComments).filter(TComments.location_id == location_id) \ + .filter(TComments.is_spam == False) + return render_template("manage-comments.html", locations=all_locations, selected=location_id, + spam_comments=spam_comments, project=project, + title="Manage Comments", action="comments") + except ValueError: + pass + + export_location(location_id) + return render_template("manage-comments.html", locations=all_locations, + selected=location_id, project=project, title="Manage Comments", + action="comments") + diff --git a/labertasche/blueprints/bp_dashboard/mail.py b/labertasche/blueprints/bp_dashboard/mail.py new file mode 100644 index 0000000..490c88c --- /dev/null +++ b/labertasche/blueprints/bp_dashboard/mail.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_dashboard +from flask import render_template, redirect, url_for +from flask_login import login_required +from labertasche.database import labertasche_db as db +from labertasche.models import TEmail +from labertasche.helper import get_id_from_project_name + + +@bp_dashboard.route('/manage-mail/') +@login_required +def dashboard_manage_mail(project: str): + """ + Shows the panel to manage email addresses + :param project: The project name to manage + :return: The template used to display the route + """ + proj_id = get_id_from_project_name(project) + + # Project does not exist, error code is used by Javascript, not Flask + if proj_id == -1: + return redirect(url_for("bp_dashboard.dashboard_project_list", error=404)) + + addresses = db.session.query(TEmail).filter(TEmail.project_id == proj_id).all() + return render_template("manage-mail.html", addresses=addresses, project=project) diff --git a/labertasche/blueprints/bp_dashboard/projects.py b/labertasche/blueprints/bp_dashboard/projects.py new file mode 100644 index 0000000..1a4439e --- /dev/null +++ b/labertasche/blueprints/bp_dashboard/projects.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_dashboard +from flask import render_template, redirect, url_for +from flask_login import login_required +from sqlalchemy import func +from labertasche.database import labertasche_db as db +from labertasche.models import TComments, TProjects +from labertasche.helper import get_id_from_project_name, dates_of_the_week + + +@bp_dashboard.route("/") +@login_required +def dashboard_project_list(): + """ + Displays an overview of all projects. + :return: The overview template. + """ + t_projects = db.session.query(TProjects).all() + projects = list() + for each in t_projects: + comments = db.session.query(TComments).filter(TComments.project_id == each.id_project) \ + .filter(TComments.is_published == True) \ + .filter(TComments.is_spam == False).count() + unpub_comments = db.session.query(TComments).filter(TComments.project_id == each.id_project) \ + .filter(TComments.is_spam == False) \ + .filter(TComments.is_published == False).count() + spam = db.session.query(TComments).filter(TComments.project_id == each.id_project) \ + .filter(TComments.is_spam == True).count() + + projects.append(dict({ + "id_project": each.id_project, + "name": each.name, + "total_comments": comments, + "total_spam": spam, + "total_unpublished": unpub_comments + })) + return render_template('project-list.html', projects=projects) + + +@bp_dashboard.route('//') +@login_required +def dashboard_project_stats(project: str): + """ + Displays the project dashboard + + :param project: The project to show + :return: The template for the route + """ + proj_id = get_id_from_project_name(project) + + # Project does not exist, error code is used by Javascript, not Flask + if proj_id == -1: + return redirect(url_for("bp_dashboard.dashboard_project_list", error=404)) + + dates = dates_of_the_week() + spam = list() + published = list() + unpublished = list() + for each in dates: + spam_comments = db.session.query(TComments).filter(TComments.project_id == proj_id) \ + .filter(func.DATE(TComments.created_on) == each.date()) \ + .filter(TComments.is_spam == True).all() + + pub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \ + .filter(TComments.project_id == proj_id) \ + .filter(TComments.is_spam == False) \ + .filter(TComments.is_published == True).all() + + unpub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \ + .filter(TComments.project_id == proj_id) \ + .filter(TComments.is_spam == False) \ + .filter(TComments.is_published == False).all() + + published.append(len(pub_comments)) + spam.append(len(spam_comments)) + unpublished.append(len(unpub_comments)) + + return render_template('project-stats.html', dates=dates, spam=spam, project=project, + published=published, unpublished=unpublished) + diff --git a/labertasche/blueprints/bp_dashboard/spam.py b/labertasche/blueprints/bp_dashboard/spam.py new file mode 100644 index 0000000..e13e928 --- /dev/null +++ b/labertasche/blueprints/bp_dashboard/spam.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_dashboard +from flask import render_template, request, redirect, url_for +from flask_login import login_required +from labertasche.database import labertasche_db as db +from labertasche.models import TLocation, TComments +from labertasche.helper import export_location, get_id_from_project_name + + +@bp_dashboard.route('/manage-spam/', methods=["GET"]) +@login_required +def dashboard_review_spam(project: str): + """ + Shows the manage spam template + :param project: The project used for displaying data + :return: The template to display for this rouet + """ + location_id = 0 + proj_id = get_id_from_project_name(project) + all_locations = db.session.query(TLocation).filter(TLocation.project_id == proj_id).all() + + # Project does not exist, error code is used by Javascript, not Flask + if proj_id == -1: + return redirect(url_for("bp_dashboard.dashboard_project_list", error=404)) + + if request.args.get('location'): + location_id = request.args.get('location') + + # no parameters found + if location_id is None: + return render_template("manage-comments.html", locations=all_locations, + selected=location_id, title="Review Spam", action="spam") + + try: + if int(location_id) >= 1: + spam_comments = db.session.query(TComments) \ + .filter(TComments.project_id == proj_id) \ + .filter(TComments.location_id == location_id) \ + .filter(TComments.is_spam == True) + + return render_template("manage-comments.html", locations=all_locations, selected=location_id, + spam_comments=spam_comments, project=project, title="Review Spam", action="spam") + except ValueError: + pass + + export_location(location_id) + return render_template("manage-comments.html", locations=all_locations, + selected=location_id, project=project, title="Review Spam", action="spam") + diff --git a/labertasche/blueprints/bp_jsconnector/__init__.py b/labertasche/blueprints/bp_jsconnector/__init__.py new file mode 100644 index 0000000..ec3ba38 --- /dev/null +++ b/labertasche/blueprints/bp_jsconnector/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from flask import Blueprint + +# Blueprint +bp_jsconnector = Blueprint("bp_jsconnector", __name__, url_prefix='/api/') + +from .projects import api_create_project, api_delete_project, api_edit_project_name +from .mail import api_toggle_email_reputation, api_reset_mail_reputation +from .comments import api_comment_allow_user, api_comment_allow_comment, \ + api_comment_block_mail, api_comments_delete_comment diff --git a/labertasche/blueprints/bp_jsconnector/comments.py b/labertasche/blueprints/bp_jsconnector/comments.py new file mode 100644 index 0000000..3f36bf4 --- /dev/null +++ b/labertasche/blueprints/bp_jsconnector/comments.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_jsconnector +from flask import request, redirect +from flask_login import login_required +from labertasche.database import labertasche_db as db +from labertasche.helper import export_location +from labertasche.models import TComments, TEmail + +# This file contains the routes for the manage comments menu point. +# They are called via GET + + +@bp_jsconnector.route('/comment-delete/', methods=['GET']) +@login_required +def api_comments_delete_comment(comment_id): + db.session.query(TComments).filter(TComments.comments_id == comment_id).delete() + db.session.commit() + + # Get location id from get params + location_id = request.args.get('location') + + export_location(location_id) + return redirect(request.referrer) + + +@bp_jsconnector.route('/comment-allow/', methods=['GET']) +@login_required +def api_comment_allow_comment(comment_id): + comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() + if comment: + setattr(comment, 'is_published', True) + setattr(comment, 'is_spam', False) + db.session.commit() + + # Get location id from get params + location_id = request.args.get('location') + + export_location(location_id) + return redirect(request.referrer) + + +@bp_jsconnector.route('/comment-allow-user/', methods=["GET"]) +@login_required +def api_comment_allow_user(comment_id): + comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() + if comment: + addr = db.session.query(TEmail).filter(TEmail.email == comment.email).first() + if addr: + setattr(addr, 'is_allowed', True) + setattr(addr, 'is_blocked', False) + else: + new_mail = { + "email": comment.email, + "is_allowed": True, + "is_blocked": False + } + db.session.add(TEmail(**new_mail)) + + # Allow all comments made by this mail address + all_comments = db.session.query(TComments).filter(TComments.email == comment.email).all() + if all_comments: + for comment in all_comments: + setattr(comment, 'is_published', True) + setattr(comment, 'is_spam', False) + + db.session.commit() + + # Get location id from get params + location_id = request.args.get('location') + + export_location(location_id) + return redirect(request.referrer) + + +@bp_jsconnector.route('/comment-block-mail/', methods=["GET"]) +@login_required +def api_comment_block_mail(comment_id): + comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() + if comment: + addr = db.session.query(TEmail).filter(TEmail.email == comment.email).first() + if addr: + setattr(addr, 'is_allowed', False) + setattr(addr, 'is_blocked', True) + else: + new_mail = { + "email": comment.first().email, + "is_allowed": False, + "is_blocked": True + } + db.session.add(TEmail(**new_mail)) + + # Delete all comments made by this mail address + db.session.query(TComments).filter(TComments.email == comment.email).delete() + db.session.commit() + + # Get location id from get params + location_id = request.args.get('location') + + export_location(location_id) + return redirect(request.referrer) + + diff --git a/labertasche/blueprints/bp_jsconnector/mail.py b/labertasche/blueprints/bp_jsconnector/mail.py new file mode 100644 index 0000000..fcfcdb5 --- /dev/null +++ b/labertasche/blueprints/bp_jsconnector/mail.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_jsconnector +from flask import request, redirect +from flask_login import login_required +from labertasche.database import labertasche_db as db +from labertasche.helper import get_id_from_project_name, export_location +from labertasche.models import TEmail, TComments +from re import match + + +@bp_jsconnector.route('/mail-toggle-status/') +@login_required +def api_toggle_email_reputation(id_email): + address = db.session.query(TEmail).filter(TEmail.id_email == id_email).first() + if address: + setattr(address, "is_allowed", (not address.is_allowed)) + setattr(address, "is_blocked", (not address.is_blocked)) + db.session.commit() + return redirect(request.referrer) + + +@bp_jsconnector.route('/mail-reset-reputation/') +@login_required +def api_reset_mail_reputation(id_email): + db.session.query(TEmail).filter(TEmail.id_email == id_email).delete() + db.session.commit() + return redirect(request.referrer) + + diff --git a/labertasche/blueprints/bp_jsconnector/projects.py b/labertasche/blueprints/bp_jsconnector/projects.py new file mode 100644 index 0000000..801e2f1 --- /dev/null +++ b/labertasche/blueprints/bp_jsconnector/projects.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_jsconnector +from flask import request, make_response, jsonify, redirect, url_for +from flask_login import login_required +from labertasche.database import labertasche_db as db +from labertasche.helper import get_id_from_project_name +from labertasche.models import TProjects, TComments, TEmail, TLocation +import re + + +@bp_jsconnector.route("/project/new", methods=['POST']) +@login_required +def api_create_project(): + """ + Called on dashboard project overview to create a new project. + + :return: A string with an error code and 'ok' as string on success. + """ + # TODO: Project name exists? + name = request.json['name'] + + if not len(name): + return make_response(jsonify(status='too-short'), 400) + if not re.match('^\\w+$', name): + return make_response(jsonify(status='invalid-name'), 400) + + proj = TProjects(name=name) + db.session.add(proj) + db.session.commit() + + return make_response(jsonify(status='ok'), 200) + + +@bp_jsconnector.route('project/edit/', methods=['POST']) +@login_required +def api_edit_project_name(name: str): + """ + Renames the project. + :param name: + :return: A string with an error code and 'ok' as string on success. + """ + # TODO: Project name exists? + new_name = request.json['name'] + + if not len(new_name): + return make_response(jsonify(status='too-short'), 400) + if not re.match('^\\w+$', new_name): + return make_response(jsonify(status='invalid-name'), 400) + + proj_id = get_id_from_project_name(name) + project = db.session.query(TProjects).filter(TProjects.id_project == proj_id) + setattr(project, 'name', new_name) + db.session.upate(project) + db.session.commit() + + return make_response(jsonify(status='ok'), 200) + + +@bp_jsconnector.route('project/delete/', methods=['GET']) +@login_required +def api_delete_project(project: str): + """ + Deletes a project from the database and all associated data + + :param project: The name of the project + :return: A string with an error code and 'ok' as string on success. + """ + # TODO: Javascript + proj_id = get_id_from_project_name(project) + if proj_id == -1: + return make_response(jsonify(status='not-found'), 400) + + # noinspection PyBroadException + try: + db.session.query(TComments).filter(TComments.project_id == proj_id).delete() + db.session.query(TLocation).filter(TLocation.project_id == proj_id).delete() + db.session.query(TEmail).filter(TEmail.project_id == proj_id).delete() + db.session.query(TProjects).filter(TProjects.id_project == proj_id).delete() + db.session.commit() + db.session.flush() + except Exception: + return make_response(jsonify(status='exception'), 400) + + return make_response(jsonify(status='ok'), 200) diff --git a/labertasche/blueprints/bp_jsconnector/spam.py b/labertasche/blueprints/bp_jsconnector/spam.py new file mode 100644 index 0000000..eddcf15 --- /dev/null +++ b/labertasche/blueprints/bp_jsconnector/spam.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_jsconnector +from flask import request, redirect +from flask_login import login_required +from labertasche.database import labertasche_db as db +from labertasche.models import TComments, TEmail +from labertasche.helper import export_location +import re + + +# @bp_jsconnector.route('/block-mail//', methods=["GET"]) +# @login_required +# def dashboard_review_spam_block_mail(location_id, comment_id): +# comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() +# if comment: +# addr = db.session.query(TEmail).filter(TEmail.email == comment.email).first() +# if addr: +# setattr(addr, 'is_allowed', False) +# setattr(addr, 'is_blocked', True) +# else: +# new_mail = { +# "email": comment.first().email, +# "is_allowed": False, +# "is_blocked": True +# } +# db.session.add(TEmail(**new_mail)) +# +# # Delete all comments made by this mail address +# db.session.query(TComments).filter(TComments.email == comment.email).delete() +# db.session.commit() +# +# url = re.match("^(.*[/])", request.referrer)[0] +# export_location(location_id) +# return redirect(f"{url}/{location_id}") +# + +# @bp_jsconnector.route('/allow-user//', methods=["GET"]) +# @login_required +# def dashboard_review_spam_allow_user(location_id, comment_id): +# comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first() +# if comment: +# addr = db.session.query(TEmail).filter(TEmail.email == comment.email).first() +# if addr: +# setattr(addr, 'is_allowed', True) +# setattr(addr, 'is_blocked', False) +# else: +# new_mail = { +# "email": comment.email, +# "is_allowed": True, +# "is_blocked": False +# } +# db.session.add(TEmail(**new_mail)) +# +# # Allow all comments made by this mail address +# all_comments = db.session.query(TComments).filter(TComments.email == comment.email).all() +# if all_comments: +# for comment in all_comments: +# setattr(comment, 'is_published', True) +# setattr(comment, 'is_spam', False) +# +# db.session.commit() +# url = re.match("^(.*[/])", request.referrer)[0] +# export_location(location_id) +# return redirect(f"{url}/{location_id}") diff --git a/labertasche/blueprints/bp_login.py b/labertasche/blueprints/bp_login/__init__.py similarity index 90% rename from labertasche/blueprints/bp_login.py rename to labertasche/blueprints/bp_login/__init__.py index 7af59ce..b164014 100644 --- a/labertasche/blueprints/bp_login.py +++ b/labertasche/blueprints/bp_login/__init__.py @@ -19,7 +19,7 @@ bp_login = Blueprint("bp_login", __name__) @bp_login.route('/', methods=['GET']) def show_login(): if current_user.is_authenticated: - return redirect(url_for('bp_dashboard.dashboard_index')) + return redirect(url_for('bp_dashboard.dashboard_project_list')) return render_template('login.html') @@ -32,7 +32,7 @@ def login(): if check_auth(username, password): login_user(User(0), remember=True) - return redirect(url_for('bp_dashboard.dashboard_index')) + return redirect(url_for('bp_dashboard.dashboard_project_list')) # Redirect get request to the login page return redirect(url_for('bp_login.show_login')) diff --git a/labertasche/helper/__init__.py b/labertasche/helper/__init__.py index 348ae3a..cc5766a 100644 --- a/labertasche/helper/__init__.py +++ b/labertasche/helper/__init__.py @@ -201,4 +201,8 @@ def get_id_from_project_name(name: str) -> int: :return: the ID of the project """ proj = db.session.query(TProjects).filter(TProjects.name == name).first() + + if proj is None: + return -1 + return proj.id_project diff --git a/labertasche/settings/__init__.py b/labertasche/settings/__init__.py index 034abb0..3f7e22f 100644 --- a/labertasche/settings/__init__.py +++ b/labertasche/settings/__init__.py @@ -28,3 +28,4 @@ class Settings: self.gravatar = conf['gravatar'] self.addons = conf['addons'] self.smileys = conf['smileys'] + self.projects = conf['projects'] diff --git a/server.py b/server.py index 445bad2..b37b929 100644 --- a/server.py +++ b/server.py @@ -14,7 +14,7 @@ from sqlalchemy import event from sqlalchemy.engine import Engine from labertasche.settings import Settings from labertasche.database import labertasche_db -from labertasche.blueprints import bp_comments, bp_login, bp_dashboard +from labertasche.blueprints import bp_comments, bp_login, bp_dashboard, bp_jsconnector from labertasche.models import TProjects from labertasche.helper import User from flask_login import LoginManager @@ -27,7 +27,9 @@ settings = Settings() # Flask App laberflask = Flask(__name__) laberflask.config.update(dict( - SESSION_COOKIE_DOMAIN=settings.system['cookie-domain'], + SESSION_COOKIE_DOMAIN=settings.system['cookie_domain'], + SESSION_COOKIE_SECURE=settings.system['cookie_secure'], + REMEMBER_COOKIE_SECURE=settings.system['cookie_secure'], DEBUG=settings.system['debug'], SECRET_KEY=settings.system['secret'], TEMPLATES_AUTO_RELOAD=True, @@ -58,6 +60,7 @@ CORS(laberflask, resources={r"/comments": {"origins": settings.system['blog_url' laberflask.register_blueprint(bp_comments) laberflask.register_blueprint(bp_dashboard) laberflask.register_blueprint(bp_login) +laberflask.register_blueprint(bp_jsconnector) # Disable Werkzeug's verbosity during development log = logging.getLogger('werkzeug') diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 122be40..78abba9 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -5,6 +5,9 @@ // # * _license : This project is under MIT License // # *********************************************************************************/ +// ------------------------------------------------------ +// Called when search for mail addresses in manage mail +// ------------------------------------------------------ function dashboard_mailsearch(search_txt) { let el = document.getElementById('mail-table'); @@ -20,19 +23,23 @@ function dashboard_mailsearch(search_txt) } } +// ------------------------------------------------------ +// Called when a new project is created, +// posts it to the server +// ------------------------------------------------------ function new_project_save() { let modal_ok = document.getElementById('modal-ok'); let modal_cancel = document.getElementById('modal-cancel'); - let short_help = document.getElementById('new-project-too-short'); + let short_help_short = document.getElementById('new-project-too-short'); let short_help_invalid = document.getElementById('new-project-invalid-name'); let name = document.getElementById('project-name').value - short_help.classList.add('is-hidden'); + short_help_short.classList.add('is-hidden'); short_help_invalid.classList.add('is-hidden'); // Validate input if (name.length === 0) { - short_help.classList.remove('is-hidden'); + short_help_short.classList.remove('is-hidden'); return false; } if (/^\w+$/.test(name) === false){ @@ -42,7 +49,7 @@ function new_project_save() { modal_ok.classList.add('is-loading'); modal_cancel.classList.add('is-hidden'); - fetch(window.location.protocol + "//" + window.location.host + '/dashboard/project/new', + fetch(window.location.protocol + "//" + window.location.host + '/api/project/new', { mode: "cors", headers: { @@ -63,9 +70,10 @@ function new_project_save() { modal_cancel.classList.remove('is-hidden'); if (result === "ok"){ hide_modal('modal-new-project'); + window.location.reload(true); } if (result === "too-short"){ - short_help.classList.remove('is-hidden'); + short_help_short.classList.remove('is-hidden'); } if (result === "invalid-name"){ short_help_invalid.classList.remove('is-hidden'); @@ -76,12 +84,19 @@ function new_project_save() { }) } + +// ------------------------------------------------------ +// Hides any modal +// ------------------------------------------------------ function hide_modal(id_name) { let el = document.getElementById(id_name); el.classList.remove("is-active"); } +// ------------------------------------------------------ +// Shows any modal +// ------------------------------------------------------ function show_modal(id_name) { let el = document.getElementById(id_name); diff --git a/templates/base.html b/templates/base.html index 776e822..5157629 100644 --- a/templates/base.html +++ b/templates/base.html @@ -25,18 +25,20 @@