Skip to content

Commit

Permalink
Merge 1c3a0d1 into 0e2d902
Browse files Browse the repository at this point in the history
  • Loading branch information
hklarner committed Aug 8, 2023
2 parents 0e2d902 + 1c3a0d1 commit cfdd8f1
Show file tree
Hide file tree
Showing 17 changed files with 231 additions and 59 deletions.
1 change: 1 addition & 0 deletions adhocracy-plus/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"apps.polls",
"apps.topicprio",
"apps.debate",
"apps.ai_reports",
)

MIDDLEWARE = (
Expand Down
Empty file added apps/ai_reports/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions apps/ai_reports/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.contrib import admin

from .models import AiReport


@admin.register(AiReport)
class AiReportAdmin(admin.ModelAdmin):
model = AiReport
fields = [
"is_pending",
"category",
"explanation",
"confidence",
]
raw_id_fields = ("comment",)
5 changes: 5 additions & 0 deletions apps/ai_reports/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class AiReportsConfig(AppConfig):
name = "apps.ai_reports"
59 changes: 59 additions & 0 deletions apps/ai_reports/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 3.2.19 on 2023-07-27 15:31

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):
initial = True

dependencies = [
("a4comments", "0013_set_project"),
]

operations = [
migrations.CreateModel(
name="AiReport",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
models.DateTimeField(
default=django.utils.timezone.now,
editable=False,
verbose_name="Created",
),
),
(
"modified",
models.DateTimeField(
blank=True, editable=False, null=True, verbose_name="Modified"
),
),
("is_pending", models.BooleanField(default=True)),
("category", models.CharField(max_length=50)),
("explanation", models.TextField()),
("confidence", models.FloatField(default=0)),
(
"comment",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="ai_report",
to="a4comments.comment",
),
),
],
options={
"abstract": False,
},
),
]
Empty file.
16 changes: 16 additions & 0 deletions apps/ai_reports/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import models

from adhocracy4.comments.models import Comment
from adhocracy4.models.base import TimeStampedModel


class AiReport(TimeStampedModel):
is_pending = models.BooleanField(default=True)
category = models.CharField(max_length=50)
explanation = models.TextField()
confidence = models.FloatField(default=0)
comment = models.OneToOneField(
Comment,
on_delete=models.CASCADE,
related_name="ai_report",
)
14 changes: 7 additions & 7 deletions apps/projects/helpers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from datetime import timedelta

from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.db.models import Q
from django.db.models import Sum
from django.utils import timezone

from adhocracy4.comments.models import Comment
from adhocracy4.reports.models import Report


def get_all_comments_project(project):
Expand All @@ -20,11 +19,12 @@ def get_num_comments_project(project):


def get_num_reports(project):
comment_ids_project = get_all_comments_project(project).values_list("id", flat=True)
comment_ct = ContentType.objects.get_for_model(Comment)
return Report.objects.filter(
content_type=comment_ct, object_pk__in=comment_ids_project
).count()
comments = get_all_comments_project(project)
comments = comments.annotate(
num_reports=Count("reports", distinct=True) + Count("ai_report", distinct=True),
)

return comments.aggregate(Sum("num_reports"))["num_reports__sum"]


def get_num_latest_comments(project, until={"days": 7}):
Expand Down
33 changes: 21 additions & 12 deletions apps/userdashboard/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from django.db.models import Count
from django.db.models import ExpressionWrapper
from django.db.models import Q
from django.db.models.fields import BooleanField
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import DjangoFilterBackend
Expand All @@ -23,9 +20,20 @@

class ModerationCommentFilterSet(DefaultsRestFilterSet):
is_reviewed = BooleanFilter()
has_reports = BooleanFilter()

defaults = {"is_reviewed": "false", "has_reports": "all"}
defaults = {"is_reviewed": "false"}

@property
def qs(self):
queryset = super().qs
reported_by = self.request.query_params.get("reported_by")

if reported_by == "ai":
queryset = queryset.filter(ai_report__isnull=False)
elif reported_by == "users":
queryset = queryset.filter(reports__isnull=False)

return queryset


class ModerationCommentPagination(PageNumberPagination):
Expand Down Expand Up @@ -62,17 +70,18 @@ def get_permission_object(self):
return self.project

def get_queryset(self):
all_comments_project = helpers.get_all_comments_project(self.project)
num_reports = Count("reports", distinct=True)
return all_comments_project.annotate(num_reports=num_reports).annotate(
has_reports=ExpressionWrapper(
Q(num_reports__gt=0), output_field=BooleanField()
)
comments = helpers.get_all_comments_project(self.project)
comments = comments.annotate(
num_reports=Count("reports", distinct=True)
+ Count("ai_report", distinct=True),
)

return comments

def update(self, request, *args, **kwargs):
if "is_blocked" in self.request.data and request.data["is_blocked"]:
if request.data.get("is_blocked"):
NotifyCreatorOnModeratorBlocked.send(self.get_object())

return super().update(request, *args, **kwargs)

@action(detail=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ const isReadFilterItems = [
]

const reportsFilterItems = [
{ label: django.gettext('Reported'), value: 'True' },
{ label: django.gettext('All comments'), value: 'All' }
{ label: django.gettext('Reported by AI'), value: 'ai' },
{ label: django.gettext('Reported by Users'), value: 'users' },
{ label: django.gettext('All comments'), value: 'off' }
]

const orderingFilterItems = [
Expand All @@ -29,7 +30,7 @@ export default class ModerationNotificationList extends Component {

this.state = {
moderationComments: [],
selectedFilters: { isRead: 'False', hasReports: 'All', ordering: '-num_reports' },
selectedFilters: { isRead: 'False', reportedBy: 'off', ordering: '-num_reports' },
numOfComments: PACKET_COMMENT_SIZE,
hasMore: null,
packetFactor: 1,
Expand Down Expand Up @@ -59,7 +60,7 @@ export default class ModerationNotificationList extends Component {
this.setState({
selectedFilters: {
...this.state.selectedFilters,
hasReports: value
reportedBy: value
},
isLoaded: false
},
Expand All @@ -81,7 +82,7 @@ export default class ModerationNotificationList extends Component {

getUrlParams () {
return '?is_reviewed=' + this.state.selectedFilters.isRead +
'&has_reports=' + this.state.selectedFilters.hasReports +
'&reported_by=' + this.state.selectedFilters.reportedBy +
'&ordering=' + this.state.selectedFilters.ordering +
'&num_of_comments=' + this.state.numOfComments
}
Expand Down Expand Up @@ -200,7 +201,7 @@ export default class ModerationNotificationList extends Component {
filterClass="filter--full dropdown dropdown-menu-end"
filterItems={reportsFilterItems}
onFilterChange={(value) => this.reportsFilterChangeHandle(value)}
selectedFilter={this.state.selectedFilters.hasReports}
selectedFilter={this.state.selectedFilters.reportedBy}
filterText={django.gettext('Filter')}
/>
</div>
Expand Down
3 changes: 3 additions & 0 deletions changelog/7538.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- new filter category for comments in moderator dashboard: "reported by ai"
Empty file added tests/ai_reports/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions tests/ai_reports/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pytest_factoryboy import register

from .factories import AiReportFactory

register(AiReportFactory)
16 changes: 16 additions & 0 deletions tests/ai_reports/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import random

import factory

from apps.ai_reports.models import AiReport
from tests.factories import CommentFactory


class AiReportFactory(factory.django.DjangoModelFactory):
class Meta:
model = AiReport

category = factory.Faker("word")
explanation = factory.Faker("sentence", nb_words=6)
confidence = random.random()
comment = factory.SubFactory(CommentFactory)
2 changes: 2 additions & 0 deletions tests/userdashboard/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from pytest_factoryboy import register

from tests.ai_reports.factories import AiReportFactory
from tests.ideas.factories import IdeaFactory
from tests.moderatorfeedback.factories import ModeratorCommentFeedbackFactory

register(IdeaFactory)
register(ModeratorCommentFeedbackFactory)
register(AiReportFactory)
57 changes: 43 additions & 14 deletions tests/userdashboard/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,13 @@ def test_moderator_can_mark_comment_read(apiclient, comment_factory, idea):


@pytest.mark.django_db
def test_queryset_and_filters(apiclient, report_factory, comment_factory, idea_factory):
def test_queryset_and_filters(
apiclient,
report_factory,
ai_report_factory,
comment_factory,
idea_factory,
):
idea = idea_factory(module__project__pk=1)
other_idea = idea_factory(module__project__pk=2)
project = idea.project
Expand All @@ -121,9 +127,10 @@ def test_queryset_and_filters(apiclient, report_factory, comment_factory, idea_f
)
comment_5 = comment_factory(content_object=other_idea)

# comment_1 with 2 reports
# comment_1 with 2 reports and 1 ai report
report_factory(content_object=comment_1)
report_factory(content_object=comment_1)
ai_report_factory(comment=comment_1)
# comment_2 with 1 report, is read
report_factory(content_object=comment_2)
# comment_3 with 1 report
Expand All @@ -141,35 +148,57 @@ def test_queryset_and_filters(apiclient, report_factory, comment_factory, idea_f
response = apiclient.get(url)
assert response.status_code == 200
assert len(response.data) == 2
comment_pks = [comment["pk"] for comment in response.data]
assert comment_pks == [comment_1.pk, comment_3.pk]
assert [comment["pk"] for comment in response.data] == [comment_1.pk, comment_3.pk]

# test default sorting is most reported (second sorting -created)
filter_string = "?is_reviewed=all"
response = apiclient.get(url + filter_string)
assert response.status_code == 200
assert len(response.data) == 4
comment_pks = [comment["pk"] for comment in response.data]
assert comment_pks == [comment_1.pk, comment_3.pk, comment_2.pk, comment_4.pk]

filter_string = "?is_reviewed=all&has_reports=False"
assert [comment["pk"] for comment in response.data] == [
comment_1.pk,
comment_3.pk,
comment_2.pk,
comment_4.pk,
]

filter_string = "?is_reviewed=all&reported_by=users"
response = apiclient.get(url + filter_string)
assert response.status_code == 200
assert len(response.data) == 3
assert [comment["pk"] for comment in response.data] == [
comment_1.pk,
comment_3.pk,
comment_2.pk,
]

filter_string = "?is_reviewed=all&reported_by=ai"
response = apiclient.get(url + filter_string)
assert response.status_code == 200
assert len(response.data) == 1
comment_pks = [comment["pk"] for comment in response.data]
assert comment_pks == [comment_4.pk]
assert [comment["pk"] for comment in response.data] == [comment_1.pk]

filter_string = "?has_reports=False"
filter_string = "?is_reviewed=all&reported_by=off"
response = apiclient.get(url + filter_string)
assert response.status_code == 200
assert len(response.data) == 0
assert len(response.data) == 4
assert [comment["pk"] for comment in response.data] == [
comment_1.pk,
comment_3.pk,
comment_2.pk,
comment_4.pk,
]

filter_string = "?is_reviewed=all&ordering=created"
response = apiclient.get(url + filter_string)
assert response.status_code == 200
assert len(response.data) == 4
comment_pks = [comment["pk"] for comment in response.data]
assert comment_pks == [comment_2.pk, comment_1.pk, comment_3.pk, comment_4.pk]
assert [comment["pk"] for comment in response.data] == [
comment_2.pk,
comment_1.pk,
comment_3.pk,
comment_4.pk,
]


@pytest.mark.django_db
Expand Down
Loading

0 comments on commit cfdd8f1

Please sign in to comment.