Skip to content

Commit

Permalink
Merge baf94d8 into cb671f2
Browse files Browse the repository at this point in the history
  • Loading branch information
orangejenny committed Jun 14, 2018
2 parents cb671f2 + baf94d8 commit b566739
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 7 deletions.
11 changes: 11 additions & 0 deletions corehq/apps/app_manager/_design/views/is_auto_generated/map.js
@@ -0,0 +1,11 @@
function(doc){
if ((doc.doc_type == 'Application' || doc.doc_type == 'LinkedApplication') && doc.is_auto_generated) {
emit([doc.domain, doc.copy_of], {
doc_type: doc.doc_type,
_id: doc._id,
copy_of: doc.copy_of,
domain: doc.domain,
is_released: doc.is_released,
});
}
}
15 changes: 15 additions & 0 deletions corehq/apps/app_manager/dbaccessors.py
Expand Up @@ -327,6 +327,21 @@ def get_built_app_ids_with_submissions_for_app_ids_and_versions(domain, app_ids_
return results


def get_auto_generated_built_apps(domain, app_id):
"""
Returns all the built apps that were automatically generated for an application id.
"""
from .models import Application
results = Application.get_db().view(
'app_manager/is_auto_generated',
startkey=[domain, app_id],
endkey=[domain, app_id, {}],
reduce=False,
include_docs=True,
).all()
return results


def get_latest_app_ids_and_versions(domain, app_id=None):
"""
Returns all the latest app_ids and versions in a dictionary.
Expand Down
4 changes: 4 additions & 0 deletions corehq/apps/app_manager/exceptions.py
Expand Up @@ -163,3 +163,7 @@ def __init__(self, *args, **kwargs):

class AppLinkError(AppManagerException):
pass


class SavedAppBuildException(AppManagerException):
pass
5 changes: 4 additions & 1 deletion corehq/apps/app_manager/models.py
Expand Up @@ -68,6 +68,7 @@
from corehq.const import USER_DATE_FORMAT, USER_TIME_FORMAT
from corehq.apps.analytics.tasks import track_workflow, send_hubspot_form, HUBSPOT_SAVED_APP_FORM_ID
from corehq.apps.app_manager.feature_support import CommCareFeatureSupportMixin
from corehq.apps.app_manager.tasks import prune_auto_generated_builds
from corehq.util.quickcache import quickcache
from corehq.util.soft_assert import soft_assert
from corehq.util.timezones.conversions import ServerTime
Expand Down Expand Up @@ -4748,6 +4749,7 @@ class ApplicationBase(VersionedDoc, SnapshotMixin,
build_comment = StringProperty()
comment_from = StringProperty()
build_broken = BooleanProperty(default=False)
is_auto_generated = BooleanProperty(default=False)
# not used yet, but nice for tagging/debugging
# currently only canonical value is 'incomplete-build',
# for when build resources aren't found where they should be
Expand Down Expand Up @@ -5302,6 +5304,8 @@ def make_build(self, comment=None, user_id=None, previous_version=None):
if not copy.is_remote_app():
copy.update_mm_map()

prune_auto_generated_builds.delay(self.domain, self._id)

return copy

def delete_app(self):
Expand Down Expand Up @@ -5401,7 +5405,6 @@ def validate_detail_screen_field(field):


class SavedAppBuild(ApplicationBase):

def to_saved_build_json(self, timezone):
data = super(SavedAppBuild, self).to_json().copy()
for key in ('modules', 'user_registration', 'external_blobs',
Expand Down
22 changes: 19 additions & 3 deletions corehq/apps/app_manager/tasks.py
Expand Up @@ -4,7 +4,8 @@

from celery.task import task

from corehq.apps.app_manager.dbaccessors import get_app
from corehq.apps.app_manager.dbaccessors import get_app, get_latest_build_id, get_auto_generated_built_apps
from corehq.apps.app_manager.exceptions import SavedAppBuildException
from corehq.apps.users.models import CommCareUser
from corehq.util.decorators import serial_task

Expand All @@ -19,14 +20,15 @@ def create_user_cases(domain_name):
@serial_task('{app._id}-{app.version}', max_retries=0, timeout=60*60)
def make_async_build(app, username):
latest_build = app.get_latest_app(released_only=False)
if latest_build.version == app.version:
if latest_build and latest_build.version == app.version:
return
errors = app.validate_app()
if not errors:
copy = app.make_build(
previous_version=latest_build,
comment=_('Auto-generated by a phone update from {}'.format(username))
comment=_('Auto-generated by a phone update. Will expire after next build if not marked released.'),
)
copy.is_auto_generated = True
copy.save(increment_version=False)


Expand All @@ -41,3 +43,17 @@ def create_build_files_for_all_app_profiles(domain, build_id):
save_app = True
if save_app:
app.save()


@task(queue='background_queue')
def prune_auto_generated_builds(domain, app_id):
last_build_id = get_latest_build_id(domain, app_id)
saved_builds = get_auto_generated_built_apps(domain, app_id)

for doc in saved_builds:
app = get_app(domain, doc['_id'])
if app.id == last_build_id or app.is_released:
continue
if not app.is_auto_generated or app.copy_of != app_id or app.id == last_build_id:
raise SavedAppBuildException("Attempted to delete build that should not be deleted")
app.delete()
51 changes: 48 additions & 3 deletions corehq/apps/app_manager/tests/test_app_manager.py
Expand Up @@ -5,20 +5,23 @@
import uuid

from mock import patch
from corehq.apps.app_manager.tests.util import add_build, patch_default_builds
from corehq.apps.app_manager.util import (add_odk_profile_after_build,
purge_report_from_mobile_ucr)
from memoized import memoized
import os
import codecs

from django.test import TestCase, SimpleTestCase

from corehq.apps.app_manager.dbaccessors import get_app, get_built_app_ids_for_app_id
from corehq.apps.app_manager.models import Application, DetailColumn, import_app, APP_V1, ApplicationBase, Module, \
ReportModule, ReportAppConfig
from corehq.apps.app_manager.tasks import make_async_build, prune_auto_generated_builds
from corehq.apps.app_manager.tests.util import add_build, patch_default_builds
from corehq.apps.app_manager.util import add_odk_profile_after_build, purge_report_from_mobile_ucr
from corehq.apps.builds.models import BuildSpec
from corehq.apps.domain.shortcuts import create_domain
from corehq.apps.userreports.models import ReportConfiguration
from corehq.util.test_utils import flag_enabled

from six.moves import zip
from six.moves import range
from io import open
Expand Down Expand Up @@ -215,6 +218,48 @@ def testBuildImportedApp(self, mock):
self._check_has_build_files(copy, self.min_paths)
self._check_legacy_odk_files(copy)

def testPruneAutoGeneratedBuilds(self):
# Build #1, manually generated
app = import_app(self._yesno_source, self.domain)
for module in app.modules:
module.get_or_create_unique_id()
app.save()
build1 = app.make_build()
build1.save()
self.assertFalse(build1.is_auto_generated)

# Build #2, auto-generated
app.save()
make_async_build(app, 'someone')
build_ids = get_built_app_ids_for_app_id(app.domain, app.id)
self.assertEqual(len(build_ids), 2)
self.assertEqual(build_ids[0], build1.id)
build2 = get_app(app.domain, build_ids[1])
self.assertTrue(build2.is_auto_generated)

# First prune: delete nothing because the auto build is the most recent
prune_auto_generated_builds(self.domain, app_id)
self.assertEqual(len(get_built_app_ids_for_app_id(app.domain, app.id)), 2)

# Build #3, manually generated
app.save()
build3 = app.make_build()
build3.save()

# Release the auto-generated build and prune again, should still delete nothing
build2.is_released = True
build2.save()
prune_auto_generated_builds(self.domain, app_id)
self.assertEqual(len(get_built_app_ids_for_app_id(app.domain, app.id)), 3)

# Un-release the auto-generated build and prune again, which should delete it
build2.is_released = False
build2.save()
prune_auto_generated_builds(self.domain, app_id)
build_ids = get_built_app_ids_for_app_id(app.domain, app.id)
self.assertEqual(len(build_ids), 2)
self.assertNotIn(build2.id, build_ids)

@patch('corehq.apps.app_manager.models.validate_xform', return_value=None)
def testRevertToCopy(self, mock):
old_name = 'old name'
Expand Down

0 comments on commit b566739

Please sign in to comment.