-
-
Notifications
You must be signed in to change notification settings - Fork 218
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
Create Invitation API [Validation] #35448
Conversation
6568200
to
1a81553
Compare
…idating Invitation creation via API
… / web user edit API
…to make it easier to tell what values are needed
…pdate web user API
…assigned locations or that primary location is defined if assigned locations is defined
…d format from spec
… used for invite creation API
… of the spec format in preparation for reuse
…for web user API. However, there wasn't as much overlap with validation used for AdminInvitesUserForm. So move validation for Web User Resource its own file and create a class to organize the validations relevant to AdminInvitesUserForm
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.
Love what you're doing here with the refactoring. Was only able to do a partial review so far.
corehq/apps/registration/forms.py
Outdated
raise forms.ValidationError(_("A user with this email address is deactivated. ")) | ||
|
||
from corehq.apps.registration.validation import AdminInvitesUserFormValidator | ||
error = AdminInvitesUserFormValidator.validate_email(self.domain, email, self.request.method == 'POST') |
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.
If an email is being cleaned, isn't this always a POST? Or at least an edit method where you'd want to do this sort of email validation?
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.
Also, I see there is no original call to django.core.validators.validate_email
here? Agree that that sort of validation should be done here, aghast that it isn't already.
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.
Lastly sidenote, not asking for this here and wasn't part of this PR obviously but WebUserInvitationForm
needs a better name cause this Form architecture is confusing everytime I look at it. Maybe WebUserInvitationValidationForm
.
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.
If an email is being cleaned, isn't this always a POST?
you're right, it's always a POST. Removed the is_post
parameter here. Also, I realized django.core.validators.validate_email
is already done as part of EmailField
definition. So I also removed that from AdminInvitesUserFormValidator.validate_email
WebUserInvitationForm needs a better name cause this Form architecture is confusing everytime I look at it. Maybe WebUserInvitationValidationForm.
I'm not sure what you mean by this? I don't think it makes sense to rename WebUserInvitationForm
to WebUserInvitationValidationForm
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.
I'm not sure what you mean by this? I don't think it makes sense to rename WebUserInvitationForm to WebUserInvitationValidationForm
The name makes me think it is the form for inviting a web user. But that's AdminInvitesUserForm
, this form is for just validating an accepted invitation from what I can tell. Maybe AcceptedWebUserInvitationForm
.
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.
oh i see, I agree that's confusing 06dd234
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.
Nice
with patch('corehq.apps.users.views.mobile.users.BaseUploadUser.upload_users'): | ||
response = self._make_post_request(file) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertEqual(response.json(), {'success': True}) |
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.
I am seeing some of the code for uploading via the API now... with the validation in this PR it seems like we're pretty close to having a complete solution. The spec from July obviously doesn't account for a web user API that uses bulk upload.. which sounds perfectly valid to me. I assume you are planning for us to use that same bulk upload/excel sheet functionality? I think that's probably best but gotta say from the D&A side it will be a bit of a curveball to upload using a CSV.
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.
Well I see you wrote all that code for WebUserResource
. I would hate to suggest not using it. It would be a bit easier on the D&A side to use normal pagination/user. Perhaps we could continue this conversation in Jira or slack.
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.
Just copying over what we talked about on slack - I don't plan to use the bulk upload functionality. I plan to implement it as a normal API to be consistent with how it's done for mobile workers
@@ -150,12 +151,16 @@ class TableauRoleValidator(ImportValidator): | |||
|
|||
def __init__(self, domain): | |||
super().__init__(domain) | |||
self.valid_role_options = [e.value for e in TableauUser.Roles] |
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.
Do you need to remove this from __init__
to get the classmethod to work? I think having this line here saves on memory for the import.
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.
Good point, that's preferable to keep it in __init__
11edd76
@@ -513,30 +547,18 @@ def _validate_uploading_user_access(self, spec): | |||
|
|||
# 2. Ensure the user is only adding the user to/removing from *new locations* that they have permission | |||
# to access. | |||
if 'location_code' in spec: | |||
locs_being_assigned = self._get_locs_being_assigned(spec) | |||
if locs_ids_being_assigned: |
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.
With this change the check that a user has access to the locations being removed will be skipped if the user tries to remove all of them. (blocking if true)
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.
I would reccomend a second variable:
user_is_editing_locations = 'location_code' in spec
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.
Thank you for catching that bug, fixed it a5ee207
I think this is equivalent - I do the location_code' in spec
within validate_spec
and only do the validations if that's true. For the snippet referenced here, I removed the conditional check so that validation will always run.
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.
So the LocationValidator
doesn't just validate that the uploading user has ability to change the locations for another user, IIRC it needs to validate that the uploading user has access to the other user at all. I.e. if the uploading user has access to location 1 and 2 they should not be able to update the profile for a user assigned to just location 3.
So the problem here is that checking 'location_code' in spec
skips that validation. Sorry this code is such a pain. I think what might be best is to extract #1
from _validate_uploading_user_access
and do that before everything else, and then everything else can go under if 'location_code' in spec
.
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.
Oh dang, thanks for catching that. 8fee5fe should fix it. Although I added some nasty looking variable and function names that i'm open to renaming.
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.
Looks good. Almost like reading English (almost). I don't think they're nasty, IMO much better than lumping all the logic in one giant method like some code I've seen. Some method names I'd even make longer.
Also note-to-self: if you have "#1" and "#2" blocks a method, consider just breaking it into 2 different methods.
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.
Only did partial review. Left a few comments. Nothing blocking.
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.
Can you link which part of CustomDataEditor
form already did the validation you deleted?
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.
@@ -40,3 +51,12 @@ def validate_profile(self, new_profile_name): | |||
profile_validator = ProfileValidator(self.domain, self.upload_user, True, self.profiles_by_name()) | |||
spec = {'user_profile': new_profile_name} | |||
return profile_validator.validate_spec(spec) | |||
|
|||
def validate_email(self, email, is_post): | |||
if is_post: |
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.
Why is_post
flag is required here? What can happen if we don't have this parameter?
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 is_post
is needed in preparation for it being used for the Create Invitation / Update Web User API. When we're creating a new invitation, we need to check that the email isn’t already associated with an existing user or invitation. But if we’re updating it, we already know the user or invitation exists, so running the same check would always fail.
|
||
|
||
def validate_assigned_locations_has_users(domain, assigned_location_ids): | ||
error_message_location_not_has_users = _("These locations cannot have users assigned because of their " |
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.
Shouldn't we remove error_message_location_not_has_users
from LocationValidator
class?
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.
yes you're right, thanks
d3b2d3a
…ocations_has_users
…post" parameter is redundant. Also, EmailField already builds in EmailValidator. So the additional validation check done in the bulk import EmailValidator is redundant when cleaning email from the form
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.
Most of the way there with this PR. This is some cool refactoring.
return error | ||
|
||
location_validator = LocationValidator(self.domain, self.requesting_user, self.location_cache, True) | ||
location_codes = list(set(assigned_location_codes + [primary_location_code])) |
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.
Doesn't really apply to this PR but I'm just sort of curious... @jingcheng16 isn't there some thing with mobile worker location assignment where the primary location needs to come first in an import or in the API? I vaguely remember some discussion a while back about that.
Doesn't look like for web user import the primary needs to come first in the set of codes, and certainly there is no validation for this in bulk import at the moment.
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.
IIRC, for both mobile and web user bulk import, it assumes that the first location listed is the primary location. So there's no validation involves, it just assumes and updates/assigns the primary location as such.
…cations are being removed locs_ids_being_assigned is empty/falsy)
validation needs to be done that uploading user can access the current locations of the editable user or invitation.
@@ -355,6 +355,10 @@ def test_cant_edit_web_user(self): | |||
validation_result = self.validator.validate_spec(user_spec) | |||
assert validation_result == self.validator.error_message_user_access | |||
|
|||
user_spec = {'username': self.editable_user.username} | |||
validation_result = self.validator.validate_spec(user_spec) | |||
assert validation_result == self.validator.error_message_user_access |
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.
👍
@@ -513,30 +547,18 @@ def _validate_uploading_user_access(self, spec): | |||
|
|||
# 2. Ensure the user is only adding the user to/removing from *new locations* that they have permission | |||
# to access. | |||
if 'location_code' in spec: | |||
locs_being_assigned = self._get_locs_being_assigned(spec) | |||
if locs_ids_being_assigned: |
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.
Looks good. Almost like reading English (almost). I don't think they're nasty, IMO much better than lumping all the logic in one giant method like some code I've seen. Some method names I'd even make longer.
Also note-to-self: if you have "#1" and "#2" blocks a method, consider just breaking it into 2 different methods.
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.
Nicely done. Some excellent refactoring done here.
The changes to the existing validation code are solely refactors and I tested locally that those areas still work as expected
Nice. Given the size of this PR, I reccomend re-testing areas that have had recent commits if you haven't already, and testing the forms that were changed, too (if you haven't already).
@@ -110,7 +110,7 @@ def test_redirect_if_invite_email_does_not_match(self): | |||
print(form.errors) | |||
self.assertTrue(form.is_valid()) | |||
|
|||
form = WebUserInvitationForm( | |||
form = AcceptedWebUserInvitationForm( |
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.
Thanks :)
Thanks for the review @AddisonDunn! I sanity tested admin invite and bulk upload validation surrounding locations and didn't run into any issues. |
Product Description
Technical Summary
USH-4757
The main change in this PR is the addition of an API validation file. This will support the upcoming API for creating Invitations and updating Web Users. The remaining changes involve refactoring to consolidate validations used when bulk uploading Web Users or creating Invitations via the AdminInvitesUserForm.
I reused relevant validations already defined for bulk user import (in user_importer.validation) since the same checks are needed. Some validations previously implemented via the clean method in Django forms were consolidated, while built-in form validations were left as-is.
The key distinction between Invitation creation via the API and via bulk user import is how existing Web Users are handled. Bulk user import checks if the Web User exists and updates it if so. In contrast, the API will have a dedicated POST endpoint for creation. This endpoint will require validating whether the Invitation or Web User already exists and returning an error if it does, mirroring the behavior of the AdminInvitesUserForm.
Feature Flag
No feature flag
Safety Assurance
Safety story
The main changes (addition of validation for Web User creation API) will not be used yet. The changes to the existing validation code are solely refactors and I tested locally that those areas still work as expected.
Automated test coverage
There exists extensive testing on bulk user imports and surrounding validation.
test_importer
checks that web user and mobile workers can be successfully uploaded/updated.test_validators
test that Location, Profile, Custom Data, etc.. validation catches invalid or unauthorized changes. I will add missing Tableau Group and Role validation.QA Plan
no QA.
Rollback instructions
Labels & Review