Skip to content

Commit

Permalink
Merge pull request #22736 from dimagi/mk-215
Browse files Browse the repository at this point in the history
Selective Build release for app profile
  • Loading branch information
mkangia committed Dec 23, 2018
2 parents 061dfcb + 99a1480 commit 1b30445
Show file tree
Hide file tree
Showing 18 changed files with 246 additions and 17 deletions.
10 changes: 9 additions & 1 deletion corehq/apps/app_manager/decorators.py
Expand Up @@ -7,9 +7,11 @@
from couchdbkit.exceptions import ResourceConflict
from django.views.decorators.http import require_POST
from django.urls import reverse
from corehq import toggles
from corehq.apps.app_manager.exceptions import CaseError
from corehq.apps.app_manager.dbaccessors import get_app
from corehq.apps.app_manager.models import AppEditingError
from corehq.apps.app_manager.util import get_latest_enabled_build_for_profile
from corehq.apps.users.decorators import require_permission
from corehq.apps.users.models import Permissions
from corehq.apps.domain.decorators import login_and_domain_required
Expand Down Expand Up @@ -67,8 +69,14 @@ def _safe_cached_download(request, *args, **kwargs):
request.GET = request.GET.copy()
request.GET.pop('username')

latest_enabled_build = None
if latest and request.GET.get('profile') and toggles.RELEASE_BUILDS_PER_PROFILE.enabled(domain):
latest_enabled_build = get_latest_enabled_build_for_profile(domain, request.GET.get('profile'))
try:
request.app = get_app(domain, app_id, latest=latest, target=target)
if latest_enabled_build:
request.app = latest_enabled_build
else:
request.app = get_app(domain, app_id, latest=latest, target=target)
if not request.app.doc_type.endswith(DELETED_SUFFIX):
response = f(request, *args, **kwargs)
if request.app.copy_of is not None and request.app.is_released:
Expand Down
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-11 22:05
from __future__ import absolute_import
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
('app_manager', '0001_linked_app_domain'),
]

operations = [
migrations.CreateModel(
name='LatestEnabledBuildProfiles',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('app_id', models.CharField(max_length=255)),
('build_profile_id', models.CharField(max_length=255)),
('version', models.IntegerField()),
('build_id', models.CharField(max_length=255)),
],
),
]
13 changes: 13 additions & 0 deletions corehq/apps/app_manager/models.py
Expand Up @@ -29,6 +29,7 @@
from django.core.cache import cache
from django.utils.translation import override, ugettext as _, ugettext
from django.utils.translation import ugettext_lazy
from django.db import models
from couchdbkit.exceptions import BadValueError

from corehq.apps.app_manager.app_schemas.case_properties import (
Expand Down Expand Up @@ -127,6 +128,8 @@
LatestAppInfo,
update_report_module_ids,
module_offers_search,
get_latest_enabled_build_for_profile,
get_enabled_build_profiles_for_version,
)
from corehq.apps.app_manager.xform import XForm, parse_xml as _parse_xml, \
validate_xform
Expand Down Expand Up @@ -6837,6 +6840,16 @@ def save(self, *args, **kwargs):
super(GlobalAppConfig, self).save(*args, **kwargs)


class LatestEnabledBuildProfiles(models.Model):
app_id = models.CharField(max_length=255)
build_profile_id = models.CharField(max_length=255)
version = models.IntegerField()
build_id = models.CharField(max_length=255)

def expire_cache(self, domain):
get_latest_enabled_build_for_profile.clear(domain, self.build_profile_id)
get_enabled_build_profiles_for_version.clear(self.build_id, self.version)

# backwards compatibility with suite-1.0.xml
FormBase.get_command_id = lambda self: id_strings.form_command(self)
FormBase.get_locale_id = lambda self: id_strings.form_locale(self)
Expand Down
11 changes: 11 additions & 0 deletions corehq/apps/app_manager/signals.py
Expand Up @@ -4,6 +4,9 @@

from corehq.apps.callcenter.app_parser import get_call_center_config_from_app
from corehq.apps.domain.models import Domain
from corehq.apps.app_manager.util import get_latest_enabled_build_for_profile
from corehq.apps.app_manager.util import get_enabled_build_profiles_for_version
from corehq import toggles
from dimagi.utils.logging import notify_exception


Expand Down Expand Up @@ -34,9 +37,17 @@ def update_callcenter_config(sender, application, **kwargs):
notify_exception(None, "Error updating CallCenter config for app build")


def expire_latest_enabled_build_profiles(sender, application, **kwargs):
if application.copy_of and toggles.RELEASE_BUILDS_PER_PROFILE.enabled(application.domain):
for build_profile_id in application.build_profiles:
get_latest_enabled_build_for_profile.clear(application.domain, build_profile_id)
get_enabled_build_profiles_for_version.clear(application.get_id, application.version)


app_post_save = Signal(providing_args=['application'])

app_post_save.connect(create_app_structure_repeat_records)
app_post_save.connect(update_callcenter_config)
app_post_save.connect(expire_latest_enabled_build_profiles)

app_post_release = Signal(providing_args=['application'])
Expand Up @@ -30,13 +30,16 @@ hqDefine("app_manager/js/releases/app_view_release_manager", function () {
var $profilesTab = $('#profiles-tab');
if ($profilesTab.length) {
var profiles = hqImport('app_manager/js/releases/language_profiles');
var latestEnabledVersions = hqImport("hqwebapp/js/initial_page_data").get(
'latest_version_for_build_profiles');
profiles.setProfileUrl(initial_page_data('application_profile_url'));
var profileManagerModel = profiles.profileManager;
var app_langs = initial_page_data("langs");
var app_profiles = initial_page_data('build_profiles');
var enable_practice_users = initial_page_data('enable_practice_users');
var practice_users = initial_page_data('practice_users');
var profileManager = profileManagerModel(app_profiles, app_langs, enable_practice_users, practice_users);
var profileManager = profileManagerModel(app_profiles, app_langs, enable_practice_users, practice_users,
latestEnabledVersions);
$profilesTab.koApplyBindings(profileManager);
}

Expand Down
Expand Up @@ -3,22 +3,24 @@ hqDefine('app_manager/js/releases/language_profiles', function () {
var _p = {};
_p.profileUrl = 'profiles/';

var profileModel = function (profileLangs, name, id, practiceUser) {
var profileModel = function (profileLangs, name, id, practiceUser, latestEnabledVersions) {
var self = {};
self.id = id;
self.langs = ko.observableArray(profileLangs);
self.name = ko.observable(name);
self.defaultLang = ko.observable(profileLangs[0]);
self.practiceUser = ko.observable(practiceUser);
self.latestEnabledVersion = ko.observable(latestEnabledVersions[self.id]);
return self;
};

function setProfileUrl(url) {
_p.profileUrl = url;
}

var profileManager = function (appProfiles, appLangs, enablePracticeUsers, practiceUsers) {
var profileManager = function (appProfiles, appLangs, enablePracticeUsers, practiceUsers, latestEnabledVersions) {
var self = {};
self.latestEnabledVersions = latestEnabledVersions;
self.app_profiles = ko.observableArray([]);
self.app_langs = appLangs;
self.enable_practice_users = enablePracticeUsers;
Expand Down Expand Up @@ -58,7 +60,7 @@ hqDefine('app_manager/js/releases/language_profiles', function () {
'placeholder': gettext(practiceUsers.length > 0 ? 'Select a user' : 'No practice mode mobile workers available'),
};
self.addProfile = function (langs, name, id, practiceUser) {
var profile = profileModel(langs, name, id, practiceUser);
var profile = profileModel(langs, name, id, practiceUser, self.latestEnabledVersions);
profile.name.subscribe(changeSaveButton);
profile.langs.subscribe(changeSaveButton);
profile.defaultLang.subscribe(changeSaveButton);
Expand Down
Expand Up @@ -352,9 +352,15 @@ hqDefine('app_manager/js/releases/releases', function () {
}
},
success: function (data) {
savedApp.is_released(data.is_released);
self.latestReleasedVersion(data.latest_released_version);
$(event.currentTarget).parent().prev('.js-release-waiting').addClass('hide');
if (data.error) {
alert(data.error);
$(event.currentTarget).parent().prev('.js-release-waiting').addClass('hide');
savedApp.is_released(isReleased);
} else {
savedApp.is_released(data.is_released);
self.latestReleasedVersion(data.latest_released_version);
$(event.currentTarget).parent().prev('.js-release-waiting').addClass('hide');
}
},
error: function () {
savedApp.is_released('error');
Expand Down
Expand Up @@ -29,6 +29,7 @@
{% initial_page_data 'app_version' app.version %}
{% initial_page_data 'application_profile_url' application_profile_url %}
{% initial_page_data 'build_profiles' app.build_profiles %}
{% initial_page_data 'latest_version_for_build_profiles' latest_version_for_build_profiles %}
{% initial_page_data 'fetch_limit' fetchLimit %}
{% initial_page_data 'latestReleasedVersion' latest_released_version %}
{% initial_page_data 'practice_users' practice_users %}
Expand Down
31 changes: 31 additions & 0 deletions corehq/apps/app_manager/templates/app_manager/download_index.html
@@ -1,13 +1,44 @@
{% extends "app_manager/source_files.html" %}
{% load hq_shared_tags %}
{% load compress %}
{% load i18n %}
{% requirejs_main 'app_manager/js/download_index_main' %}


{% block page_title %}
{{ app.name }}: Build #{{ app.version }}{% if app.build_comment %}: {{ app.build_comment }}{% endif %}
{% endblock page_title %}

{% block app_profiles %}
{% if request|toggle_enabled:"RELEASE_BUILDS_PER_PROFILE" %}
<h2>App Profiles</h2>
<table class="table table-condensed">
<tr>
<th class="col-sm-2">Name</th>
<th class="col-sm-2">Languages</th>
<th class="col-sm-2">Action</th>
</tr>
{% for build_profile_id, build_profile in app.build_profiles.items %}
<tr>
<td class="col-sm-2">{{ build_profile.name }}</td>
<td class="col-sm-2">{{ build_profile.langs|join:", " }}</td>
<td class="col-sm-2">
{% if build_profile_id in enabled_build_profiles %}
<a href='{% url "toggle_build_profile" app.domain app.id build_profile_id %}?action=disable'>
{% trans 'Revoke restriction' %}
</a>
{% else %}
<a href='{% url "toggle_build_profile" app.domain app.id build_profile_id %}?action=enable'>
{% trans 'Restrict to this version' %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

{% block downloads %}
<h2>Downloads</h2>
<table class="table table-condensed">
Expand Down
@@ -1,4 +1,6 @@
{% load i18n %}
{% load hq_shared_tags %}

<div data-bind="saveButton: saveButton"></div>
<div class="panel panel-appmanager">
<div class="panel-heading">
Expand Down Expand Up @@ -46,7 +48,19 @@ <h4 class="panel-title-nolink">
</thead>
<tbody data-bind="foreach: app_profiles">
<tr class="row form-group">
<td class="col-sm-3"><span class="form-inline"><input class="form-control" data-bind="value: $data.name"/></span></td>
<td class="col-sm-3">
<span class="form-inline">
<input class="form-control" data-bind="value: $data.name"/>
{% if request|toggle_enabled:"RELEASE_BUILDS_PER_PROFILE" %}
<div data-bind="visible: $data.latestEnabledVersion">
<b>
{% trans "Latest Enabled Version:" %}
<span data-bind="text: $data.latestEnabledVersion"></span>
</b>
</div>
{% endif %}
</span>
</td>
<td class="col-sm-3">
<select class="language-select form-control"
multiple="true"
Expand Down
Expand Up @@ -21,6 +21,7 @@
{% endif %}
</p>

{% block app_profiles %}{% endblock %}
{% block downloads %}{% endblock %}

<h2>Resource Files</h2>
Expand Down
4 changes: 3 additions & 1 deletion corehq/apps/app_manager/urls.py
Expand Up @@ -28,7 +28,7 @@
edit_app_langs, edit_app_attr, edit_app_ui_translations, get_app_ui_translations, rearrange, odk_qr_code,
odk_media_qr_code, odk_install, short_url, short_odk_url, save_copy, revert_to_copy, delete_copy, list_apps,
direct_ccz, download_index, download_file, get_form_questions, pull_master_app, edit_add_ons,
update_linked_whitelist, overwrite_module_case_list, app_settings
update_linked_whitelist, overwrite_module_case_list, app_settings, toggle_build_profile,
)
from corehq.apps.app_manager.views.modules import ExistingCaseTypesView
from corehq.apps.translations.views import (
Expand Down Expand Up @@ -184,6 +184,8 @@
url(r'^api/list_apps/$', list_apps, name='list_apps'),
url(r'^api/download_ccz/$', direct_ccz, name='direct_ccz'),
url(r'^download/(?P<app_id>[\w-]+)/$', download_index, name='download_index'),
url(r'^build_profile/(?P<build_id>[\w-]+)/toggle/(?P<build_profile_id>[\w-]+)$', toggle_build_profile,
name='toggle_build_profile'),
# the order of these download urls is important
url(r'^download/(?P<app_id>[\w-]+)/CommCare.ccz$', DownloadCCZ.as_view(),
name=DownloadCCZ.name),
Expand Down
35 changes: 33 additions & 2 deletions corehq/apps/app_manager/util.py
Expand Up @@ -21,13 +21,14 @@
from django.core.cache import cache
from django.http import Http404
from django.utils.translation import ugettext as _
from django.db.models import Max

from corehq import toggles
from corehq.apps.app_manager.dbaccessors import (
get_apps_in_domain, get_app
)
from corehq.apps.app_manager.exceptions import SuiteError, SuiteValidationError, PracticeUserException
from corehq.apps.app_manager.xpath import DOT_INTERPOLATE_PATTERN, UserCaseXPath
from corehq.apps.app_manager.xpath import UserCaseXPath
from corehq.apps.builds.models import CommCareBuildConfig
from corehq.apps.app_manager.tasks import create_user_cases
from corehq.util.soft_assert import soft_assert
Expand All @@ -36,7 +37,8 @@
AUTO_SELECT_USERCASE,
USERCASE_TYPE,
USERCASE_ID,
USERCASE_PREFIX)
USERCASE_PREFIX,
)
from corehq.apps.app_manager.xform import XForm, XFormException, parse_xml
from corehq.apps.users.models import CommCareUser
from corehq.util.quickcache import quickcache
Expand Down Expand Up @@ -675,3 +677,32 @@ def get_form_source_download_url(xform):
xform.build_id,
app.get_form_filename(module=form.get_module(), form=form),
])


@quickcache(['domain', 'profile_id'], timeout=24 * 60 * 60)
def get_latest_enabled_build_for_profile(domain, profile_id):
from corehq.apps.app_manager.models import LatestEnabledBuildProfiles
latest_enabled_build = (LatestEnabledBuildProfiles.objects.
filter(build_profile_id=profile_id)
.order_by('-version')
.first())
if latest_enabled_build:
return get_app(domain, latest_enabled_build.build_id)


@quickcache(['build_id', 'version'], timeout=24 * 60 * 60)
def get_enabled_build_profiles_for_version(build_id, version):
from corehq.apps.app_manager.models import LatestEnabledBuildProfiles
return list(LatestEnabledBuildProfiles.objects.filter(
build_id=build_id, version=version).values_list('build_profile_id', flat=True))


def get_latest_enabled_versions_per_profile(app_id):
from corehq.apps.app_manager.models import LatestEnabledBuildProfiles
# a dict with each profile id mapped to its latest enabled version number, if present
return {
build_profile['build_profile_id']: build_profile['version__max']
for build_profile in
LatestEnabledBuildProfiles.objects.filter(app_id=app_id).values('build_profile_id').annotate(
Max('version'))
}
1 change: 1 addition & 0 deletions corehq/apps/app_manager/views/__init__.py
Expand Up @@ -107,6 +107,7 @@
short_odk_url,
short_url,
update_build_comment,
toggle_build_profile,
)
from corehq.apps.app_manager.views.schedules import (
edit_schedule_phases,
Expand Down
6 changes: 6 additions & 0 deletions corehq/apps/app_manager/views/apps.py
Expand Up @@ -65,6 +65,7 @@
get_settings_values,
app_doc_types,
get_and_assert_practice_user_in_domain,
get_latest_enabled_versions_per_profile,
)
from corehq.apps.app_manager.views.utils import back_to_main, get_langs, \
validate_langs, update_linked_app, clear_xmlns_app_id_cache
Expand Down Expand Up @@ -342,6 +343,10 @@ def get_apps_base_context(request, domain, app):
except ESError:
notify_exception(request, 'Error getting practice mode mobile workers')

latest_version_for_build_profiles = {}
if toggles.RELEASE_BUILDS_PER_PROFILE.enabled(domain):
latest_version_for_build_profiles = get_latest_enabled_versions_per_profile(app.get_id)

context.update({
'show_advanced': show_advanced,
'show_report_modules': toggles.MOBILE_UCR.enabled(domain),
Expand All @@ -350,6 +355,7 @@ def get_apps_base_context(request, domain, app):
'show_shadow_forms': show_advanced,
'show_training_modules': toggles.TRAINING_MODULE.enabled(domain) and app.enable_training_modules,
'practice_users': [{"id": u['_id'], "text": u["username"]} for u in practice_users],
'latest_version_for_build_profiles': latest_version_for_build_profiles,
})

return context
Expand Down

0 comments on commit 1b30445

Please sign in to comment.