Skip to content

Commit

Permalink
Merge pull request #32431 from dimagi/sr/link-workflow2
Browse files Browse the repository at this point in the history
Convert FORM_LINK_WORKFLOW FF to a Standard plan privilege
  • Loading branch information
sravfeyn authored Jan 31, 2023
2 parents a1b92af + 6202729 commit 36979ed
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 20 deletions.
1 change: 1 addition & 0 deletions corehq/apps/accounting/bootstrap/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
privileges.DAILY_SAVED_EXPORT,
privileges.ZAPIER_INTEGRATION,
privileges.PRACTICE_MOBILE_WORKERS,
privileges.FORM_LINK_WORKFLOW,
]


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 3.2.16 on 2022-12-20 06:20

from django.core.management import call_command
from django.db import migrations

from corehq.privileges import FORM_LINK_WORKFLOW
from corehq.util.django_migrations import skip_on_fresh_install


@skip_on_fresh_install
def _grandfather_form_link_workflow_privs(apps, schema_editor):
call_command('cchq_prbac_bootstrap')
call_command(
'cchq_prbac_grandfather_privs',
FORM_LINK_WORKFLOW,
skip_edition='Paused,Community,Standard',
noinput=True,
)


class Migration(migrations.Migration):

dependencies = [
('accounting', '0063_replace_linked_projects_ff_with_erm'),
]

operations = [
migrations.RunPython(_grandfather_form_link_workflow_privs),
]
3 changes: 3 additions & 0 deletions corehq/apps/accounting/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ProductPlanNotFoundError,
)
from corehq.apps.domain.models import Domain
from corehq.toggles import domain_has_privilege_from_toggle
from corehq.util.quickcache import quickcache
from corehq.util.view_utils import absolute_reverse

Expand Down Expand Up @@ -132,6 +133,8 @@ def domain_has_privilege(domain, privilege_slug, **assignment):
return False
if plan_version.role.has_privilege(privilege):
return True
elif domain_has_privilege_from_toggle(privilege_slug, domain):
return True
except ProductPlanNotFoundError:
return False
except AccountingError:
Expand Down
12 changes: 6 additions & 6 deletions corehq/apps/app_manager/tests/test_build_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from django.test import SimpleTestCase

from corehq import privileges
from corehq.apps.app_manager.const import (
REGISTRY_WORKFLOW_LOAD_CASE,
REGISTRY_WORKFLOW_SMART_LINK,
Expand All @@ -21,7 +22,7 @@
Module,
)
from corehq.apps.app_manager.tests.app_factory import AppFactory
from corehq.util.test_utils import flag_enabled
from corehq.util.test_utils import flag_enabled, privilege_enabled


@patch('corehq.apps.app_manager.models.validate_xform', return_value=None)
Expand Down Expand Up @@ -262,7 +263,6 @@ def test_search_module_errors_instances(self, *args):
[error['type'] for error in factory.app.validate_app()]
)

@flag_enabled('FORM_LINK_WORKFLOW')
def test_form_module_validation(self, *args):
factory = AppFactory(build_version='2.24.0')
app = factory.app
Expand All @@ -283,7 +283,7 @@ def test_form_module_validation(self, *args):
'form': {'id': 0, 'name': {'en': 'register form 0'}},
}, errors)

@flag_enabled('FORM_LINK_WORKFLOW')
@privilege_enabled(privileges.FORM_LINK_WORKFLOW)
def test_form_link_validation_ok(self, *args):
factory = AppFactory(build_version='2.24.0', include_xmlns=True)
m0, m0f0 = factory.new_basic_module('m0', 'frog')
Expand All @@ -298,7 +298,7 @@ def test_form_link_validation_ok(self, *args):
errors = factory.app.validate_app()
self.assertNotIn('bad form link', [error['type'] for error in errors])

@flag_enabled('FORM_LINK_WORKFLOW')
@privilege_enabled(privileges.FORM_LINK_WORKFLOW)
def test_form_link_validation_mismatched_module(self, *args):
factory = AppFactory(build_version='2.24.0', include_xmlns=True)
m0, m0f0 = factory.new_basic_module('m0', 'frog')
Expand All @@ -319,7 +319,7 @@ def test_form_link_validation_mismatched_module(self, *args):
'form': {'id': 0, 'name': {'en': 'm0 form 0'}},
}, errors)

@flag_enabled('FORM_LINK_WORKFLOW')
@privilege_enabled(privileges.FORM_LINK_WORKFLOW)
def test_form_link_validation_shadow_module_ok(self, *args):
factory = AppFactory(build_version='2.9.0')
m0, m0f0 = factory.new_basic_module('parent', 'mother')
Expand All @@ -335,7 +335,7 @@ def test_form_link_validation_shadow_module_ok(self, *args):
errors = factory.app.validate_app()
self.assertNotIn('bad form link', [error['type'] for error in errors])

@flag_enabled('FORM_LINK_WORKFLOW')
@privilege_enabled(privileges.FORM_LINK_WORKFLOW)
def test_form_link_validation_mismatched_shadow_module(self, *args):
factory = AppFactory(build_version='2.9.0')
m0, m0f0 = factory.new_basic_module('m0', 'mother')
Expand Down
4 changes: 2 additions & 2 deletions corehq/apps/app_manager/views/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ def should_edit(attribute):
)
if (should_edit("form_links_xpath_expressions")
and should_edit("form_links_form_ids")
and toggles.FORM_LINK_WORKFLOW.enabled(domain)):
and domain_has_privilege(domain, privileges.FORM_LINK_WORKFLOW)):
form_link_data = zip(
request.POST.getlist('form_links_xpath_expressions'),
request.POST.getlist('form_links_form_ids'),
Expand Down Expand Up @@ -808,7 +808,7 @@ def get_form_view_context_and_template(request, domain, form, langs, current_lan
if module.root_module_id and not module.root_module.put_in_root:
if not module.root_module.is_multi_select():
form_workflows[WORKFLOW_PARENT_MODULE] = _("Parent Menu: ") + trans(module.root_module.name, langs)
allow_form_workflow = toggles.FORM_LINK_WORKFLOW.enabled(domain)
allow_form_workflow = domain_has_privilege(domain, privileges.FORM_LINK_WORKFLOW)
if allow_form_workflow or form.post_form_workflow == WORKFLOW_FORM:
form_workflows[WORKFLOW_FORM] = _("Link to other form or menu")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ def ensure_roles(self, roles, dry_run=False):
Role(slug=privileges.LOADTEST_USERS,
name='Loadtest Users',
description='Allows creating loadtest users'),
Role(slug=privileges.FORM_LINK_WORKFLOW,
name='Link to other forms',
description='Link to other forms in End of Form Navigation'),
]

BOOTSTRAP_PLANS = [
Expand Down
4 changes: 2 additions & 2 deletions corehq/apps/hqwebapp/session_details_endpoint/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,15 @@ def test_with_hmac_signing_fail(self):
self.assertEqual(401, response.status_code)

@softer_assert()
@flag_enabled('FORM_LINK_WORKFLOW')
@flag_enabled('SECURE_SESSION_TIMEOUT')
@flag_enabled('CALC_XPATHS', is_preview=True)
def test_session_details_view_toggles(self):
toggles.all_toggles()
data = json.dumps({'sessionId': self.session_key, 'domain': 'domain'})
response = _post_with_hmac(self.url, data, content_type="application/json")
self.assertEqual(200, response.status_code)
expected_response = self.expected_response.copy()
expected_response['enabled_toggles'] = ['FORM_LINK_WORKFLOW']
expected_response['enabled_toggles'] = ['SECURE_SESSION_TIMEOUT']
expected_response['enabled_previews'] = ['CALC_XPATHS']
self.assertJSONEqual(response.content, expected_response)

Expand Down
4 changes: 4 additions & 0 deletions corehq/privileges.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@

LOADTEST_USERS = 'loadtest_users'

FORM_LINK_WORKFLOW = 'form_link_workflow'

MAX_PRIVILEGES = [
LOOKUP_TABLES,
API_ACCESS,
Expand Down Expand Up @@ -134,6 +136,7 @@
RELEASE_MANAGEMENT,
LITE_RELEASE_MANAGEMENT,
LOADTEST_USERS,
FORM_LINK_WORKFLOW,
]

# These are special privileges related to their own rates in a SoftwarePlanVersion
Expand Down Expand Up @@ -196,4 +199,5 @@ def get_name_from_privilege(cls, privilege):
RELEASE_MANAGEMENT: _("Enterprise Release Management"),
LITE_RELEASE_MANAGEMENT: _("Multi-Environment Release Management"),
LOADTEST_USERS: _('Loadtest Users'),
FORM_LINK_WORKFLOW: _("Link to other forms in End of Form Navigation"),
}.get(privilege, privilege)
77 changes: 67 additions & 10 deletions corehq/toggles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from memoized import memoized

from corehq.extensions import extension_point, ResultFormat

from corehq import privileges
from .models import Toggle
from .shortcuts import set_toggle, toggle_enabled

Expand Down Expand Up @@ -537,7 +537,13 @@ def all_toggles_by_name_in_scope(scope_dict, toggle_class=StaticToggle):
result = {}
for toggle_name, toggle in scope_dict.items():
if not toggle_name.startswith('__'):
if isinstance(toggle, toggle_class):
if toggle_class == FrozenPrivilegeToggle:
# Include only FrozenPrivilegeToggle types
include = type(toggle) == FrozenPrivilegeToggle
else:
# Exclude FrozenPrivilegeToggle but include other subclasses such as FeatureRelease
include = isinstance(toggle, toggle_class) and type(toggle) != FrozenPrivilegeToggle
if include:
result[toggle_name] = toggle
return result

Expand Down Expand Up @@ -1109,14 +1115,6 @@ def _enable_search_index(domain, enabled):
[NAMESPACE_DOMAIN]
)

FORM_LINK_WORKFLOW = StaticToggle(
'form_link_workflow',
'Form linking workflow available on forms',
TAG_SOLUTIONS_CONDITIONAL,
[NAMESPACE_DOMAIN],
help_link='https://confluence.dimagi.com/display/saas/Form+Link+Workflow+Feature+Flag',
)

SECURE_SESSION_TIMEOUT = StaticToggle(
'secure_session_timeout',
"USH: Allow domain to override default length of inactivity timeout",
Expand Down Expand Up @@ -2407,3 +2405,62 @@ def _commtrackify(domain_name, toggle_is_enabled):
""",
parent_toggles=[EMBEDDED_TABLEAU]
)


class FrozenPrivilegeToggle(StaticToggle):
"""
A special toggle to represent a legacy toggle that should't be
edited via the UI or the code and its new associated privilege.
This can be used when releasing a domain-only Toggle to general
availability as a new paid privilege to support domains that
may not have the privilege but had the toggle enabled historically.
To do this, simply change the toggle type to FrozenPrivilegeToggle
and pass the privilege as the first argument to it.
For e.g.
If a toggle were defined as below
MY_DOMAIN_TOGGLE = StaticToggle(
'toggle_name',
'Title',
TAG_PRODUCT,
namespaces=[NAMESPACE_DOMAIN],
description='Description'
)
It can be converted to a FrozenPrivilegeToggle by defining.
MY_DOMAIN_TOGGLE = FrozenPrivilegeToggle(
privilege_name
'toggle_name',
'Title',
TAG_PRODUCT,
namespaces=[NAMESPACE_DOMAIN],
description='Description'
)
"""

def __init__(self, privilege_slug, *args, **kwargs):
self.privilege_slug = privilege_slug
super(FrozenPrivilegeToggle, self).__init__(*args, **kwargs)


def frozen_toggles_by_privilege():
return {
t.privilege_slug: t
for t in all_toggles_by_name_in_scope(globals(), FrozenPrivilegeToggle).values()
}


def domain_has_privilege_from_toggle(privilege_slug, domain):
toggle = frozen_toggles_by_privilege().get(privilege_slug)
return toggle and toggle.enabled(domain)


FORM_LINK_WORKFLOW = FrozenPrivilegeToggle(
privileges.FORM_LINK_WORKFLOW,
'form_link_workflow',
'Form linking workflow available on forms',
TAG_SOLUTIONS_CONDITIONAL,
[NAMESPACE_DOMAIN],
help_link='https://confluence.dimagi.com/display/saas/Form+Link+Workflow+Feature+Flag',
)
1 change: 1 addition & 0 deletions migrations.lock
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ accounting
0061_remove_enterprise_v1
0062_add_release_management_to_enterprise
0063_replace_linked_projects_ff_with_erm
0064_add_form_link_workflow_priv
admin
0001_initial
0002_logentry_remove_auto_add
Expand Down

0 comments on commit 36979ed

Please sign in to comment.