Skip to content
Merged
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
16 changes: 11 additions & 5 deletions .github/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
name-template: "$RESOLVED_VERSION"
tag-template: "v$RESOLVED_VERSION"
version-resolver:
major:
default: patch
categories:
- title: "Major"
type: version-resolver
semver-increment: major
labels:
- "Type: Major"
- "major"
minor:
- title: "Minor"
type: version-resolver
semver-increment: minor
labels:
- "Type: Minor"
- "minor"
patch:
- title: "Patch"
type: version-resolver
semver-increment: patch
labels:
- "Type: Patch"
- "patch"
default: patch
categories:
- title: "🔒 Security"
labels:
- "Type: Security"
Expand Down
21 changes: 12 additions & 9 deletions hypha/apply/activity/adapters/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,16 +284,19 @@ def handle_determination(self, determination, source, **kwargs):
)

def handle_batch_determination(self, determinations, sources, **kwargs):
submissions = sources
# Batch messages are sent one submission at a time, see
# `AdapterBase.batch_recipients`.
submission = sources[0]
determination = determinations[submission.id]
if not determination.send_notice:
return
kwargs.pop("source")
for submission in submissions:
determination = determinations[submission.id]
return self.render_message(
"messages/email/determination.html",
source=submission,
determination=determination,
**kwargs,
)
return self.render_message(
"messages/email/determination.html",
source=submission,
determination=determination,
**kwargs,
)

def handle_ready_for_review(self, request, source, **kwargs):
if settings.SEND_READY_FOR_REVIEW:
Expand Down
8 changes: 8 additions & 0 deletions hypha/apply/determinations/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ class Meta:
label = _("Send Notice")
icon = "tick-inverse"

def get_field_kwargs(self, struct_value):
kwargs = super().get_field_kwargs(struct_value)
# A required BooleanField has to be checked to validate, which would
# make it impossible to submit a determination without notifying the
# applicant - the only thing this field is for.
kwargs["required"] = False
return kwargs

def get_searchable_content(self, value, data):
return None

Expand Down
82 changes: 80 additions & 2 deletions hypha/apply/determinations/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@

from django.contrib.messages.storage.fallback import FallbackStorage
from django.contrib.sessions.middleware import SessionMiddleware
from django.core import mail
from django.test import RequestFactory, override_settings
from django.urls import reverse_lazy

from hypha.apply.activity.models import Activity
from hypha.apply.determinations.blocks import (
DeterminationMessageBlock,
SendNoticeBlock,
)
from hypha.apply.determinations.options import ACCEPTED, NEEDS_MORE_INFO, REJECTED
from hypha.apply.determinations.views import BatchDeterminationCreateView
from hypha.apply.funds.models.co_applicants import (
CoApplicant,
CoApplicantInvite,
CoApplicantInviteStatus,
)
from hypha.apply.funds.tests.factories import ApplicationSubmissionFactory
from hypha.apply.funds.models.forms import RoundBaseDeterminationForm
from hypha.apply.funds.tests.factories import ApplicationSubmissionFactory, RoundFactory
from hypha.apply.projects.models.project import CONTRACTING, DRAFT
from hypha.apply.users.roles import APPLICANT_GROUP_NAME
from hypha.apply.users.tests.factories import (
Expand All @@ -25,7 +31,11 @@
)
from hypha.apply.utils.testing import BaseViewTestCase

from .factories import DeterminationFactory
from .factories import (
DeterminationFactory,
DeterminationFormFactory,
DeterminationFormFieldsFactory,
)


def make_co_applicant(submission, user):
Expand Down Expand Up @@ -675,6 +685,74 @@ def test_cant_see_detailed_data_as_co_applicant_when_disabled(self):
self.assertFalse(response.context["show_detailed_data"])


@override_settings(SEND_MESSAGES=True)
class BatchDeterminationStreamFormTestCase(BaseViewTestCase):
"""Batch determinations for submissions using streamfield determination forms."""

user_factory = StaffFactory
url_name = "funds:submissions:determinations:{}"
base_view_name = "batch"

def setUp(self):
super().setUp()
self.determination_form = DeterminationFormFactory()
round_page = RoundFactory()
RoundBaseDeterminationForm.objects.create(
round=round_page, form=self.determination_form
)
self.submissions = ApplicationSubmissionFactory.create_batch(
2, round=round_page
)
mail.outbox.clear()

def field_id(self, block_type):
return next(
field.id
for field in self.determination_form.form_fields
if isinstance(field.block, block_type)
)

def batch_determine(self, message, send_notice=True):
url = (
self.url(None)
+ "?submissions="
+ ",".join([str(submission.id) for submission in self.submissions])
+ "&action=rejected"
)
data = DeterminationFormFieldsFactory.form_response(
self.determination_form.form_fields,
{self.field_id(DeterminationMessageBlock): message},
)
if not send_notice:
# An unchecked checkbox is simply not submitted.
del data[self.field_id(SendNoticeBlock)]
return self.client.post(url, data, secure=True, follow=True)

def applicant_emails(self, submission):
return [email for email in mail.outbox if submission.user.email in email.to]

def test_determination_message_is_included_in_applicant_email(self):
message = "Sorry, not this time."
self.batch_determine(message)

# Every submission in the batch gets its own email, about its own
# submission and carrying the determination message.
for submission in self.submissions:
emails = self.applicant_emails(submission)
self.assertEqual(len(emails), 1)
self.assertIn(message, emails[0].body)
self.assertIn(submission.get_absolute_url(), emails[0].body)

def test_no_applicant_email_if_send_notice_is_unchecked(self):
self.batch_determine("Sorry, not this time.", send_notice=False)

for submission in self.submissions:
determination = submission.determinations.first()
self.assertIsNotNone(determination)
self.assertFalse(determination.send_notice)
self.assertEqual(self.applicant_emails(submission), [])


class UserDeterminationFormTestCase(BaseViewTestCase):
user_factory = UserFactory
url_name = "funds:submissions:determinations:{}"
Expand Down
31 changes: 18 additions & 13 deletions hypha/apply/determinations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,24 @@ def form_valid(self, form):
}
sources = submissions.filter(id__in=list(determinations))

# The streamfield determination form data is not stored by the form itself,
# it has to be copied onto the newly created determinations. This must
# happen before any notifications are sent as the determination message is
# part of the email to the applicant.
if sources and sources[0].is_determination_form_attached:
defined_fields = self.get_defined_fields()
for determination in determinations.values():
determination.form_fields = defined_fields
determination.message = form.cleaned_data[
determination.message_field.id
]
determination.send_notice = (
form.cleaned_data[determination.send_notice_field.id]
if determination.send_notice_field
else True
)
determination.save()

base_message = _("Successfully determined as {outcome}: ").format(
outcome=determinations[sources[0].id].clean_outcome
)
Expand All @@ -253,12 +271,6 @@ def form_valid(self, form):
).format(title=submission.title_text_display),
)
else:
if submission.is_determination_form_attached:
determination.form_fields = self.get_defined_fields()
determination.message = form.cleaned_data[
determination.message_field.id
]
determination.save()
transition = transition_from_outcome(
form.cleaned_data.get("outcome"), submission
)
Expand Down Expand Up @@ -449,13 +461,6 @@ def form_valid(self, form):
return HttpResponseRedirect(self.submission.get_absolute_url())

with transaction.atomic():
messenger(
MESSAGES.DETERMINATION_OUTCOME,
request=self.request,
user=self.object.author,
submission=self.object.submission,
related=self.object,
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was old code that has not done anything for a long time.

proposal_form = form.cleaned_data.get("proposal_form")
transition = transition_from_outcome(
int(self.object.outcome), self.submission
Expand Down
Loading