Skip to content

Commit

Permalink
Merge branch 'master' into jc/commcare_client_version_compliance_report
Browse files Browse the repository at this point in the history
  • Loading branch information
mjriley committed Dec 9, 2024
2 parents fcf3c61 + b878399 commit d72d426
Show file tree
Hide file tree
Showing 76 changed files with 13,670 additions and 9,198 deletions.
105 changes: 105 additions & 0 deletions corehq/apps/api/tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from django.test import TestCase
from unittest.mock import patch

from corehq.apps.api.validation import WebUserResourceValidator
from corehq.apps.domain.models import Domain
from corehq.apps.users.models import WebUser
from corehq.util.test_utils import flag_enabled, flag_disabled


class TestWebUserResourceValidator(TestCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.domain = Domain(name="test-domain", is_active=True)
cls.domain.save()
cls.addClassCleanup(cls.domain.delete)
cls.requesting_user = WebUser.create(cls.domain.name, "[email protected]", "123", None, None)
cls.validator = WebUserResourceValidator(cls.domain.name, cls.requesting_user)

@classmethod
def tearDownClass(cls):
cls.requesting_user.delete(None, None)
super().tearDownClass()

def test_validate_parameters(self):
params = {"email": "[email protected]", "role": "Admin"}
self.assertIsNone(self.validator.validate_parameters(params))

invalid_params = {"invalid_param": "value"}
self.assertEqual(self.validator.validate_parameters(invalid_params), "Invalid parameter(s): invalid_param")

@flag_enabled('TABLEAU_USER_SYNCING')
@patch('corehq.apps.users.models.WebUser.has_permission', return_value=True)
def test_validate_parameters_with_tableau_edit_permission(self, mock_has_permission):
params = {"email": "[email protected]", "role": "Admin", "tableau_role": "Viewer"}
self.assertIsNone(self.validator.validate_parameters(params))

@flag_disabled('TABLEAU_USER_SYNCING')
@patch('corehq.apps.users.models.WebUser.has_permission', return_value=False)
def test_validate_parameters_without_tableau_edit_permission(self, mock_has_permission):
params = {"email": "[email protected]", "role": "Admin", "tableau_role": "Viewer"}
self.assertEqual(self.validator.validate_parameters(params),
"You do not have permission to edit Tableau Configuration.")

@patch('corehq.apps.registration.validation.domain_has_privilege', return_value=True)
def test_validate_parameters_with_profile_permission(self, mock_domain_has_privilege):
params = {"email": "[email protected]", "role": "Admin", "profile": "some_profile"}
self.assertIsNone(self.validator.validate_parameters(params))

@patch('corehq.apps.registration.validation.domain_has_privilege', return_value=False)
def test_validate_parameters_without_profile_permission(self, mock_domain_has_privilege):
params = {"email": "[email protected]", "role": "Admin", "profile": "some_profile"}
self.assertEqual(self.validator.validate_parameters(params),
"This domain does not have user profile privileges.")

@patch('corehq.apps.registration.validation.domain_has_privilege', return_value=True)
def test_validate_parameters_with_location_privilege(self, mock_domain_has_privilege):
params = {"email": "[email protected]", "role": "Admin", "primary_location": "some_location"}
self.assertIsNone(self.validator.validate_parameters(params))
params = {"email": "[email protected]", "role": "Admin", "assigned_locations": "some_location"}
self.assertIsNone(self.validator.validate_parameters(params))

@patch('corehq.apps.registration.validation.domain_has_privilege', return_value=False)
def test_validate_parameters_without_location_privilege(self, mock_domain_has_privilege):
params = {"email": "[email protected]", "role": "Admin", "primary_location": "some_location"}
self.assertEqual(self.validator.validate_parameters(params),
"This domain does not have locations privileges.")

params = {"email": "[email protected]", "role": "Admin", "assigned_locations": "some_location"}
self.assertEqual(self.validator.validate_parameters(params),
"This domain does not have locations privileges.")

def test_validate_email(self):
self.assertIsNone(self.validator.validate_email("[email protected]", True))

self.assertEqual(self.validator.validate_email("[email protected]", True),
"A user with this email address is already in "
"this project or has a pending invitation.")

deactivated_user = WebUser.create(self.domain.name, "[email protected]", "123", None, None)
deactivated_user.is_active = False
deactivated_user.save()
self.assertEqual(self.validator.validate_email("[email protected]", True),
"A user with this email address is deactivated. ")

def test_validate_locations(self):
with patch('corehq.apps.user_importer.validation.LocationValidator.validate_spec') as mock_validate_spec:
mock_validate_spec.return_value = None
self.assertIsNone(self.validator.validate_locations(self.requesting_user.username,
["loc1", "loc2"], "loc1"))

actual_spec = mock_validate_spec.call_args[0][0]
self.assertEqual(actual_spec['username'], self.requesting_user.username)
self.assertCountEqual(actual_spec['location_code'], ["loc1", "loc2"])

self.assertEqual(
self.validator.validate_locations(self.requesting_user.username, ["loc1", "loc2"], "loc3"),
"Primary location must be one of the user's locations"
)

self.assertEqual(
self.validator.validate_locations(self.requesting_user.username, ["loc1", "loc2"], ""),
"Primary location can't be empty if the user has any locations set"
)
87 changes: 87 additions & 0 deletions corehq/apps/api/validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from memoized import memoized

from corehq.apps.custom_data_fields.models import CustomDataFieldsDefinition
from corehq.apps.reports.util import get_allowed_tableau_groups_for_domain
from corehq.apps.user_importer.importer import SiteCodeToLocationCache
from corehq.apps.user_importer.validation import (
RoleValidator,
ProfileValidator,
LocationValidator,
TableauGroupsValidator,
TableauRoleValidator,
CustomDataValidator,
EmailValidator,
)
from corehq.apps.users.validation import validate_primary_location_assignment
from corehq.apps.registration.validation import AdminInvitesUserFormValidator


class WebUserResourceValidator():
def __init__(self, domain, requesting_user):
self.domain = domain
self.requesting_user = requesting_user

@property
def roles_by_name(self):
from corehq.apps.users.views.utils import get_editable_role_choices
return {role[1]: role[0] for role in get_editable_role_choices(self.domain, self.requesting_user,
allow_admin_role=True)}

@property
@memoized
def profiles_by_name(self):
from corehq.apps.users.views.mobile.custom_data_fields import UserFieldsView
return CustomDataFieldsDefinition.get_profiles_by_name(self.domain, UserFieldsView.field_type)

@property
def location_cache(self):
return SiteCodeToLocationCache(self.domain)

def validate_parameters(self, parameters):
allowed_params = ['email', 'role', 'primary_location', 'assigned_locations',
'profile', 'custom_user_data', 'tableau_role', 'tableau_groups']
invalid_params = [param for param in parameters if param not in allowed_params]
if invalid_params:
return f"Invalid parameter(s): {', '.join(invalid_params)}"
return AdminInvitesUserFormValidator.validate_parameters(self.domain, self.requesting_user, parameters)

def validate_role(self, role):
spec = {'role': role}
return RoleValidator(self.domain, self.roles_by_name()).validate_spec(spec)

def validate_profile(self, new_profile_name):
profile_validator = ProfileValidator(self.domain, self.requesting_user, True, self.profiles_by_name())
spec = {'user_profile': new_profile_name}
return profile_validator.validate_spec(spec)

def validate_custom_data(self, custom_data, profile_name):
custom_data_validator = CustomDataValidator(self.domain, self.profiles_by_name())
spec = {'data': custom_data, 'user_profile': profile_name}
return custom_data_validator.validate_spec(spec)

def validate_email(self, email, is_post):
if is_post:
error = AdminInvitesUserFormValidator.validate_email(self.domain, email)
if error:
return error
email_validator = EmailValidator(self.domain, 'email')
spec = {'email': email}
return email_validator.validate_spec(spec)

def validate_locations(self, editable_user, assigned_location_codes, primary_location_code):
error = validate_primary_location_assignment(primary_location_code, assigned_location_codes)
if error:
return error

location_validator = LocationValidator(self.domain, self.requesting_user, self.location_cache, True)
location_codes = list(set(assigned_location_codes + [primary_location_code]))
spec = {'location_code': location_codes,
'username': editable_user}
return location_validator.validate_spec(spec)

def validate_tableau_group(self, tableau_groups):
allowed_groups_for_domain = get_allowed_tableau_groups_for_domain(self.domain) or []
return TableauGroupsValidator.validate_tableau_groups(allowed_groups_for_domain, tableau_groups)

def validate_tableau_role(self, tableau_role):
return TableauRoleValidator.validate_tableau_role(tableau_role)
20 changes: 8 additions & 12 deletions corehq/apps/app_manager/views/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hashlib
import json
import logging
from xml.sax.saxutils import escape

from django.conf import settings
from django.contrib import messages
Expand Down Expand Up @@ -109,8 +110,8 @@
)
from corehq.apps.data_dictionary.util import (
add_properties_to_data_dictionary,
get_case_property_description_dict,
get_case_property_deprecated_dict,
get_case_property_description_dict,
)
from corehq.apps.domain.decorators import (
LoginAndDomainMixin,
Expand Down Expand Up @@ -406,18 +407,13 @@ def should_edit(attribute):

if should_edit('custom_instances'):
instances = json.loads(request.POST.get('custom_instances'))
try: # validate that custom instances can be added into the XML
for instance in instances:
etree.fromstring(
"<instance id='{}' src='{}' />".format(
instance.get('instanceId'),
instance.get('instancePath')
for instance in instances:
for key in ['instanceId', 'instancePath']:
val = instance.get(key)
if val != escape(val):
raise AppMisconfigurationError(
_("'{val}' is an invalid custom instance {key}").format(val=val, key=key)
)
)
except etree.XMLSyntaxError as error:
raise AppMisconfigurationError(
_("There was an issue with your custom instances: {}").format(error)
)

form.custom_instances = [
CustomInstance(
Expand Down
12 changes: 9 additions & 3 deletions corehq/apps/case_search/tests/test_filter_dsl.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,20 @@ def test_datetime_system_property_filter_optimized(self, mock_get_timezone):

@freeze_time('2023-05-16T13:01:51Z')
@flag_enabled('CASE_SEARCH_INDEXED_METADATA')
@patch("corehq.apps.case_search.xpath_functions.comparison.get_timezone_for_domain",
return_value=pytz.timezone('America/Los_Angeles'))
def test_system_date_property_comparison(self, mock_get_timezone):
def test_system_datetime_property_comparison(self):
parsed = parse_xpath("last_modified < datetime-add(now(), 'weeks', -2)")
expected_filter = filters.date_range('modified_on', lt='2023-05-02T13:01:51+00:00')
built_filter = build_filter_from_ast(parsed, SearchFilterContext("domain"))
self.checkQuery(built_filter, expected_filter, is_raw_query=True)

@freeze_time('2023-05-16T13:01:51Z')
@flag_enabled('CASE_SEARCH_INDEXED_METADATA')
def test_system_datetime_property_match(self):
parsed = parse_xpath("last_modified = now()")
expected_filter = filters.term('modified_on', '2023-05-16T13:01:51+00:00')
built_filter = build_filter_from_ast(parsed, SearchFilterContext("domain"))
self.checkQuery(built_filter, expected_filter, is_raw_query=True)

def test_not_filter(self):
parsed = parse_xpath("not(name = 'farid')")
expected_filter = filters.NOT(case_property_query('name', 'farid'))
Expand Down
2 changes: 2 additions & 0 deletions corehq/apps/case_search/xpath_functions/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ def _create_system_datetime_query(domain, meta_property, op, value, node):
raise CaseFilterError(str(e), serialize(node))

if isinstance(date_or_datetime, datetime):
if op == EQ:
return filters.term(meta_property.es_field_name, value)
range_kwargs = {RANGE_OP_MAPPING[op]: date_or_datetime}
else:
timezone = get_timezone_for_domain(domain)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,17 @@ hqDefine("cloudcare/js/formplayer/menus/api", [
appCollection: appCollection.models.map(
appItem => appItem.attributes._id + ' / ' + appItem.attributes.copy_of),
}
var errorMsg = 'The application could not be found.'
if (params.appId) {
// Likely due to a link followed from an old build.
errorMsg = errorMsg + ' If you clicked on a link, that link may be outdated.'
}
if (!params.preview) {
// Make sure the user has access to the app
if (!app) {
FormplayerFrontend.trigger(
'showError',
gettext('The application could not be found'),
gettext(errorMsg),
false,
true,
additionalSentryData
Expand Down
4 changes: 2 additions & 2 deletions corehq/apps/commtrack/static/commtrack/js/sms.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ hqDefine('commtrack/js/sms', [
var valid = true;

if (!self.keyword()) {
self.keywordError('required');
self.keywordError(gettext('SMS keyword is required.'));
valid = false;
}
if (!self.caption()) {
self.captionError('required');
self.captionError(gettext('Name is required.'));
valid = false;
}

Expand Down
8 changes: 6 additions & 2 deletions corehq/apps/commtrack/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,12 @@ def encode_if_needed(val):


def _fetch_ending_numbers(s):
matcher = re.compile(r"\d*$")
return matcher.search(s).group()
postfix = ''
for char in s[::-1]:
if not char.isdigit():
break
postfix = char + postfix
return postfix


def generate_code(object_name, existing_codes):
Expand Down
12 changes: 12 additions & 0 deletions corehq/apps/custom_data_fields/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ def get_profile_required_for_user_type_list(cls, domain, field_type):
return profile_required_for_user_type_list
return None

@classmethod
def get_profiles_by_name(cls, domain, field_type):
definition = cls.get(domain, field_type)
if definition:
profiles = definition.get_profiles()
return {
profile.name: profile
for profile in profiles
}
else:
return {}

class FieldFilterConfig:
def __init__(self, required_only=False, is_required_check_func=None):
self.required_only = required_only
Expand Down

This file was deleted.

Loading

0 comments on commit d72d426

Please sign in to comment.