Skip to content

Commit

Permalink
Merge pull request #181 from dimagi/pkv/push-notification-messages
Browse files Browse the repository at this point in the history
Push notification messages
  • Loading branch information
pxwxnvermx authored Nov 8, 2023
2 parents 04ada52 + c73c489 commit 7a0458c
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 4.2.5 on 2023-11-02 09:09

from django.db import migrations
from django_celery_beat.models import PeriodicTask, CrontabSchedule


def create_send_inactive_notification_periodic_task(apps, schema_editor):
schedule, _ = CrontabSchedule.objects.get_or_create(
minute="00",
hour="7",
day_of_week="*",
day_of_month="*",
month_of_year="*",
)
PeriodicTask.objects.update_or_create(
crontab=schedule,
name="send_inactive_notifications",
task="commcare_connect.opportunity.task.send_notification_inactive_users",
)


def delete_send_inactive_notification_periodic_task(apps, schema_editor):
PeriodicTask.objects.get(
name="send_inactive_notifications",
task="commcare_connect.opportunity.task.send_notification_inactive_users",
).delete()


class Migration(migrations.Migration):
dependencies = [
("opportunity", "0025_opportunity_short_description"),
]

operations = [
migrations.RunPython(
create_send_inactive_notification_periodic_task, delete_send_inactive_notification_periodic_task
)
]
71 changes: 70 additions & 1 deletion commcare_connect/opportunity/tasks.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import datetime

from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.utils.timezone import now
from django.utils.translation import gettext

from commcare_connect.connect_id_client import fetch_users
from commcare_connect.connect_id_client import fetch_users, send_message_bulk
from commcare_connect.connect_id_client.models import 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_empty_payment_table, export_user_visit_data
from commcare_connect.opportunity.forms import DateRanges
from commcare_connect.opportunity.models import (
CompletedModule,
DeliverUnit,
LearnModule,
Opportunity,
OpportunityAccess,
OpportunityClaim,
UserVisit,
VisitValidationStatus,
)
from commcare_connect.users.helpers import invite_user
from commcare_connect.users.models import User
from commcare_connect.utils.datetime import is_date_before
from config import celery_app


Expand Down Expand Up @@ -73,3 +81,64 @@ def generate_payment_export(opportunity_id: int, export_format: str):
content = content.encode()
default_storage.save(export_tmp_name, ContentFile(content))
return export_tmp_name


@celery_app.task()
def send_notification_inactive_users():
opportunity_accesses = OpportunityAccess.objects.filter(
opportunity__active=True,
opportunity__end_date__gt=datetime.date.today(),
).select_related("opportunity")
messages = []
for access in opportunity_accesses:
message = _get_inactive_message(access)
if message:
messages.append(message)
send_message_bulk(messages)


def _get_inactive_message(access: OpportunityAccess):
has_claimed_opportunity = OpportunityClaim.objects.filter(opportunity_access=access).exists()
if has_claimed_opportunity:
message = _check_deliver_inactive(access)
else:
# Send notification if user has completed learn modules and has not claimed the opportunity
if access.learn_progress == 100:
message = _get_deliver_message(access)
else:
message = _get_learn_message(access)
return message


def _get_learn_message(access: OpportunityAccess):
last_user_learn_module = (
CompletedModule.objects.filter(user=access.user, opportunity=access.opportunity).order_by("date").last()
)
if last_user_learn_module and is_date_before(last_user_learn_module.date, days=3):
return Message(
usernames=[access.user.username],
title=gettext(f"Resume your learning journey for {access.opportunity.name}"),
body=gettext(
f"You have not completed your learning for {access.opportunity.name}."
"Please complete the learning modules to start delivering visits."
),
)


def _check_deliver_inactive(access: OpportunityAccess):
last_user_deliver_visit = (
UserVisit.objects.filter(user=access.user, opportunity=access.opportunity).order_by("visit_date").last()
)
if last_user_deliver_visit and is_date_before(last_user_deliver_visit.visit_date, days=2):
return _get_deliver_message(access)


def _get_deliver_message(access: OpportunityAccess):
return Message(
usernames=[access.user.username],
title=gettext(f"Resume your job for {access.opportunity.name}"),
body=gettext(
f"You have not completed your delivery visits for {access.opportunity.name}."
"To maximise your payout complete all the required service delivery."
),
)
92 changes: 88 additions & 4 deletions commcare_connect/opportunity/tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
import datetime
from unittest import mock

import pytest

from commcare_connect.connect_id_client.models import ConnectIdUser
from commcare_connect.opportunity.models import OpportunityAccess
from commcare_connect.opportunity.tasks import add_connect_users
from commcare_connect.opportunity.tests.factories import OpportunityFactory
from commcare_connect.opportunity.models import Opportunity, OpportunityAccess
from commcare_connect.opportunity.tasks import _get_inactive_message, add_connect_users
from commcare_connect.opportunity.tests.factories import (
CompletedModuleFactory,
LearnModuleFactory,
OpportunityClaimFactory,
OpportunityFactory,
UserVisitFactory,
)
from commcare_connect.users.models import User


class TestConnectUserCreation:
@pytest.mark.django_db
def test_add_connect_user(self):
def test_add_connect_user(self, httpx_mock):
opportunity = OpportunityFactory()
with (
mock.patch("commcare_connect.opportunity.tasks.fetch_users") as fetch_users,
mock.patch("commcare_connect.users.helpers.send_sms"),
):
httpx_mock.add_response(
method="POST",
json={
"all_success": True,
"responses": [
{"username": "test", "status": "success"},
{"username": "test2", "status": "success"},
],
},
)
fetch_users.return_value = [
ConnectIdUser(username="test", phone_number="+15555555555", name="a"),
ConnectIdUser(username="test2", phone_number="+12222222222", name="b"),
Expand All @@ -33,3 +50,70 @@ def test_add_connect_user(self):
user2 = User.objects.filter(username="test2")
assert len(user2) == 1
assert len(OpportunityAccess.objects.filter(user=user2.first(), opportunity=opportunity)) == 1


def test_send_inactive_notification_learn_inactive_message(mobile_user: User, opportunity: Opportunity):
learn_modules = LearnModuleFactory.create_batch(2, app=opportunity.learn_app)
CompletedModuleFactory.create(
date=datetime.datetime.now() - datetime.timedelta(days=3),
user=mobile_user,
opportunity=opportunity,
module=learn_modules[0],
)
access = OpportunityAccess.objects.get(user=mobile_user, opportunity=opportunity)
message = _get_inactive_message(access)
assert message.usernames[0] == mobile_user.username
assert message.title == f"Resume your learning journey for {opportunity.name}"


def test_send_inactive_notification_deliver_inactive_message(mobile_user: User, opportunity: Opportunity):
learn_modules = LearnModuleFactory.create_batch(2, app=opportunity.learn_app)
for learn_module in learn_modules:
CompletedModuleFactory.create(
user=mobile_user,
opportunity=opportunity,
module=learn_module,
date=datetime.datetime.now() - datetime.timedelta(days=2),
)
access = OpportunityAccess.objects.get(user=mobile_user, opportunity=opportunity)
OpportunityClaimFactory.create(opportunity_access=access, end_date=opportunity.end_date)
UserVisitFactory.create(
user=mobile_user, opportunity=opportunity, visit_date=datetime.datetime.now() - datetime.timedelta(days=2)
)

message = _get_inactive_message(access)
assert message.usernames[0] == mobile_user.username
assert message.title == f"Resume your job for {opportunity.name}"


def test_send_inactive_notification_not_claimed_deliver_message(mobile_user: User, opportunity: Opportunity):
learn_modules = LearnModuleFactory.create_batch(2, app=opportunity.learn_app)
for learn_module in learn_modules:
CompletedModuleFactory.create(
user=mobile_user,
opportunity=opportunity,
module=learn_module,
date=datetime.datetime.now() - datetime.timedelta(days=2),
)
access = OpportunityAccess.objects.get(user=mobile_user, opportunity=opportunity)
message = _get_inactive_message(access)
assert message.usernames[0] == mobile_user.username
assert message.title == f"Resume your job for {opportunity.name}"


def test_send_inactive_notification_active_user(mobile_user: User, opportunity: Opportunity):
learn_modules = LearnModuleFactory.create_batch(2, app=opportunity.learn_app)
for learn_module in learn_modules:
CompletedModuleFactory.create(
user=mobile_user,
opportunity=opportunity,
module=learn_module,
date=datetime.datetime.now() - datetime.timedelta(days=2),
)
access = OpportunityAccess.objects.get(user=mobile_user, opportunity=opportunity)
OpportunityClaimFactory.create(opportunity_access=access, end_date=opportunity.end_date)
UserVisitFactory.create(
user=mobile_user, opportunity=opportunity, visit_date=datetime.datetime.now() - datetime.timedelta(days=1)
)
message = _get_inactive_message(access)
assert message is None
21 changes: 19 additions & 2 deletions commcare_connect/opportunity/visit_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@

from django.core.files.uploadedfile import UploadedFile
from django.db import transaction
from django.utils.translation import gettext
from tablib import Dataset

from commcare_connect.connect_id_client import send_message_bulk
from commcare_connect.connect_id_client.models import Message
from commcare_connect.opportunity.models import (
Opportunity,
OpportunityAccess,
Expand Down Expand Up @@ -192,12 +195,26 @@ def _bulk_update_payments(opportunity: Opportunity, imported_data: Dataset) -> P
seen_users = set()
missing_users = set()
with transaction.atomic():
messages = []
usernames = list(payments)
users = OpportunityAccess.objects.filter(user__username__in=usernames, opportunity=opportunity).select_related(
"user"
)
for access in users:
Payment.objects.create(opportunity_access=access, amount=payments[access.user.username])
seen_users.add(access.user.username)
username = access.user.username
amount = payments[username]
Payment.objects.create(opportunity_access=access, amount=amount)
seen_users.add(username)
messages.append(
Message(
usernames=[username],
title=gettext("Payment received"),
body=gettext(
f"You have received a payment of {access.opportunity.currency} {amount} for "
f"{access.opportunity.name}. Click on this notification for more information on the payment."
),
)
)
send_message_bulk(messages)
missing_users = set(usernames) - seen_users
return PaymentImportStatus(seen_users, missing_users)
10 changes: 5 additions & 5 deletions commcare_connect/organization/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ def send_org_invite(membership_id, host_user_id):
invite_url = build_absolute_uri(None, location)
message = f"""Hi,
You have been invited to join {membership.organization.name} on Commcare Connect by {host_user.name}.
The invite can be accepted by visiting the link.
You have been invited to join {membership.organization.name} on Commcare Connect by {host_user.name}.
The invite can be accepted by visiting the link.
{invite_url}
{invite_url}
Thank You,
Commcare Connect"""
Thank You,
Commcare Connect"""
send_mail(
subject=f"{host_user.name} has invite you to join '{membership.organization.name}' on CommCare Connect",
message=message,
Expand Down
13 changes: 13 additions & 0 deletions commcare_connect/users/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from allauth.utils import build_absolute_uri
from django.conf import settings
from django.urls import reverse
from django.utils.translation import gettext

from commcare_connect.connect_id_client import send_message
from commcare_connect.connect_id_client.models import Message
from commcare_connect.organization.models import Organization
from commcare_connect.utils.sms import send_sms

Expand Down Expand Up @@ -48,3 +51,13 @@ def invite_user(user, opportunity_access):
if not user.phone_number:
return
send_sms(user.phone_number, body)
message = Message(
usernames=[user.username],
title=gettext(
f"You have been invited to a CommCare Connect opportunity - {opportunity_access.opportunity.name}"
),
body=gettext(
f"You have been invited to a new job in Commcare Connect - {opportunity_access.opportunity.name}"
),
)
send_message(message)
8 changes: 8 additions & 0 deletions commcare_connect/utils/datetime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import datetime

from django.utils.timezone import now


def is_date_before(date: datetime.datetime, days: int):
before_date = now() - datetime.timedelta(days=days)
return date.date() == before_date.date()

0 comments on commit 7a0458c

Please sign in to comment.