PageRenderTime 63ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/corehq/apps/app_manager/util.py

https://github.com/dimagi/commcare-hq
Python | 731 lines | 664 code | 40 blank | 27 comment | 26 complexity | 7355e740854dbb717cd7787ca173bb94 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1
  1. import json
  2. import logging
  3. import os
  4. import re
  5. import uuid
  6. from collections import OrderedDict, namedtuple
  7. from copy import deepcopy
  8. from django.core.cache import cache
  9. from django.db.models import Max
  10. from django.http import Http404
  11. from django.urls import reverse
  12. from django.utils.translation import gettext as _
  13. import yaml
  14. from couchdbkit import ResourceNotFound
  15. from couchdbkit.exceptions import DocTypeError
  16. from dimagi.utils.couch import CriticalSection
  17. from corehq import toggles
  18. from corehq.apps.app_manager.const import (
  19. AUTO_SELECT_USERCASE,
  20. REGISTRY_WORKFLOW_LOAD_CASE,
  21. REGISTRY_WORKFLOW_SMART_LINK,
  22. USERCASE_ID,
  23. USERCASE_PREFIX,
  24. USERCASE_TYPE,
  25. )
  26. from corehq.apps.app_manager.dbaccessors import get_app, get_apps_in_domain
  27. from corehq.apps.app_manager.exceptions import (
  28. AppManagerException,
  29. PracticeUserException,
  30. SuiteError,
  31. SuiteValidationError,
  32. XFormException,
  33. )
  34. from corehq.apps.app_manager.tasks import create_usercases
  35. from corehq.apps.app_manager.xform import XForm, parse_xml
  36. from corehq.apps.app_manager.xpath import UsercaseXPath
  37. from corehq.apps.builds.models import CommCareBuildConfig
  38. from corehq.apps.domain.models import Domain
  39. from corehq.apps.locations.models import SQLLocation
  40. from corehq.apps.users.models import CommCareUser
  41. from corehq.util.quickcache import quickcache
  42. from corehq.util.soft_assert import soft_assert
  43. logger = logging.getLogger(__name__)
  44. CASE_XPATH_SUBSTRING_MATCHES = [
  45. "instance('casedb')",
  46. 'session/data/case_id',
  47. "#case",
  48. "#parent",
  49. "#host",
  50. ]
  51. USERCASE_XPATH_SUBSTRING_MATCHES = [
  52. "#user",
  53. UsercaseXPath().case(),
  54. ]
  55. def app_doc_types():
  56. from corehq.apps.app_manager.models import Application, RemoteApp, LinkedApplication
  57. return {
  58. 'Application': Application,
  59. 'Application-Deleted': Application,
  60. 'RemoteApp': RemoteApp,
  61. 'RemoteApp-Deleted': RemoteApp,
  62. 'LinkedApplication': LinkedApplication,
  63. 'LinkedApplication-Deleted': LinkedApplication
  64. }
  65. def is_linked_app(app_or_doc, include_deleted=False):
  66. return _get_doc_type(app_or_doc) in ('LinkedApplication', 'LinkedApplication-Deleted')
  67. def is_remote_app(app_or_doc, include_deleted=False):
  68. return _get_doc_type(app_or_doc) in ('RemoteApp', 'RemoteApp-Deleted')
  69. def _get_doc_type(app_or_doc):
  70. if hasattr(app_or_doc, 'doc_type'):
  71. doc_type = app_or_doc.doc_type
  72. elif 'doc_type' in app_or_doc:
  73. doc_type = app_or_doc['doc_type']
  74. assert doc_type
  75. return doc_type
  76. def _prepare_xpath_for_validation(xpath):
  77. prepared_xpath = xpath.lower()
  78. prepared_xpath = prepared_xpath.replace('"', "'")
  79. prepared_xpath = re.compile(r'\s').sub('', prepared_xpath)
  80. return prepared_xpath
  81. def _check_xpath_for_matches(xpath, substring_matches=None, pattern_matches=None):
  82. prepared_xpath = _prepare_xpath_for_validation(xpath)
  83. substring_matches = substring_matches or []
  84. pattern_matches = pattern_matches or []
  85. return any([
  86. re.compile(pattern).search(prepared_xpath) for pattern in pattern_matches
  87. ] + [
  88. substring in prepared_xpath for substring in substring_matches
  89. ])
  90. def xpath_references_case(xpath):
  91. # We want to determine here if the xpath references any cases other
  92. # than the user case. To determine if the xpath references the user
  93. # case, see xpath_references_usercase()
  94. # Assumes xpath has already been dot interpolated as needed.
  95. for substring in USERCASE_XPATH_SUBSTRING_MATCHES:
  96. xpath = xpath.replace(substring, '')
  97. return _check_xpath_for_matches(
  98. xpath,
  99. substring_matches=CASE_XPATH_SUBSTRING_MATCHES,
  100. )
  101. def xpath_references_usercase(xpath):
  102. # Assumes xpath has already been dot interpolated as needed.
  103. return _check_xpath_for_matches(
  104. xpath,
  105. substring_matches=USERCASE_XPATH_SUBSTRING_MATCHES,
  106. )
  107. def split_path(path):
  108. path_parts = path.split('/')
  109. name = path_parts.pop(-1)
  110. path = '/'.join(path_parts)
  111. return path, name
  112. def first_elem(elem_list):
  113. return elem_list[0] if elem_list else None
  114. def generate_xmlns():
  115. return str(uuid.uuid4()).upper()
  116. def save_xform(app, form, xml):
  117. def change_xmlns(xform, old_xmlns, new_xmlns):
  118. data = xform.data_node.render().decode('utf-8')
  119. data = data.replace(old_xmlns, new_xmlns, 1)
  120. xform.instance_node.remove(xform.data_node.xml)
  121. xform.instance_node.append(parse_xml(data))
  122. return xform.render()
  123. try:
  124. xform = XForm(xml, domain=app.domain)
  125. except XFormException:
  126. pass
  127. else:
  128. GENERIC_XMLNS = "http://www.w3.org/2002/xforms"
  129. uid = generate_xmlns()
  130. tag_xmlns = xform.data_node.tag_xmlns
  131. new_xmlns = form.xmlns or "http://openrosa.org/formdesigner/%s" % uid
  132. if not tag_xmlns or tag_xmlns == GENERIC_XMLNS: # no xmlns
  133. xml = change_xmlns(xform, GENERIC_XMLNS, new_xmlns)
  134. else:
  135. forms = [form_
  136. for form_ in app.get_xmlns_map().get(tag_xmlns, [])
  137. if form_.form_type != 'shadow_form']
  138. if len(forms) > 1 or (len(forms) == 1 and forms[0] is not form):
  139. if new_xmlns == tag_xmlns:
  140. new_xmlns = "http://openrosa.org/formdesigner/%s" % uid
  141. # form most likely created by app.copy_form(...)
  142. # or form is being updated with source copied from other form
  143. xml = change_xmlns(xform, tag_xmlns, new_xmlns)
  144. form.source = xml.decode('utf-8')
  145. if form.is_registration_form():
  146. # For registration forms, assume that the first question is the
  147. # case name unless something else has been specified
  148. questions = form.get_questions([app.default_language])
  149. if hasattr(form.actions, 'open_case'):
  150. path = form.actions.open_case.name_update.question_path
  151. if path:
  152. name_questions = [q for q in questions if q['value'] == path]
  153. if not len(name_questions):
  154. path = None
  155. if not path and len(questions):
  156. form.actions.open_case.name_update.question_path = questions[0]['value']
  157. return xml
  158. CASE_TYPE_REGEX = r'^[\w-]+$'
  159. _case_type_regex = re.compile(CASE_TYPE_REGEX)
  160. def is_valid_case_type(case_type, module):
  161. """
  162. >>> from corehq.apps.app_manager.models import Module, AdvancedModule
  163. >>> is_valid_case_type('foo', Module())
  164. True
  165. >>> is_valid_case_type('foo-bar', Module())
  166. True
  167. >>> is_valid_case_type('foo bar', Module())
  168. False
  169. >>> is_valid_case_type('', Module())
  170. False
  171. >>> is_valid_case_type(None, Module())
  172. False
  173. >>> is_valid_case_type('commcare-user', Module())
  174. False
  175. >>> is_valid_case_type('commcare-user', AdvancedModule())
  176. True
  177. """
  178. from corehq.apps.app_manager.models import AdvancedModule
  179. matches_regex = bool(_case_type_regex.match(case_type or ''))
  180. prevent_usercase_type = (case_type != USERCASE_TYPE or isinstance(module, AdvancedModule))
  181. return matches_regex and prevent_usercase_type
  182. def module_case_hierarchy_has_circular_reference(module):
  183. from corehq.apps.app_manager.suite_xml.utils import get_select_chain
  184. try:
  185. get_select_chain(module.get_app(), module)
  186. return False
  187. except SuiteValidationError:
  188. return True
  189. def is_usercase_in_use(domain_name):
  190. domain_obj = Domain.get_by_name(domain_name) if domain_name else None
  191. return domain_obj and domain_obj.usercase_enabled
  192. def get_settings_values(app):
  193. try:
  194. profile = app.profile
  195. except AttributeError:
  196. profile = {}
  197. hq_settings = dict([
  198. (attr, app[attr])
  199. for attr in app.properties() if not hasattr(app[attr], 'pop')
  200. ])
  201. if getattr(app, 'use_custom_suite', False):
  202. hq_settings.update({'custom_suite': getattr(app, 'custom_suite', None)})
  203. hq_settings['build_spec'] = app.build_spec.to_string()
  204. # the admin_password hash shouldn't be sent to the client
  205. hq_settings.pop('admin_password', None)
  206. # convert int to string
  207. hq_settings['mobile_ucr_restore_version'] = str(hq_settings.get('mobile_ucr_restore_version', '1.0'))
  208. domain_obj = Domain.get_by_name(app.domain)
  209. return {
  210. 'properties': profile.get('properties', {}),
  211. 'features': profile.get('features', {}),
  212. 'hq': hq_settings,
  213. '$parent': {
  214. 'doc_type': app.get_doc_type(),
  215. '_id': app.get_id,
  216. 'domain': app.domain,
  217. 'commtrack_enabled': domain_obj.commtrack_enabled,
  218. }
  219. }
  220. def add_odk_profile_after_build(app_build):
  221. """caller must save"""
  222. profile = app_build.create_profile(is_odk=True)
  223. app_build.lazy_put_attachment(profile, 'files/profile.ccpr')
  224. # hack this in for records
  225. app_build.odk_profile_created_after_build = True
  226. def create_temp_sort_column(sort_element, order):
  227. """
  228. Used to create a column for the sort only properties to
  229. add the field to the list of properties and app strings but
  230. not persist anything to the detail data.
  231. """
  232. from corehq.apps.app_manager.models import DetailColumn
  233. col = DetailColumn(
  234. model='case',
  235. field=sort_element.field,
  236. format='invisible',
  237. header=sort_element.display,
  238. )
  239. col._i = order
  240. return col
  241. def get_correct_app_class(doc):
  242. try:
  243. return app_doc_types()[doc['doc_type']]
  244. except KeyError:
  245. raise DocTypeError(doc['doc_type'])
  246. def languages_mapping():
  247. mapping = cache.get('__languages_mapping')
  248. if not mapping:
  249. with open('submodules/langcodes/langs.json', encoding='utf-8') as langs_file:
  250. lang_data = json.load(langs_file)
  251. mapping = dict([(l["two"], l["names"]) for l in lang_data])
  252. mapping["default"] = ["Default Language"]
  253. cache.set('__languages_mapping', mapping, 12*60*60)
  254. return mapping
  255. def version_key(ver):
  256. """
  257. A key function that takes a version and returns a numeric value that can
  258. be used for sorting
  259. >>> version_key('2')
  260. 2000000
  261. >>> version_key('2.9')
  262. 2009000
  263. >>> version_key('2.10')
  264. 2010000
  265. >>> version_key('2.9.1')
  266. 2009001
  267. >>> version_key('2.9.1.1')
  268. 2009001
  269. >>> version_key('2.9B')
  270. Traceback (most recent call last):
  271. ...
  272. ValueError: invalid literal for int() with base 10: '9B'
  273. """
  274. padded = ver + '.0.0'
  275. values = padded.split('.')
  276. return int(values[0]) * 1000000 + int(values[1]) * 1000 + int(values[2])
  277. def get_commcare_versions(request_user):
  278. versions = [i.version for i in get_commcare_builds(request_user)]
  279. return sorted(versions, key=version_key)
  280. def get_commcare_builds(request_user):
  281. can_view_superuser_builds = (request_user.is_superuser
  282. or toggles.IS_CONTRACTOR.enabled(request_user.username))
  283. return [
  284. i.build
  285. for i in CommCareBuildConfig.fetch().menu
  286. if can_view_superuser_builds or not i.superuser_only
  287. ]
  288. def actions_use_usercase(actions):
  289. return (('usercase_update' in actions and actions['usercase_update'].update) or
  290. ('usercase_preload' in actions and actions['usercase_preload'].preload))
  291. def advanced_actions_use_usercase(actions):
  292. return any(c.auto_select and c.auto_select.mode == AUTO_SELECT_USERCASE for c in actions.load_update_cases)
  293. def enable_usercase(domain_name):
  294. with CriticalSection(['enable_usercase_' + domain_name]):
  295. domain_obj = Domain.get_by_name(domain_name, strict=True)
  296. if not domain_obj: # copying domains passes in an id before name is saved
  297. domain_obj = Domain.get(domain_name)
  298. if not domain_obj.usercase_enabled:
  299. domain_obj.usercase_enabled = True
  300. domain_obj.save()
  301. create_usercases.delay(domain_name)
  302. def prefix_usercase_properties(properties):
  303. return {'{}{}'.format(USERCASE_PREFIX, prop) for prop in properties}
  304. def module_offers_registry_search(module):
  305. return (
  306. module_offers_search(module)
  307. and module.get_app().supports_data_registry
  308. and module.search_config.data_registry
  309. )
  310. def module_loads_registry_case(module):
  311. return (
  312. module_offers_registry_search(module)
  313. and module.search_config.data_registry_workflow == REGISTRY_WORKFLOW_LOAD_CASE
  314. )
  315. def module_uses_smart_links(module):
  316. return (
  317. module_offers_registry_search(module)
  318. and module.search_config.data_registry_workflow == REGISTRY_WORKFLOW_SMART_LINK
  319. )
  320. def module_offers_search(module):
  321. from corehq.apps.app_manager.models import AdvancedModule, Module, ShadowModule
  322. return (
  323. isinstance(module, (Module, AdvancedModule, ShadowModule)) and
  324. module.search_config and
  325. (module.search_config.properties or
  326. module.search_config.default_properties)
  327. )
  328. def module_uses_inline_search(module):
  329. """In 'inline search' mode the query and post are added to the form entry directly instead
  330. of creating a separate RemoteRequest entry."""
  331. return (
  332. module_offers_search(module)
  333. and module.search_config.inline_search
  334. and module.search_config.auto_launch
  335. )
  336. def get_cloudcare_session_data(domain_name, form, couch_user):
  337. from corehq.apps.app_manager.suite_xml.sections.entries import EntriesHelper
  338. datums = EntriesHelper.get_new_case_id_datums_meta(form)
  339. session_data = {datum.id: uuid.uuid4().hex for datum in datums}
  340. if couch_user.doc_type == 'CommCareUser': # smsforms.app.start_session could pass a CommCareCase
  341. try:
  342. extra_datums = EntriesHelper.get_extra_case_id_datums(form)
  343. except SuiteError as err:
  344. _assert = soft_assert(['nhooper_at_dimagi_dot_com'.replace('_at_', '@').replace('_dot_', '.')])
  345. _assert(False, 'Domain "%s": %s' % (domain_name, err))
  346. else:
  347. if EntriesHelper.any_usercase_datums(extra_datums):
  348. usercase_id = couch_user.get_usercase_id()
  349. if usercase_id:
  350. session_data[USERCASE_ID] = usercase_id
  351. return session_data
  352. def update_form_unique_ids(app_source, ids_map, update_all=True):
  353. """
  354. Accepts an ids_map translating IDs in app_source to the desired replacement
  355. ID. Form IDs not present in ids_map will be given new random UUIDs.
  356. """
  357. from corehq.apps.app_manager.models import form_id_references, jsonpath_update
  358. app_source = deepcopy(app_source)
  359. attachments = app_source['_attachments']
  360. def change_form_unique_id(form, old_id, new_id):
  361. form['unique_id'] = new_id
  362. if f"{old_id}.xml" in attachments:
  363. attachments[f"{new_id}.xml"] = attachments.pop(f"{old_id}.xml")
  364. # once Application.wrap includes deleting user_registration
  365. # we can remove this
  366. if 'user_registration' in app_source:
  367. del app_source['user_registration']
  368. new_ids_by_old = {}
  369. for m, module in enumerate(app_source['modules']):
  370. for f, form in enumerate(module['forms']):
  371. old_id = form['unique_id']
  372. if update_all or old_id in ids_map:
  373. new_id = ids_map.get(old_id, uuid.uuid4().hex)
  374. new_ids_by_old[old_id] = new_id
  375. change_form_unique_id(form, old_id, new_id)
  376. for reference_path in form_id_references:
  377. for reference in reference_path.find(app_source):
  378. if reference.value in new_ids_by_old:
  379. jsonpath_update(reference, new_ids_by_old[reference.value])
  380. return app_source
  381. def update_report_module_ids(app_source):
  382. """Make new report UUIDs so they stay unique
  383. Otherwise there would be multiple reports in the restore with the same UUID
  384. Set the report slug to the old UUID so any xpath expressions referencing
  385. the report by ID continue to work, if only in mobile UCR v2
  386. """
  387. app_source = deepcopy(app_source)
  388. for module in app_source['modules']:
  389. if module['module_type'] == 'report':
  390. for config in module['report_configs']:
  391. if not config.get('report_slug'):
  392. config['report_slug'] = config['uuid']
  393. config['uuid'] = uuid.uuid4().hex
  394. return app_source
  395. def _app_callout_templates():
  396. """Load app callout templates from config file on disk
  397. Generator function defers file access until needed, acts like a
  398. constant thereafter.
  399. """
  400. path = os.path.join(
  401. os.path.dirname(__file__),
  402. 'static', 'app_manager', 'json', 'vellum-app-callout-templates.yml'
  403. )
  404. if os.path.exists(path):
  405. with open(path, encoding='utf-8') as f:
  406. data = yaml.safe_load(f)
  407. else:
  408. logger.info("not found: %s", path)
  409. data = []
  410. while True:
  411. yield data
  412. app_callout_templates = _app_callout_templates()
  413. def purge_report_from_mobile_ucr(report_config):
  414. """
  415. Called when a report is deleted, this will remove any references to it in
  416. mobile UCR modules.
  417. """
  418. if not toggles.MOBILE_UCR.enabled(report_config.domain):
  419. return False
  420. did_purge_something = False
  421. for app in get_apps_in_domain(report_config.domain):
  422. save_app = False
  423. for module in app.modules:
  424. if module.module_type == 'report':
  425. valid_report_configs = [
  426. app_config for app_config in module.report_configs
  427. if app_config.report_id != report_config._id
  428. ]
  429. if len(valid_report_configs) != len(module.report_configs):
  430. module.report_configs = valid_report_configs
  431. save_app = True
  432. if save_app:
  433. app.save()
  434. did_purge_something = True
  435. return did_purge_something
  436. SortOnlyElement = namedtuple("SortOnlyElement", "field, sort_element, order")
  437. def get_sort_and_sort_only_columns(detail_columns, sort_elements):
  438. """
  439. extracts out info about columns that are added as only sort fields and columns added as both
  440. sort and display fields
  441. """
  442. sort_elements = OrderedDict((s.field, (s, i + 1)) for i, s in enumerate(sort_elements))
  443. sort_columns = {}
  444. for column in detail_columns:
  445. sort_element, order = sort_elements.pop(column.field, (None, None))
  446. if sort_element:
  447. sort_columns[column.field] = (sort_element, order)
  448. sort_only_elements = [
  449. SortOnlyElement(field, element, element_order)
  450. for field, (element, element_order) in sort_elements.items()
  451. ]
  452. return sort_only_elements, sort_columns
  453. def get_and_assert_practice_user_in_domain(practice_user_id, domain):
  454. # raises PracticeUserException if CommCareUser with practice_user_id is not a practice mode user
  455. # or if user doesn't belong to domain
  456. try:
  457. user = CommCareUser.get(practice_user_id)
  458. if not user.domain == domain:
  459. raise ResourceNotFound
  460. except ResourceNotFound:
  461. raise PracticeUserException(
  462. _("Practice User with id {id} not found, please make sure you have not deleted this user").format(
  463. id=practice_user_id)
  464. )
  465. if not user.is_demo_user:
  466. raise PracticeUserException(
  467. _("User {username} is not a practice user, please turn on practice mode for this user").format(
  468. username=user.username)
  469. )
  470. if user.is_deleted():
  471. raise PracticeUserException(
  472. _("User {username} has been deleted, you can't use that user as practice user").format(
  473. username=user.username)
  474. )
  475. if not user.is_active:
  476. raise PracticeUserException(
  477. _("User {username} has been deactivated, you can't use that user as practice user").format(
  478. username=user.username)
  479. )
  480. return user
  481. def get_form_source_download_url(xform):
  482. """Returns the download url for the form source for a submitted XForm
  483. """
  484. if not xform.build_id:
  485. return None
  486. try:
  487. app = get_app(xform.domain, xform.build_id)
  488. except Http404:
  489. return None
  490. if app.is_remote_app():
  491. return None
  492. try:
  493. form = app.get_forms_by_xmlns(xform.xmlns)[0]
  494. except IndexError:
  495. return None
  496. return reverse("app_download_file", args=[
  497. xform.domain,
  498. xform.build_id,
  499. app.get_form_filename(module=form.get_module(), form=form),
  500. ])
  501. @quickcache(['domain', 'profile_id'], timeout=24 * 60 * 60)
  502. def get_latest_enabled_build_for_profile(domain, profile_id):
  503. from corehq.apps.app_manager.models import LatestEnabledBuildProfiles
  504. latest_enabled_build = (LatestEnabledBuildProfiles.objects.
  505. filter(build_profile_id=profile_id, active=True)
  506. .order_by('-version')
  507. .first())
  508. if latest_enabled_build:
  509. return get_app(domain, latest_enabled_build.build_id)
  510. @quickcache(['domain', 'location_id', 'app_id'], timeout=24 * 60 * 60)
  511. def get_latest_app_release_by_location(domain, location_id, app_id):
  512. """
  513. for a location search for enabled app releases for all parent locations.
  514. Child location's setting takes precedence over parent
  515. """
  516. from corehq.apps.app_manager.models import AppReleaseByLocation
  517. location = SQLLocation.active_objects.get(location_id=location_id)
  518. location_and_ancestor_ids = location.get_ancestors(include_self=True).values_list(
  519. 'location_id', flat=True).reverse()
  520. # get all active enabled releases and order by version desc to get one with the highest version in the end
  521. # for a location. Do not use the first object itself in order to respect the location hierarchy and use
  522. # the closest location to determine the valid active release
  523. latest_enabled_releases = {
  524. release.location_id: release.build_id
  525. for release in
  526. AppReleaseByLocation.objects.filter(
  527. location_id__in=location_and_ancestor_ids, app_id=app_id, domain=domain, active=True).order_by(
  528. 'version')
  529. }
  530. for loc_id in location_and_ancestor_ids:
  531. build_id = latest_enabled_releases.get(loc_id)
  532. if build_id:
  533. return get_app(domain, build_id)
  534. def expire_get_latest_app_release_by_location_cache(app_release_by_location):
  535. """
  536. expire cache for the location and its descendants for the app corresponding to this enabled app release
  537. why? : Latest enabled release for a location is dependent on restrictions added for
  538. itself and its ancestors. Hence we expire the cache for location and its descendants for which the
  539. latest enabled release would depend on this location
  540. """
  541. location = SQLLocation.active_objects.get(location_id=app_release_by_location.location_id)
  542. location_and_descendants = location.get_descendants(include_self=True)
  543. for loc in location_and_descendants:
  544. get_latest_app_release_by_location.clear(app_release_by_location.domain, loc.location_id,
  545. app_release_by_location.app_id)
  546. @quickcache(['app_id'], timeout=24 * 60 * 60)
  547. def get_latest_enabled_versions_per_profile(app_id):
  548. from corehq.apps.app_manager.models import LatestEnabledBuildProfiles
  549. # a dict with each profile id mapped to its latest enabled version number, if present
  550. return {
  551. build_profile['build_profile_id']: build_profile['version__max']
  552. for build_profile in
  553. LatestEnabledBuildProfiles.objects.filter(app_id=app_id, active=True).values('build_profile_id').annotate(
  554. Max('version'))
  555. }
  556. def get_app_id_from_form_unique_id(domain, form_unique_id):
  557. """
  558. Do not use. This is here to support migrations and temporary cose for *removing*
  559. the constraint that form ids be lgobally unique. It will stop working as more
  560. duplicated form unique ids appear.
  561. """
  562. return _get_app_ids_by_form_unique_id(domain).get(form_unique_id)
  563. @quickcache(['domain'], timeout=1 * 60 * 60)
  564. def _get_app_ids_by_form_unique_id(domain):
  565. apps = get_apps_in_domain(domain, include_remote=False)
  566. app_ids = {}
  567. for app in apps:
  568. for module in app.modules:
  569. for form in module.get_forms():
  570. if form.unique_id in app_ids:
  571. raise AppManagerException("Could not identify app for form {}".format(form.unique_id))
  572. app_ids[form.unique_id] = app.get_id
  573. return app_ids
  574. def extract_instance_id_from_nodeset_ref(nodeset):
  575. # note: for simplicity, this only returns the first instance ref in the event there are multiple.
  576. # if that's ever a problem this method could be changed in the future to return a list
  577. if nodeset:
  578. matches = re.findall(r"instance\('(.*?)'\)", nodeset)
  579. return matches[0] if matches else None
  580. def wrap_transition_from_old_update_case_action(properties_dict):
  581. """
  582. This function assists wrap functions for changes to the FormActions and AdvancedFormActions models.
  583. A modification of UpdateCaseAction to use a ConditionalCaseUpdate instead of a simple question path
  584. was part of these changes. It also used as part of a follow-up migration.
  585. """
  586. if(properties_dict):
  587. first_prop_value = list(properties_dict.values())[0]
  588. # If the dict just holds question paths (strings) as values we want to translate the old
  589. # type of UpdateCaseAction model to the new.
  590. if isinstance(first_prop_value, str):
  591. new_dict_values = {}
  592. for case_property, question_path in properties_dict.items():
  593. new_dict_values[case_property] = {
  594. 'question_path': question_path
  595. }
  596. return new_dict_values
  597. return properties_dict