Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Mobile UCR Versions for Domains (Script) #35158

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
114 changes: 114 additions & 0 deletions corehq/mobile_ucr_v2_update_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# This script updates the latest versions of all apps across domains that are using v1 with no manual references
# Steps followed
# 1. Get All domains with mobile ucr flag enabled
# 2. Get all apps for domain with latest released versions and mobile ucr versions that are not v2
# 3. For each app, if it contains no V1 UCR references, update the version to 2

# How to run
# Can be run in django shell. Paste the script and execute the function process()
# File is stored in home directory of cchq user.

# V1 Examples
# https://staging.commcarehq.org/a/test-commcare-superset/apps/view/f940fcc83bae44b8a0adaf719673fd1e/form/a0f3c5b483c645e78b6f89ee0b3b3c03/source/#form/table_child_count


import json
import re
import traceback

from corehq.apps.app_manager.dbaccessors import (
get_latest_app_ids_and_versions,
get_apps_by_id,
)
from corehq.apps.app_manager.const import MOBILE_UCR_VERSION_2
from corehq.toggles import MOBILE_UCR
from corehq.toggles.shortcuts import find_domains_with_toggle_enabled
from corehq.util.log import with_progress_bar


def save_in_log(file, data):
print(data)
file.write(data + '\n')


def save_as_ndjson(path, data):
with open(path, 'a') as file:
print(json.dumps(data, separators=(',', ':')), file=file)


def read_ndjson_file(path):
with open(path, 'r') as file:
return [json.loads(line) for line in file.readlines()]


def has_non_v2_form(domain, app, log_file):
for form in app.get_forms():
save_in_log(log_file, f"Processing Form: {domain}: {form.name}")
# The second condition should always be False if the first one is
# but just as a precaution we check for it
if V1_FIXTURE_IDENTIFIER in form.source or re.findall(V1_ALL_REFERENCES, form.source):
zandre-eng marked this conversation as resolved.
Show resolved Hide resolved
save_in_log(log_file, f"App Contains V1 Refs: {domain}: {app.name}")
return True
return False


def update_app(domain, app, log_file):
save_in_log(log_file, f"Updating App: {domain}: {app.name}: {app.id}")
app.mobile_ucr_restore_version = MOBILE_UCR_VERSION_2
app.save()


PROCESSED_DOMAINS_PATH = '/home/zandre/cchq/updated_domains.ndjson'
LOG_FILE = '/home/zandre/cchq/update_to_v2_ucr_script.log'

ajeety4 marked this conversation as resolved.
Show resolved Hide resolved
V1_FIXTURE_IDENTIFIER = 'src="jr://fixture/commcare:reports'
V1_FIXTURE_PATTERN = r'<.*src="jr://fixture/commcare:reports.*>'
V1_REFERENCES_PATTERN = r"<.*instance\('reports'\)/reports/.*>"
V1_ALL_REFERENCES = f"{V1_FIXTURE_PATTERN}|{V1_REFERENCES_PATTERN}"
zandre-eng marked this conversation as resolved.
Show resolved Hide resolved


skip_domains = []


def process():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the flow of code in the file made it a bit hard to follow along. Just reminding on keeping things in a file in logical order. I am big fan on vertical formatting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see how this might be difficult with all the helper functions at the top. I've refactored this to have the main process function at the top instead eac2bcf.

try:
processed_domains = read_ndjson_file(PROCESSED_DOMAINS_PATH)
except FileNotFoundError:
processed_domains = []
zandre-eng marked this conversation as resolved.
Show resolved Hide resolved

mobile_ucr_domains = find_domains_with_toggle_enabled(MOBILE_UCR)

log_file = open(LOG_FILE, 'a')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this would leave the connection to the file open even after the script exits abruptly, so we should this with with option. This way you also don't need to worry about explicitly calling close.
with open(LOG_FILE, 'a') as log_file:

Does mode 'a' create the file if its missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking through the code, I can remove the need for passing the log file around to the various helper functions. Since we already know what the file path is, we can just use this directly in the helper function for adding to the log. Addressed in 27c1c25.

Does mode 'a' create the file if its missing?

It does. Here is an article for more info on the append mode.


save_in_log(log_file, f"Number of domains with mobile ucr flag enabled: {len(mobile_ucr_domains)} ")

for domain in with_progress_bar(mobile_ucr_domains):
if domain in processed_domains:
save_in_log(log_file, f"Already processed domain: {domain}")
continue
if domain in skip_domains:
save_in_log(log_file, f"Skipped domain: {domain}")
continue

save_in_log(log_file, f"Processing domain: {domain} ...")
app_ids = list(get_latest_app_ids_and_versions(domain))
apps = get_apps_by_id(domain, app_ids)
for app in apps:
try:
# Don't look at app.is_released since the latest version might not be released yet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming that we are just picking app and not releases.
I believe one way to confirm that is by checking copy_of property on the application. Is it blank for applications and not blank for releases. it would be a good check to have though I assume you are anyway only picking up applications and not releases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming that we are just picking app and not releases.

Could you please elaborate on what you mean by app vs release? Are you referring to app versions that have been marked as "Released" or are you referring to the app versions themselves?

This comment refers that we are picking the latest version of each app and we don't want to consider app.is_released since the latest version of an app might still be set to "Test" and not "Released"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to app versions that have been marked as "Released" or are you referring to the app versions themselves?

app versions or app builds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are getting the applications themselves using get_apps_by_id() which returns a list of Application objects. Since we are only getting the latest release of each app, these retrieved application objects should be the current build of the app.

@mkangia I'm not sure if the above answers your question, but let me know if it doesn't and I'd be happy to offline on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are only getting the latest release of each app, these retrieved application objects should be the current build of the app.

I think there is a misunderstanding of how apps work.

So, an application is actually a running doc and the versions are saved instances of that application at different points(versions). My understanding is that here we want to update the application and not the latest created version/build.

Seems like we are okay here since you are using get_latest_app_ids_and_versions which is using app_manager/applications_brief couch view which only keeps applications and not builds since it only saves docs that have copy_of set as null

ajeety4 marked this conversation as resolved.
Show resolved Hide resolved
if app.mobile_ucr_restore_version != '2.0':
save_in_log(log_file, f"Processing App: {domain}: {app.name}: {app.id}")
if not has_non_v2_form(domain, app, log_file):
update_app(domain, app, log_file)
else:
save_in_log(
log_file,
f"App contains V1 references and couldn't updated: {domain}: {app.name}: {app.id}",
)
except Exception as e:
save_in_log(log_file, f"Error occurred for {domain}: {str(e)}")
save_in_log(log_file, traceback.format_exc())
continue
save_as_ndjson(PROCESSED_DOMAINS_PATH, domain)

log_file.close()
Loading