From 136ec736e3eef883652ee5af1179357b4588194f Mon Sep 17 00:00:00 2001 From: Domeniko Gentner Date: Wed, 2 Dec 2020 13:51:31 +0100 Subject: [PATCH] Implemented replies The database will need to be rebuilt, because of a previous mistake of code completion setting a field to boolean instead of int. Added flaskmigrate as dependency New Javascript function for replies, to be able to switch a reply on and off during commenting To use replies, one will need to add another hugo range block, see docs for details --- Pipfile | 1 + Pipfile.lock | 45 +++++++++++++++++++++++++-- README.md | 19 +++++++++-- js/labertasche.js | 41 +++++++++++++++--------- labertasche/blueprints/bp_comments.py | 19 +++++++++-- labertasche/helper/__init__.py | 14 ++++----- labertasche/mail/__init__.py | 10 ++---- labertasche/models/t_comments.py | 2 +- server.py | 4 +++ 9 files changed, 118 insertions(+), 37 deletions(-) diff --git a/Pipfile b/Pipfile index 1ac692e..1210db9 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ flask-login = "*" sqlalchemy = "*" requests = "*" py3-validate-email = "*" +flask-migrate = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 8c7a9d0..7b9ea1d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "57134ef6f8a30aa46c1ab6263e62e14edbb27d6df2911fc6b2140dde8c49d27c" + "sha256": "bda9276f38dcb49704cadb2f9097ecdfa1dafdd4e4b3d6666dfcb92d24f0ea57" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,13 @@ ] }, "default": { + "alembic": { + "hashes": [ + "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c", + "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245" + ], + "version": "==1.4.3" + }, "antispam": { "hashes": [ "sha256:e188b424ea9b76c408a592a5ff60eb1280f45f26b404db4d5e96123f485de39b" @@ -82,6 +89,14 @@ "index": "pypi", "version": "==0.5.0" }, + "flask-migrate": { + "hashes": [ + "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732", + "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee" + ], + "index": "pypi", + "version": "==2.5.3" + }, "flask-sqlalchemy": { "hashes": [ "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", @@ -111,6 +126,13 @@ ], "version": "==2.11.2" }, + "mako": { + "hashes": [ + "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27", + "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9" + ], + "version": "==1.1.3" + }, "markupsafe": { "hashes": [ "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", @@ -151,21 +173,38 @@ }, "py3-validate-email": { "hashes": [ - "sha256:3bbb264b49c0ae09afdb2738956f00b0e8dd7e079e2d079b2e9b6688de474d28" + "sha256:e5815a929c064face7b6e775f290f157ab52c1c88d56d27d031a02a185b991e3" ], "index": "pypi", - "version": "==0.2.10" + "version": "==0.2.12" + }, + "python-dateutil": { + "hashes": [ + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "version": "==2.8.1" + }, + "python-editor": { + "hashes": [ + "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", + "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", + "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" + ], + "version": "==1.0.4" }, "pyyaml": { "hashes": [ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" diff --git a/README.md b/README.md index 941d5f9..50ef115 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Once you can see the administrative page, you can start integrating it into Hugo In the project folder is a small javascript file. You will need to add this to Hugo. I suggest using Hugo's asset pipeline to integrate it into your site and merge it with your current javascript. One thing is important to know: this script only does the bare bones post request to the comment backend. -Anything else must be done by yourself, such as messages about minimum length etc. +Any frontend work must be done by yourself, such as messages about minimum length etc. But don't worry: The function is making use of a callback, where you can receive various messages with error codes and act on them. See the javascript file for an example callback. @@ -101,8 +101,12 @@ Within that template the following structure is needed: {{ if (fileExists $location ) }} {{ $dataJ := getJSON $location }} {{ range $dataJ.comments }} - + {# HTML and template codes here #} {{ end }} + {# This is to display replies to this comment, you can use them same variables #} + {{ range where $dataJ.replies "replied_to" .comment_id }} + {# HTML and template codes here for replies #} + {{end}} {{ end }} ``` @@ -128,6 +132,17 @@ Here is a base skeleton to start out: ``` +This is the recommended element on each top post if you want to utilize replies: + +``` +
+ + reply + +
+``` + Please take note of the `id` on each element, these are mandatory, as well as the function call for the `onclick` event. Again, style as needed and add more Javascript to your gusto. Make sure to implement the callback, otherwise the Javascript will crash. The `data-remote=` needs to have the URL where you host this program, as well as the path to diff --git a/js/labertasche.js b/js/labertasche.js index 6e6bcd1..74f5270 100644 --- a/js/labertasche.js +++ b/js/labertasche.js @@ -6,17 +6,15 @@ // *********************************************************************************/ /* - Callback example. - Possible messages: - - post-min-length - post-max-length - post-invalid-json - post-duplicate - post-internal-server-error - post-success - post-before-fetch +//Callback example for post. Possible messages: +// post-min-length +// post-max-length +// post-invalid-json +// post-duplicate +// post-internal-server-error +// post-success +// post-before-fetch function labertasche_callback(state) { if (state === "post-before-fetch"){ @@ -35,20 +33,33 @@ function labertasche_callback(state) } } + +// Callback for initiating and cancelling replies. +// Posstible message: 'on' and 'off' +function labertasche_reply_callback() +{ + if (state === "on"){ + } + + if (state === "off"){ + } +} + */ function labertasche_post_comment(btn, callback) { let remote = document.getElementById('labertasche-comment-section').dataset.remote; - let comment = document.getElementById('labertasche-text').value; - let mail = document.getElementById('labertasche-mail').value; + let comment = document.getElementById('labertasche-text').value.trim(); + let mail = document.getElementById('labertasche-mail').value.trim(); + let reply = document.getElementById('labertasche-replied-to'); if (mail.length <= 0 || comment.length < 40){ callback('post-min-length'); if(btn) { - btn.preventDefault(); + return false; } - return + return false; } callback('post-before-fetch'); @@ -65,7 +76,7 @@ function labertasche_post_comment(btn, callback) body: JSON.stringify({ "email": mail, "content": comment, "location": window.location.pathname, - "replied_to": null // TODO: future feature: replies? + "replied_to": reply.value }) }) .then(async function(response){ diff --git a/labertasche/blueprints/bp_comments.py b/labertasche/blueprints/bp_comments.py index 2583706..bd5a557 100644 --- a/labertasche/blueprints/bp_comments.py +++ b/labertasche/blueprints/bp_comments.py @@ -64,11 +64,21 @@ def check_and_insert_new_comment(): for key, value in smileys.items(): content = content.replace(key, value) + # Validate replied_to field is integer + replied_to = new_comment['replied_to'] + try: + if replied_to: + replied_to = int(replied_to) + + # not a valid id at all + except ValueError: + return make_response(jsonify(status="bad-reply"), 400) + # Update values new_comment.update({"content": content}) new_comment.update({"email": new_comment['email'].strip()}) new_comment.update({"location": location}) - new_comment.update({"replied_to": None}) # Not (yet?) implemented + new_comment.update({"replied_to": replied_to}) # Check mail if not sender.validate(new_comment['email']): @@ -92,6 +102,9 @@ def check_and_insert_new_comment(): if not email.is_allowed: is_spam = True if email.is_allowed: + # This forces the comment to be not spam if the address is in the allowed list, + # but the commenter will still need to confirm it to avoid brute + # force attacks against this feature is_spam = False # Look for location @@ -99,7 +112,7 @@ def check_and_insert_new_comment(): .filter(TLocation.location == new_comment['location']) if loc_query.first(): - # Set existing location id + # Location exists, set existing location id new_comment.update({'location_id': loc_query.first().id_location}) # TComments does not have this field new_comment.pop("location") @@ -143,7 +156,9 @@ def check_and_insert_new_comment(): 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("---------------------------------------------") return make_response(jsonify(status="post-internal-server-error"), 400) export_location(t_comment.location_id) diff --git a/labertasche/helper/__init__.py b/labertasche/helper/__init__.py index 7a78e9f..2cd3199 100644 --- a/labertasche/helper/__init__.py +++ b/labertasche/helper/__init__.py @@ -145,16 +145,18 @@ def export_location(location_id: int) -> bool: loc_query = db.session.query(TLocation).filter(TLocation.id_location == location_id).first() if loc_query: - print(f"has loc_query") comments = db.session.query(TComments).filter(TComments.is_spam != True) \ - .filter(TComments.is_published == True) \ - .filter(TComments.location_id == loc_query.id_location) \ - .filter(TComments.replied_to == None) + .filter(TComments.is_published == True) \ + .filter(TComments.location_id == loc_query.id_location) bundle = { - "comments": [] + "comments": [], + "replies": [] } for comment in comments: + if comment.replied_to is not None: + bundle["replies"].append(alchemy_query_to_dict(comment)) + continue bundle['comments'].append(alchemy_query_to_dict(comment)) path_loc = re_match(".*(?=/)", loc_query.location)[0] @@ -162,12 +164,10 @@ def export_location(location_id: int) -> bool: system = Settings().system out = Path(f"{system['output']}/{path_loc}.json") out = out.absolute() - print(out) folder = out.parents[0] folder.mkdir(parents=True, exist_ok=True) with out.open('w') as fp: json.dump(bundle, fp) - print(bundle) return True diff --git a/labertasche/mail/__init__.py b/labertasche/mail/__init__.py index b49b29e..3ac951f 100644 --- a/labertasche/mail/__init__.py +++ b/labertasche/mail/__init__.py @@ -14,7 +14,7 @@ from platform import system from smtplib import SMTP_SSL, SMTPHeloError, SMTPAuthenticationError, SMTPException from ssl import create_default_context from labertasche.settings import Settings -from validate_email import validate_email +from validate_email import validate_email_or_fail from secrets import token_urlsafe @@ -91,10 +91,6 @@ class mail: def validate(self, addr): # validate email - is_valid = validate_email(email_address=addr, - check_regex=True, - check_mx=False, - dns_timeout=10, - use_blacklist=True, - debug=False) + is_valid = validate_email_or_fail(email_address=addr, check_regex=True, check_mx=False, + dns_timeout=10, use_blacklist=True, debug=False) return is_valid diff --git a/labertasche/models/t_comments.py b/labertasche/models/t_comments.py index 8c8e1f9..e2a23a1 100644 --- a/labertasche/models/t_comments.py +++ b/labertasche/models/t_comments.py @@ -28,7 +28,7 @@ class TComments(db.Model): is_published = db.Column(db.Boolean, nullable=False) is_spam = db.Column(db.Boolean, nullable=False) spam_score = db.Column(db.Float, nullable=False) - replied_to = db.Column(db.Boolean, nullable=True) + replied_to = db.Column(db.Integer, ForeignKey('t_comments.comments_id'), nullable=True) confirmation = db.Column(db.Text, nullable=True) deletion = db.Column(db.Text, nullable=True) gravatar = db.Column(db.Text, nullable=True) diff --git a/server.py b/server.py index 1b93525..09c9d79 100644 --- a/server.py +++ b/server.py @@ -17,6 +17,7 @@ from labertasche.database import labertasche_db from labertasche.blueprints import bp_comments, bp_login, bp_dashboard from labertasche.helper import User from flask_login import LoginManager +from flask_migrate import Migrate # Load settings @@ -33,6 +34,9 @@ laberflask.config.update(dict( SQLALCHEMY_TRACK_MODIFICATIONS=False )) +# flask migrate +migrate = Migrate(laberflask, labertasche_db, render_as_batch=True) + # CORS CORS(laberflask, resources={r"/comments": {"origins": settings.system['blog_url']}})