Skip to content
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

Add ConnectID Invite/De-Link/Re-Link features #34967

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions corehq/apps/domain/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ def authenticate(self, request, username, password):
domain=couch_user.domain,
commcare_user__username=couch_user.username
)
if not link.is_active:
return None

return link.commcare_user

Expand Down
20 changes: 17 additions & 3 deletions corehq/apps/user_importer/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@
InvitationStatus
)
from corehq.const import USER_CHANGE_VIA_BULK_IMPORTER
from corehq.toggles import DOMAIN_PERMISSIONS_MIRROR, TABLEAU_USER_SYNCING
from corehq.toggles import (
DOMAIN_PERMISSIONS_MIRROR,
TABLEAU_USER_SYNCING,
COMMCARE_CONNECT,
)
from corehq.apps.sms.util import validate_phone_number

from dimagi.utils.logging import notify_error
Expand Down Expand Up @@ -103,9 +107,13 @@
"User Syncing is enabled can upload files with 'Tableau Role' and/or 'Tableau Groups' fields."
))

if COMMCARE_CONNECT.enabled(domain):
allowed_headers.add('send_connectid_invite')

illegal_headers = headers - allowed_headers - conditionally_allowed_headers


if is_web_upload:

Check failure on line 116 in corehq/apps/user_importer/importer.py

View workflow job for this annotation

GitHub Actions / Flake8

corehq/apps/user_importer/importer.py#L116

Too many blank lines (2) (E303)
missing_headers = web_required_headers - headers
else:
missing_headers = required_headers - headers
Expand Down Expand Up @@ -504,6 +512,9 @@

def _parse_password(self):
from corehq.apps.user_importer.validation import is_password
if self.column_values["send_connectid_invite"]:
# password login is disabled for connectid
password = ''
if self.row.get('password'):
password = str(self.row.get('password'))
elif self.column_values["send_confirmation_sms"]:
Expand Down Expand Up @@ -537,10 +548,10 @@
}

for v in ['is_active', 'is_account_confirmed', 'send_confirmation_email',
'remove_web_user', 'send_confirmation_sms']:
'remove_web_user', 'send_confirmation_sms', 'send_connectid_invite']:
values[v] = spec_value_to_boolean_or_none(self.row, v)

if values["send_confirmation_sms"] and not values["user_id"]:
if (values["send_confirmation_sms"] or values["send_connectid_invite"]) and not values["user_id"]:
values["is_account_confirmed"] = False
else:
values["is_account_confirmed"] = values["is_account_confirmed"]
Expand Down Expand Up @@ -698,6 +709,9 @@
send_account_confirmation_if_necessary(self.user)
if cv["send_confirmation_sms"]:
send_account_confirmation_sms_if_necessary(self.user)
elif cv["send_connectid_invite"]:
from corehq.apps.users.views.mobile.users import deliver_connectid_invite
deliver_connectid_invite(self.user)


class WebUserRow(BaseUserRow):
Expand Down
7 changes: 6 additions & 1 deletion corehq/apps/user_importer/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def get_user_import_validators(domain_obj, all_specs, is_web_user_import, all_us
NewUserPasswordValidator(domain),
PasswordValidator(domain) if validate_passwords else noop,
GroupValidator(domain, allowed_groups),
ConfirmationSmsValidator(domain)
ConfirmationSmsValidator(domain),
ConnectIdInviteValidator(domain)
]


Expand Down Expand Up @@ -501,6 +502,10 @@ def validate_spec(self, spec):
return self.error_existing_user.format(self.confirmation_sms_header, errors_formatted)


class ConnectIdInviteValidator(ConfirmationSmsValidator):
confirmation_sms_header = "send_connectid_invite"


class LocationValidator(ImportValidator):
error_message_user_access = _("Based on your locations you do not have permission to edit this user or user "
"invitation")
Expand Down
32 changes: 28 additions & 4 deletions corehq/apps/users/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from corehq.pillows.utils import MOBILE_USER_TYPE, WEB_USER_TYPE
from corehq.feature_previews import USE_LOCATION_DISPLAY_NAME
from corehq.toggles import (
COMMCARE_CONNECT,
TWO_STAGE_USER_PROVISIONING,
TWO_STAGE_USER_PROVISIONING_BY_SMS,
)
Expand Down Expand Up @@ -670,6 +671,13 @@
),
required=False,
)
account_invite_by_cid = forms.BooleanField(
label=gettext_noop("Invite using ConnectID phone number?"),
help_text=gettext_noop(
"If checked, the user will be sent an SMS to join the project using their ConnectID app."
),
required=False,
)
phone_number = forms.CharField(
required=False,
label=gettext_noop("Phone Number"),
Expand Down Expand Up @@ -768,10 +776,16 @@
data_bind='value: send_account_confirmation_email',
)

if TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain):
# cid => connect-id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might be better to just have connect_id in the variable than use cid and add a comment to explain it

provision_by_cid = COMMCARE_CONNECT.enabled(self.domain)

provision_by_sms = TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain)

if provision_by_sms or provision_by_cid:
varname = 'account_invite_by_cid' if provision_by_cid else 'force_account_confirmation_by_sms'
confirm_account_by_sms_field = crispy.Field(
'force_account_confirmation_by_sms',
data_bind='checked: force_account_confirmation_by_sms',
varname,
data_bind=f'checked: {varname}',
)
phone_number_field = crispy.Div(
crispy.Field(
Expand Down Expand Up @@ -867,9 +881,14 @@
<i class="fa fa-warning"></i> {disabled_email}
<!-- /ko -->
<!-- ko if: !($root.stagedUser().force_account_confirmation())
&& $root.stagedUser().force_account_confirmation_by_sms() -->
&& ($root.stagedUser().force_account_confirmation_by_sms()
|| $root.stagedUser().account_invite_by_cid) -->
<i class="fa fa-warning"></i> {disabled_phone}
<!-- /ko -->
<!-- ko if: !($root.stagedUser().force_account_confirmation())
&& $root.stagedUser().account_invite_by_cid() -->
<i class="fa fa-warning"></i> {disabled_cid}
<!-- /ko -->
<!-- /ko -->
</p>
'''.format(
Expand All @@ -892,6 +911,11 @@
"will set their own password on confirming "
"their account phone number."
),
disabled_cid = _(

Check failure on line 914 in corehq/apps/users/forms.py

View workflow job for this annotation

GitHub Actions / Flake8

corehq/apps/users/forms.py#L914

Unexpected spaces around keyword / parameter equals (E251)

Check failure on line 914 in corehq/apps/users/forms.py

View workflow job for this annotation

GitHub Actions / Flake8

corehq/apps/users/forms.py#L914

Unexpected spaces around keyword / parameter equals (E251)
"Setting a password is disabled. The user "
"will be to access by logging into their "
"ConnectID app."
),
short=_("Password must have at least {password_length} characters."
).format(password_length=settings.MINIMUM_PASSWORD_LENGTH)
)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.14 on 2024-08-12 09:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0075_hqapikey_encrypted_key'),
]

operations = [
migrations.AddField(
model_name='connectiduserlink',
name='is_active',
field=models.BooleanField(default=True),
),
]
1 change: 1 addition & 0 deletions corehq/apps/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3315,6 +3315,7 @@ class ConnectIDUserLink(models.Model):
connectid_username = models.TextField()
commcare_user = models.ForeignKey(User, related_name='connectid_user', on_delete=models.CASCADE)
domain = models.TextField()
is_active = models.BooleanField(default=True)

class Meta:
unique_together = ('domain', 'commcare_user')
58 changes: 49 additions & 9 deletions corehq/apps/users/static/users/js/mobile_workers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* as well as a minimum length requirment (the length is configurable).
* - If any validation is being used, we automatically generate a suggested password that passes validation.
*/
'use strict';

Check warning on line 18 in corehq/apps/users/static/users/js/mobile_workers.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'use strict' is unnecessary inside of modules

hqDefine("users/js/mobile_workers",[
'jquery',
Expand Down Expand Up @@ -45,7 +45,7 @@
locationsWidgets,
customDataFields
) {
'use strict';

Check warning on line 48 in corehq/apps/users/static/users/js/mobile_workers.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'use strict' is unnecessary inside of modules
// These are used as css classes, so the values of success/warning/error need to be what they are.
var STATUS = {
NONE: '',
Expand All @@ -72,9 +72,11 @@
email: '',
send_account_confirmation_email: false,
force_account_confirmation_by_sms: false,
account_invite_by_cid: false,
phone_number: '',
is_active: true,
is_account_confirmed: true,
is_connect_link_active: null,
deactivate_after_date: '',
});

Expand All @@ -95,20 +97,19 @@
self.sendConfirmationEmailEnabled = ko.observable(self.force_account_confirmation());

// used by two-stage sms provisioning
self.phoneRequired = ko.observable(self.force_account_confirmation_by_sms());
self.phoneRequired = ko.observable(self.force_account_confirmation_by_sms() || self.account_invite_by_cid());

self.passwordEnabled = ko.observable(!(self.force_account_confirmation_by_sms() || self.force_account_confirmation()));
self.passwordEnabled = ko.observable(!(
self.force_account_confirmation_by_sms() || self.force_account_confirmation() || self.account_invite_by_cid())
);

self.action_error = ko.observable(''); // error when activating/deactivating a user

self.edit_url = ko.computed(function () {
return initialPageData.reverse('edit_commcare_user', self.user_id());
});

self.is_active.subscribe(function (newValue) {
var urlName = newValue ? 'activate_commcare_user' : 'deactivate_commcare_user',
$modal = $('#' + (newValue ? 'activate_' : 'deactivate_') + self.user_id());

var toggle_active = function($modal, urlName) {

Check failure on line 112 in corehq/apps/users/static/users/js/mobile_workers.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Identifier 'toggle_active' is not in camel case

Check failure on line 112 in corehq/apps/users/static/users/js/mobile_workers.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Missing space before function parentheses
$modal.find(".btn").addSpinnerToButton();
$.ajax({
method: 'POST',
Expand All @@ -126,6 +127,18 @@
self.action_error(gettext("Issue communicating with server. Try again."));
},
});
};

self.is_active.subscribe(function (newValue) {
var urlName = newValue ? 'activate_commcare_user' : 'deactivate_commcare_user',
$modal = $('#' + (newValue ? 'activate_' : 'deactivate_') + self.user_id());
toggle_active($modal, urlName);
});

self.is_connect_link_active.subscribe(function (newValue) {
var urlName = newValue ? 'activate_connectid_link' : 'deactivate_connectid_link',
$modal = $('#' + (newValue ? 'activate_connect_link_' : 'deactivate_connect_link_') + self.user_id());
toggle_active($modal, urlName);
});

self.sendConfirmationEmail = function () {
Expand Down Expand Up @@ -178,6 +191,31 @@
});
};

self.sendConnectIDInvite = function () {
var urlName = 'send_connectid_invite';
var $modal = $('#confirm_' + self.user_id());

$modal.find(".btn").addSpinnerToButton();
$.ajax({
method: 'POST',
url: initialPageData.reverse(urlName, self.user_id()),
success: function (data) {
$modal.modal('hide');
if (data.success) {
self.action_error('');
} else {
self.action_error(data.error);
}

},
error: function () {
$modal.modal('hide');
$modal.find(".btn").removeSpinnerFromButton();
self.action_error(gettext("Issue communicating with server. Try again."));
},
});
};

return self;
};

Expand Down Expand Up @@ -294,7 +332,7 @@
return self.STATUS.DISABLED;
}

if (self.stagedUser().force_account_confirmation_by_sms()) {
if (self.stagedUser().force_account_confirmation_by_sms() || self.stagedUser().account_invite_by_cid()) {
return self.STATUS.DISABLED;
}

Expand Down Expand Up @@ -511,7 +549,7 @@
user.send_account_confirmation_email(false);
}
});
user.force_account_confirmation_by_sms.subscribe(function (enabled) {
var handlePhoneRequired = function (enabled) {
if (enabled) {
// make phone number required
user.phoneRequired(true);
Expand All @@ -525,7 +563,9 @@
// enable password input
user.passwordEnabled(true);
}
});
};
user.force_account_confirmation_by_sms.subscribe(handlePhoneRequired);
user.account_invite_by_cid.subscribe(handlePhoneRequired);
});

self.initializeUser = function () {
Expand Down
Loading
Loading