Skip to content

Commit

Permalink
add unregistered participants field to project insights to account fo…
Browse files Browse the repository at this point in the history
…r the

new feature in the poll module which allows participating without
account
  • Loading branch information
goapunk committed Nov 6, 2024
1 parent 87db814 commit 554836c
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 42 deletions.
31 changes: 31 additions & 0 deletions apps/projects/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from django.utils import timezone

from adhocracy4.comments.models import Comment
from adhocracy4.polls.models import Answer
from adhocracy4.polls.models import Vote
from adhocracy4.projects.models import Project
from adhocracy4.reports.models import Report


Expand Down Expand Up @@ -41,3 +44,31 @@ def get_num_reported_unread_comments(project):
.filter(num_reports__gt=0)
.count()
)


def get_num_unregistered_participants(project: Project) -> int:
"""Returns the number of unregistered users which participated in project.
Parameters
----------
project : The project to get the number for.
"""

answers = (
Answer.objects.filter(
question__poll__module__project=project, content_id__isnull=False
)
.values_list("content_id", flat=True)
.distinct()
.order_by()
)
votes = (
Vote.objects.filter(
choice__question__poll__module__project=project, content_id__isnull=False
)
.exclude(content_id__in=answers)
.values_list("content_id", flat=True)
.distinct()
.order_by()
)
return len(answers) + len(votes)
3 changes: 3 additions & 0 deletions apps/projects/insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from apps.interactiveevents.models import LiveQuestion
from apps.mapideas.models import MapIdea
from apps.projects.helpers import get_all_comments_project
from apps.projects.helpers import get_num_unregistered_participants
from apps.projects.models import ProjectInsight
from apps.topicprio.models import Topic

Expand All @@ -26,6 +27,7 @@ def create_insight(project: Project) -> ProjectInsight:
ideas = Idea.objects.filter(module__in=modules)
map_ideas = MapIdea.objects.filter(module__in=modules)
comments = get_all_comments_project(project=project)
unregistered_participants = get_num_unregistered_participants(project=project)
proposals = Proposal.objects.filter(module__in=modules)
polls = Poll.objects.filter(module__in=modules)
votes = Vote.objects.filter(choice__question__poll__in=polls)
Expand Down Expand Up @@ -66,6 +68,7 @@ def create_insight(project: Project) -> ProjectInsight:
insight, _ = ProjectInsight.objects.get_or_create(project=project)

insight.comments = comments.count()
insight.unregistered_participants = unregistered_participants
insight.ratings = sum(x.count() for x in rating_objects)
insight.written_ideas = sum(x.count() for x in idea_objects)
insight.poll_answers = votes.count() + answers.count()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-11-04 15:18

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("a4_candy_projects", "0006_initialize_insights"),
]

operations = [
migrations.AddField(
model_name="projectinsight",
name="unregistered_participants",
field=models.PositiveIntegerField(default=0),
),
]
6 changes: 5 additions & 1 deletion apps/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class ProjectInsight(base.TimeStampedModel):
Project, related_name="insight", on_delete=models.CASCADE
)
active_participants = models.ManyToManyField(settings.AUTH_USER_MODEL)
unregistered_participants = models.PositiveIntegerField(default=0)
comments = models.PositiveIntegerField(default=0)
ratings = models.PositiveIntegerField(default=0)
written_ideas = models.PositiveIntegerField(default=0)
Expand Down Expand Up @@ -134,7 +135,10 @@ def create_insight_context(insight: ProjectInsight) -> dict:
show_ideas = bool(blueprint_types.intersection({"BS", "IC", "MBS", "MIC", "PB"}))

counts = [
(_("active participants"), insight.active_participants.count()),
(
_("active participants"),
insight.active_participants.count() + insight.unregistered_participants,
),
(_("comments"), insight.comments),
(_("ratings"), insight.ratings),
]
Expand Down
13 changes: 11 additions & 2 deletions apps/projects/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from adhocracy4.comments.models import Comment
from adhocracy4.polls.models import Answer
from adhocracy4.polls.models import Vote
from adhocracy4.polls.signals import poll_voted
from adhocracy4.projects.models import Project
from adhocracy4.ratings.models import Rating
from apps.budgeting.models import Proposal
Expand Down Expand Up @@ -93,6 +94,14 @@ def increase_poll_answers_count(sender, instance, created, **kwargs):

insight, _ = ProjectInsight.objects.get_or_create(project=project)
insight.poll_answers += 1
if instance.creator:
insight.active_participants.add(instance.creator.id)
insight.save()


@receiver(poll_voted)
def increase_poll_participant_count(sender, poll, creator, content_id, **kwargs):
insight, _ = ProjectInsight.objects.get_or_create(project=poll.module.project)
if creator:
insight.active_participants.add(creator.id)
else:
insight.unregistered_participants += 1
insight.save()
4 changes: 2 additions & 2 deletions apps/userdashboard/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@


class Config(AppConfig):
name = 'apps.userdashboard'
label = 'a4_candy_userdashboard'
name = "apps.userdashboard"
label = "a4_candy_userdashboard"
7 changes: 2 additions & 5 deletions apps/userdashboard/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@

class ModerationDetailRouterMixin(CustomRouterMixin):

prefix_regex = (
r'userdashboard/moderation/(?P<project_pk>[\d]+)/{prefix}'
)
prefix_regex = r"userdashboard/moderation/(?P<project_pk>[\d]+)/{prefix}"


class ModerationDetailDefaultRouter(ModerationDetailRouterMixin,
routers.DefaultRouter):
class ModerationDetailDefaultRouter(ModerationDetailRouterMixin, routers.DefaultRouter):
pass
13 changes: 7 additions & 6 deletions apps/userdashboard/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
from adhocracy4.modules.predicates import is_allowed_moderate_project
from apps.users.predicates import is_moderator

rules.add_perm('a4_candy_userdashboard.view_moderation_dashboard',
is_moderator)
rules.add_perm("a4_candy_userdashboard.view_moderation_dashboard", is_moderator)

rules.add_perm('a4_candy_userdashboard.view_moderation_comment',
is_allowed_moderate_project)
rules.add_perm(
"a4_candy_userdashboard.view_moderation_comment", is_allowed_moderate_project
)

rules.add_perm('a4_candy_userdashboard.change_moderation_comment',
is_allowed_moderate_project)
rules.add_perm(
"a4_candy_userdashboard.change_moderation_comment", is_allowed_moderate_project
)
40 changes: 25 additions & 15 deletions apps/userdashboard/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,29 @@
from . import views

urlpatterns = [
path('overview/',
views.UserDashboardOverviewView.as_view(),
name='userdashboard-overview'),
path('moderation/',
views.UserDashboardModerationView.as_view(),
name='userdashboard-moderation'),
path('overview/activities/',
views.UserDashboardActivitiesView.as_view(),
name='userdashboard-activities'),
path('overview/following/',
views.UserDashboardFollowingView.as_view(),
name='userdashboard-following'),
re_path(r'^moderation/detail/(?P<slug>[-\w_]+)/$',
views.UserDashboardModerationDetailView.as_view(),
name='userdashboard-moderation-detail'),
path(
"overview/",
views.UserDashboardOverviewView.as_view(),
name="userdashboard-overview",
),
path(
"moderation/",
views.UserDashboardModerationView.as_view(),
name="userdashboard-moderation",
),
path(
"overview/activities/",
views.UserDashboardActivitiesView.as_view(),
name="userdashboard-activities",
),
path(
"overview/following/",
views.UserDashboardFollowingView.as_view(),
name="userdashboard-following",
),
re_path(
r"^moderation/detail/(?P<slug>[-\w_]+)/$",
views.UserDashboardModerationDetailView.as_view(),
name="userdashboard-moderation-detail",
),
]
2 changes: 2 additions & 0 deletions tests/polls/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@

register(factories.AnswerFactory)
register(factories.OpenQuestionFactory, "open_question")
register(factories.QuestionFactory)
register(factories.ChoiceFactory)
register(factories.PollFactory)
register(ProjectInsightFactory)
113 changes: 108 additions & 5 deletions tests/polls/test_project_with_polls_view.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import pytest
from django.urls import reverse
from rest_framework import status

from adhocracy4.polls import phases
from adhocracy4.polls.models import Answer
from adhocracy4.polls.models import Poll
from adhocracy4.polls.models import Vote
from adhocracy4.test.helpers import freeze_phase
from adhocracy4.test.helpers import setup_phase
from apps.projects.models import ProjectInsight


@pytest.mark.django_db
Expand All @@ -20,16 +25,16 @@ def test_project_with_single_poll_module_and_insights(
)

project_insight = project_insight_factory(project=project)
poll_answers = project_insight.poll_answers
assert hasattr(project, "insight")

poll = Poll.objects.first()
question = open_question_factory(poll=poll)

# post save signal in answer obj is called for
# adding the creator of the answer to the insights
answer = answer_factory(question=question)

assert project_insight.active_participants.first() == answer.creator
# post save signal in answer obj is called to increase the poll answer count
answer_factory(question=question)
project_insight.refresh_from_db()
assert project_insight.poll_answers == poll_answers + 1

url = reverse(
"project-detail",
Expand All @@ -38,3 +43,101 @@ def test_project_with_single_poll_module_and_insights(

response = client.get(url)
assert "insight_label" in response.context_data.keys()


@pytest.mark.django_db
def test_normal_user_vote_is_added_as_participant(
user, apiclient, poll_factory, phase_factory, question_factory, choice_factory
):

phase, module, project, _ = setup_phase(
phase_factory, poll_factory, phases.VotingPhase
)

poll = Poll.objects.first()
question = question_factory(poll=poll)
choice1 = choice_factory(question=question)
choice_factory(question=question)
open_question = question_factory(poll=poll, is_open=True)

assert Vote.objects.count() == 0

apiclient.force_authenticate(user=user)

url = reverse("polls-vote", kwargs={"pk": poll.pk})

data = {
"votes": {
question.pk: {
"choices": [choice1.pk],
"other_choice_answer": "",
"open_answer": "",
},
open_question.pk: {
"choices": [],
"other_choice_answer": "",
"open_answer": "an open answer",
},
},
"agreed_terms_of_use": True,
}

with freeze_phase(phase):
response = apiclient.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED

insight = ProjectInsight.objects.first()

assert Vote.objects.count() == 1
assert Answer.objects.count() == 1
assert insight.active_participants.count() == 1


@pytest.mark.django_db
def test_unregistered_user_vote_is_added_as_participant(
user, apiclient, poll_factory, phase_factory, question_factory, choice_factory
):

phase, module, project, _ = setup_phase(
phase_factory, poll_factory, phases.VotingPhase
)

poll = Poll.objects.first()
poll.allow_unregistered_users = True
poll.save()
question = question_factory(poll=poll)
choice1 = choice_factory(question=question)
choice_factory(question=question)
open_question = question_factory(poll=poll, is_open=True)

assert Vote.objects.count() == 0

url = reverse("polls-vote", kwargs={"pk": poll.pk})

data = {
"votes": {
question.pk: {
"choices": [choice1.pk],
"other_choice_answer": "",
"open_answer": "",
},
open_question.pk: {
"choices": [],
"other_choice_answer": "",
"open_answer": "an open answer",
},
},
"agreed_terms_of_use": True,
"captcha": "testpass:1",
}

with freeze_phase(phase):
response = apiclient.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED

insight = ProjectInsight.objects.first()

assert Vote.objects.count() == 1
assert Answer.objects.count() == 1
assert insight.active_participants.count() == 0
assert insight.unregistered_participants == 1
Loading

0 comments on commit 554836c

Please sign in to comment.