Skip to content

Commit

Permalink
Merge pull request #19 from dimagi/pkv/hq-apps-integration
Browse files Browse the repository at this point in the history
Link opportunity to CCHQ Apps
  • Loading branch information
pxwxnvermx authored Jul 11, 2023
2 parents f2135c8 + 5f77551 commit 7617c95
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 29 deletions.
3 changes: 2 additions & 1 deletion commcare_connect/opportunity/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.contrib import admin

from commcare_connect.opportunity.models import Opportunity
from commcare_connect.opportunity.models import CommCareApp, Opportunity

# Register your models here.


admin.site.register(Opportunity)
admin.site.register(CommCareApp)
45 changes: 44 additions & 1 deletion commcare_connect/opportunity/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django import forms

from commcare_connect.opportunity.models import Opportunity
from commcare_connect.opportunity.models import CommCareApp, Opportunity
from commcare_connect.users.models import Organization


class OpportunityChangeForm(forms.ModelForm):
Expand All @@ -13,3 +14,45 @@ class OpportunityCreationForm(forms.ModelForm):
class Meta:
model = Opportunity
fields = ["name", "description"]

learn_app = forms.ChoiceField()
deliver_app = forms.ChoiceField()

def __init__(self, *args, **kwargs):
self.applications = kwargs.pop("applications", [])
self.user = kwargs.pop("user", {})
self.org_slug = kwargs.pop("org_slug", "")
super().__init__(*args, **kwargs)

choices = [(app["id"], app["name"]) for app in self.applications]
self.fields["learn_app"] = forms.ChoiceField(choices=choices)
self.fields["deliver_app"] = forms.ChoiceField(choices=choices)

def clean(self):
cleaned_data = super().clean()
if cleaned_data["learn_app"] == cleaned_data["deliver_app"]:
self.add_error("learn_app", "Learn app and Deliver app cannot be same")
self.add_error("deliver_app", "Learn app and Deliver app cannot be same")

def save(self, commit=True):
for app in self.applications:
if app["id"] == self.cleaned_data["learn_app"]:
self.instance.learn_app, _ = CommCareApp.objects.get_or_create(
cc_app_id=app["id"],
name=app["name"],
cc_domain=app["domain"],
defaults={"created_by": self.user.email, "modified_by": self.user.email},
)

if app["id"] == self.cleaned_data["deliver_app"]:
self.instance.deliver_app, _ = CommCareApp.objects.get_or_create(
cc_app_id=app["id"],
name=app["name"],
cc_domain=app["domain"],
defaults={"created_by": self.user.email, "modified_by": self.user.email},
)

self.instance.created_by = self.user.email
self.instance.modified_by = self.user.email
self.instance.organization = Organization.objects.filter(slug=self.org_slug).first()
return super().save(commit=commit)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.1 on 2023-06-22 11:50

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("opportunity", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="CommCareApp",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_by", models.CharField(max_length=255)),
("modified_by", models.CharField(max_length=255)),
("date_created", models.DateTimeField(auto_now_add=True)),
("date_modified", models.DateTimeField(auto_now=True)),
("cc_domain", models.CharField(max_length=255)),
("cc_app_id", models.CharField(max_length=50)),
("name", models.CharField(max_length=255)),
("description", models.TextField()),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="opportunity",
name="deliver_app",
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to="opportunity.commcareapp"
),
),
migrations.AddField(
model_name="opportunity",
name="learn_app",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="learn_app_opportunities",
to="opportunity.commcareapp",
),
),
]
27 changes: 26 additions & 1 deletion commcare_connect/opportunity/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
from django.db import models

from commcare_connect.users.models import BaseModel, Organization
from commcare_connect.users.models import Organization
from commcare_connect.utils.db import BaseModel


class CommCareApp(BaseModel):
cc_domain = models.CharField(max_length=255)
cc_app_id = models.CharField(max_length=50)
name = models.CharField(max_length=255)
description = models.TextField()

def __str__(self):
return self.name


class Opportunity(BaseModel):
Expand All @@ -13,3 +24,17 @@ class Opportunity(BaseModel):
name = models.CharField(max_length=255)
description = models.TextField()
active = models.BooleanField(default=True)
learn_app = models.ForeignKey(
CommCareApp,
on_delete=models.CASCADE,
related_name="learn_app_opportunities",
null=True,
)
deliver_app = models.ForeignKey(
CommCareApp,
on_delete=models.CASCADE,
null=True,
)

def __str__(self):
return self.name
35 changes: 20 additions & 15 deletions commcare_connect/opportunity/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.generic import CreateView, ListView, UpdateView

from commcare_connect.opportunity.forms import OpportunityChangeForm, OpportunityCreationForm
from commcare_connect.opportunity.models import Opportunity
from commcare_connect.users.models import Organization
from commcare_connect.utils.commcarehq_api import get_applications_for_user


@method_decorator(login_required, name="dispatch")
class OpportunityList(ListView):
class OrganizationUserMixin(LoginRequiredMixin, UserPassesTestMixin):
def test_func(self):
return self.request.user.organizations.filter(organization__slug=self.kwargs.get("org_slug")).exists()


class OpportunityList(OrganizationUserMixin, ListView):
model = Opportunity
paginate_by = 10

Expand All @@ -22,28 +25,30 @@ def get_queryset(self):
return Opportunity.objects.filter(organization__slug=self.kwargs["org_slug"])


@method_decorator(login_required, name="dispatch")
class OpportunityCreate(CreateView):
class OpportunityCreate(OrganizationUserMixin, CreateView):
template_name = "opportunity/opportunity_create.html"
form_class = OpportunityCreationForm

def get_success_url(self):
return reverse("opportunity:list", args=(self.kwargs.get("org_slug"),))

def form_valid(self, form):
form.instance.created_by = self.request.user.email
form.instance.modified_by = self.request.user.email
form.instance.organization = Organization.objects.filter(slug=self.kwargs["org_slug"]).first()
return super().form_valid(form)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "Create new opportunity"
return context

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["applications"] = get_applications_for_user(self.request.user)
kwargs["user"] = self.request.user
kwargs["org_slug"] = self.kwargs.get("org_slug")
return kwargs

def test_func(self):
return self.request.user.organizations.filter(organization__slug=self.kwargs.get("org_slug")).exists()


@method_decorator(login_required, name="dispatch")
class OpportunityEdit(UpdateView):
class OpportunityEdit(OrganizationUserMixin, UpdateView):
model = Opportunity
template_name = "opportunity/opportunity_create.html"
form_class = OpportunityChangeForm
Expand Down
12 changes: 1 addition & 11 deletions commcare_connect/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,7 @@
from django.utils.translation import gettext_lazy as _

from commcare_connect.users.managers import UserManager
from commcare_connect.utils.db import slugify_uniquely


class BaseModel(models.Model):
created_by = models.CharField(max_length=255)
modified_by = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)

class Meta:
abstract = True
from commcare_connect.utils.db import BaseModel, slugify_uniquely


class User(AbstractUser):
Expand Down
62 changes: 62 additions & 0 deletions commcare_connect/utils/commcarehq_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import datetime

import requests
from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
from django.conf import settings
from django.utils import timezone


def refresh_access_token(user):
social_app = SocialApp.objects.filter(provider="commcarehq").first()
social_acc = SocialAccount.objects.filter(user=user).first()
social_token = SocialToken.objects.filter(account=social_acc).first()

if social_token.expires_at > timezone.now():
return social_token

response = requests.post(
f"{settings.COMMCARE_HQ_URL}/oauth/token/",
data={
"grant_type": "refresh_token",
"client_id": social_app.client_id,
"client_secret": social_app.secret,
"refresh_token": social_token.token_secret,
},
)
data = response.json()

if data.get("access_token", ""):
social_token.token = data["access_token"]
social_token.token_secret = data["refresh_token"]
social_token.expires_at = timezone.now() + datetime.timedelta(seconds=900)
social_token.save()

return social_token


def get_domains_for_user(user):
social_token = refresh_access_token(user)
response = requests.get(
f"{settings.COMMCARE_HQ_URL}/api/v0.5/user_domains/",
headers={"Authorization": f"Bearer {social_token}"},
)
data = response.json()
domains = [domain["domain_name"] for domain in data["objects"]]
return domains


def get_applications_for_user(user):
social_token = refresh_access_token(user)
domains = get_domains_for_user(user)
applications = []

for domain in domains:
response = requests.get(
f"{settings.COMMCARE_HQ_URL}/a/{domain}/api/v0.5/application/",
headers={"Authorization": f"Bearer {social_token}"},
)
data = response.json()
for application in data.get("objects", []):
applications.append({"id": application.get("id"), "name": application.get("name"), "domain": domain})

return applications
11 changes: 11 additions & 0 deletions commcare_connect/utils/db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
from django.db import models
from django.utils.text import slugify


class BaseModel(models.Model):
created_by = models.CharField(max_length=255)
modified_by = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)

class Meta:
abstract = True


def slugify_uniquely(value, model, slugfield="slug"):
"""Returns a slug on a name which is unique within a model's table
Taken from https://code.djangoproject.com/wiki/SlugifyUniquely
Expand Down
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@
SOCIALACCOUNT_ADAPTER = "commcare_connect.users.adapters.SocialAccountAdapter"
# https://django-allauth.readthedocs.io/en/latest/forms.html
SOCIALACCOUNT_FORMS = {"signup": "commcare_connect.users.forms.UserSocialSignupForm"}
SOCIALACCOUNT_STORE_TOKENS = True

# django-rest-framework
# -------------------------------------------------------------------------------
Expand Down

0 comments on commit 7617c95

Please sign in to comment.