Skip to content

Commit 5b1ce46

Browse files
committed
fix: set is_marked_spam to True for SpamModeration.Status SPAM or SPAM_LIKELY
- fix tests - add asdf & direnv to .gitignore and .dockerignore
1 parent 0b8d8db commit 5b1ce46

File tree

9 files changed

+167
-111
lines changed

9 files changed

+167
-111
lines changed

.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
.direnv/
2+
.tool-versions
3+
.envrc
14
.git
25
.yarn/cache
36
.yarn/install-state.gz

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,7 @@ vignettes/*.pdf
180180

181181
# End of https://www.toptal.com/developers/gitignore/api/r
182182

183+
# asdf & direnv
184+
.direnv/
185+
.tool-versions
186+
.envrc

django/core/mixins.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
from django.shortcuts import redirect
88
from django.utils import timezone
99
from rest_framework import serializers
10+
from rest_framework.decorators import action
1011
from rest_framework.exceptions import NotFound
1112
from rest_framework.response import Response
12-
from rest_framework.decorators import action
1313

1414
from .models import SpamModeration
15-
from .permissions import ViewRestrictedObjectPermissions, ModeratorPermissions
15+
from .permissions import ModeratorPermissions, ViewRestrictedObjectPermissions
1616

1717
logger = logging.getLogger(__name__)
1818

@@ -249,11 +249,11 @@ class SpamCatcherViewSetMixin:
249249

250250
def perform_create(self, serializer: serializers.Serializer):
251251
super().perform_create(serializer)
252-
self.handle_spam_detection(serializer)
252+
self.create_or_update_spam_moderation_object(serializer)
253253

254254
def perform_update(self, serializer):
255255
super().perform_update(serializer)
256-
self.handle_spam_detection(serializer)
256+
self.create_or_update_spam_moderation_object(serializer)
257257

258258
def _validate_content_object(self, instance):
259259
# make sure that the instance has a spam_moderation attribute as well as the
@@ -294,10 +294,12 @@ def mark_spam(self, request, **kwargs):
294294
spam_moderation.save()
295295
return redirect(instance.get_list_url())
296296

297-
def handle_spam_detection(self, serializer: serializers.Serializer):
297+
def create_or_update_spam_moderation_object(
298+
self, serializer: serializers.Serializer
299+
):
298300
try:
299301
self._validate_content_object(serializer.instance)
300-
self._record_spam(
302+
self._create_or_update_spam_moderation_object(
301303
serializer.instance,
302304
(
303305
serializer.context["spam_context"]
@@ -308,24 +310,27 @@ def handle_spam_detection(self, serializer: serializers.Serializer):
308310
except ValueError as e:
309311
logger.warning("Cannot flag %s as spam: %s", serializer.instance, e)
310312

311-
def _record_spam(self, instance, spam_context: dict = None):
313+
def _create_or_update_spam_moderation_object(
314+
self, instance, spam_context: dict = None
315+
):
312316
content_type = ContentType.objects.get_for_model(type(instance))
317+
default_status = (
318+
SpamModeration.Status.SPAM_LIKELY
319+
if spam_context
320+
else SpamModeration.Status.SCHEDULED_FOR_CHECK
321+
)
322+
default_spam_moderation = {
323+
"status": default_status,
324+
"detection_method": (
325+
spam_context.get("detection_method", "") if spam_context else ""
326+
),
327+
"detection_details": (
328+
spam_context.get("detection_details", "") if spam_context else ""
329+
),
330+
}
313331

314-
# SpamModeration updates the content instance on save
315-
spam_moderation, created = SpamModeration.objects.get_or_create(
332+
SpamModeration.objects.update_or_create(
316333
content_type=content_type,
317334
object_id=instance.id,
318-
defaults={
319-
"status": SpamModeration.Status.SCHEDULED_FOR_CHECK,
320-
"detection_method": (
321-
spam_context["detection_method"] if spam_context else ""
322-
),
323-
"detection_details": (
324-
spam_context["detection_details"] if spam_context else ""
325-
),
326-
},
335+
defaults=default_spam_moderation,
327336
)
328-
329-
if not created:
330-
spam_moderation.status = SpamModeration.Status.SCHEDULED_FOR_CHECK
331-
spam_moderation.save()

django/core/models.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import logging
2-
import pathlib
31
from datetime import timedelta
42
from enum import Enum
3+
import logging
4+
import pathlib
55

66
from allauth.account.models import EmailAddress
77
from django import forms
88
from django.conf import settings
99
from django.contrib.auth import get_user_model
1010
from django.contrib.auth.models import Group, User
11-
from django.contrib.postgres.fields import ArrayField
1211
from django.contrib.contenttypes.fields import GenericForeignKey
1312
from django.contrib.contenttypes.models import ContentType
13+
from django.contrib.postgres.fields import ArrayField
1414
from django.db import models, transaction
1515
from django.urls import reverse
1616
from django.utils import timezone
@@ -134,16 +134,15 @@ def deploy_environment(self):
134134

135135
class SpamModeration(models.Model):
136136
class Status(models.TextChoices):
137-
UNREVIEWED = "unreviewed", _("Unreviewed")
138137
SPAM = "spam", _("Confirmed spam")
139138
NOT_SPAM = "not_spam", _("Confirmed not spam")
140139
SCHEDULED_FOR_CHECK = "scheduled_for_check", _("Scheduled for check by LLM")
141-
SPAM_LIKELY = "spam_likely", _("Marked spam by LLM")
142-
NOT_SPAM_LIKELY = "not_spam_likely", _("Marked as not spam by LLM")
140+
SPAM_LIKELY = "spam_likely", _("Automatically marked as spam")
141+
NOT_SPAM_LIKELY = "not_spam_likely", _("Automatically marked as not spam")
143142

144143
status = models.CharField(
145144
choices=Status.choices,
146-
default=Status.UNREVIEWED,
145+
default=Status.SCHEDULED_FOR_CHECK,
147146
max_length=32,
148147
)
149148
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
@@ -195,7 +194,10 @@ def update_related_object(self):
195194
related_object = self.content_object
196195
if hasattr(related_object, "is_marked_spam"):
197196
related_object.spam_moderation = self
198-
related_object.is_marked_spam = self.status == self.Status.SPAM
197+
related_object.is_marked_spam = self.status in {
198+
self.Status.SPAM,
199+
self.Status.SPAM_LIKELY,
200+
}
199201
related_object.save()
200202

201203
def __str__(self):

django/core/settings/defaults.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,9 @@ def is_test(self):
527527
DISCOURSE_API_KEY = read_secret("discourse_api_key", "unconfigured")
528528
DISCOURSE_API_USERNAME = os.getenv("DISCOURSE_API_USERNAME", "unconfigured")
529529

530-
LLM_SPAM_CHECK_API_KEY = read_secret("llm_spam_check_api_key", "unconfigured")
530+
LLM_SPAM_CHECK_API_KEY = (
531+
read_secret("llm_spam_check_api_key", "unconfigured") or "unconfigured"
532+
)
531533

532534
# https://docs.djangoproject.com/en/4.2/ref/settings/#templates
533535
TEMPLATES = [

django/core/tests/test_views.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import logging
22

33
from django.conf import settings
4-
from rest_framework.test import APIClient
5-
6-
from django.urls import reverse
74
from django.test import TestCase
5+
from django.urls import reverse
6+
from rest_framework.test import APIClient
87

8+
from core.models import ComsesGroups, Event, Job, SpamModeration
99
from core.tests.base import UserFactory
1010
from core.tests.permissions_base import BaseViewSetTestCase
1111
from core.views import EventViewSet, JobViewSet
12-
from core.models import Job, Event, SpamModeration, ComsesGroups
13-
from .base import JobFactory, EventFactory
14-
12+
from .base import EventFactory, JobFactory
1513

1614
logger = logging.getLogger(__name__)
1715

@@ -162,9 +160,13 @@ def test_event_creation_with_honeypot_spam(self):
162160
)
163161
self.assertResponseCreated(response)
164162
event = Event.objects.get(title=data["title"])
165-
self.assertTrue(event.is_marked_spam)
163+
166164
self.assertIsNotNone(event.spam_moderation)
165+
self.assertEqual(
166+
event.spam_moderation.status, SpamModeration.Status.SPAM_LIKELY
167+
)
167168
self.assertEqual(event.spam_moderation.detection_method, "honeypot")
169+
self.assertTrue(event.is_marked_spam)
168170

169171
def test_job_creation_with_timer_spam(self):
170172
# FIXME: should incorporate how long a typical request takes to resolve
@@ -179,9 +181,11 @@ def test_job_creation_with_timer_spam(self):
179181
)
180182
self.assertResponseCreated(response)
181183
job = Job.objects.get(title=data["title"])
182-
self.assertTrue(job.is_marked_spam)
184+
183185
self.assertIsNotNone(job.spam_moderation)
186+
self.assertEqual(job.spam_moderation.status, SpamModeration.Status.SPAM_LIKELY)
184187
self.assertEqual(job.spam_moderation.detection_method, "form_submit_time")
188+
self.assertTrue(job.is_marked_spam)
185189

186190
def test_mark_spam(self):
187191
data = self.event_factory.get_request_data()
@@ -193,18 +197,29 @@ def test_mark_spam(self):
193197
format="json",
194198
)
195199
event = Event.objects.get(title=data["title"])
200+
self.assertIsNotNone(event.spam_moderation)
201+
self.assertEqual(
202+
event.spam_moderation.status, SpamModeration.Status.SCHEDULED_FOR_CHECK
203+
)
204+
# by default, all created objects will have is_marked_spam = False unless spam_moderation.status is explicitly SPAM or SPAM_LIKELY
196205
self.assertFalse(event.is_marked_spam)
197-
self.assertIsNone(event.spam_moderation)
206+
198207
response = self.client.post(
199208
reverse("core:event-mark-spam", kwargs={"pk": event.id}),
200209
data,
201210
HTTP_ACCEPT="application/json",
202211
format="json",
203212
)
213+
204214
event.refresh_from_db()
205-
# non-moderators cannot mark content as spam
215+
# non-moderators cannot mark content as spam (set status to SPAM)
206216
self.assertEquals(response.status_code, 403)
217+
self.assertIsNotNone(event.spam_moderation)
218+
self.assertEqual(
219+
event.spam_moderation.status, SpamModeration.Status.SCHEDULED_FOR_CHECK
220+
)
207221
self.assertFalse(event.is_marked_spam)
222+
208223
# check moderator
209224
self.client.login(
210225
username=self.moderator.username, password=self.user_factory.password
@@ -217,12 +232,17 @@ def test_mark_spam(self):
217232
format="json",
218233
)
219234
event.refresh_from_db()
220-
self.assertTrue(event.is_marked_spam)
221235
self.assertIsNotNone(event.spam_moderation)
236+
self.assertEqual(event.spam_moderation.status, SpamModeration.Status.SPAM)
237+
self.assertTrue(event.is_marked_spam)
238+
222239
event.mark_not_spam(self.moderator)
223240
event.refresh_from_db()
224-
self.assertFalse(event.is_marked_spam)
241+
225242
self.assertIsNotNone(event.spam_moderation)
243+
self.assertEqual(event.spam_moderation.status, SpamModeration.Status.NOT_SPAM)
244+
self.assertFalse(event.is_marked_spam)
245+
226246
# check superuser
227247
self.client.login(
228248
username=self.superuser.username, password=self.user_factory.password
@@ -234,9 +254,10 @@ def test_mark_spam(self):
234254
format="json",
235255
)
236256
event.refresh_from_db()
237-
self.assertTrue(event.is_marked_spam)
257+
238258
self.assertIsNotNone(event.spam_moderation)
239259
self.assertEqual(event.spam_moderation.status, SpamModeration.Status.SPAM)
260+
self.assertTrue(event.is_marked_spam)
240261

241262
def test_event_creation_without_spam(self):
242263
data = self.event_factory.get_request_data()
@@ -248,8 +269,12 @@ def test_event_creation_without_spam(self):
248269
)
249270
self.assertResponseCreated(response)
250271
event = Event.objects.get(title=data["title"])
272+
273+
self.assertIsNotNone(event.spam_moderation)
274+
self.assertEqual(
275+
event.spam_moderation.status, SpamModeration.Status.SCHEDULED_FOR_CHECK
276+
)
251277
self.assertFalse(event.is_marked_spam)
252-
self.assertIsNone(event.spam_moderation)
253278

254279
def test_job_update_with_spam(self):
255280
data = self.job_factory.get_request_data()
@@ -261,8 +286,13 @@ def test_job_update_with_spam(self):
261286
)
262287
self.assertResponseCreated(response)
263288
job = Job.objects.get(title=data["title"])
289+
290+
self.assertIsNotNone(job.spam_moderation)
291+
self.assertEqual(
292+
job.spam_moderation.status, SpamModeration.Status.SCHEDULED_FOR_CHECK
293+
)
264294
self.assertFalse(job.is_marked_spam)
265-
self.assertIsNone(job.spam_moderation)
295+
266296
data = self.job_factory.get_request_data(
267297
honeypot_value="spammy content",
268298
elapsed_time=settings.SPAM_LIKELY_SECONDS_THRESHOLD + 1,
@@ -274,9 +304,11 @@ def test_job_update_with_spam(self):
274304
format="json",
275305
)
276306
job.refresh_from_db()
277-
self.assertTrue(job.is_marked_spam)
307+
278308
self.assertIsNotNone(job.spam_moderation)
309+
self.assertEqual(job.spam_moderation.status, SpamModeration.Status.SPAM_LIKELY)
279310
self.assertEqual(job.spam_moderation.detection_method, "honeypot")
311+
self.assertTrue(job.is_marked_spam)
280312

281313
def test_exclude_spam_from_public_views(self):
282314
data = self.event_factory.get_request_data(honeypot_value="spammy content")

django/curator/serializers.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
from rest_framework import serializers
2-
from core.models import SpamModeration
3-
from django.contrib.contenttypes.models import ContentType
4-
from rest_framework import serializers
5-
from core.models import Event, Job, SpamModeration
6-
from django.contrib.contenttypes.models import ContentType
72

3+
from core.models import Event, Job, SpamModeration
84
from library.models import Codebase
95

106

@@ -61,7 +57,7 @@ class Meta:
6157

6258

6359
class SpamUpdateSerializer(serializers.Serializer):
64-
object_id = serializers.IntegerField()
60+
id = serializers.IntegerField()
6561
is_spam = serializers.BooleanField()
6662
spam_indicators = serializers.ListField(
6763
child=serializers.CharField(), required=False

0 commit comments

Comments
 (0)