Add allow_anonymous namespace setting for email-free commenting
When enabled, commenters can post with just a display name (no email required). Comments are attached to UserSurrogate instead of User. Trade-offs for anonymous commenters: - No email notifications for replies - Cannot edit their comments - Cannot log in to manage comments - No cross-site identity Works with existing moderation (hide_unless_approved) - anonymous comments are never auto-approved when moderation is enabled.
This commit is contained in:
parent
8b353fc7ec
commit
19415fff06
7 changed files with 153 additions and 53 deletions
|
|
@ -52,6 +52,7 @@ PROTECTED_ATTRIBUTES = {
|
|||
"google_analytics_id": None,
|
||||
"hide_unverified": False,
|
||||
"hide_unless_approved": False,
|
||||
"allow_anonymous": False,
|
||||
"hide_powered_by": False,
|
||||
"mathjax": False,
|
||||
"link_protection": False,
|
||||
|
|
@ -92,6 +93,8 @@ class Namespace(RBase, Base):
|
|||
hide_unverified = Column(Boolean, default=False)
|
||||
# should a node be hidden until approved by a moderator?
|
||||
hide_unless_approved = Column(Boolean, default=False)
|
||||
# allow anonymous commenting (name only, no email required)
|
||||
allow_anonymous = Column(Boolean, default=False)
|
||||
# should we hide the poweredby Remarkbox logo?
|
||||
hide_powered_by = Column(Boolean, default=False)
|
||||
# should the list of root nodes in this namespace be public or hidden?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
"""Add allow_anonymous column to namespace
|
||||
|
||||
Revision ID: 108519de76ac
|
||||
Revises: 5188e62d0afb
|
||||
Create Date: 2025-12-20 11:05:36.134829
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '108519de76ac'
|
||||
down_revision = '5188e62d0afb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('rb_namespace', sa.Column('allow_anonymous', sa.Boolean(), nullable=True, server_default='0'))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('rb_namespace', 'allow_anonymous')
|
||||
|
|
@ -39,6 +39,11 @@
|
|||
If checked, hide new comments until approved by a moderator.
|
||||
<br>
|
||||
<br>
|
||||
<label>Allow Anonymous Comments:</label>
|
||||
<input type="checkbox" name="allow-anonymous-checkbox" id="allow-anonymous-checkbox" {% if request.namespace.allow_anonymous %}checked{% endif %}></input>
|
||||
If checked, allow comments without email verification. Commenters enter a display name only. They cannot edit comments or receive reply notifications.
|
||||
<br>
|
||||
<br>
|
||||
<label>Enable Link Protection:</label>
|
||||
<input type="checkbox" name="link-protection-checkbox" id="link-protection-checkbox" {% if request.namespace.link_protection %}checked{% endif %}></input>
|
||||
If checked, replace all links in comments with <code>[link removed]</code>.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
{% if not request.user.authenticated %}
|
||||
{% if request.namespace.allow_anonymous %}
|
||||
{# Anonymous mode: show name field instead of email #}
|
||||
<input
|
||||
name = "anonymous_name"
|
||||
type = "text"
|
||||
id = "anonymous_name_input"
|
||||
class = "common-text-input"
|
||||
value = ""
|
||||
placeholder = "Name (optional)"
|
||||
maxlength = "64" />
|
||||
{% else %}
|
||||
{# Normal mode: require email #}
|
||||
<input
|
||||
name = "email2"
|
||||
type = "email"
|
||||
|
|
@ -16,16 +28,16 @@
|
|||
{% if request.email %}style="display: none;"{% endif %}
|
||||
required />
|
||||
|
||||
{% if show_whats_next %}
|
||||
{% if show_whats_next %}
|
||||
<center>
|
||||
<p class="whats-next"><b>What's next?</b> check your email to log in!</p>
|
||||
</center>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if show_whats_next_notifications %}
|
||||
{% if show_whats_next_notifications %}
|
||||
<center>
|
||||
<p class="whats-next"><b>What's next?</b> verify your email address for reply notifications!</p>
|
||||
</center>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ def namespace_settings(request):
|
|||
)
|
||||
|
||||
hide_unless_approved_checkbox = p.get("hide-unless-approved-checkbox", "off")
|
||||
allow_anonymous_checkbox = p.get("allow-anonymous-checkbox", "off")
|
||||
link_protection_checkbox = p.get("link-protection-checkbox", "off")
|
||||
reverse_order_checkbox = p.get("reverse-order-checkbox", "off")
|
||||
group_conversations_checkbox = p.get("group-conversations-checkbox", "off")
|
||||
|
|
@ -68,6 +69,7 @@ def namespace_settings(request):
|
|||
hide_powered_by_checkbox = p.get("hide-powered-by-checkbox", "off")
|
||||
|
||||
hide_unless_approved = checkbox_to_bool(hide_unless_approved_checkbox)
|
||||
allow_anonymous = checkbox_to_bool(allow_anonymous_checkbox)
|
||||
link_protection = checkbox_to_bool(link_protection_checkbox)
|
||||
reverse_order = checkbox_to_bool(reverse_order_checkbox)
|
||||
group_conversations = checkbox_to_bool(group_conversations_checkbox)
|
||||
|
|
@ -146,6 +148,17 @@ def namespace_settings(request):
|
|||
)
|
||||
)
|
||||
|
||||
if allow_anonymous != request.namespace.allow_anonymous:
|
||||
request.namespace.allow_anonymous = allow_anonymous
|
||||
request.session.flash(
|
||||
(
|
||||
"You turned {} allow_anonymous".format(
|
||||
allow_anonymous_checkbox
|
||||
),
|
||||
"success",
|
||||
)
|
||||
)
|
||||
|
||||
if link_protection != request.namespace.link_protection:
|
||||
request.namespace.link_protection = link_protection
|
||||
request.session.flash(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from pyramid.csrf import check_csrf_token
|
|||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from remarkbox.models import create_root_node
|
||||
from remarkbox.models import create_root_node, get_or_create_user_surrogate_by_name
|
||||
|
||||
from . import get_referer_or_home, get_node_route_uri, set_node_to_pending_in_session
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ def new_thread(request):
|
|||
"""Display new page and handle posting of form."""
|
||||
thread_title = request.params.get("thread_title", "")
|
||||
thread_data = request.params.get("thread_data", "")
|
||||
anonymous_name = request.params.get("anonymous_name", "").strip()
|
||||
|
||||
if request.spam:
|
||||
return request.spam
|
||||
|
|
@ -32,59 +33,76 @@ def new_thread(request):
|
|||
if thread_title and thread_data:
|
||||
# handle the submitted form new/create form.
|
||||
|
||||
if request.user is None:
|
||||
# Handle anonymous mode vs regular mode
|
||||
user_surrogate = None
|
||||
if request.namespace.allow_anonymous and not request.user:
|
||||
# Anonymous mode: create or get a surrogate
|
||||
if not anonymous_name:
|
||||
anonymous_name = "Anonymous"
|
||||
user_surrogate = get_or_create_user_surrogate_by_name(
|
||||
request.dbsession, anonymous_name, request.namespace
|
||||
)
|
||||
elif request.user is None:
|
||||
# Regular mode: require email/user
|
||||
request.session.flash(
|
||||
("Press the back button to fix your email address", "error")
|
||||
)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
else:
|
||||
# create a new root node.
|
||||
node = create_root_node()
|
||||
# create a new root node.
|
||||
node = create_root_node()
|
||||
node.namespace = request.namespace
|
||||
node.ip_address = unicode(request.client_addr)
|
||||
node.title = thread_title
|
||||
node.set_data(thread_data)
|
||||
|
||||
# Handle anonymous vs authenticated user
|
||||
if user_surrogate:
|
||||
# Anonymous mode: attach surrogate, mark as verified
|
||||
node.user_surrogate = user_surrogate
|
||||
node.verified = True
|
||||
node_event = None # No notifications for anonymous posts
|
||||
request.dbsession.add(user_surrogate)
|
||||
else:
|
||||
# Normal mode: attach user
|
||||
node.user = request.user
|
||||
node.verified = request.user.authenticated
|
||||
node.namespace = request.namespace
|
||||
node.ip_address = unicode(request.client_addr)
|
||||
|
||||
node.title = thread_title
|
||||
node.set_data(thread_data)
|
||||
|
||||
node_event = node.new_event(request.user, "created")
|
||||
|
||||
request.dbsession.add(node)
|
||||
request.dbsession.add(node_event)
|
||||
request.dbsession.add(request.user)
|
||||
request.dbsession.add(node.namespace)
|
||||
request.dbsession.flush()
|
||||
|
||||
request.dbsession.add(node)
|
||||
if node_event:
|
||||
request.dbsession.add(node_event)
|
||||
request.dbsession.add(node.namespace)
|
||||
request.dbsession.flush()
|
||||
|
||||
if node_event:
|
||||
# TODO: schedule_notification expects the request to have a node.
|
||||
request.node = node
|
||||
|
||||
schedule_notifications(request, node_event)
|
||||
|
||||
msg = ("Your post was successful!", "success")
|
||||
request.session.flash(msg)
|
||||
msg = ("Your post was successful!", "success")
|
||||
request.session.flash(msg)
|
||||
|
||||
# set return_to to the node's URI.
|
||||
return_to = get_node_route_uri(request, node)
|
||||
# set return_to to the node's URI.
|
||||
return_to = get_node_route_uri(request, node)
|
||||
|
||||
if node.verified == True:
|
||||
# Redirect to new node if verified.
|
||||
return HTTPFound(return_to)
|
||||
# Anonymous users are always verified, redirect immediately
|
||||
if user_surrogate or node.verified:
|
||||
return HTTPFound(return_to)
|
||||
|
||||
set_node_to_pending_in_session(request, node)
|
||||
set_node_to_pending_in_session(request, node)
|
||||
|
||||
# Redirect to join-or-log-in, posting email and submit.
|
||||
uri = request.route_url(
|
||||
route_name="basic-join-or-log-in",
|
||||
_query={
|
||||
"email": request.user.email,
|
||||
"return-to": return_to,
|
||||
"submit": True,
|
||||
},
|
||||
)
|
||||
return HTTPFound(uri)
|
||||
# Redirect to join-or-log-in, posting email and submit.
|
||||
uri = request.route_url(
|
||||
route_name="basic-join-or-log-in",
|
||||
_query={
|
||||
"email": request.user.email,
|
||||
"return-to": return_to,
|
||||
"submit": True,
|
||||
},
|
||||
)
|
||||
return HTTPFound(uri)
|
||||
|
||||
return {
|
||||
"title": "Create a new thread",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from . import (
|
|||
)
|
||||
|
||||
from remarkbox.lib.notify import schedule_notifications
|
||||
from remarkbox.models import get_or_create_user_surrogate_by_name
|
||||
|
||||
try:
|
||||
unicode("")
|
||||
|
|
@ -27,6 +28,7 @@ except:
|
|||
def reply_node(request):
|
||||
"""handle posting of reply form from show-node pages."""
|
||||
thread_data = request.params.get("thread_data", "")
|
||||
anonymous_name = request.params.get("anonymous_name", "").strip()
|
||||
|
||||
# return early if spam attribute is truthy.
|
||||
if request.spam:
|
||||
|
|
@ -43,8 +45,17 @@ def reply_node(request):
|
|||
request.session.flash(("No remarks for the disabled.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
# flash error and return early if user is None.
|
||||
if request.user is None:
|
||||
# Handle anonymous mode vs regular mode
|
||||
user_surrogate = None
|
||||
if request.namespace.allow_anonymous and not request.user:
|
||||
# Anonymous mode: create or get a surrogate
|
||||
if not anonymous_name:
|
||||
anonymous_name = "Anonymous"
|
||||
user_surrogate = get_or_create_user_surrogate_by_name(
|
||||
request.dbsession, anonymous_name, request.namespace
|
||||
)
|
||||
elif request.user is None:
|
||||
# Regular mode: require email/user
|
||||
request.session.flash(
|
||||
("Press the back button to fix your email address", "error")
|
||||
)
|
||||
|
|
@ -72,16 +83,30 @@ def reply_node(request):
|
|||
|
||||
# STEP 2: attach a brand new child node to parent node.
|
||||
child = parent.new_child()
|
||||
child.user = request.user
|
||||
child.ip_address = unicode(request.client_addr)
|
||||
child.verified = request.user.authenticated
|
||||
child.set_data(thread_data, namespace=request.namespace)
|
||||
child_event = child.new_event(request.user, "commented")
|
||||
|
||||
# Handle anonymous vs authenticated user
|
||||
if user_surrogate:
|
||||
# Anonymous mode: attach surrogate, mark as verified (no email to verify)
|
||||
child.user_surrogate = user_surrogate
|
||||
child.verified = True
|
||||
child_event = None # No notifications for anonymous comments
|
||||
request.dbsession.add(user_surrogate)
|
||||
else:
|
||||
# Normal mode: attach user
|
||||
child.user = request.user
|
||||
child.verified = request.user.authenticated
|
||||
child_event = child.new_event(request.user, "commented")
|
||||
|
||||
if request.namespace.hide_unless_approved:
|
||||
# by default comments are approved, unless Namespace hide_unless_approved
|
||||
# is enabled, moderators nodes are always auto approved.
|
||||
child.approved = request.namespace.is_moderator(request.user)
|
||||
if request.user:
|
||||
child.approved = request.namespace.is_moderator(request.user)
|
||||
else:
|
||||
# Anonymous users are never auto-approved when moderation is on
|
||||
child.approved = False
|
||||
|
||||
# STEP 3: update root's changed timestamp.
|
||||
# TODO: maybe we should find a better way to "bump" a thread.
|
||||
|
|
@ -90,29 +115,29 @@ def reply_node(request):
|
|||
parent._invalidate_cache()
|
||||
|
||||
# STEP 4: commit to database.
|
||||
request.dbsession.add(request.user)
|
||||
if request.user:
|
||||
request.dbsession.add(request.user)
|
||||
request.dbsession.add(child)
|
||||
request.dbsession.add(child_event)
|
||||
if child_event:
|
||||
request.dbsession.add(child_event)
|
||||
request.dbsession.add(parent)
|
||||
request.dbsession.add(parent.root)
|
||||
request.dbsession.flush()
|
||||
|
||||
schedule_notifications(request, child_event)
|
||||
if child_event:
|
||||
schedule_notifications(request, child_event)
|
||||
|
||||
msg = ("Your post was successful!", "success")
|
||||
request.session.flash(msg)
|
||||
|
||||
### TODO: everything below this is pretty much crap code...
|
||||
# and likely deserves a flowchart...
|
||||
|
||||
# set return_to URI.
|
||||
if request.mode == "embed":
|
||||
return_to = get_embed_route_uri(request, child.root.uri.data, child.id)
|
||||
else:
|
||||
return_to = get_node_route_uri(request, child.root, child.id)
|
||||
|
||||
if child.verified == True:
|
||||
# Redirect to new node if user and new node is verified.
|
||||
# Anonymous users are always verified, redirect immediately
|
||||
if user_surrogate or child.verified:
|
||||
return HTTPFound(return_to)
|
||||
|
||||
set_node_to_pending_in_session(request, child)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue