Skip to content

Commit

Permalink
add user invite and acceptance workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
calellowitz committed Sep 12, 2023
1 parent d1189c2 commit bad92b5
Show file tree
Hide file tree
Showing 13 changed files with 153 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.5 on 2023-09-12 15:11

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("opportunity", "0015_remove_opportunityaccess_date_claimed"),
]

operations = [
migrations.AddField(
model_name="opportunityaccess",
name="accepted",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="opportunityaccess",
name="invite_id",
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddIndex(
model_name="opportunityaccess",
index=models.Index(fields=["invite_id"], name="opportunity_invite__bc4919_idx"),
),
]
5 changes: 5 additions & 0 deletions commcare_connect/opportunity/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ class OpportunityAccess(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE)
date_learn_started = models.DateTimeField(null=True)
accepted = models.BooleanField(default=False)
invite_id = models.CharField(max_length=50, null=True, blank=True)

class Meta:
indexes = [models.Index(fields=["invite_id"])]

# TODO: Convert to a field and calculate this property CompletedModule is saved
@property
Expand Down
6 changes: 4 additions & 2 deletions commcare_connect/opportunity/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from commcare_connect.opportunity.export import export_user_visit_data
from commcare_connect.opportunity.forms import DateRanges
from commcare_connect.opportunity.models import LearnModule, Opportunity, OpportunityAccess, VisitValidationStatus
from commcare_connect.users.helpers import invite_user
from commcare_connect.users.models import User
from config import celery_app

Expand Down Expand Up @@ -38,8 +39,9 @@ def add_connect_users(user_list: list[str], opportunity_id: str):
)
data = result.json()
for user in data["found_users"]:
u, _ = User.objects.get_or_create(username=user["username"])
OpportunityAccess.objects.get_or_create(user=u, opportunity_id=opportunity_id)
u, _ = User.objects.get_or_create(username=user["username"], phone_number=user["phone_number"], name=user["name"])
opportunity_access = OpportunityAccess.objects.get_or_create(user=u, opportunity_id=opportunity_id)
invite_user(user, opportunity_access)


@celery_app.task()
Expand Down
9 changes: 6 additions & 3 deletions commcare_connect/opportunity/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ def test_add_connect_user(self):
with mock.patch("commcare_connect.opportunity.tasks.requests.get") as request:
request.return_value.json.return_value = {
"found_users": [
{"username": "test", "phone_number": "+15555555555"},
{"username": "test2", "phone_number": "+12222222222"},
{"username": "test", "phone_number": "+15555555555", "name": "a"},
{"username": "test2", "phone_number": "+12222222222", "name": "b"},
]
}
add_connect_users(["+15555555555", "+12222222222"], opportunity.id)

user = User.objects.filter(username="test")
user_list = User.objects.filter(username="test")
assert len(user) == 1
user = user_list[0]
assert user.name == "a"
assert user.phone_number == "+15555555555"
assert len(OpportunityAccess.objects.filter(user=user.first(), opportunity=opportunity)) == 1

user2 = User.objects.filter(username="test2")
Expand Down
2 changes: 2 additions & 0 deletions commcare_connect/opportunity/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
OpportunityUserLearnProgress,
OpportunityUserTableView,
OpportunityUserVisitTableView,
accept_invite,
download_visit_export,
export_user_visits,
update_visit_status_import,
Expand All @@ -31,4 +32,5 @@
view=OpportunityUserLearnProgress.as_view(),
name="user_learn_progress",
),
path("accept_invite/<slug:invite_id>", view=accept_invite, name="accept_invite"),
]
11 changes: 11 additions & 0 deletions commcare_connect/opportunity/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,14 @@ def update_visit_status_import(request, org_slug=None, pk=None):
message += status.get_missing_message()
messages.success(request, mark_safe(message))
return redirect("opportunity:detail", org_slug, pk)


@require_GET
def accept_invite(request, invite_id):
try:
o = OpportunityAccess.objects.get(invite_id=invite_id)
except OpportunityAccess.DoesNotExist:
return HttpResponse("This link is invalid. Please try again", status=404)
o.accepted = True
o.save()
return HttpResponse("Thank you for accepting the invitation. Open your CommCare Connect App to see more information about the opportunity and begin learning")
14 changes: 14 additions & 0 deletions commcare_connect/users/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from uuid import uuid4

import requests
from twilio.rest import Client

from django.conf import settings

from commcare_connect.organization.models import Organization
from commcare_connect.utils.sms import send_sms


def get_organization_for_request(request, view_kwargs):
Expand Down Expand Up @@ -32,3 +37,12 @@ def create_hq_user(user, domain, api_key):
if hq_request.status_code == 201:
return True
return False


def invite_user(user, opportunity_access):
invite_id = uuid4()
opportunity_access.invite_id = invite_id
opportunity_access.save()
url = "https://connect.dimagi.com/opportunity/accept_invite/{invite_id}"
body = f"You have been invited to a new job in Commcare Connect. Click the following link to share your information with the project and find out more {url}"
send_sms(user.phone_number, body)
18 changes: 18 additions & 0 deletions commcare_connect/users/migrations/0010_user_phone_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-09-12 15:11

from django.db import migrations
import phonenumber_field.modelfields


class Migration(migrations.Migration):
dependencies = [
("users", "0009_alter_user_options_alter_user_email_and_more"),
]

operations = [
migrations.AddField(
model_name="user",
name="phone_number",
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None),
),
]
3 changes: 3 additions & 0 deletions commcare_connect/users/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from phonenumber_field.modelfields import PhoneNumberField

from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.db import models
Expand Down Expand Up @@ -33,6 +35,7 @@ class User(AbstractUser):
},
null=True,
)
phone_number = PhoneNumberField(null=True, blank=True)

USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
Expand Down
10 changes: 10 additions & 0 deletions commcare_connect/utils/sms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import twilio.rest import Client
from django.conf import settings

def send_sms(to, body):
client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
message = client.messages.create(
body=body,
to=to,
messaging_service_sid=settings.TWILIO_MESSAGING_SERVICE
)
7 changes: 7 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"drf_spectacular",
"oauth2_provider",
"django_tables2",
"phonenumber_field",
]

LOCAL_APPS = [
Expand Down Expand Up @@ -315,3 +316,9 @@
CONNECTID_CLIENT_SECRET,
),
}

BASE_ADDRESS = env("BASE_ADDRESS", default="http://localhost:8000")

TWILIO_ACCOUNT_SID = env("TWILIO_SID", default=None)
TWILIO_AUTH_TOKEN = env("TWILIO_TOKEN", default=None)
TWILIO_MESSAGING_SERVICE = env("TWILIO_MESSAGING_SERVICE", default=None)
2 changes: 2 additions & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jsonpath-ng
quickcache
tablib[xlsx]
flatten-dict
twilio

# Django
# ------------------------------------------------------------------------------
Expand All @@ -20,6 +21,7 @@ django-model-utils
django-allauth
django-celery-beat
django-crispy-forms
django-phonenumber-field[phonenumberslite]
crispy-bootstrap5
django-redis
# Django REST Framework
Expand Down
49 changes: 45 additions & 4 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/base.txt requirements/base.in
# pip-compile --allow-unsafe --config=pyproject.toml --output-file=requirements/base.txt requirements/base.in
#
aiohttp==3.8.5
# via
# aiohttp-retry
# twilio
aiohttp-retry==2.8.3
# via twilio
aiosignal==1.3.1
# via aiohttp
amqp==5.1.1
# via kombu
anyio==3.7.1
Expand All @@ -14,8 +22,13 @@ argon2-cffi-bindings==21.2.0
# via argon2-cffi
asgiref==3.7.2
# via django
async-timeout==4.0.3
# via
# aiohttp
# redis
attrs==23.1.0
# via
# aiohttp
# jsonschema
# referencing
billiard==4.1.0
Expand All @@ -34,7 +47,9 @@ cffi==1.15.1
# argon2-cffi-bindings
# cryptography
charset-normalizer==3.2.0
# via requests
# via
# aiohttp
# requests
click==8.1.6
# via
# celery
Expand Down Expand Up @@ -71,6 +86,7 @@ django==4.2.5
# django-crispy-forms
# django-model-utils
# django-oauth-toolkit
# django-phonenumber-field
# django-redis
# django-tables2
# django-timezone-field
Expand All @@ -92,6 +108,8 @@ django-model-utils==4.3.1
# via -r requirements/base.in
django-oauth-toolkit==2.3.0
# via -r requirements/base.in
django-phonenumber-field[phonenumberslite]==7.1.0
# via -r requirements/base.in
django-redis==5.3.0
# via -r requirements/base.in
django-tables2==2.6.0
Expand All @@ -106,8 +124,14 @@ drf-spectacular==0.26.4
# via -r requirements/base.in
et-xmlfile==1.1.0
# via openpyxl
exceptiongroup==1.1.3
# via anyio
flatten-dict==0.4.2
# via -r requirements/base.in
frozenlist==1.4.0
# via
# aiohttp
# aiosignal
h11==0.14.0
# via httpcore
h2==4.1.0
Expand All @@ -127,6 +151,7 @@ idna==3.4
# anyio
# httpx
# requests
# yarl
inflection==0.5.1
# via drf-spectacular
jsonpath-ng==1.5.3
Expand All @@ -139,12 +164,18 @@ jwcrypto==1.5.0
# via django-oauth-toolkit
kombu==5.3.2
# via celery
multidict==6.0.4
# via
# aiohttp
# yarl
oauthlib==3.2.2
# via
# django-oauth-toolkit
# requests-oauthlib
openpyxl==3.1.2
# via tablib
phonenumberslite==8.13.20
# via django-phonenumber-field
pillow==10.0.0
# via -r requirements/base.in
ply==3.11
Expand All @@ -154,7 +185,9 @@ prompt-toolkit==3.0.39
pycparser==2.21
# via cffi
pyjwt[crypto]==2.8.0
# via django-allauth
# via
# django-allauth
# twilio
python-crontab==3.0.0
# via django-celery-beat
python-dateutil==2.8.2
Expand All @@ -169,6 +202,7 @@ pytz==2023.3
# via
# django-timezone-field
# djangorestframework
# twilio
pyyaml==6.0.1
# via drf-spectacular
quickcache==0.5.4
Expand All @@ -186,6 +220,7 @@ requests==2.31.0
# django-allauth
# django-oauth-toolkit
# requests-oauthlib
# twilio
requests-oauthlib==1.3.1
# via django-allauth
rpds-py==0.9.2
Expand All @@ -209,6 +244,10 @@ tablib[xlsx]==3.5.0
# via -r requirements/base.in
text-unidecode==1.3
# via python-slugify
twilio==8.8.0
# via -r requirements/base.in
typing-extensions==4.7.1
# via asgiref
tzdata==2023.3
# via
# celery
Expand All @@ -230,3 +269,5 @@ whitenoise==6.5.0
# via -r requirements/base.in
wrapt==1.15.0
# via deprecated
yarl==1.9.2
# via aiohttp

0 comments on commit bad92b5

Please sign in to comment.