Пример #1
0
    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))
Пример #2
0
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()
Пример #3
0
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
Пример #4
0
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
Пример #5
0
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)
Пример #6
0
 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
Пример #7
0
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
Пример #8
0
 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)
Пример #9
0
    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')))
Пример #10
0
 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,
     }
Пример #11
0
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
Пример #12
0
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()
Пример #13
0
 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})"
Пример #14
0
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
Пример #15
0
 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)
     }
Пример #16
0
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])
Пример #17
0
    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]
            },
        }
Пример #18
0
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
Пример #19
0
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))