def remove_web_user_from_domain(domain, user, username, upload_user, user_change_logger=None, is_web_upload=False): if not user or not user.is_member_of(domain): if is_web_upload: remove_invited_web_user(domain, username) if user_change_logger: user_change_logger.add_info( UserChangeMessage.invitation_revoked_for_domain(domain)) else: raise UserUploadError( _("You cannot remove a web user that is not a member of this project." " {web_user} is not a member.").format(web_user=user)) elif username == upload_user.username: raise UserUploadError( _("You cannot remove yourself from a domain via bulk upload")) else: user.delete_domain_membership(domain) user.save() if user_change_logger: user_change_logger.add_info( UserChangeMessage.domain_removal(domain))
def _get_or_create_commcare_user(domain, user_id, username, is_account_confirmed, web_user_username, password, upload_user): if user_id: user = CommCareUser.get_by_user_id(user_id, domain) if not user: raise UserUploadError( _("User with ID '{user_id}' not found").format(user_id=user_id, domain=domain)) check_changing_username(user, username) # note: explicitly not including "None" here because that's the default value if not set. # False means it was set explicitly to that value if is_account_confirmed is False and not web_user_username: raise UserUploadError( _("You can only set 'Is Account Confirmed' to 'False' on a new User." )) else: kwargs = {} if is_account_confirmed is not None and not web_user_username: kwargs['is_account_confirmed'] = is_account_confirmed user = CommCareUser.create(domain, username, password, created_by=upload_user, created_via=USER_CHANGE_VIA_BULK_IMPORTER, commit=False, **kwargs) return user
def check_headers(user_specs, domain, is_web_upload=False): messages = [] headers = set(user_specs.fieldnames) # Backwards warnings for (old_name, new_name) in old_headers.items(): if old_name in headers: messages.append( _("'The column header '{old_name}' is deprecated, please use '{new_name}' instead." ).format(old_name=old_name, new_name=new_name)) headers.discard(old_name) if DOMAIN_PERMISSIONS_MIRROR.enabled(domain): allowed_headers.add('domain') illegal_headers = headers - allowed_headers if is_web_upload: missing_headers = web_required_headers - headers else: missing_headers = required_headers - headers for header_set, label in (missing_headers, 'required'), (illegal_headers, 'illegal'): if header_set: messages.append( _('The following are {label} column headers: {headers}.'). format(label=label, headers=', '.join(header_set))) if messages: raise UserUploadError('\n'.join(messages))
def get_location_from_site_code(site_code, location_cache): if isinstance(site_code, str): site_code = site_code.lower() elif isinstance(site_code, int): site_code = str(site_code) else: raise UserUploadError( _("Unexpected format received for site code '%(site_code)s'") % {'site_code': site_code}) try: return location_cache.get(site_code) except SQLLocation.DoesNotExist: raise UserUploadError( _("Could not find organization with site code '%(site_code)s'") % {'site_code': site_code})
def update_user_data(self, data, uncategorized_data, profile, domain_info): # Add in existing data. Don't use metadata - we don't want to add profile-controlled fields. current_profile_id = self.user.user_data.get(PROFILE_SLUG) for key, value in self.user.user_data.items(): if key not in data: data[key] = value if profile: profile_obj = domain_info.profiles_by_name[profile] data[PROFILE_SLUG] = profile_obj.id for key in profile_obj.fields.keys(): self.user.pop_metadata(key) try: self.user.update_metadata(data) except ValueError as e: raise UserUploadError(str(e)) if uncategorized_data: self.user.update_metadata(uncategorized_data) # Clear blank user data so that it can be purged by remove_unused_custom_fields_from_users_task for key in dict(data, **uncategorized_data): value = self.user.metadata[key] if value is None or value == '': self.user.pop_metadata(key) new_profile_id = self.user.user_data.get(PROFILE_SLUG) if new_profile_id and new_profile_id != current_profile_id: profile_name = domain_info.profile_name_by_id[new_profile_id] self.logger.add_info( UserChangeMessage.profile_info(new_profile_id, profile_name))
def check_changing_username(user, username): if username and user.username != username: raise UserUploadError( _('Changing usernames is not supported: %(username)r to %(new_username)r' ) % { 'username': user.username, 'new_username': username })
def remove_web_user_from_domain(domain, user, username, upload_user, is_web_upload=False): if not user or not user.is_member_of(domain): if is_web_upload: remove_invited_web_user(domain, username) else: raise UserUploadError( _("You cannot remove a web user that is not a member of this project." " {web_user} is not a member.").format(web_user=user)) elif username == upload_user.username: raise UserUploadError( _("You cannot remove a yourself from a domain via bulk upload")) else: user.delete_domain_membership(domain) user.save()
def remove_invited_web_user(domain, username): try: invitation = Invitation.objects.get(domain=domain, email=username) except Invitation.DoesNotExist: raise UserUploadError( _("You cannot remove a web user that is not a member or invited to this project. " "{username} is not a member or invited.").format( username=username)) invitation.delete()
def check_headers(user_specs): messages = [] headers = set(user_specs.fieldnames) # Backwards warnings for (old_name, new_name) in old_headers.items(): if old_name in headers: messages.append( _("'The column header '{old_name}' is deprecated, please use '{new_name}' instead.").format( old_name=old_name, new_name=new_name )) headers.discard(old_name) illegal_headers = headers - allowed_headers missing_headers = required_headers - headers for header_set, label in (missing_headers, 'required'), (illegal_headers, 'illegal'): if header_set: messages.append(_('The following are {label} column headers: {headers}.').format( label=label, headers=', '.join(header_set))) if messages: raise UserUploadError('\n'.join(messages))
def get_domain_info(domain, upload_domain, user_specs, domain_info_by_domain, upload_user=None, group_memoizer=None, is_web_upload=False): from corehq.apps.users.views.mobile.custom_data_fields import UserFieldsView from corehq.apps.users.views.utils import get_editable_role_choices domain_info = domain_info_by_domain.get(domain) if domain_info: return domain_info if domain == upload_domain: domain_group_memoizer = group_memoizer or GroupMemoizer(domain) else: domain_group_memoizer = GroupMemoizer(domain) domain_group_memoizer.load_all() can_assign_locations = domain_has_privilege(domain, privileges.LOCATIONS) location_cache = None if can_assign_locations: location_cache = SiteCodeToLocationCache(domain) domain_obj = Domain.get_by_name(domain) if domain_obj is None: raise UserUploadError( _("Domain cannot be set to '{domain}'".format(domain=domain))) allowed_group_names = [ group.name for group in domain_group_memoizer.groups ] profiles_by_name = {} profile_name_by_id = {} domain_user_specs = [ spec for spec in user_specs if spec.get('domain', upload_domain) == domain ] if is_web_upload: roles_by_name = { role[1]: role[0] for role in get_editable_role_choices( domain, upload_user, allow_admin_role=True) } validators = get_user_import_validators( domain_obj, domain_user_specs, True, allowed_roles=list(roles_by_name), upload_domain=upload_domain, ) else: roles_by_name = { role.name: role.get_qualified_id() for role in UserRole.objects.get_by_domain(domain) } definition = CustomDataFieldsDefinition.get(domain, UserFieldsView.field_type) if definition: profiles = definition.get_profiles() profiles_by_name = {profile.name: profile for profile in profiles} profile_name_by_id = { profile.pk: profile.name for profile in profiles } validators = get_user_import_validators(domain_obj, domain_user_specs, False, allowed_group_names, list(roles_by_name), list(profiles_by_name), upload_domain) domain_info = DomainInfo(validators, can_assign_locations, location_cache, roles_by_name, profiles_by_name, profile_name_by_id, domain_group_memoizer) domain_info_by_domain[domain] = domain_info return domain_info
def check_user_role(username, role): if not role: raise UserUploadError( _("You cannot upload a web user without a role. {username} does not have " "a role").format(username=username))
def check_can_upload_web_users(upload_user): if not upload_user.can_edit_web_users(): raise UserUploadError( _("Only users with the edit web users permission can upload web users" ))
def create_or_update_commcare_users_and_groups(upload_domain, user_specs, upload_user, upload_record_id, group_memoizer=None, update_progress=None): """" Creates and Updates CommCare Users For the associated web user username passed, for each CommCareUser if corresponding web user is present if web user has confirmed account but not a member of domain adds them to the domain with same role and primary location as the CommCareUser if already a member of domain update their role and primary location to be same as that of the CommCareUser else creates or updates user invitation sets Invitation with the CommCare user's role and primary location All changes to users only, are tracked using UserChangeLogger, as an audit trail. """ from corehq.apps.user_importer.helpers import CommCareUserImporter, WebUserImporter domain_info_by_domain = {} ret = {"errors": [], "rows": []} current = 0 update_deactivate_after_date = EnterpriseMobileWorkerSettings.is_domain_using_custom_deactivation( upload_domain) for row in user_specs: if update_progress: update_progress(current) current += 1 username = row.get('username') domain = row.get('domain') or upload_domain username = normalize_username(str(username), domain) if username else None status_row = { 'username': username, 'row': row, } try: domain_info = get_domain_info(domain, upload_domain, user_specs, domain_info_by_domain, group_memoizer) for validator in domain_info.validators: validator(row) except UserUploadError as e: status_row['flag'] = str(e) ret['rows'].append(status_row) continue data = row.get('data', {}) email = row.get('email') group_names = list(map(str, row.get('group') or [])) language = row.get('language') name = row.get('name') password = row.get('password') uncategorized_data = row.get('uncategorized_data', {}) user_id = row.get('user_id') location_codes = row.get('location_code', []) if 'location_code' in row else None location_codes = format_location_codes(location_codes) role = row.get('role', None) profile = row.get('user_profile', None) web_user_username = row.get('web_user') phone_numbers = row.get('phone-number', []) if 'phone-number' in row else None deactivate_after = row.get( 'deactivate_after', None) if update_deactivate_after_date else None if isinstance(deactivate_after, datetime): deactivate_after = deactivate_after.strftime("%m-%Y") row['deactivate_after'] = deactivate_after try: password = str(password) if password else None is_active = spec_value_to_boolean_or_none(row, 'is_active') is_account_confirmed = spec_value_to_boolean_or_none( row, 'is_account_confirmed') send_account_confirmation_email = spec_value_to_boolean_or_none( row, 'send_confirmation_email') remove_web_user = spec_value_to_boolean_or_none( row, 'remove_web_user') user = _get_or_create_commcare_user(domain, user_id, username, is_account_confirmed, web_user_username, password, upload_user) commcare_user_importer = CommCareUserImporter( upload_domain, domain, user, upload_user, is_new_user=not bool(user_id), via=USER_CHANGE_VIA_BULK_IMPORTER, upload_record_id=upload_record_id) if user_id: if is_password(password): commcare_user_importer.update_password(password) # overwrite password in results so we do not save it to the db status_row['row']['password'] = '******' status_row['flag'] = 'updated' else: status_row['flag'] = 'created' if phone_numbers is not None: phone_numbers = clean_phone_numbers(phone_numbers) commcare_user_importer.update_phone_numbers(phone_numbers) if name: commcare_user_importer.update_name(name) commcare_user_importer.update_user_data(data, uncategorized_data, profile, domain_info) if update_deactivate_after_date: commcare_user_importer.update_deactivate_after( deactivate_after) if language: commcare_user_importer.update_language(language) if email: commcare_user_importer.update_email(email) if is_active is not None: commcare_user_importer.update_status(is_active) # Do this here so that we validate the location code before we # save any other information to the user, this way either all of # the user's information is updated, or none of it # Do not update location info if the column is not included at all if domain_info.can_assign_locations and location_codes is not None: commcare_user_importer.update_locations( location_codes, domain_info) if role: role_qualified_id = domain_info.roles_by_name[role] commcare_user_importer.update_role(role_qualified_id) elif not commcare_user_importer.logger.is_new_user and 'role' in row: commcare_user_importer.update_role('none') if web_user_username: user.update_metadata({'login_as_user': web_user_username}) user.save() log = commcare_user_importer.save_log() if web_user_username: check_can_upload_web_users(domain, upload_user) web_user = CouchUser.get_by_username(web_user_username) if web_user: web_user_importer = WebUserImporter( upload_domain, domain, web_user, upload_user, is_new_user=False, via=USER_CHANGE_VIA_BULK_IMPORTER, upload_record_id=upload_record_id) user_change_logger = web_user_importer.logger else: web_user_importer = None user_change_logger = None if remove_web_user: remove_web_user_from_domain(domain, web_user, username, upload_user, user_change_logger) else: check_user_role(username, role) if not web_user and is_account_confirmed: raise UserUploadError( _("You can only set 'Is Account Confirmed' to 'True' on an existing Web User. " f"{web_user_username} is a new username."). format(web_user_username=web_user_username)) if web_user and not web_user.is_member_of( domain) and is_account_confirmed: # add confirmed account to domain # role_qualified_id would be be present here as confirmed in check_user_role web_user_importer.add_to_domain( role_qualified_id, user.location_id) elif not web_user or not web_user.is_member_of(domain): create_or_update_web_user_invite( web_user_username, domain, role_qualified_id, upload_user, user.location_id, user_change_logger, send_email=send_account_confirmation_email) elif web_user.is_member_of(domain): # edit existing user in the domain web_user_importer.update_role(role_qualified_id) if location_codes is not None: web_user_importer.update_primary_location( user.location_id) web_user.save() if web_user_importer: web_user_importer.save_log() if send_account_confirmation_email and not web_user_username: send_account_confirmation_if_necessary(user) if is_password(password): # Without this line, digest auth doesn't work. # With this line, digest auth works. # Other than that, I'm not sure what's going on # Passing use_primary_db=True because of https://dimagi-dev.atlassian.net/browse/ICDS-465 user.get_django_user( use_primary_db=True).check_password(password) group_change_message = commcare_user_importer.update_user_groups( domain_info, group_names) try: domain_info.group_memoizer.save_updated() except BulkSaveError as e: _error_message = ( "Oops! We were not able to save some of your group changes. " "Please make sure no one else is editing your groups " "and try again.") logging.exception(('BulkSaveError saving groups. ' 'User saw error message "%s". Errors: %s') % (_error_message, e.errors)) ret['errors'].append(_error_message) if log and group_change_message: log.change_messages.update(group_change_message) log.save() elif group_change_message: log = commcare_user_importer.logger.save_only_group_changes( group_change_message) except ValidationError as e: status_row['flag'] = e.message except (UserUploadError, CouchUser.Inconsistent) as e: status_row['flag'] = str(e) ret["rows"].append(status_row) return ret
def create_or_update_web_users(upload_domain, user_specs, upload_user, update_progress=None): domain_info_by_domain = {} ret = {"errors": [], "rows": []} current = 0 for row in user_specs: if update_progress: update_progress(current) current += 1 role_updated = False username = row.get('username') domain = row.get('domain') or upload_domain status_row = { 'username': username, 'row': row, } domain_info = get_domain_info(domain, upload_domain, user_specs, domain_info_by_domain, upload_user=upload_user, is_web_upload=True) try: for validator in domain_info.validators: validator(row) except UserUploadError as e: status_row['flag'] = str(e) ret['rows'].append(status_row) continue role = row.get('role', None) status = row.get('status') location_codes = row.get('location_code', []) if 'location_code' in row else None location_codes = format_location_codes(location_codes) try: remove = spec_value_to_boolean_or_none(row, 'remove') check_user_role(username, role) role_qualified_id = domain_info.roles_by_name[role] check_can_upload_web_users(upload_user) user = CouchUser.get_by_username(username, strict=True) if user: check_changing_username(user, username) if remove: remove_web_user_from_domain(domain, user, username, upload_user, is_web_upload=True) else: membership = user.get_domain_membership(domain) if membership: modify_existing_user_in_domain(domain, domain_info, location_codes, membership, role_qualified_id, upload_user, user) else: create_or_update_web_user_invite( username, domain, role_qualified_id, upload_user, user.location_id) status_row['flag'] = 'updated' else: if remove: remove_invited_web_user(domain, username) status_row['flag'] = 'updated' else: if status == "Invited": try: invitation = Invitation.objects.get(domain=domain, email=username) except Invitation.DoesNotExist: raise UserUploadError( _("You can only set 'Status' to 'Invited' on a pending Web User." " {web_user} is not yet invited.").format( web_user=username)) if invitation.email_status == InvitationStatus.BOUNCED and invitation.email == username: raise UserUploadError( _("The email has bounced for this user's invite. Please try " "again with a different username").format( web_user=username)) user_invite_loc_id = None if domain_info.can_assign_locations and location_codes is not None: # set invite location to first item in location_codes if len(location_codes) > 0: user_invite_loc = get_location_from_site_code( location_codes[0], domain_info.location_cache) user_invite_loc_id = user_invite_loc.location_id create_or_update_web_user_invite(username, domain, role_qualified_id, upload_user, user_invite_loc_id) status_row['flag'] = 'invited' except (UserUploadError, CouchUser.Inconsistent) as e: status_row['flag'] = str(e) ret["rows"].append(status_row) return ret
def _update_change_messages(self, change_messages): for slug in change_messages: if slug in self.change_messages: raise UserUploadError(f"Double Entry for {slug}") self.change_messages.update(change_messages)
def __call__(self, spec): error_message = self.validate_spec(spec) if error_message: raise UserUploadError(error_message)
def create_or_update_users_and_groups(upload_domain, user_specs, upload_user, group_memoizer=None, update_progress=None): domain_info_by_domain = {} def _get_domain_info(domain): domain_info = domain_info_by_domain.get(domain) if domain_info: return domain_info if domain == upload_domain: domain_group_memoizer = group_memoizer or GroupMemoizer(domain) else: domain_group_memoizer = GroupMemoizer(domain) domain_group_memoizer.load_all() can_assign_locations = domain_has_privilege(domain, privileges.LOCATIONS) location_cache = None if can_assign_locations: location_cache = SiteCodeToLocationCache(domain) domain_obj = Domain.get_by_name(domain) allowed_group_names = [group.name for group in domain_group_memoizer.groups] roles_by_name = {role.name: role for role in UserRole.by_domain(domain)} domain_user_specs = [spec for spec in user_specs if spec.get('domain', upload_domain) == domain] validators = get_user_import_validators( domain_obj, domain_user_specs, allowed_group_names, list(roles_by_name), upload_domain ) domain_info = DomainInfo( validators, can_assign_locations, location_cache, roles_by_name, domain_group_memoizer ) domain_info_by_domain[domain] = domain_info return domain_info ret = {"errors": [], "rows": []} current = 0 try: for row in user_specs: if update_progress: update_progress(current) current += 1 username = row.get('username') domain = row.get('domain') or upload_domain username = normalize_username(str(username), domain) if username else None status_row = { 'username': username, 'row': row, } domain_info = _get_domain_info(domain) try: for validator in domain_info.validators: validator(row) except UserUploadError as e: status_row['flag'] = str(e) ret['rows'].append(status_row) continue data = row.get('data') email = row.get('email') group_names = list(map(str, row.get('group') or [])) language = row.get('language') name = row.get('name') password = row.get('password') phone_number = row.get('phone-number') uncategorized_data = row.get('uncategorized_data') user_id = row.get('user_id') location_codes = row.get('location_code') or [] if location_codes and not isinstance(location_codes, list): location_codes = [location_codes] # ignore empty location_codes = [code for code in location_codes if code] role = row.get('role', None) web_user = row.get('web_user') try: password = str(password) if password else None is_active = spec_value_to_boolean_or_none(row, 'is_active') is_account_confirmed = spec_value_to_boolean_or_none(row, 'is_account_confirmed') send_account_confirmation_email = spec_value_to_boolean_or_none(row, 'send_confirmation_email') remove_web_user = spec_value_to_boolean_or_none(row, 'remove_web_user') if user_id: user = CommCareUser.get_by_user_id(user_id, domain) if not user: raise UserUploadError(_( "User with ID '{user_id}' not found" ).format(user_id=user_id, domain=domain)) if username and user.username != username: raise UserUploadError(_( 'Changing usernames is not supported: %(username)r to %(new_username)r' ) % {'username': user.username, 'new_username': username}) # note: explicitly not including "None" here because that's the default value if not set. # False means it was set explicitly to that value if is_account_confirmed is False and not web_user: raise UserUploadError(_( "You can only set 'Is Account Confirmed' to 'False' on a new User." )) if is_password(password): user.set_password(password) # overwrite password in results so we do not save it to the db status_row['row']['password'] = '******' status_row['flag'] = 'updated' else: kwargs = {} if is_account_confirmed is not None and not web_user: kwargs['is_account_confirmed'] = is_account_confirmed user = CommCareUser.create(domain, username, password, created_by=upload_user, created_via=USER_CHANGE_VIA_BULK_IMPORTER, commit=False, **kwargs) status_row['flag'] = 'created' if phone_number: user.add_phone_number(_fmt_phone(phone_number), default=True) if name: user.set_full_name(str(name)) if data: user.user_data.update(data) if uncategorized_data: user.user_data.update(uncategorized_data) if language: user.language = language if email: user.email = email.lower() if is_active is not None: user.is_active = is_active if domain_info.can_assign_locations: # Do this here so that we validate the location code before we # save any other information to the user, this way either all of # the user's information is updated, or none of it location_ids = [] for code in location_codes: loc = get_location_from_site_code(code, domain_info.location_cache) location_ids.append(loc.location_id) locations_updated = set(user.assigned_location_ids) != set(location_ids) primary_location_removed = (user.location_id and not location_ids or user.location_id not in location_ids) if primary_location_removed: user.unset_location(commit=False) if locations_updated: user.reset_locations(location_ids, commit=False) if role: role_qualified_id = domain_info.roles_by_name[role].get_qualified_id() user.set_role(domain, role_qualified_id) if web_user: user.user_data.update({'login_as_user': web_user}) user.save() if web_user: if not upload_user.can_edit_web_users(): raise UserUploadError(_( "Only users with the edit web users permission can upload web users" )) current_user = CouchUser.get_by_username(web_user) if remove_web_user: if not current_user or not current_user.is_member_of(domain): raise UserUploadError(_( "You cannot remove a web user that is not a member of this project. {web_user} is not a member.").format(web_user=web_user) ) else: current_user.delete_domain_membership(domain) current_user.save() else: if not role: raise UserUploadError(_( "You cannot upload a web user without a role. {web_user} does not have a role").format(web_user=web_user) ) if not current_user and is_account_confirmed: raise UserUploadError(_( "You can only set 'Is Account Confirmed' to 'True' on an existing Web User. {web_user} is a new username.").format(web_user=web_user) ) if current_user and not current_user.is_member_of(domain) and is_account_confirmed: current_user.add_as_web_user(domain, role=role_qualified_id, location_id=user.location_id) elif not current_user or not current_user.is_member_of(domain): invite_data = { 'email': web_user, 'invited_by': upload_user.user_id, 'invited_on': datetime.utcnow(), 'domain': domain, 'role': role_qualified_id, 'supply_point': user.location_id } invite = Invitation(**invite_data) invite.save() if send_account_confirmation_email: invite.send_activation_email() elif current_user.is_member_of(domain): # edit existing user in the domain current_user.set_role(domain, role_qualified_id) if user.location_id: current_user.set_location(domain, user.location_id) else: current_user.unset_location(domain) current_user.save() if send_account_confirmation_email and not web_user: send_account_confirmation_if_necessary(user) if is_password(password): # Without this line, digest auth doesn't work. # With this line, digest auth works. # Other than that, I'm not sure what's going on # Passing use_primary_db=True because of https://dimagi-dev.atlassian.net/browse/ICDS-465 user.get_django_user(use_primary_db=True).check_password(password) for group in domain_info.group_memoizer.by_user_id(user.user_id): if group.name not in group_names: group.remove_user(user) for group_name in group_names: domain_info.group_memoizer.by_name(group_name).add_user(user, save=False) except (UserUploadError, CouchUser.Inconsistent) as e: status_row['flag'] = str(e) ret["rows"].append(status_row) finally: try: for domain_info in domain_info_by_domain.values(): domain_info.group_memoizer.save_all() except BulkSaveError as e: _error_message = ( "Oops! We were not able to save some of your group changes. " "Please make sure no one else is editing your groups " "and try again." ) logging.exception(( 'BulkSaveError saving groups. ' 'User saw error message "%s". Errors: %s' ) % (_error_message, e.errors)) ret['errors'].append(_error_message) return ret
def create_or_update_users_and_groups(domain, user_specs, group_memoizer=None, update_progress=None): ret = {"errors": [], "rows": []} group_memoizer = group_memoizer or GroupMemoizer(domain) group_memoizer.load_all() current = 0 can_assign_locations = domain_has_privilege(domain, privileges.LOCATIONS) if can_assign_locations: location_cache = SiteCodeToLocationCache(domain) domain_obj = Domain.get_by_name(domain) allowed_group_names = [group.name for group in group_memoizer.groups] roles_by_name = {role.name: role for role in UserRole.by_domain(domain)} validators = get_user_import_validators(domain_obj, user_specs, allowed_group_names, list(roles_by_name)) try: for row in user_specs: if update_progress: update_progress(current) current += 1 username = row.get('username') status_row = { 'username': username, 'row': row, } try: for validator in validators: validator(row) except UserUploadError as e: status_row['flag'] = str(e) ret['rows'].append(status_row) continue data = row.get('data') email = row.get('email') group_names = list(map(str, row.get('group') or [])) language = row.get('language') name = row.get('name') password = row.get('password') phone_number = row.get('phone-number') uncategorized_data = row.get('uncategorized_data') user_id = row.get('user_id') location_codes = row.get('location_code') or [] if location_codes and not isinstance(location_codes, list): location_codes = [location_codes] # ignore empty location_codes = [code for code in location_codes if code] role = row.get('role', '') try: username = normalize_username(str(username), domain) if username else None password = str(password) if password else None is_active = spec_value_to_boolean_or_none(row, 'is_active') is_account_confirmed = spec_value_to_boolean_or_none( row, 'is_account_confirmed') if user_id: user = CommCareUser.get_by_user_id(user_id, domain) if not user: raise UserUploadError( _("User with ID '{user_id}' not found").format( user_id=user_id, domain=domain)) if username and user.username != username: raise UserUploadError( _('Changing usernames is not supported: %(username)r to %(new_username)r' ) % { 'username': user.username, 'new_username': username }) # note: explicitly not including "None" here because that's the default value if not set. # False means it was set explicitly to that value if is_account_confirmed is False: raise UserUploadError( _(f"You can only set 'Is Account Confirmed' to 'False' on a new User." )) if is_password(password): user.set_password(password) status_row['flag'] = 'updated' else: kwargs = {} if is_account_confirmed is not None: kwargs['is_account_confirmed'] = is_account_confirmed user = CommCareUser.create(domain, username, password, commit=False, **kwargs) status_row['flag'] = 'created' if phone_number: user.add_phone_number(_fmt_phone(phone_number), default=True) if name: user.set_full_name(str(name)) if data: user.user_data.update(data) if uncategorized_data: user.user_data.update(uncategorized_data) if language: user.language = language if email: user.email = email.lower() if is_active is not None: user.is_active = is_active if can_assign_locations: # Do this here so that we validate the location code before we # save any other information to the user, this way either all of # the user's information is updated, or none of it location_ids = [] for code in location_codes: loc = get_location_from_site_code(code, location_cache) location_ids.append(loc.location_id) locations_updated = set( user.assigned_location_ids) != set(location_ids) primary_location_removed = ( user.location_id and not location_ids or user.location_id not in location_ids) if primary_location_removed: user.unset_location(commit=False) if locations_updated: user.reset_locations(location_ids, commit=False) if role: user.set_role(domain, roles_by_name[role].get_qualified_id()) user.save() if is_password(password): # Without this line, digest auth doesn't work. # With this line, digest auth works. # Other than that, I'm not sure what's going on # Passing use_primary_db=True because of https://dimagi-dev.atlassian.net/browse/ICDS-465 user.get_django_user( use_primary_db=True).check_password(password) for group in group_memoizer.by_user_id(user.user_id): if group.name not in group_names: group.remove_user(user) for group_name in group_names: group_memoizer.by_name(group_name).add_user(user, save=False) except (UserUploadError, CouchUser.Inconsistent) as e: status_row['flag'] = str(e) ret["rows"].append(status_row) finally: try: group_memoizer.save_all() except BulkSaveError as e: _error_message = ( "Oops! We were not able to save some of your group changes. " "Please make sure no one else is editing your groups " "and try again.") logging.exception(('BulkSaveError saving groups. ' 'User saw error message "%s". Errors: %s') % (_error_message, e.errors)) ret['errors'].append(_error_message) return ret
def create_or_update_users_and_groups(upload_domain, user_specs, upload_user, group_memoizer=None, update_progress=None): domain_info_by_domain = {} ret = {"errors": [], "rows": []} current = 0 try: for row in user_specs: if update_progress: update_progress(current) current += 1 log_user_create = False log_role_update = False username = row.get('username') domain = row.get('domain') or upload_domain username = normalize_username(str(username), domain) if username else None status_row = { 'username': username, 'row': row, } domain_info = get_domain_info(domain, upload_domain, user_specs, domain_info_by_domain, group_memoizer) try: for validator in domain_info.validators: validator(row) except UserUploadError as e: status_row['flag'] = str(e) ret['rows'].append(status_row) continue data = row.get('data', {}) email = row.get('email') group_names = list(map(str, row.get('group') or [])) language = row.get('language') name = row.get('name') password = row.get('password') phone_number = row.get('phone-number') uncategorized_data = row.get('uncategorized_data', {}) user_id = row.get('user_id') location_codes = row.get('location_code', []) if 'location_code' in row else None location_codes = format_location_codes(location_codes) role = row.get('role', None) profile = row.get('user_profile', None) web_user = row.get('web_user') try: password = str(password) if password else None is_active = spec_value_to_boolean_or_none(row, 'is_active') is_account_confirmed = spec_value_to_boolean_or_none( row, 'is_account_confirmed') send_account_confirmation_email = spec_value_to_boolean_or_none( row, 'send_confirmation_email') remove_web_user = spec_value_to_boolean_or_none( row, 'remove_web_user') if user_id: user = CommCareUser.get_by_user_id(user_id, domain) if not user: raise UserUploadError( _("User with ID '{user_id}' not found").format( user_id=user_id, domain=domain)) check_changing_username(user, username) # note: explicitly not including "None" here because that's the default value if not set. # False means it was set explicitly to that value if is_account_confirmed is False and not web_user: raise UserUploadError( _("You can only set 'Is Account Confirmed' to 'False' on a new User." )) if is_password(password): user.set_password(password) # overwrite password in results so we do not save it to the db status_row['row']['password'] = '******' status_row['flag'] = 'updated' else: kwargs = {} if is_account_confirmed is not None and not web_user: kwargs['is_account_confirmed'] = is_account_confirmed user = CommCareUser.create( domain, username, password, created_by=upload_user, created_via=USER_CHANGE_VIA_BULK_IMPORTER, commit=False, **kwargs) log_user_create = True status_row['flag'] = 'created' if phone_number: user.add_phone_number(_fmt_phone(phone_number), default=True) if name: user.set_full_name(str(name)) # Add in existing data. Don't use metadata - we don't want to add profile-controlled fields. for key, value in user.user_data.items(): if key not in data: data[key] = value if profile: profile_obj = domain_info.profiles_by_name[profile] data[PROFILE_SLUG] = profile_obj.id for key in profile_obj.fields.keys(): user.pop_metadata(key) try: user.update_metadata(data) except ValueError as e: raise UserUploadError(str(e)) if uncategorized_data: user.update_metadata(uncategorized_data) # Clear blank user data so that it can be purged by remove_unused_custom_fields_from_users_task for key in dict(data, **uncategorized_data): value = user.metadata[key] if value is None or value == '': user.pop_metadata(key) if language: user.language = language if email: user.email = email.lower() if is_active is not None: user.is_active = is_active if domain_info.can_assign_locations and location_codes is not None: # Do this here so that we validate the location code before we # save any other information to the user, this way either all of # the user's information is updated, or none of it # Do not update location info if the column is not included at all location_ids = find_location_id(location_codes, domain_info.location_cache) locations_updated, primary_loc_removed = check_modified_user_loc( location_ids, user.location_id, user.assigned_location_ids) if primary_loc_removed: user.unset_location(commit=False) if locations_updated: user.reset_locations(location_ids, commit=False) if role: role_qualified_id = domain_info.roles_by_name[ role].get_qualified_id() user_current_role = user.get_role(domain=domain) log_role_update = not ( user_current_role and user_current_role.get_qualified_id() == role_qualified_id) if log_role_update: user.set_role(domain, role_qualified_id) if web_user: user.update_metadata({'login_as_user': web_user}) user.save() if log_user_create: user.log_user_create(upload_user, USER_CHANGE_VIA_BULK_IMPORTER) if log_role_update: log_user_role_update(domain, user, upload_user, USER_CHANGE_VIA_BULK_IMPORTER) if web_user: check_can_upload_web_users(upload_user) current_user = CouchUser.get_by_username(web_user) if remove_web_user: remove_web_user_from_domain(domain, current_user, username, upload_user) else: check_user_role(username, role) if not current_user and is_account_confirmed: raise UserUploadError( _("You can only set 'Is Account Confirmed' to 'True' on an existing Web User. {web_user} is a new username." ).format(web_user=web_user)) if current_user and not current_user.is_member_of( domain) and is_account_confirmed: current_user.add_as_web_user( domain, role=role_qualified_id, location_id=user.location_id) elif not current_user or not current_user.is_member_of( domain): create_or_update_web_user_invite( web_user, domain, role_qualified_id, upload_user, user.location_id, send_email=send_account_confirmation_email) elif current_user.is_member_of(domain): # edit existing user in the domain current_user.set_role(domain, role_qualified_id) if location_codes is not None: if user.location_id: current_user.set_location( domain, user.location_id) else: current_user.unset_location(domain) current_user.save() if send_account_confirmation_email and not web_user: send_account_confirmation_if_necessary(user) if is_password(password): # Without this line, digest auth doesn't work. # With this line, digest auth works. # Other than that, I'm not sure what's going on # Passing use_primary_db=True because of https://dimagi-dev.atlassian.net/browse/ICDS-465 user.get_django_user( use_primary_db=True).check_password(password) for group in domain_info.group_memoizer.by_user_id( user.user_id): if group.name not in group_names: group.remove_user(user) for group_name in group_names: domain_info.group_memoizer.by_name(group_name).add_user( user, save=False) except (UserUploadError, CouchUser.Inconsistent) as e: status_row['flag'] = str(e) ret["rows"].append(status_row) finally: try: for domain_info in domain_info_by_domain.values(): domain_info.group_memoizer.save_all() except BulkSaveError as e: _error_message = ( "Oops! We were not able to save some of your group changes. " "Please make sure no one else is editing your groups " "and try again.") logging.exception(('BulkSaveError saving groups. ' 'User saw error message "%s". Errors: %s') % (_error_message, e.errors)) ret['errors'].append(_error_message) return ret