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
2 changes: 1 addition & 1 deletion assets
Submodule assets updated 1 files
+174 −0 openapi-schema.yaml
8 changes: 8 additions & 0 deletions eap/dev_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def get(self, request):
"pending_pfa": "email/eap/pending_pfa.html",
"approved_eap": "email/eap/approved.html",
"reminder": "email/eap/reminder.html",
"share_eap": "email/eap/share_eap.html",
}

if type_param not in template_map:
Expand Down Expand Up @@ -113,6 +114,13 @@ def get(self, request):
"national_society": "Test National Society",
"disaster_type": "Flood",
},
"share_eap": {
"registration_id": 1,
"eap_type": "simplified",
"country_name": "Test Country",
"national_society": "Test National Society",
"disaster_type": "Flood",
},
}

context = context_map.get(type_param)
Expand Down
8 changes: 8 additions & 0 deletions eap/filter_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,11 @@ class FullEAPFilterSet(BaseEAPFilterSet):
class Meta:
model = FullEAP
fields = ("eap_registration",)


class EAPShareUserFilterSet(filters.FilterSet):
id = filters.NumberFilter(field_name="id", lookup_expr="exact")

class Meta:
model = EAPRegistration
fields = ("id",)
24 changes: 24 additions & 0 deletions eap/migrations/0004_eapregistration_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.26 on 2026-01-28 11:09

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("eap", "0003_eapaction_eapcontact_eapfile_eapimpact_and_more"),
]

operations = [
migrations.AddField(
model_name="eapregistration",
name="users",
field=models.ManyToManyField(
blank=True,
related_name="user_eap",
to=settings.AUTH_USER_MODEL,
verbose_name="users",
),
),
]
8 changes: 8 additions & 0 deletions eap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,14 @@ class EAPRegistration(EAPBaseModel):
blank=True,
)

# Users involved in the EAP
users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
verbose_name=_("users"),
related_name="user_eap",
)

# Validated Budget file
validated_budget_file = SecureFileField(
upload_to="eap/files/validated_budgets/",
Expand Down
34 changes: 34 additions & 0 deletions eap/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
generate_export_eap_pdf,
send_approved_email,
send_eap_resubmission_email,
send_eap_share_email,
send_feedback_email,
send_feedback_email_for_resubmitted_eap,
send_new_eap_registration_email,
Expand Down Expand Up @@ -95,6 +96,39 @@ def update(self, instance, validated_data: dict[str, typing.Any]):
return super().update(instance, validated_data)


class EAPShareUserSerializer(serializers.ModelSerializer):
users = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
many=True,
required=True,
)

users_details = UserNameSerializer(source="users", many=True, read_only=True)

class Meta:
model = EAPRegistration
fields = (
"users",
"users_details",
)
read_only_fields = ("id",)

def update(self, instance: EAPRegistration, validated_data: dict[str, typing.Any]) -> EAPRegistration:
existing_user_ids = set(instance.users.values_list("id", flat=True))
new_users = [user for user in validated_data["users"] if user.id not in existing_user_ids]
instance = super().update(instance, validated_data)

if new_users:
transaction.on_commit(
lambda: send_eap_share_email.delay(
eap_registration_id=instance.id,
recipient_emails=[user.email for user in new_users],
)
)

return instance


class EAPFileInputSerializer(serializers.Serializer):
file = serializers.ListField(child=serializers.FileField(required=True))

Expand Down
20 changes: 20 additions & 0 deletions eap/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,23 @@ def send_deadline_reminder_email(eap_registration_id: int):
instance.save(update_fields=["deadline_remainder_sent_at"])

return True


@shared_task
def send_eap_share_email(eap_registration_id: int, recipient_emails: list[str]):
instance = EAPRegistration.objects.filter(id=eap_registration_id).first()
if not instance or not recipient_emails:
return None

email_context = get_eap_registration_email_context(instance)
email_subject = f"EAP shared: {instance.country} {instance.disaster_type}"
email_body = render_to_string("email/eap/share_eap.html", email_context)
email_type = "Shared EAP"

send_notification(
subject=email_subject,
recipients=recipient_emails,
html=email_body,
mailtype=email_type,
)
return True
74 changes: 74 additions & 0 deletions eap/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,80 @@ def test_active_eaps(self):
},
)

@mock.patch("eap.serializers.send_eap_share_email.delay")
def test_share_eap(self, send_eap_share_email):
eap_registration = EAPRegistrationFactory.create(
country=self.country,
eap_type=EAPType.SIMPLIFIED_EAP,
national_society=self.national_society,
disaster_type=self.disaster_type,
partners=[self.partner1.id, self.partner2.id],
created_by=self.country_admin,
modified_by=self.country_admin,
status=EAPStatus.UNDER_REVIEW,
)
user1, user2, user3 = UserFactory.create_batch(3)

url = f"/api/v2/eap-registration/{eap_registration.id}/share/"
data = {
"users": [
user1.id,
user3.id,
],
}
self.authenticate()

with self.capture_on_commit_callbacks(execute=True):
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, 200)

# Check if notification email sent
send_eap_share_email.assert_called_with(
eap_registration_id=eap_registration.id,
recipient_emails=[user1.email, user3.email],
)

# Check if the users has been added
eap_registration.refresh_from_db()
self.assertEqual(eap_registration.users.count(), 2)

# Test removing a user
data = {
"users": [
user1.id,
user2.id,
],
}

with self.capture_on_commit_callbacks(execute=True):
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, 200)
eap_registration.refresh_from_db()
self.assertEqual(eap_registration.users.count(), 2, response.data)

# Check notification email sent again with only updated user
send_eap_share_email.assert_called_with(
eap_registration_id=eap_registration.id,
recipient_emails=[user2.email],
)

# NOTE: test list of EAP Share Users
url = "/api/v2/eap-share-users/"
self.authenticate()
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], 1, response.data)
returned_user_ids = [user["id"] for user in response.data["results"][0]["users_details"]]
# count should be 2
self.assertEqual(len(returned_user_ids), 2)

# NOTE: test with filter by EAP Registration Id
url = f"/api/v2/eap-share-users/?id={eap_registration.id}"
self.authenticate()
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], 1, response.data)


class EAPSimplifiedTestCase(APITestCase):
def setUp(self):
Expand Down
23 changes: 23 additions & 0 deletions eap/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ def get_file_url(file_obj):
return file_obj.file.url


def get_share_eap_email_context(instance):
from eap.serializers import EAPRegistrationSerializer

eap_registration_data = EAPRegistrationSerializer(instance).data

# NOTE: Matching the FRONTEND URLs with the email reference links
if instance.get_eap_type_enum == EAPType.SIMPLIFIED_EAP:
eap_type = "simplified"
elif instance.get_eap_type_enum == EAPType.FULL_EAP:
eap_type = "full"
else:
eap_type = None

return {
"registration_id": eap_registration_data["id"],
"country_name": eap_registration_data["country_details"]["name"],
"disaster_type": eap_registration_data["disaster_type_details"]["name"],
"eap_type": eap_type,
"eap_type_display": eap_registration_data.get("eap_type_display"),
"frontend_url": settings.GO_WEB_URL,
}


def get_eap_registration_email_context(instance):
from eap.serializers import EAPRegistrationSerializer

Expand Down
41 changes: 41 additions & 0 deletions eap/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from eap.filter_set import (
EAPRegistrationFilterSet,
EAPShareUserFilterSet,
FullEAPFilterSet,
SimplifiedEAPFilterSet,
)
Expand All @@ -32,6 +33,7 @@
EAPFileSerializer,
EAPGlobalFilesSerializer,
EAPRegistrationSerializer,
EAPShareUserSerializer,
EAPStatusSerializer,
EAPValidatedBudgetFileSerializer,
FullEAPSerializer,
Expand Down Expand Up @@ -169,6 +171,45 @@ def upload_validated_budget_file(
serializer.save()
return response.Response(serializer.data)

@extend_schema(
request=EAPShareUserSerializer,
responses={200: None},
)
@action(
detail=True,
url_path="share",
methods=["post"],
serializer_class=EAPShareUserSerializer,
permission_classes=[permissions.IsAuthenticated, DenyGuestUserPermission],
)
def share(
self,
request,
id: int,
):
eap_registration: EAPRegistration = self.get_object()
serializer = EAPShareUserSerializer(
eap_registration,
data=request.data,
context={"request": request},
)
serializer.is_valid(raise_exception=True)
serializer.save()
return response.Response(status=status.HTTP_200_OK)


class EAPShareUserViewSet(
viewsets.ReadOnlyModelViewSet,
):
queryset = EAPRegistration.objects.all()
lookup_field = "id"
serializer_class = EAPShareUserSerializer
permission_classes = [permissions.IsAuthenticated, DenyGuestUserPermission]
filterset_class = EAPShareUserFilterSet

def get_queryset(self) -> QuerySet[EAPRegistration]:
return super().get_queryset().prefetch_related("users").order_by("-created_at").distinct()


class SimplifiedEAPViewSet(EAPModelViewSet):
queryset = SimplifiedEAP.objects.all()
Expand Down
1 change: 1 addition & 0 deletions main/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
router.register(r"full-eap", eap_views.FullEAPViewSet, basename="full_eap")
router.register(r"eap-file", eap_views.EAPFileViewSet, basename="eap_file")
router.register(r"eap/global-files", eap_views.EAPGlobalFilesViewSet, basename="eap_global_files")
router.register(r"eap-share-users", eap_views.EAPShareUserViewSet, basename="eap_share_users")

admin.site.site_header = "IFRC Go administration"
admin.site.site_title = "IFRC Go admin"
Expand Down
1 change: 0 additions & 1 deletion notifications/templates/email/eap/registration.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
</table>

<!-- Signature -->
<p>You can check the progress of this EAP <a href="{{ frontend_url }}/eap-registration/{{ registration_id }}/">here.</a></p>
<p style="margin-top:16px;">
Kind regards,<br>
{{ ns_contact_name }}<br>
Expand Down
21 changes: 21 additions & 0 deletions notifications/templates/email/eap/share_eap.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% include "design/head3.html" %}

<table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#ffffff">
<tr>
<td align="center" class="pb-30" style="padding-bottom:10px; font-family: Arial, sans-serif;">

You have been added to an EAP Application:

{% if eap_type %}
<a href="{{ frontend_url }}/eap/{{ registration_id }}/{{ eap_type }}">
{{ country_name }}: {{ disaster_type }} of type {{ eap_type_display }}
</a>
{% else %}
<strong>{{ country_name }}: {{ disaster_type }}</strong>
{% endif %}

</td>
</tr>
</table>

{% include "design/foot1.html" %}
Loading