def _release_app(self, domain_link, model, user, build_and_release=False): if toggles.MULTI_MASTER_LINKED_DOMAINS.enabled( domain_link.linked_domain): return self._error_tuple(_("Multi master flag is in use")) app_id = model['detail']['app_id'] found = False error_prefix = "" try: for linked_app in get_apps_in_domain(domain_link.linked_domain, include_remote=False): if is_linked_app( linked_app) and linked_app.family_id == app_id: found = True app = update_linked_app(linked_app, app_id, user.user_id) if not found: return self._error_tuple(_("Could not find app")) if build_and_release: error_prefix = _("Updated app but did not build or release: ") build = app.make_build() build.is_released = True build.save(increment_version=False) except Exception as e: # intentionally broad return self._error_tuple(error_prefix + str(e))
def multimedia_ajax(request, domain, app_id): app = get_app(domain, app_id) if not is_remote_app(app): try: multimedia_state = app.check_media_state() except ReportConfigurationNotFoundError: return JsonResponse( {"message": _("One of the Report menus is misconfigured, please try again after they are fixed")}, status=500) context = { 'multimedia_state': multimedia_state, 'domain': domain, 'app': app, 'is_linked_app': is_linked_app(app), } if toggles.MULTI_MASTER_LINKED_DOMAINS.enabled_for_request(request): missing_paths = {p for p in app.all_media_paths() if p not in app.multimedia_map} import_apps = get_apps_in_domain(domain, include_remote=False) import_app_counts = { a.id: len(missing_paths.intersection(a.multimedia_map.keys())) for a in import_apps } import_apps = [a for a in import_apps if import_app_counts[a.id]] context.update({ 'import_apps': import_apps, 'import_app_counts': import_app_counts, }) return render(request, "app_manager/partials/settings/multimedia_ajax.html", context) else: raise Http404()
def get_linked_apps_for_domain(domain): linked_apps = [] apps = get_apps_in_domain(domain, include_remote=False) for app in apps: if is_linked_app(app): linked_apps.append(app) return linked_apps
def unlink_app(linked_app): if not is_linked_app(linked_app): return None converted_app = linked_app.convert_to_application() converted_app.save() return converted_app
def _copy_app_helper(request, from_app_id, to_domain, to_app_name): extra_properties = {'name': to_app_name} app_copy = import_app_util(from_app_id, to_domain, extra_properties, request) if is_linked_app(app_copy): app_copy = app_copy.convert_to_application() app_copy.save() return back_to_main(request, app_copy.domain, app_id=app_copy._id)
def process_upload(self): if self.app.logo_refs is None: self.app.logo_refs = {} ref = super(ProcessLogoFileUploadView, self).process_upload() self.app.logo_refs[self.filename] = ref['ref'] if is_linked_app(self.app): self.app.linked_app_logo_refs[self.filename] = ref['ref'] self.app.save() return ref
def unlink_app(linked_app): if not is_linked_app(linked_app): return None converted_app = linked_app.convert_to_application() # reset family_id since the link is being removed converted_app.family_id = None converted_app.save() return converted_app
def _get_apps(self): master_list = {} linked_list = {} briefs = get_brief_apps_in_domain(self.domain, include_remote=False) for brief in briefs: if is_linked_app(brief): linked_list[brief._id] = brief else: master_list[brief._id] = brief return (master_list, linked_list)
def __init__(self, from_domain, app, *args, **kwargs): super(CopyApplicationForm, self).__init__(*args, **kwargs) fields = ['domain', 'name', 'toggles', 'build_id'] self.from_domain = from_domain if app: self.fields['name'].initial = app.name if toggles.LINKED_DOMAINS.enabled( self.from_domain) and not is_linked_app(app): fields.append(PrependedText('linked', '')) self.helper = FormHelper() self.helper.label_class = 'col-sm-3 col-md-4 col-lg-2' self.helper.field_class = 'col-sm-9 col-md-8 col-lg-6' self.helper.layout = crispy.Layout( crispy.Fieldset( _('Copy Application for Editing') if is_linked_app(app) else _('Copy Application'), *fields), crispy.Hidden('app', app.get_id), hqcrispy.FormActions( StrictButton(_('Copy'), type='button', css_class='btn-primary')))
def _app_dict(self, app): lang, langs = get_langs(self.request, app) return { 'VELLUM_TYPES': VELLUM_TYPES, 'form_name_map': _get_name_map(app), 'lang': lang, 'langs': langs, 'app_langs': app.langs, 'app_id': app.id, 'app_name': app.name, 'read_only': is_linked_app(app) or app.id != app.origin_id, 'app_version': app.version, 'latest_app_id': app.origin_id, }
def get_upstream_and_downstream_apps(domain): """ Return 2 lists of app_briefs The upstream_list contains apps that originated in the specified domain The downstream_list contains apps that have been pulled from a domain upstream of the specified domain """ upstream_list = {} downstream_list = {} briefs = get_brief_apps_in_domain(domain, include_remote=False) for brief in briefs: if is_linked_app(brief): downstream_list[brief._id] = brief else: upstream_list[brief._id] = brief return upstream_list, downstream_list
def multimedia_ajax(request, domain, app_id): app = get_app(domain, app_id) if not is_remote_app(app): try: multimedia_state = app.check_media_state() except ReportConfigurationNotFoundError: return JsonResponse( {"message": _("One of the Report menus is misconfigured, please try again after they are fixed")}, status=500) context = { 'multimedia_state': multimedia_state, 'domain': domain, 'app': app, 'is_linked_app': is_linked_app(app), } return render(request, "app_manager/partials/settings/multimedia_ajax.html", context) else: raise Http404()
def get_smart_link_function(self): # Returns XPath that will evaluate to a URL. # For example, return value could be # concat('https://www.cchq.org/a/', $domain, '/app/v1/123/smartlink/', '?arg1=', $arg1, '&arg2=', $arg2) # Which could evaluate to # https://www.cchq.org/a/mydomain/app/v1/123/smartlink/?arg1=abd&arg2=def app_id = self.app.upstream_app_id if is_linked_app( self.app) else self.app.origin_id url = absolute_reverse( "session_endpoint", args=["---", app_id, self.module.session_endpoint_id]) prefix, suffix = url.split("---") params = "" argument_ids = self.endpoint_argument_ids if argument_ids: params = f", '?{argument_ids[-1]}=', ${argument_ids[-1]}" for argument_id in argument_ids[:-1]: params += f", '&{argument_id}=', ${argument_id}" return f"concat('{prefix}', $domain, '{suffix}'{params})"
def get_app_view_context(request, app): """ This provides the context to render commcare settings on Edit Application Settings page This is where additional app or domain specific context can be added to any individual commcare-setting defined in commcare-app-settings.yaml or commcare-profile-settings.yaml """ context = {} settings_layout = copy.deepcopy( get_commcare_settings_layout(app) ) for section in settings_layout: new_settings = [] for setting in section['settings']: toggle_name = setting.get('toggle') if toggle_name and not toggle_enabled(request, toggle_name): continue privilege_name = setting.get('privilege') if privilege_name and not has_privilege(request, privilege_name): continue disable_if_true = setting.get('disable_if_true') if disable_if_true and getattr(app, setting['id']): continue if is_linked_app(app): if setting['id'] in app.SUPPORTED_SETTINGS: if setting['id'] not in app.linked_app_attrs: setting['is_inherited'] = True new_settings.append(setting) section['settings'] = new_settings app_view_options = { 'permissions': { 'cloudcare': has_privilege(request, privileges.CLOUDCARE), 'case_sharing_groups': has_privilege(request, privileges.CASE_SHARING_GROUPS), }, 'sections': settings_layout, 'urls': { 'save': reverse("edit_commcare_settings", args=(app.domain, app.id)), }, 'user': { 'is_previewer': request.couch_user.is_previewer(), }, 'values': get_settings_values(app), 'warning': _("This is not an allowed value for this field"), } if (app.get_doc_type() == 'Application' and toggles.CUSTOM_PROPERTIES.enabled(request.domain) and 'custom_properties' in getattr(app, 'profile', {})): custom_properties_array = [{'key': p[0], 'value': p[1]} for p in app.profile.get('custom_properties').items()] app_view_options.update({'customProperties': custom_properties_array}) context.update({ 'app_view_options': app_view_options, }) build_config = CommCareBuildConfig.fetch() options = build_config.get_menu() if not request.user.is_superuser and not toggles.IS_CONTRACTOR.enabled(request.user.username): options = [option for option in options if not option.superuser_only] options_map = defaultdict(lambda: {"values": [], "value_names": []}) for option in options: builds = options_map[option.build.major_release()] builds["values"].append(option.build.to_string()) builds["value_names"].append(option.get_label()) if "default" not in builds: app_ver = MAJOR_RELEASE_TO_VERSION[option.build.major_release()] builds["default"] = build_config.get_default(app_ver).to_string() def _get_setting(setting_type, setting_id): # get setting dict from settings_layout if not settings_layout: return None matched = [x for x in [ setting for section in settings_layout for setting in section['settings'] ] if x['type'] == setting_type and x['id'] == setting_id] if matched: return matched[0] else: return None build_spec_setting = _get_setting('hq', 'build_spec') if build_spec_setting: build_spec_setting['options_map'] = options_map build_spec_setting['default_app_version'] = app.application_version practice_user_setting = _get_setting('hq', 'practice_mobile_worker_id') if practice_user_setting and has_privilege(request, privileges.PRACTICE_MOBILE_WORKERS): try: practice_users = get_practice_mode_mobile_workers(request.domain) except ESError: notify_exception(request, 'Error getting practice mode mobile workers') practice_users = [] practice_user_setting['values'] = [''] + [u['_id'] for u in practice_users] practice_user_setting['value_names'] = [_('Not set')] + [u['username'] for u in practice_users] context.update({ 'bulk_ui_translation_upload': { 'action': reverse('upload_bulk_ui_translations', args=(app.domain, app.get_id)), 'download_url': reverse('download_bulk_ui_translations', args=(app.domain, app.get_id)), 'adjective': _("U\u200BI translation"), 'plural_noun': _("U\u200BI translations"), }, 'bulk_app_translation_upload': { 'action': reverse('upload_bulk_app_translations', args=(app.domain, app.get_id)), 'download_url': reverse('download_bulk_app_translations', args=(app.domain, app.get_id)), 'adjective': _("app translation"), 'plural_noun': _("app translations"), 'can_select_language': toggles.BULK_UPDATE_MULTIMEDIA_PATHS.enabled_for_request(request), 'can_validate_app_translations': toggles.VALIDATE_APP_TRANSLATIONS.enabled_for_request(request), }, }) context.update({ 'bulk_ui_translation_form': get_bulk_upload_form( context, context_key="bulk_ui_translation_upload", ), 'bulk_app_translation_form': get_bulk_upload_form( context, context_key="bulk_app_translation_upload", form_class=AppTranslationsBulkUploadForm, ), }) context.update({ 'smart_lang_display_enabled': getattr(app, 'smart_lang_display', False) }) context.update({ 'is_linked_app': is_linked_app(app), 'is_remote_app': is_remote_app(app), }) if is_linked_app(app): try: master_versions_by_id = app.get_latest_master_releases_versions() master_briefs = [brief for brief in app.get_master_app_briefs() if brief.id in master_versions_by_id] except RemoteRequestError: messages.error(request, "Unable to reach remote master server. Please try again later.") master_versions_by_id = {} master_briefs = [] upstream_brief = {} for b in master_briefs: if b.id == app.upstream_app_id: upstream_brief = b context.update({ 'master_briefs': master_briefs, 'master_versions_by_id': master_versions_by_id, 'multiple_masters': app.enable_multi_master and len(master_briefs) > 1, 'upstream_version': app.upstream_version, 'upstream_brief': upstream_brief, 'upstream_url': _get_upstream_url(app, request.couch_user), 'upstream_url_template': _get_upstream_url(app, request.couch_user, master_app_id='---'), }) return context
def linked_app_names(self, domain): return { app._id: app.name for app in get_brief_apps_in_domain(domain) if is_linked_app(app) }
def push_models(master_domain, models, linked_domains, build_apps, username): domain_links_by_linked_domain = { link.linked_domain: link for link in get_linked_domains(master_domain) } user = CouchUser.get_by_username(username) errors_by_domain = defaultdict(list) successes_by_domain = defaultdict(list) for linked_domain in linked_domains: if linked_domain not in domain_links_by_linked_domain: errors_by_domain[linked_domain].append( _("Project space {} is no longer linked to {}. No content " "was released to it.").format(master_domain, linked_domain)) continue domain_link = domain_links_by_linked_domain[linked_domain] for model in models: try: found = False updated_app = False built_app = False if model['type'] == MODEL_APP: app_id = model['detail']['app_id'] for linked_app in get_apps_in_domain(linked_domain, include_remote=False): if is_linked_app( linked_app) and linked_app.family_id == app_id: found = True if toggles.MULTI_MASTER_LINKED_DOMAINS.enabled( linked_domain): errors_by_domain[linked_domain].append( textwrap.dedent( _(""" Could not update {} because multi master flag is in use """.strip()).format(model['name']))) continue app = update_linked_app(linked_app, app_id, user.user_id) updated_app = True if build_apps: build = app.make_build() build.is_released = True build.save(increment_version=False) built_app = True elif model['type'] == MODEL_REPORT: report_id = model['detail']['report_id'] for linked_report in get_report_configs_for_domain( linked_domain): if linked_report.report_meta.master_id == report_id: found = True update_linked_ucr(domain_link, linked_report.get_id) elif (model['type'] == MODEL_CASE_SEARCH and not toggles.SYNC_SEARCH_CASE_CLAIM.enabled(linked_domain) ): errors_by_domain[linked_domain].append( textwrap.dedent( _(""" Could not update {} because case claim flag is not on """.strip()).format(model['name']))) continue else: found = True update_model_type(domain_link, model['type'], model_detail=model['detail']) domain_link.update_last_pull(model['type'], user._id, model_details=model['detail']) if found: successes_by_domain[linked_domain].append( _("{} was updated").format(model['name'])) else: errors_by_domain[linked_domain].append( _("Could not find {}").format(model['name'])) except Exception as e: # intentionally broad if model[ 'type'] == MODEL_APP and updated_app and build_apps and not built_app: # Updating an app can be a 2-step process, make it clear which one failed errors_by_domain[linked_domain].append( textwrap.dedent( _(""" Updated {} but could not make and release build: {} """.strip()).format(model['name'], str(e)))) else: errors_by_domain[linked_domain].append( textwrap.dedent( _(""" Could not update {}: {} """.strip()).format(model['name'], str(e)))) notify_exception( None, "Exception pushing linked domains: {}".format(e)) subject = _("Linked project release complete.") if errors_by_domain: subject += _(" Errors occurred.") error_domain_count = len(errors_by_domain) success_domain_count = len(linked_domains) - error_domain_count message = _(""" Release complete. {} project(s) succeeded. {} The following content was released: {} The following linked project spaces received content: """).format( success_domain_count, _("{} project(s) encountered errors.").format(error_domain_count) if error_domain_count else "", "\n".join(["- " + m['name'] for m in models])) for linked_domain in linked_domains: if linked_domain not in errors_by_domain: message += _("\n- {} updated successfully").format(linked_domain) else: message += _("\n- {} encountered errors:").format(linked_domain) for msg in errors_by_domain[linked_domain] + successes_by_domain[ linked_domain]: message += "\n - " + msg send_mail_async.delay(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email or user.username])
def page_context(self): timezone = get_timezone_for_request() def _link_context(link, timezone=timezone): return { 'linked_domain': link.linked_domain, 'master_domain': link.qualified_master, 'remote_base_url': link.remote_base_url, 'is_remote': link.is_remote, 'last_update': server_to_user_time(link.last_pull, timezone) if link.last_pull else 'Never', } model_status = [] linked_models = dict(LINKED_MODELS) master_link = get_domain_master_link(self.domain) if master_link: linked_apps = { app._id: app for app in get_brief_apps_in_domain(self.domain) if is_linked_app(app) } models_seen = set() history = DomainLinkHistory.objects.filter( link=master_link ).annotate(row_number=RawSQL( 'row_number() OVER (PARTITION BY model, model_detail ORDER BY date DESC)', [])) for action in history: models_seen.add(action.model) if action.row_number != 1: # first row is the most recent continue name = linked_models[action.model] update = { 'type': action.model, 'name': name, 'last_update': server_to_user_time(action.date, timezone), 'detail': action.model_detail, 'can_update': True } if action.model == 'app': app_name = 'Unknown App' if action.model_detail: detail = action.wrapped_detail app = linked_apps.pop(detail.app_id, None) app_name = app.name if app else detail.app_id if app: update['detail'] = action.model_detail else: update['can_update'] = False else: update['can_update'] = False update['name'] = '{} ({})'.format(name, app_name) model_status.append(update) # Add in models that have never been synced for model, name in LINKED_MODELS: if model not in models_seen and model != 'app': model_status.append({ 'type': model, 'name': name, 'last_update': ugettext('Never'), 'detail': None, 'can_update': True }) # Add in apps that have never been synced if linked_apps: for app in linked_apps.values(): update = { 'type': 'app', 'name': '{} ({})'.format(linked_models['app'], app.name), 'last_update': None, 'detail': AppLinkDetail(app_id=app._id).to_json(), 'can_update': True } model_status.append(update) return { 'domain': self.domain, 'timezone': timezone.localize(datetime.utcnow()).tzname(), 'view_data': { 'master_link': _link_context(master_link) if master_link else None, 'model_status': sorted(model_status, key=lambda m: m['name']), 'linked_domains': [ _link_context(link) for link in get_linked_domains(self.domain) ], 'models': [{ 'slug': model[0], 'name': model[1] } for model in LINKED_MODELS] }, }
def _get_form_designer_view(request, domain, app, module, form): if app and app.copy_of: messages.warning( request, _("You tried to edit a form that was from a previous release, so " "we have directed you to the latest version of your application." )) return back_to_main(request, domain, app_id=app.id) if not form.can_edit_in_vellum: messages.warning( request, _("You tried to edit this form in the Form Builder. " "However, your administrator has locked this form against editing " "in the form builder, so we have redirected you to " "the form's front page instead.")) return back_to_main(request, domain, app_id=app.id, form_unique_id=form.unique_id) if is_linked_app(app): messages.warning( request, _("You tried to edit this form in the Form Builder. " "However, this is a linked application and you can only make changes to the " "upstream version.")) return back_to_main(request, domain, app_id=app.id) send_hubspot_form(HUBSPOT_FORM_BUILDER_FORM_ID, request) def _form_too_large(_app, _form): # form less than 0.1MB, anything larger starts to have # performance issues with fullstory return _app.blobs['{}.xml'.format( _form.unique_id)]['content_length'] > 102400 context = get_apps_base_context(request, domain, app) context.update(locals()) vellum_options = _get_base_vellum_options(request, domain, form, context['lang']) vellum_options['core'] = _get_vellum_core_context(request, domain, app, module, form, context['lang']) vellum_options['plugins'] = _get_vellum_plugins(domain, form, module) vellum_options['features'] = _get_vellum_features(request, domain, app) context['vellum_options'] = vellum_options context.update({ 'vellum_debug': settings.VELLUM_DEBUG, 'nav_form': form, 'formdesigner': True, 'include_fullstory': not _form_too_large(app, form), 'CKEDITOR_BASEPATH': "app_manager/js/vellum/lib/ckeditor/", 'show_live_preview': should_show_preview_app( request, app, request.couch_user.username, ), 'show_ui_notification_to_hide_translations': (len(app.langs) > 2), }) context.update(_get_requirejs_context()) if request.user.is_superuser: context.update({ 'notification_options': _get_notification_options(request, domain, app, form) }) notify_form_opened(domain, request.couch_user, app.id, form.unique_id) response = render(request, "app_manager/form_designer.html", context) set_lang_cookie(response, context['lang']) return response
def edit_app_attr(request, domain, app_id, attr): """ Called to edit any (supported) app attribute, given by attr """ app = get_app(domain, app_id) try: hq_settings = json.loads(request.body.decode('utf-8'))['hq'] except ValueError: hq_settings = request.POST can_use_case_sharing = has_privilege(request, privileges.CASE_SHARING_GROUPS) attributes = [ 'all', 'recipients', 'name', 'text_input', 'platform', 'build_spec', 'use_custom_suite', 'custom_suite', 'admin_password', 'comment', 'use_j2me_endpoint', # Application only 'cloudcare_enabled', 'case_sharing', 'translation_strategy', 'auto_gps_capture', # RemoteApp only 'profile_url', 'manage_urls', 'mobile_ucr_restore_version', ] if attr not in attributes: return HttpResponseBadRequest() def should_edit(attribute): return attribute == attr or ('all' == attr and attribute in hq_settings) def parse_sync_interval(interval): try: return int(interval) except ValueError: pass resp = {"update": {}} # For either type of app def _always_allowed(x): return True easy_attrs = ( ('build_spec', BuildSpec.from_string, _always_allowed), ('practice_mobile_worker_id', None, _always_allowed), ('case_sharing', None, lambda x: can_use_case_sharing or getattr(app, x)), ('cloudcare_enabled', None, _always_allowed), ('manage_urls', None, _always_allowed), ('name', None, _always_allowed), ('platform', None, _always_allowed), ('recipients', None, _always_allowed), ('text_input', None, _always_allowed), ('use_custom_suite', None, _always_allowed), ('secure_submissions', None, _always_allowed), ('translation_strategy', None, _always_allowed), ('auto_gps_capture', None, _always_allowed), ('use_grid_menus', None, _always_allowed), ('grid_form_menus', None, _always_allowed), ('target_commcare_flavor', None, _always_allowed), ('comment', None, _always_allowed), ('custom_base_url', None, _always_allowed), ('use_j2me_endpoint', None, _always_allowed), ('mobile_ucr_restore_version', None, _always_allowed), ('location_fixture_restore', None, _always_allowed), ) for attribute, transformation, can_set_attr in easy_attrs: if should_edit(attribute): value = hq_settings[attribute] if transformation: value = transformation(value) if can_set_attr(attribute): setattr(app, attribute, value) if is_linked_app(app) and attribute in app.SUPPORTED_SETTINGS: app.linked_app_attrs.update({ attribute: value, }) if should_edit("name"): clear_app_cache(request, domain) name = hq_settings['name'] resp['update'].update({ '.variable-app_name': name, '[data-id="{id}"]'.format(id=app_id): ApplicationsTab.make_app_title(app), }) if should_edit("build_spec"): resp['update']['commcare-version'] = app.commcare_minor_release if should_edit("practice_mobile_worker_id"): user_id = hq_settings['practice_mobile_worker_id'] if not app.enable_practice_users: app.practice_mobile_worker_id = None elif user_id: get_and_assert_practice_user_in_domain(user_id, request.domain) if should_edit("admin_password"): admin_password = hq_settings.get('admin_password') if admin_password: app.set_admin_password(admin_password) # For Normal Apps if should_edit("cloudcare_enabled"): if app.get_doc_type() not in ("Application",): raise Exception("App type %s does not support Web Apps" % app.get_doc_type()) if not has_privilege(request, privileges.CLOUDCARE): app.cloudcare_enabled = False def require_remote_app(): if not is_remote_app(app): raise Exception("App type %s does not support profile url" % app.get_doc_type()) # For RemoteApps if should_edit("profile_url"): require_remote_app() app['profile_url'] = hq_settings['profile_url'] if should_edit("manage_urls"): require_remote_app() app.save(resp) # this is a put_attachment, so it has to go after everything is saved if should_edit("custom_suite"): app.set_custom_suite(hq_settings['custom_suite']) return HttpResponse(json.dumps(resp))