-
-
Notifications
You must be signed in to change notification settings - Fork 221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
KYC Config Page #35716
KYC Config Page #35716
Changes from all commits
a99ebb2
ded9395
db8aa5d
45087b2
ec2da8a
38766c2
bba14f9
ed257df
ec44801
7e2b4e7
8bc0145
a5014e3
7e14fd3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import json | ||
|
||
from django import forms | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from crispy_forms import bootstrap as twbscrispy | ||
from crispy_forms import layout as crispy | ||
from crispy_forms.helper import FormHelper | ||
|
||
from corehq.apps.integration.kyc.models import ( | ||
KycConfig, | ||
KycProviders, | ||
UserDataStore, | ||
) | ||
from corehq.apps.userreports.ui.fields import JsonField | ||
from corehq.motech.models import ConnectionSettings | ||
|
||
|
||
class KycConfigureForm(forms.ModelForm): | ||
|
||
class Meta: | ||
model = KycConfig | ||
fields = [ | ||
'user_data_store', | ||
'other_case_type', | ||
'provider', | ||
'api_field_to_user_data_map', | ||
'connection_settings', | ||
] | ||
|
||
user_data_store = forms.ChoiceField( | ||
label=_('User Data Store'), | ||
required=True, | ||
choices=UserDataStore.CHOICES, | ||
) | ||
other_case_type = forms.CharField( | ||
label=_('Other Case Type'), | ||
required=False, | ||
) | ||
provider = forms.ChoiceField( | ||
label=_('Provider'), | ||
required=True, | ||
choices=KycProviders.choices, | ||
) | ||
api_field_to_user_data_map = JsonField( | ||
label=_('API Field to User Data Map'), | ||
required=True, | ||
expected_type=list, | ||
) | ||
connection_settings = forms.ModelChoiceField( | ||
label=_('Connection Settings'), | ||
required=True, | ||
queryset=None, | ||
) | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.instance = kwargs.pop('instance') | ||
self.fields['connection_settings'].queryset = ConnectionSettings.objects.filter( | ||
domain=self.instance.domain | ||
) | ||
|
||
self.helper = FormHelper() | ||
self.helper.form_tag = False | ||
|
||
self.helper.layout = crispy.Layout( | ||
crispy.Div( | ||
crispy.Field( | ||
'user_data_store', | ||
x_init='user_data_store = $el.value', | ||
x_model='user_data_store', | ||
), | ||
crispy.Div( | ||
'other_case_type', | ||
x_init='other_case_type = $el.value', | ||
x_show='otherCaseTypeChoice === user_data_store', | ||
), | ||
crispy.Div( | ||
'provider', | ||
x_init='provider = $el.value', | ||
x_show='showProvider', | ||
), | ||
crispy.Div( | ||
'api_field_to_user_data_map', | ||
x_init='api_field_to_user_data_map = $el.value', | ||
css_id='api-mapping', | ||
), | ||
crispy.Field( | ||
'connection_settings', | ||
x_init='connection_settings = $el.value', | ||
), | ||
twbscrispy.StrictButton( | ||
_('Save'), | ||
type='submit', | ||
css_class='btn btn-primary', | ||
), | ||
x_data=json.dumps({ | ||
'user_data_store': self.instance.user_data_store, | ||
'showProvider': len(KycProviders.choices) > 1, | ||
'otherCaseTypeChoice': UserDataStore.OTHER_CASE_TYPE, | ||
}), | ||
) | ||
) | ||
|
||
def clean(self): | ||
user_data_store = self.cleaned_data['user_data_store'] | ||
other_case_type = self.cleaned_data['other_case_type'] | ||
if user_data_store == UserDataStore.OTHER_CASE_TYPE and not other_case_type: | ||
self.add_error('other_case_type', _('Please specify a value')) | ||
elif user_data_store != UserDataStore.OTHER_CASE_TYPE: | ||
self.cleaned_data['other_case_type'] = None | ||
return self.cleaned_data |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
from django.test import TestCase | ||
from django.urls import reverse | ||
|
||
from corehq.apps.domain.shortcuts import create_domain | ||
from corehq.apps.integration.kyc.views import KycConfigurationView | ||
from corehq.apps.users.models import WebUser | ||
from corehq.util.test_utils import flag_enabled | ||
|
||
|
||
class TestKycConfigurationView(TestCase): | ||
domain = 'test-domain' | ||
urlname = KycConfigurationView.urlname | ||
|
||
@classmethod | ||
def setUpClass(cls): | ||
super().setUpClass() | ||
cls.domain_obj = create_domain(cls.domain) | ||
cls.username = 'test-user' | ||
cls.password = '1234' | ||
cls.webuser = WebUser.create( | ||
cls.domain, | ||
cls.username, | ||
cls.password, | ||
None, | ||
None, | ||
is_admin=True, | ||
) | ||
cls.webuser.save() | ||
|
||
@classmethod | ||
def tearDownClass(cls): | ||
cls.webuser.delete(None, None) | ||
cls.domain_obj.delete() | ||
super().tearDownClass() | ||
|
||
@property | ||
def endpoint(self): | ||
return reverse(self.urlname, args=(self.domain,)) | ||
|
||
@property | ||
def login_endpoint(self): | ||
return reverse('domain_login', kwargs={'domain': self.domain}) | ||
|
||
def _make_request(self, is_logged_in=True): | ||
if is_logged_in: | ||
self.client.login(username=self.username, password=self.password) | ||
return self.client.get(self.endpoint) | ||
|
||
def test_not_logged_in(self): | ||
response = self._make_request(is_logged_in=False) | ||
self.assertRedirects(response, f'/accounts/login/?next={self.endpoint}') | ||
|
||
def test_ff_not_enabled(self): | ||
response = self._make_request() | ||
self.assertEqual(response.status_code, 404) | ||
|
||
@flag_enabled('KYC_VERIFICATION') | ||
def test_success(self): | ||
response = self._make_request() | ||
self.assertEqual(response.status_code, 200) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from django.urls import re_path as url | ||
|
||
from corehq.apps.integration.kyc.views import KycConfigurationView | ||
|
||
|
||
urlpatterns = [ | ||
url(r'^configure/$', KycConfigurationView.as_view(), | ||
name=KycConfigurationView.urlname), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from django.urls import reverse | ||
from django.utils.decorators import method_decorator | ||
from django.utils.translation import gettext as _ | ||
|
||
from corehq import toggles | ||
from corehq.apps.domain.decorators import login_required | ||
from corehq.apps.domain.views.base import BaseDomainView | ||
from corehq.apps.hqwebapp.decorators import use_bootstrap5 | ||
from corehq.apps.integration.kyc.forms import KycConfigureForm | ||
from corehq.apps.integration.kyc.models import KycConfig | ||
from corehq.util.htmx_action import HqHtmxActionMixin | ||
|
||
|
||
@method_decorator(login_required, name='dispatch') | ||
@method_decorator(use_bootstrap5, name='dispatch') | ||
@method_decorator(toggles.KYC_VERIFICATION.required_decorator(), name='dispatch') | ||
class KycConfigurationView(BaseDomainView, HqHtmxActionMixin): | ||
section_name = _("Data") | ||
urlname = 'kyc_configuration' | ||
template_name = 'kyc/kyc_config_base.html' | ||
page_title = _('KYC Configuration') | ||
|
||
form_class = KycConfigureForm | ||
form_template_partial_name = 'kyc/partials/kyc_config_form_partial.html' | ||
|
||
@property | ||
def page_url(self): | ||
return reverse(self.urlname, args=[self.domain]) | ||
|
||
@property | ||
def section_url(self): | ||
return reverse(self.urlname, args=(self.domain,)) | ||
|
||
@property | ||
def page_context(self): | ||
return { | ||
'kyc_config_form': self.config_form, | ||
} | ||
|
||
@property | ||
def config(self): | ||
try: | ||
# Currently a domain can only save one config so we shouldn't | ||
# expect more than one per domain | ||
return KycConfig.objects.get(domain=self.domain) | ||
except KycConfig.DoesNotExist: | ||
return KycConfig(domain=self.domain) | ||
|
||
@property | ||
def config_form(self): | ||
if self.request.method == 'POST': | ||
return self.form_class(self.request.POST, instance=self.config) | ||
return self.form_class(instance=self.config) | ||
|
||
def post(self, request, *args, **kwargs): | ||
form = self.config_form | ||
show_success = False | ||
if form.is_valid(): | ||
form.save(commit=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I presume the fact that the form is a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is correct. The |
||
show_success = True | ||
|
||
context = { | ||
'kyc_config_form': form, | ||
'show_success': show_success, | ||
} | ||
return self.render_htmx_partial_response(request, self.form_template_partial_name, context) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Generated by Django 4.2.17 on 2025-01-29 08:43 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('integration', '0007_alter_kycconfig_api_field_to_user_data_map'), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name='kycconfig', | ||
name='provider', | ||
field=models.CharField(choices=[('mtn_kyc', 'MTN KYC')], default='mtn_kyc', max_length=25), | ||
), | ||
migrations.AddConstraint( | ||
model_name='kycconfig', | ||
constraint=models.UniqueConstraint(fields=('domain', 'provider',), name='unique_domain_provider'), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import $ from "jquery"; | ||
import baseAce from "hqwebapp/js/base_ace"; | ||
import "hqwebapp/js/htmx_and_alpine"; | ||
|
||
document.body.addEventListener("htmx:afterSwap", function () { | ||
const $jsonField = $("#api-mapping").find("textarea"); | ||
if (!$jsonField) { | ||
return; | ||
} | ||
|
||
// We need to wait for the JSON field to be rendered before initializing the Ace editor | ||
setTimeout(() => { | ||
const jsonWidget = baseAce.initJsonWidget($jsonField); | ||
const fieldVal = $jsonField.val(); | ||
jsonWidget.getSession().setValue(fieldVal); | ||
}, 50); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{% extends "hqwebapp/bootstrap5/base_section.html" %} | ||
{% load hq_shared_tags %} | ||
{% load i18n %} | ||
{% js_entry 'integration/js/kyc/kyc_configure' %} | ||
|
||
{% block page_content %} | ||
<div>{% include 'kyc/partials/kyc_config_form_partial.html' %}</div> | ||
{% endblock %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{% load crispy_forms_tags %} | ||
{% load i18n %} | ||
|
||
<div id="kyc-configure-form"> | ||
{% if show_success %} | ||
<div class="alert alert-success alert-dismissable fade show" role="alert"> | ||
{% blocktrans %} | ||
Configuration saved successfully! | ||
{% endblocktrans %} | ||
<button | ||
type="button" | ||
class="btn-close float-end" | ||
data-bs-dismiss="alert" | ||
aria-label="{% trans Close %}" | ||
></button> | ||
</div> | ||
{% endif %} | ||
|
||
<form | ||
hx-post="{{ request.path_info }}" | ||
hx-target="#kyc-configure-form" | ||
hx-swap="outerHTML" | ||
> | ||
{% crispy kyc_config_form %} | ||
</form> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The model field is
api_field_to_user_data_map = jsonfield.JSONField(default=dict)
. Should this bedict
? i.e.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The model field has been updated to take
list
as a default type here. The expected format for the API mapping will be something like this: