* Separated routes into single files for better management * Added sql for streamlined example data * Streamlined function names, streamlined template file names - Removed 2 functions with unnecessary code * Added javascript for project overview * New library: tippy.js for tooltips * Added new configuration options for projects * Added cookie secure setting to flaskprojects
parent
edd9ce7585
commit
fdec8f74c8
30 changed files with 833 additions and 399 deletions
@ -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 |
||||
|
@ -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"}]} |
@ -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" |
||||
} |
||||
] |
||||
} |
||||
{"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"}]} |
@ -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) |
||||
; |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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('/<project>/') |
||||
@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/<int:location_id>', 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/<int:location_id>', 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/<int:id_email>') |
||||
@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/<int:id_email>') |
||||
@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/<int:location_id>/<int:comment_id>', 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/<int:location_id>/<int:comment_id>', 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/<int:location_id>/<int:comment_id>', 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/<int:location_id>/<int:comment_id>', 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}") |
@ -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 |
@ -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('<project>/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") |
||||
|
@ -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('<project>/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) |
@ -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('/<project>/') |
||||
@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) |
||||
|
@ -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('<project>/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") |
||||
|
@ -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 |
@ -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/<int:comment_id>', 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/<int:comment_id>', 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/<int:comment_id>', 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/<int:comment_id>', 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) |
||||
|
||||
|
@ -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/<int:id_email>') |
||||
@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/<int:id_email>') |
||||
@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) |
||||
|
||||
|
@ -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/<name>', 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/<project>', 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) |
@ -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/<int:location_id>/<int:comment_id>', 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/<int:location_id>/<int:comment_id>', 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}") |
@ -1,78 +0,0 @@ |
||||
{% extends "base.html" %} |
||||
{% block main %} |
||||
<div style="min-height: 100vh;" class="container bg-deepmatte p-6 brdr-yayellow"> |
||||
<h1 class="title has-text-white has-text-centered">Review Spam</h1> |
||||
<div class="field"> |
||||
<form method="post" action="/dashboard/review-spam/"> |
||||
<div class="control"> |
||||
<div class="select"> |
||||
<select name="selected_location" onchange="this.form.submit();"> |
||||
<option value="-1">Select the article</option> |
||||
{% for each in locations %} |
||||
{% if selected is defined %} |
||||
{% if selected | string() == each.id_location | string() %} |
||||
<option selected="selected" value="{{ each.id_location }}">{{ each.location }}</option> |
||||
{% else %} |
||||
<option value="{{ each.id_location }}">{{ each.location }}</option> |
||||
{% endif %} |
||||
{% else %} |
||||
<option value="{{ each.id_location }}">{{ each.location }}</option> |
||||
{% endif %} |
||||
{% endfor %} |
||||
</select> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
<div> |
||||
{% if spam_comments is defined %} |
||||
{% for comment in spam_comments %} |
||||
<article> |
||||
<div class="media mb-5 brdr-yayellow my-shadow-subtle bg-compliment"> |
||||
<figure class="media-left ml-0 mb-0"> |
||||
<p class="image is-128x128"> |
||||
<img alt="gravatar portrait" src="https://gravatar.com/avatar/{{comment.gravatar}}?size=128"> |
||||
</p> |
||||
</figure> |
||||
<div class="media-content"> |
||||
<div class="content mr-5 mt-2"> |
||||
<a title="comment ID" id="comment_{{comment.comments_id}}" href="#comment_{{comment.comments_id}}">#{{comment.comments_id}}</a> |
||||
Posted by <span class="fg-yellow">{{comment.email}}</span> |
||||
on <small class="fg-yellow">{{comment.created_on}}</small> |
||||
spam score: <span title="The higher this is, the less likely it is spam" class="fg-yellow">{{ comment.spam_score }}</span> |
||||
<br><br> |
||||
<span class="mt-5"> |
||||
{{comment.content}} |
||||
</span> |
||||
</div> |
||||
<nav class="level is-mobile"> |
||||
<a title="Delete this comment" |
||||
class="level-item" |
||||
href="/dashboard/delete-comment/{{ selected }}/{{ comment.comments_id }}"> |
||||
<span class="icon is-medium"><i class="mdi mdi-24px mdi-trash-can"></i></span> |
||||
</a> |
||||
<a title="Delete comment and block mail address" |
||||
class="level-item" |
||||
href="/dashboard/block-mail/{{ selected }}/{{ comment.comments_id }}"> |
||||
<span class="icon is-medium"><i class="mdi mdi-24px mdi-shield-lock"></i></span> |
||||
</a> |
||||
<a title="Publish this comment, don't allow mail" |
||||
class="level-item" |
||||
href="/dashboard/allow-comment/{{ selected }}/{{ comment.comments_id }}"> |
||||
<span class="icon is-medium"><i class="mdi mdi-24px mdi-check"></i></span> |
||||
</a> |
||||
<a title="Allow email to bypass spam detection and allow comment" |
||||
class="level-item" |
||||
href="/dashboard/allow-user/{{ selected }}/{{ comment.comments_id }}"> |
||||
<span class="icon is-medium"><i class="mdi mdi-24px mdi-check-all"></i></span> |
||||
</a> |
||||
</nav> |
||||
</div> |
||||
</div> |
||||
</article> |
||||
{% endfor %} |
||||
{% endif %} |
||||
</div> |
||||
</div> |
||||
|
||||
{% endblock %} |
Loading…
Reference in new issue