splent-feature-comments 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. splent_feature_comments-0.1.0/MANIFEST.in +8 -0
  2. splent_feature_comments-0.1.0/PKG-INFO +5 -0
  3. splent_feature_comments-0.1.0/pyproject.toml +52 -0
  4. splent_feature_comments-0.1.0/setup.cfg +4 -0
  5. splent_feature_comments-0.1.0/src/splent_feature_comments.egg-info/PKG-INFO +5 -0
  6. splent_feature_comments-0.1.0/src/splent_feature_comments.egg-info/SOURCES.txt +26 -0
  7. splent_feature_comments-0.1.0/src/splent_feature_comments.egg-info/dependency_links.txt +1 -0
  8. splent_feature_comments-0.1.0/src/splent_feature_comments.egg-info/top_level.txt +1 -0
  9. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/__init__.py +19 -0
  10. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/assets/dist/splent_feature_comments.bundle.js +20 -0
  11. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/assets/dist/splent_feature_comments.bundle.js.map +1 -0
  12. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/commands.py +21 -0
  13. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/config.py +19 -0
  14. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/forms.py +6 -0
  15. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/hooks.py +38 -0
  16. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/migrations/alembic.ini +36 -0
  17. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/migrations/env.py +9 -0
  18. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/migrations/script.py.mako +24 -0
  19. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/migrations/versions/comments0001_initial.py +40 -0
  20. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/models.py +25 -0
  21. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/repositories.py +7 -0
  22. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/routes.py +78 -0
  23. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/seeders.py +10 -0
  24. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/services.py +25 -0
  25. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/signals.py +12 -0
  26. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/templates/comments/admin/list.html +83 -0
  27. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/templates/comments/index.html +10 -0
  28. splent_feature_comments-0.1.0/src/splent_io/splent_feature_comments/templates/comments/thread.html +93 -0
@@ -0,0 +1,8 @@
1
+ # Compiled frontend bundles
2
+ recursive-include src/splent_io/splent_feature_comments/assets/dist *.js *.css *.map
3
+
4
+ # Jinja templates (feature views + hook fragments)
5
+ recursive-include src/splent_io/splent_feature_comments/templates *.html
6
+
7
+ # Alembic migration config and scripts
8
+ recursive-include src/splent_io/splent_feature_comments/migrations *.py *.ini *.mako
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_comments
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80.3.1", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "splent_feature_comments"
7
+ version = "0.1.0"
8
+ readme = "README.md"
9
+ requires-python = ">=3.13"
10
+
11
+ dependencies = [
12
+
13
+ ]
14
+
15
+ [tool.setuptools]
16
+ package-dir = { "" = "src" }
17
+ include-package-data = true
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+ exclude = ["*.tests", "*.tests.*"]
22
+
23
+ [tool.splent]
24
+ cli_version = "1.11.0"
25
+
26
+ # ── Feature Contract (auto-generated) ────────────────────────────────────────
27
+ # Do not edit manually — re-run `splent feature:contract --write` to refresh.
28
+ [tool.splent.contract]
29
+ description = "comments feature"
30
+
31
+ [tool.splent.contract.provides]
32
+ routes = ["/admin/comments", "/admin/comments/<int:comment_id>/approve", "/admin/comments/<int:comment_id>/delete", "/comments/<int:post_id>"]
33
+ blueprints = []
34
+ models = ["Comment"]
35
+ commands = ["hello"]
36
+ hooks = ["layout.authenticated_sidebar", "post.comments"]
37
+ services = ["CommentsService"]
38
+ signals = ["comment-created", "comment-submitting"]
39
+ translations = []
40
+ docker = []
41
+
42
+ [tool.splent.contract.requires]
43
+ features = []
44
+ env_vars = []
45
+ signals = []
46
+
47
+ [tool.splent.contract.extensible]
48
+ services = ["CommentsService"]
49
+ templates = ["comments/admin/list.html", "comments/index.html", "comments/thread.html"]
50
+ models = ["Comment"]
51
+ hooks = ["layout.authenticated_sidebar", "post.comments"]
52
+ routes = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_comments
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
@@ -0,0 +1,26 @@
1
+ MANIFEST.in
2
+ pyproject.toml
3
+ src/splent_feature_comments.egg-info/PKG-INFO
4
+ src/splent_feature_comments.egg-info/SOURCES.txt
5
+ src/splent_feature_comments.egg-info/dependency_links.txt
6
+ src/splent_feature_comments.egg-info/top_level.txt
7
+ src/splent_io/splent_feature_comments/__init__.py
8
+ src/splent_io/splent_feature_comments/commands.py
9
+ src/splent_io/splent_feature_comments/config.py
10
+ src/splent_io/splent_feature_comments/forms.py
11
+ src/splent_io/splent_feature_comments/hooks.py
12
+ src/splent_io/splent_feature_comments/models.py
13
+ src/splent_io/splent_feature_comments/repositories.py
14
+ src/splent_io/splent_feature_comments/routes.py
15
+ src/splent_io/splent_feature_comments/seeders.py
16
+ src/splent_io/splent_feature_comments/services.py
17
+ src/splent_io/splent_feature_comments/signals.py
18
+ src/splent_io/splent_feature_comments/assets/dist/splent_feature_comments.bundle.js
19
+ src/splent_io/splent_feature_comments/assets/dist/splent_feature_comments.bundle.js.map
20
+ src/splent_io/splent_feature_comments/migrations/alembic.ini
21
+ src/splent_io/splent_feature_comments/migrations/env.py
22
+ src/splent_io/splent_feature_comments/migrations/script.py.mako
23
+ src/splent_io/splent_feature_comments/migrations/versions/comments0001_initial.py
24
+ src/splent_io/splent_feature_comments/templates/comments/index.html
25
+ src/splent_io/splent_feature_comments/templates/comments/thread.html
26
+ src/splent_io/splent_feature_comments/templates/comments/admin/list.html
@@ -0,0 +1,19 @@
1
+ from splent_framework.blueprints.base_blueprint import create_blueprint
2
+ from splent_framework.services.service_locator import register_service
3
+
4
+ from splent_io.splent_feature_comments.services import CommentsService
5
+
6
+ comments_bp = create_blueprint(__name__)
7
+
8
+
9
+ def init_feature(app):
10
+ from splent_framework.assets.asset_registry import register_asset
11
+
12
+ register_service(app, "CommentsService", CommentsService)
13
+ register_asset(
14
+ "css", "comments.assets", order=100, subfolder="css", filename="comments.css"
15
+ )
16
+
17
+
18
+ def inject_context_vars(app):
19
+ return {}
@@ -0,0 +1,20 @@
1
+ /******/ (() => { // webpackBootstrap
2
+ /*!*********************************************************************************************!*\
3
+ !*** ../splent_feature_comments/src/splent_io/splent_feature_comments/assets/js/scripts.js ***!
4
+ \*********************************************************************************************/
5
+ // Entry point for splent_feature_comments frontend assets.
6
+ // Add your JavaScript here. Webpack compiles this into assets/dist/splent_feature_comments.bundle.js
7
+ //
8
+ // To load the compiled bundle in the product layout, register it in hooks.py:
9
+ //
10
+ // from splent_framework.hooks.template_hooks import register_template_hook
11
+ // from flask import url_for
12
+ //
13
+ // def comments_scripts():
14
+ // return '<script src="' + url_for("comments.assets", subfolder="dist", filename="splent_feature_comments.bundle.js") + '"></script>'
15
+ //
16
+ // register_template_hook("layout.scripts", comments_scripts)
17
+
18
+ /******/ })()
19
+ ;
20
+ //# sourceMappingURL=splent_feature_comments.bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splent_feature_comments.bundle.js","mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://diversolab_app/../splent_feature_comments/src/splent_io/splent_feature_comments/assets/js/scripts.js"],"sourcesContent":["// Entry point for splent_feature_comments frontend assets.\n// Add your JavaScript here. Webpack compiles this into assets/dist/splent_feature_comments.bundle.js\n//\n// To load the compiled bundle in the product layout, register it in hooks.py:\n//\n// from splent_framework.hooks.template_hooks import register_template_hook\n// from flask import url_for\n//\n// def comments_scripts():\n// return '<script src=\"' + url_for(\"comments.assets\", subfolder=\"dist\", filename=\"splent_feature_comments.bundle.js\") + '\"></script>'\n//\n// register_template_hook(\"layout.scripts\", comments_scripts)\n"],"names":[],"sourceRoot":""}
@@ -0,0 +1,21 @@
1
+ """
2
+ CLI commands contributed by splent_feature_comments.
3
+
4
+ These commands are auto-discovered by the framework and exposed in the
5
+ SPLENT CLI under the ``feature:comments`` group.
6
+
7
+ Usage::
8
+
9
+ splent feature:comments hello
10
+ """
11
+
12
+ import click
13
+
14
+
15
+ @click.command("hello")
16
+ def hello():
17
+ """Example command — replace with your own."""
18
+ click.echo(" Hello from splent_feature_comments!")
19
+
20
+
21
+ cli_commands = [hello]
@@ -0,0 +1,19 @@
1
+ """
2
+ comments feature configuration.
3
+
4
+ Injects environment variables into Flask app.config.
5
+ Add your feature's env vars here so the framework can track them.
6
+
7
+ To regenerate from source code: splent feature:inject-config splent_feature_comments
8
+ """
9
+
10
+ import os # noqa: F401 — used when adding env vars below
11
+
12
+
13
+ def inject_config(app):
14
+ app.config.update(
15
+ {
16
+ # Add your feature's env vars here, e.g.:
17
+ # "MY_VAR": os.getenv("MY_VAR", "default_value"),
18
+ }
19
+ )
@@ -0,0 +1,6 @@
1
+ from flask_wtf import FlaskForm
2
+ from wtforms import SubmitField
3
+
4
+
5
+ class SplentFeatureCommentsForm(FlaskForm):
6
+ submit = SubmitField("Save splent_feature_comments")
@@ -0,0 +1,38 @@
1
+ from flask import render_template, request, url_for
2
+
3
+ from splent_framework.hooks.template_hooks import register_template_hook
4
+ from splent_framework.services.service_locator import service_proxy
5
+
6
+
7
+ def post_comments(post):
8
+ """Rendered into the post detail through the 'post.comments' hook.
9
+
10
+ Receives the current post and renders the approved comments plus the
11
+ submission form (with a Turnstile widget if cloudflare is installed).
12
+ """
13
+ comments = service_proxy("CommentsService").approved_for(post.id)
14
+ return render_template("comments/thread.html", post=post, comments=comments)
15
+
16
+
17
+ def comments_sidebar_link():
18
+ active = (
19
+ "active"
20
+ if request.endpoint and request.endpoint.startswith("comments.admin")
21
+ else ""
22
+ )
23
+ pending = 0
24
+ try:
25
+ pending = len(service_proxy("CommentsService").pending())
26
+ except Exception:
27
+ pass
28
+ badge = f' <span class="badge bg-danger">{pending}</span>' if pending else ""
29
+ return (
30
+ f'<li class="sidebar-item {active}">'
31
+ f'<a class="sidebar-link" href="{url_for("comments.admin_index")}">'
32
+ '<i class="align-middle" data-feather="message-square"></i> '
33
+ f'<span class="align-middle">Comments</span>{badge}</a></li>'
34
+ )
35
+
36
+
37
+ register_template_hook("post.comments", post_comments)
38
+ register_template_hook("layout.authenticated_sidebar", comments_sidebar_link)
@@ -0,0 +1,36 @@
1
+ [alembic]
2
+ script_location = migrations
3
+
4
+ [loggers]
5
+ keys = root,sqlalchemy,alembic
6
+
7
+ [handlers]
8
+ keys = console
9
+
10
+ [formatters]
11
+ keys = generic
12
+
13
+ [logger_root]
14
+ level = WARN
15
+ handlers = console
16
+ qualname =
17
+
18
+ [logger_sqlalchemy]
19
+ level = WARN
20
+ handlers =
21
+ qualname = sqlalchemy.engine
22
+
23
+ [logger_alembic]
24
+ level = INFO
25
+ handlers =
26
+ qualname = alembic
27
+
28
+ [handler_console]
29
+ class = StreamHandler
30
+ args = (sys.stderr,)
31
+ level = NOTSET
32
+ formatter = generic
33
+
34
+ [formatter_generic]
35
+ format = %(levelname)-5.5s [%(name)s] %(message)s
36
+ datefmt = %H:%M:%S
@@ -0,0 +1,9 @@
1
+ """Alembic migration environment for splent_feature_comments."""
2
+
3
+ from splent_io.splent_feature_comments import models # noqa
4
+ from splent_framework.migrations.feature_env import run_feature_migrations
5
+
6
+ FEATURE_NAME = "splent_feature_comments"
7
+ FEATURE_TABLES = set()
8
+
9
+ run_feature_migrations(FEATURE_NAME, FEATURE_TABLES)
@@ -0,0 +1,24 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade():
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade():
24
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,40 @@
1
+ """comment table.
2
+
3
+ Revision ID: comments0001
4
+ Revises:
5
+ """
6
+
7
+ import sqlalchemy as sa
8
+ from alembic import op
9
+
10
+ revision = "comments0001"
11
+ down_revision = None
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade():
17
+ op.create_table(
18
+ "comment",
19
+ sa.Column("id", sa.Integer(), nullable=False),
20
+ sa.Column("post_id", sa.Integer(), nullable=False),
21
+ sa.Column("author_name", sa.String(length=128), nullable=False),
22
+ sa.Column("author_email", sa.String(length=255), nullable=True),
23
+ sa.Column("content", sa.Text(), nullable=False),
24
+ sa.Column("created_at", sa.DateTime(), nullable=True),
25
+ sa.Column("approved", sa.Boolean(), nullable=True),
26
+ sa.ForeignKeyConstraint(["post_id"], ["post.id"], ondelete="CASCADE"),
27
+ sa.PrimaryKeyConstraint("id"),
28
+ )
29
+ op.create_index(op.f("ix_comment_post_id"), "comment", ["post_id"], unique=False)
30
+ op.create_index(
31
+ op.f("ix_comment_created_at"), "comment", ["created_at"], unique=False
32
+ )
33
+ op.create_index(op.f("ix_comment_approved"), "comment", ["approved"], unique=False)
34
+
35
+
36
+ def downgrade():
37
+ op.drop_index(op.f("ix_comment_approved"), table_name="comment")
38
+ op.drop_index(op.f("ix_comment_created_at"), table_name="comment")
39
+ op.drop_index(op.f("ix_comment_post_id"), table_name="comment")
40
+ op.drop_table("comment")
@@ -0,0 +1,25 @@
1
+ from datetime import datetime
2
+
3
+ from splent_framework.db import db
4
+
5
+
6
+ class Comment(db.Model):
7
+ """A comment on a post. New comments start unapproved (moderation)."""
8
+
9
+ __tablename__ = "comment"
10
+
11
+ id = db.Column(db.Integer, primary_key=True)
12
+ post_id = db.Column(
13
+ db.Integer,
14
+ db.ForeignKey("post.id", ondelete="CASCADE"),
15
+ nullable=False,
16
+ index=True,
17
+ )
18
+ author_name = db.Column(db.String(128), nullable=False)
19
+ author_email = db.Column(db.String(255), default="")
20
+ content = db.Column(db.Text, nullable=False)
21
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
22
+ approved = db.Column(db.Boolean, default=False, index=True)
23
+
24
+ def __repr__(self):
25
+ return f"Comment<{self.id} on post {self.post_id}>"
@@ -0,0 +1,7 @@
1
+ from splent_io.splent_feature_comments.models import Comment
2
+ from splent_framework.repositories.BaseRepository import BaseRepository
3
+
4
+
5
+ class CommentsRepository(BaseRepository):
6
+ def __init__(self):
7
+ super().__init__(Comment)
@@ -0,0 +1,78 @@
1
+ from flask import flash, redirect, render_template, request, url_for
2
+ from flask_login import login_required
3
+
4
+ from splent_io.splent_feature_comments import comments_bp
5
+ from splent_io.splent_feature_comments.models import Comment
6
+ from splent_io.splent_feature_comments.signals import (
7
+ comment_created,
8
+ comment_submitting,
9
+ )
10
+ from splent_framework.db import db
11
+ from splent_framework.services.service_locator import service_proxy
12
+
13
+ comments_service = service_proxy("CommentsService")
14
+
15
+
16
+ # =====================================================================
17
+ # PUBLIC — submit a comment (rendered into the post via the hook)
18
+ # =====================================================================
19
+ @comments_bp.route("/comments/<int:post_id>", methods=["POST"])
20
+ def create(post_id):
21
+ # Let any listener (a captcha provider, a spam filter…) veto the submission.
22
+ # comments knows nothing about captchas — they connect to this signal.
23
+ results = comment_submitting.send(
24
+ None, post_id=post_id, form=request.form, remoteip=request.remote_addr
25
+ )
26
+ if any(value is False for _, value in results):
27
+ flash("Spam check failed — please try again.", "danger")
28
+ return redirect(request.referrer or url_for("post.index"))
29
+
30
+ name = (request.form.get("author_name") or "").strip()
31
+ content = (request.form.get("content") or "").strip()
32
+ if not (name and content):
33
+ flash("Name and comment are required.", "danger")
34
+ return redirect(request.referrer or url_for("post.index"))
35
+
36
+ comment = Comment(
37
+ post_id=post_id,
38
+ author_name=name,
39
+ author_email=(request.form.get("author_email") or "").strip(),
40
+ content=content,
41
+ approved=False,
42
+ )
43
+ db.session.add(comment)
44
+ db.session.commit()
45
+ comment_created.send(None, comment=comment)
46
+ flash("Your comment was submitted and is awaiting moderation.", "success")
47
+ return redirect(request.referrer or url_for("post.index"))
48
+
49
+
50
+ # =====================================================================
51
+ # ADMIN — moderation
52
+ # =====================================================================
53
+ @comments_bp.route("/admin/comments", methods=["GET"])
54
+ @login_required
55
+ def admin_index():
56
+ return render_template(
57
+ "comments/admin/list.html", comments=comments_service.all_comments()
58
+ )
59
+
60
+
61
+ @comments_bp.route("/admin/comments/<int:comment_id>/approve", methods=["POST"])
62
+ @login_required
63
+ def admin_approve(comment_id):
64
+ comment = Comment.query.get_or_404(comment_id)
65
+ comment.approved = True
66
+ db.session.commit()
67
+ flash("Comment approved.", "success")
68
+ return redirect(url_for("comments.admin_index"))
69
+
70
+
71
+ @comments_bp.route("/admin/comments/<int:comment_id>/delete", methods=["POST"])
72
+ @login_required
73
+ def admin_delete(comment_id):
74
+ comment = Comment.query.get_or_404(comment_id)
75
+ db.session.delete(comment)
76
+ db.session.commit()
77
+ flash("Comment removed.", "success")
78
+ return redirect(url_for("comments.admin_index"))
@@ -0,0 +1,10 @@
1
+ from splent_framework.seeders.BaseSeeder import BaseSeeder
2
+
3
+
4
+ class SplentFeatureCommentsSeeder(BaseSeeder):
5
+ def run(self):
6
+ data = [
7
+ # Create any Model object you want to make seed
8
+ ]
9
+
10
+ self.seed(data)
@@ -0,0 +1,25 @@
1
+ from splent_io.splent_feature_comments.models import Comment
2
+ from splent_io.splent_feature_comments.repositories import CommentsRepository
3
+ from splent_framework.services.BaseService import BaseService
4
+
5
+
6
+ class CommentsService(BaseService):
7
+ def __init__(self):
8
+ super().__init__(CommentsRepository())
9
+
10
+ def approved_for(self, post_id):
11
+ return (
12
+ Comment.query.filter_by(post_id=post_id, approved=True)
13
+ .order_by(Comment.created_at.asc())
14
+ .all()
15
+ )
16
+
17
+ def pending(self):
18
+ return (
19
+ Comment.query.filter_by(approved=False)
20
+ .order_by(Comment.created_at.desc())
21
+ .all()
22
+ )
23
+
24
+ def all_comments(self):
25
+ return Comment.query.order_by(Comment.created_at.desc()).all()
@@ -0,0 +1,12 @@
1
+ """Signals emitted by the comments feature.
2
+
3
+ comments stays decoupled from anti-spam: it emits ``comment-submitting`` and any
4
+ listener (a captcha provider, a spam filter…) may return False to veto the
5
+ submission. ``comment-created`` fires after a comment is stored, for things like
6
+ notifications or counters.
7
+ """
8
+
9
+ from splent_framework.signals.signal_utils import define_signal
10
+
11
+ comment_submitting = define_signal("comment-submitting", "splent_feature_comments")
12
+ comment_created = define_signal("comment-created", "splent_feature_comments")
@@ -0,0 +1,83 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}{{ _('Comments') }}{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container section">
7
+
8
+ <header class="page-header">
9
+ <h1>{{ _('Comments') }}</h1>
10
+ <p class="text-muted">{{ _('Moderate comments submitted by visitors.') }}</p>
11
+ </header>
12
+
13
+ {# Flash messages #}
14
+ {% with messages = get_flashed_messages(with_categories=true) %}
15
+ {% if messages %}
16
+ <div class="admin-flashes">
17
+ {% for category, message in messages %}
18
+ <div class="alert alert-{{ category }}">{{ message }}</div>
19
+ {% endfor %}
20
+ </div>
21
+ {% endif %}
22
+ {% endwith %}
23
+
24
+ <table class="table">
25
+ <thead>
26
+ <tr>
27
+ <th>{{ _('Author') }}</th>
28
+ <th>{{ _('Comment') }}</th>
29
+ <th>{{ _('Date') }}</th>
30
+ <th>{{ _('Status') }}</th>
31
+ <th>{{ _('Actions') }}</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ {% for c in comments %}
36
+ <tr>
37
+ <td>
38
+ <strong>{{ c.author_name }}</strong>
39
+ {% if c.author_email %}
40
+ <br><span class="text-muted">{{ c.author_email }}</span>
41
+ {% endif %}
42
+ </td>
43
+ <td>{{ c.content|truncate(100, true) }}</td>
44
+ <td>{{ c.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
45
+ <td>
46
+ {% if c.approved %}
47
+ <span class="badge bg-success">{{ _('approved') }}</span>
48
+ {% else %}
49
+ <span class="badge bg-warning text-dark">{{ _('pending') }}</span>
50
+ {% endif %}
51
+ </td>
52
+ <td class="comment-actions">
53
+ {% if not c.approved %}
54
+ <form method="POST"
55
+ action="{{ url_for('comments.admin_approve', comment_id=c.id) }}"
56
+ class="d-inline">
57
+ <button type="submit" class="btn btn-sm btn-success">
58
+ {{ _('Approve') }}
59
+ </button>
60
+ </form>
61
+ {% endif %}
62
+ <form method="POST"
63
+ action="{{ url_for('comments.admin_delete', comment_id=c.id) }}"
64
+ class="d-inline"
65
+ onsubmit="return confirm('{{ _('Delete this comment?') }}');">
66
+ <button type="submit" class="btn btn-sm btn-danger">
67
+ {{ _('Delete') }}
68
+ </button>
69
+ </form>
70
+ </td>
71
+ </tr>
72
+ {% else %}
73
+ <tr>
74
+ <td colspan="5" class="text-muted text-center">
75
+ {{ _('No comments yet.') }}
76
+ </td>
77
+ </tr>
78
+ {% endfor %}
79
+ </tbody>
80
+ </table>
81
+
82
+ </div>
83
+ {% endblock %}
@@ -0,0 +1,10 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}Comments{% endblock %}
4
+
5
+ {% block content %}
6
+
7
+ {% endblock %}
8
+
9
+ {% block scripts %}
10
+ {% endblock %}
@@ -0,0 +1,93 @@
1
+ {# ------------------------------------------------------------------ #}
2
+ {# Comments thread partial. #}
3
+ {# Rendered INTO the post detail page (NOT a full page, no extends). #}
4
+ {# Context: `post`, `comments` (approved, oldest first). #}
5
+ {# ------------------------------------------------------------------ #}
6
+ <section class="section comments-thread">
7
+ <div class="container">
8
+
9
+ <h2 class="comments-thread__heading">
10
+ {{ _('Comments') }} ({{ comments|length }})
11
+ </h2>
12
+
13
+ {# Flash messages #}
14
+ {% with messages = get_flashed_messages(with_categories=true) %}
15
+ {% if messages %}
16
+ <div class="comments-thread__flashes">
17
+ {% for category, message in messages %}
18
+ <div class="alert alert-{{ category }}">{{ message }}</div>
19
+ {% endfor %}
20
+ </div>
21
+ {% endif %}
22
+ {% endwith %}
23
+
24
+ {# Comment list #}
25
+ <ul class="comments-list">
26
+ {% for c in comments %}
27
+ <li class="comment">
28
+ <div class="comment__header">
29
+ <strong class="comment__author">{{ c.author_name }}</strong>
30
+ <time class="comment__date text-muted"
31
+ datetime="{{ c.created_at.isoformat() }}">
32
+ {{ c.created_at.strftime('%d %b %Y') }}
33
+ </time>
34
+ </div>
35
+ <p class="comment__content">{{ c.content }}</p>
36
+ </li>
37
+ {% else %}
38
+ <li class="comment comment--empty text-muted">
39
+ {{ _('No comments yet. Be the first!') }}
40
+ </li>
41
+ {% endfor %}
42
+ </ul>
43
+
44
+ {# Comment form — only when the post still accepts comments #}
45
+ {% if post.comment_status != 'closed' %}
46
+ <form class="comment-form"
47
+ method="POST"
48
+ action="{{ url_for('comments.create', post_id=post.id) }}">
49
+
50
+ <div class="form-group">
51
+ <label for="comment-author-name">{{ _('Name') }}</label>
52
+ <input type="text"
53
+ id="comment-author-name"
54
+ name="author_name"
55
+ class="form-control"
56
+ required>
57
+ </div>
58
+
59
+ <div class="form-group">
60
+ <label for="comment-author-email">{{ _('Email (optional)') }}</label>
61
+ <input type="email"
62
+ id="comment-author-email"
63
+ name="author_email"
64
+ class="form-control">
65
+ </div>
66
+
67
+ <div class="form-group">
68
+ <label for="comment-content">{{ _('Comment') }}</label>
69
+ <textarea id="comment-content"
70
+ name="content"
71
+ class="form-control"
72
+ rows="4"
73
+ required></textarea>
74
+ </div>
75
+
76
+ {# Captcha widget (cloudflare / recaptcha) — empty when none installed #}
77
+ {% if captcha_widget is defined %}
78
+ {{ captcha_script() }}
79
+ {{ captcha_widget() }}
80
+ {% endif %}
81
+
82
+ <button type="submit" class="btn btn-primary">
83
+ {{ _('Post comment') }}
84
+ </button>
85
+
86
+ <p class="comment-form__note text-muted">
87
+ {{ _('Comments are moderated before appearing.') }}
88
+ </p>
89
+ </form>
90
+ {% endif %}
91
+
92
+ </div>
93
+ </section>