Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions comments/services/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,12 @@ def create_comment(
if root:
is_private = root.is_private

if not is_private and user.is_bot and not user.is_primary_bot:
raise PermissionDenied("Only your primary bot can post public comments.")
if not is_private and user.is_bot and not user.allow_public_comments_if_bot:
raise PermissionDenied(
"Bots cannot post public comments by default. "
"Use is_private=true for private comments, or ask an admin to "
"enable public commenting for this bot (allow_public_comments_if_bot)."
)

with transaction.atomic():
obj = Comment(
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_comments/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from rest_framework.exceptions import ValidationError

from comments.models import KeyFactorVote, KeyFactorDriver, KeyFactorBaseRate
from rest_framework.exceptions import PermissionDenied

from comments.services.common import create_comment, soft_delete_comment
from comments.services.key_factors.common import (
key_factor_vote,
Expand Down Expand Up @@ -376,3 +378,43 @@ def test_base_rate_freshness_ignores_time_decay(user1, post):

freshness = calculate_freshness_base_rate(kf, votes)
assert freshness == pytest.approx(1.666, abs=0.001)


def test_bot_cannot_post_public_comments(post):
bot = factory_user(is_bot=True, allow_public_comments_if_bot=False)
with pytest.raises(PermissionDenied):
create_comment(user=bot, on_post=post, text="Public comment", is_private=False)


def test_bot_can_post_private_comments(post):
bot = factory_user(is_bot=True, allow_public_comments_if_bot=False)
comment = create_comment(
user=bot, on_post=post, text="Private comment", is_private=True
)
assert comment.is_private is True


def test_bot_with_allow_public_comments_if_bot_can_post_public(post):
bot = factory_user(is_bot=True, allow_public_comments_if_bot=True)
comment = create_comment(
user=bot, on_post=post, text="Public comment", is_private=False
)
assert comment.is_private is False


def test_bot_cannot_reply_to_public_thread(post):
user = factory_user()
parent = create_comment(user=user, on_post=post, text="Public parent")
bot = factory_user(is_bot=True, allow_public_comments_if_bot=False)
with pytest.raises(PermissionDenied):
create_comment(user=bot, on_post=post, text="Reply", parent=parent)


def test_bot_can_reply_to_private_thread(post):
user = factory_user()
parent = create_comment(
user=user, on_post=post, text="Private parent", is_private=True
)
bot = factory_user(is_bot=True, allow_public_comments_if_bot=False)
reply = create_comment(user=bot, on_post=post, text="Reply", parent=parent)
assert reply.is_private is True
9 changes: 8 additions & 1 deletion users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,13 @@ def has_add_permission(self, request, obj=None):
class BotInline(admin.TabularInline):
model = User
fk_name = "bot_owner"
fields = ["username", "email", "is_active", "is_primary_bot"]
fields = [
"username",
"email",
"is_active",
"is_primary_bot",
"allow_public_comments_if_bot",
]
readonly_fields = ["username", "email", "is_active", "is_bot"]
extra = 0
show_change_link = True
Expand All @@ -209,6 +215,7 @@ class UserAdmin(admin.ModelAdmin):
"is_spam",
"is_bot",
"is_primary_bot",
"allow_public_comments_if_bot",
"bot_owner",
"duration_joined_to_last_login",
"authored_posts",
Expand Down
31 changes: 31 additions & 0 deletions users/migrations/0019_add_allow_public_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0018_add_auth_revoked_at"),
]

operations = [
migrations.AddField(
model_name="user",
name="allow_public_comments_if_bot",
field=models.BooleanField(
default=False,
help_text=(
"Allow this bot to post public comments. "
"By default, bots can only post private comments. "
"An admin can enable this for select bots."
),
),
),
migrations.AddConstraint(
model_name="user",
constraint=models.CheckConstraint(
check=models.Q(("is_bot", True), ("allow_public_comments_if_bot", False), _connector="OR"),
name="user_allow_public_comments_if_bot_only_for_bots",
violation_error_message="allow_public_comments_if_bot can only be set for bot accounts",
),
),
]
24 changes: 24 additions & 0 deletions users/migrations/0020_alter_user_is_primary_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0019_add_allow_public_comments"),
]

operations = [
migrations.AlterField(
model_name="user",
name="is_primary_bot",
field=models.BooleanField(
db_index=True,
default=False,
help_text=(
"Marks the user's primary bot. The primary"
" bot is eligible for prizes, counts toward"
" peer scores, and appears on leaderboards."
),
),
),
]
20 changes: 17 additions & 3 deletions users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,17 @@ class InterfaceType(models.TextChoices):
default=False,
db_index=True,
help_text=(
"Marks the user’s primary bot. Only the primary bot can post public comments, "
"be eligible for prizes, count toward peer scores, "
"and appear on leaderboards."
"Marks the user's primary bot. The primary bot is "
"eligible for prizes, counts toward peer scores, "
"and appears on leaderboards."
),
)
allow_public_comments_if_bot = models.BooleanField(
default=False,
help_text=(
"Allow this bot to post public comments. "
"By default, bots can only post private comments. "
"An admin can enable this for select bots."
),
)
bot_owner = models.ForeignKey(
Expand Down Expand Up @@ -173,6 +181,12 @@ class Meta:
name="unique_primary_bot_per_bot_owner",
violation_error_message="Bot owner could have only one primary bot",
),
models.CheckConstraint(
check=models.Q(is_bot=True)
| models.Q(allow_public_comments_if_bot=False),
name="user_allow_public_comments_if_bot_only_for_bots",
violation_error_message="allow_public_comments_if_bot can only be set for bot accounts",
),
]

def check_can_activate(self):
Expand Down
Loading