From a183001ad33c692330ebf3c020208f12fef724f2 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 19 Nov 2024 21:54:10 +0530 Subject: [PATCH 1/9] added resend invite flow --- .../0061_userinvite_notification_date.py | 17 ++++++++++++ commcare_connect/opportunity/models.py | 1 + commcare_connect/opportunity/tables.py | 23 +++++++++++----- commcare_connect/opportunity/tasks.py | 27 +++++++++++-------- commcare_connect/opportunity/urls.py | 2 ++ commcare_connect/opportunity/views.py | 26 ++++++++++++++++++ commcare_connect/static/js/project.js | 9 +++++++ .../opportunity/opportunity_detail.html | 21 +++++++++++++++ 8 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 commcare_connect/opportunity/migrations/0061_userinvite_notification_date.py diff --git a/commcare_connect/opportunity/migrations/0061_userinvite_notification_date.py b/commcare_connect/opportunity/migrations/0061_userinvite_notification_date.py new file mode 100644 index 00000000..e91f8a41 --- /dev/null +++ b/commcare_connect/opportunity/migrations/0061_userinvite_notification_date.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2024-11-19 05:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("opportunity", "0060_completedwork_payment_date"), + ] + + operations = [ + migrations.AddField( + model_name="userinvite", + name="notification_date", + field=models.DateTimeField(null=True), + ), + ] diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index cf763f5d..35a30ef2 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -656,6 +656,7 @@ class UserInvite(models.Model): opportunity_access = models.OneToOneField(OpportunityAccess, on_delete=models.CASCADE, null=True, blank=True) message_sid = models.CharField(max_length=50, null=True, blank=True) status = models.CharField(max_length=50, choices=UserInviteStatus.choices, default=UserInviteStatus.invited) + notification_date = models.DateTimeField(null=True) class FormJsonValidationRules(models.Model): diff --git a/commcare_connect/opportunity/tables.py b/commcare_connect/opportunity/tables.py index e86969a8..af270325 100644 --- a/commcare_connect/opportunity/tables.py +++ b/commcare_connect/opportunity/tables.py @@ -178,17 +178,26 @@ def render_display_name(self, record): return record.opportunity_access.display_name def render_view_profile(self, record): - invite_delete_url = reverse( - "opportunity:user_invite_delete", - args=(self.org_slug, record.opportunity.id, record.id), - ) if not getattr(record.opportunity_access, "accepted", False): + invite_delete_url = reverse( + "opportunity:user_invite_delete", + args=(self.org_slug, record.opportunity.id, record.id), + ) + resend_invite_url = invite_delete_url = reverse( + "opportunity:resend_user_invite", + args=(self.org_slug, record.opportunity.id, record.id), + ) return format_html( ( - '' + """
+ + +
""" ), + resend_invite_url, invite_delete_url, ) url = reverse( diff --git a/commcare_connect/opportunity/tasks.py b/commcare_connect/opportunity/tasks.py index b5450cc1..6b5efafc 100644 --- a/commcare_connect/opportunity/tasks.py +++ b/commcare_connect/opportunity/tasks.py @@ -13,7 +13,7 @@ from tablib import Dataset from commcare_connect.connect_id_client import fetch_users, filter_users, send_message, send_message_bulk -from commcare_connect.connect_id_client.models import Message +from commcare_connect.connect_id_client.models import ConnectIdUser, Message from commcare_connect.opportunity.app_xml import get_connect_blocks_for_app, get_deliver_units_for_app from commcare_connect.opportunity.export import ( export_catchment_area_table, @@ -85,16 +85,20 @@ def add_connect_users( status=UserInviteStatus.not_found, ) for user in found_users: - u, _ = User.objects.update_or_create( - username=user.username, defaults={"phone_number": user.phone_number, "name": user.name} - ) - opportunity_access, _ = OpportunityAccess.objects.get_or_create(user=u, opportunity_id=opportunity_id) - UserInvite.objects.update_or_create( - opportunity_id=opportunity_id, - phone_number=user.phone_number, - defaults={"opportunity_access": opportunity_access}, - ) - invite_user.delay(u.pk, opportunity_access.pk) + update_user_and_send_invite(user, opportunity_id) + + +def update_user_and_send_invite(user: ConnectIdUser, opp_id): + u, _ = User.objects.update_or_create( + username=user.username, defaults={"phone_number": user.phone_number, "name": user.name} + ) + opportunity_access, _ = OpportunityAccess.objects.get_or_create(user=u, opportunity_id=opp_id) + UserInvite.objects.update_or_create( + opportunity_id=opp_id, + phone_number=user.phone_number, + defaults={"opportunity_access": opportunity_access}, + ) + invite_user.delay(u.pk, opportunity_access.pk) @celery_app.task() @@ -115,6 +119,7 @@ def invite_user(user_id, opportunity_access_id): opportunity_access=opportunity_access, defaults={ "message_sid": sms_status.sid, + "notification_date": now() if sms_status.sid else None, "status": UserInviteStatus.accepted if opportunity_access.accepted else UserInviteStatus.invited, }, ) diff --git a/commcare_connect/opportunity/urls.py b/commcare_connect/opportunity/urls.py index ef5cae31..15a988f2 100644 --- a/commcare_connect/opportunity/urls.py +++ b/commcare_connect/opportunity/urls.py @@ -38,6 +38,7 @@ payment_import, payment_report, reject_visit, + resend_user_invite, revoke_user_suspension, send_message_mobile_users, suspend_user, @@ -113,4 +114,5 @@ path("/invoice/create/", views.invoice_create, name="invoice_create"), path("/invoice/approve/", views.invoice_approve, name="invoice_approve"), path("/user_invite_delete//", views.user_invite_delete, name="user_invite_delete"), + path("/resend_invite/", resend_user_invite, name="resend_user_invite"), ] diff --git a/commcare_connect/opportunity/views.py b/commcare_connect/opportunity/views.py index 185f8b88..fdcc87de 100644 --- a/commcare_connect/opportunity/views.py +++ b/commcare_connect/opportunity/views.py @@ -24,6 +24,7 @@ from django_tables2.export import TableExport from geopy import distance +from commcare_connect.connect_id_client import fetch_users from commcare_connect.form_receiver.serializers import XFormSerializer from commcare_connect.opportunity.api.serializers import remove_opportunity_access_cache from commcare_connect.opportunity.forms import ( @@ -65,6 +66,7 @@ PaymentInvoice, PaymentUnit, UserInvite, + UserInviteStatus, UserVisit, VisitReviewStatus, VisitValidationStatus, @@ -92,8 +94,10 @@ generate_user_status_export, generate_visit_export, generate_work_status_export, + invite_user, send_push_notification_task, send_sms_task, + update_user_and_send_invite, ) from commcare_connect.opportunity.visit_import import ( ImportException, @@ -1233,3 +1237,25 @@ def user_invite_delete(request, org_slug, opp_id, pk): invite = get_object_or_404(UserInvite, pk=pk, opportunity=opportunity) invite.delete() return HttpResponse(status=200, headers={"HX-Trigger": "userStatusReload"}) + + +@org_admin_required +@require_POST +@csrf_exempt +def resend_user_invite(request, org_slug, opp_id, pk): + user_invite = get_object_or_404(UserInvite, id=pk) + + if user_invite.status == UserInviteStatus.not_found: + found_user_list = fetch_users([user_invite.phone_number]) + if not found_user_list: + return HttpResponse("The user is not registered on Connect ID yet. Please ask them to sign up first.") + + update_user_and_send_invite(found_user_list[0], opp_id=pk) + + if user_invite.notification_date and (now() - user_invite.notification_date) < datetime.timedelta(days=1): + return HttpResponse("You can only send one invitation per user every 24 hours. Please try again later.") + + user = User.objects.get(phone_number=user_invite.phone_number) + access, _ = OpportunityAccess.objects.get_or_create(user=user, opportunity_id=pk) + invite_user.delay(user.id, access.pk) + return HttpResponse("The invitation has been successfully resent to the user.") diff --git a/commcare_connect/static/js/project.js b/commcare_connect/static/js/project.js index 83ba289b..05b31f49 100644 --- a/commcare_connect/static/js/project.js +++ b/commcare_connect/static/js/project.js @@ -15,6 +15,15 @@ function refreshTooltips() { } window.refreshTooltips = refreshTooltips; +function handleResendInviteResponse(event) { + const response = event.detail.elt; + const resendModal = new bootstrap.Modal( + document.getElementById('resendInviteModal'), + ); + resendModal.show(); +} +window.handleResendInviteResponse = handleResendInviteResponse; + window.mapboxgl = mapboxgl; window.circle = circle; diff --git a/commcare_connect/templates/opportunity/opportunity_detail.html b/commcare_connect/templates/opportunity/opportunity_detail.html index b1aef96d..6df5e55a 100644 --- a/commcare_connect/templates/opportunity/opportunity_detail.html +++ b/commcare_connect/templates/opportunity/opportunity_detail.html @@ -391,6 +391,8 @@

+ + {% endblock content %} {% block modal %} @@ -568,4 +570,23 @@

{% translate "Import Catchment Areas" %} + + + + {% endblock modal %} From f72eddb021e8ca303c1e5a30bcbdb1eeebdf5845 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 26 Nov 2024 11:50:55 +0530 Subject: [PATCH 2/9] fixed the flow issues --- commcare_connect/opportunity/views.py | 18 ++++++++++-------- .../opportunity/opportunity_detail.html | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/commcare_connect/opportunity/views.py b/commcare_connect/opportunity/views.py index fdcc87de..dc1f18bd 100644 --- a/commcare_connect/opportunity/views.py +++ b/commcare_connect/opportunity/views.py @@ -1245,17 +1245,19 @@ def user_invite_delete(request, org_slug, opp_id, pk): def resend_user_invite(request, org_slug, opp_id, pk): user_invite = get_object_or_404(UserInvite, id=pk) - if user_invite.status == UserInviteStatus.not_found: + if user_invite.notification_date and (now() - user_invite.notification_date) < datetime.timedelta(days=1): + return HttpResponse("You can only send one invitation per user every 24 hours. Please try again later.") + + if user_invite.status != UserInviteStatus.not_found: found_user_list = fetch_users([user_invite.phone_number]) if not found_user_list: return HttpResponse("The user is not registered on Connect ID yet. Please ask them to sign up first.") - update_user_and_send_invite(found_user_list[0], opp_id=pk) - - if user_invite.notification_date and (now() - user_invite.notification_date) < datetime.timedelta(days=1): - return HttpResponse("You can only send one invitation per user every 24 hours. Please try again later.") + connect_user = found_user_list[0] + update_user_and_send_invite(connect_user, opp_id=pk) + else: + user = User.objects.get(phone_number=user_invite.phone_number) + access, _ = OpportunityAccess.objects.get_or_create(user=user, opportunity_id=pk) + invite_user.delay(user.id, access.pk) - user = User.objects.get(phone_number=user_invite.phone_number) - access, _ = OpportunityAccess.objects.get_or_create(user=user, opportunity_id=pk) - invite_user.delay(user.id, access.pk) return HttpResponse("The invitation has been successfully resent to the user.") diff --git a/commcare_connect/templates/opportunity/opportunity_detail.html b/commcare_connect/templates/opportunity/opportunity_detail.html index 6df5e55a..4d813c72 100644 --- a/commcare_connect/templates/opportunity/opportunity_detail.html +++ b/commcare_connect/templates/opportunity/opportunity_detail.html @@ -574,7 +574,7 @@

{% translate "Import Catchment Areas" %}