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
projects
Domeniko Gentner 2 years ago
parent cfe4cf076a
commit 136ec736e3
  1. 1
      Pipfile
  2. 45
      Pipfile.lock
  3. 19
      README.md
  4. 41
      js/labertasche.js
  5. 19
      labertasche/blueprints/bp_comments.py
  6. 14
      labertasche/helper/__init__.py
  7. 10
      labertasche/mail/__init__.py
  8. 2
      labertasche/models/t_comments.py
  9. 4
      server.py

@ -15,6 +15,7 @@ flask-login = "*"
sqlalchemy = "*"
requests = "*"
py3-validate-email = "*"
flask-migrate = "*"
[requires]
python_version = "3.8"

45
Pipfile.lock generated

@ -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"

@ -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:
</div>
```
This is the recommended element on each top post if you want to utilize replies:
```
<div class="">
<a href="#labertasche-comment-section"
onclick="labertasche_reply_to({{.comment_id}}, labertasche_reply_callback);">
reply
</a>
</div>
```
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

@ -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){

@ -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)

@ -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

@ -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

@ -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)

@ -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']}})

Loading…
Cancel
Save