From 70344796f16ecc84db2e1e4eccd8b718e473e357 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 23 May 2024 10:36:59 -0400 Subject: [PATCH] make push notifs configurable, i18n, & only for users missing labels Make the text for push notifications configurable, translatable, and filterable to only include users with at least one recent trip that has no user input. The new config field 'push_notifications' has 'title' and 'message' (the text, per language) and 'recent_user_input_threshold' (the number of days to consider for determining 'recent user input'). Note the caveat about partially-labeled trips. --- bin/push/push_remind.py | 109 +++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 19 deletions(-) diff --git a/bin/push/push_remind.py b/bin/push/push_remind.py index cda1cdcc1..6182757b5 100644 --- a/bin/push/push_remind.py +++ b/bin/push/push_remind.py @@ -1,13 +1,59 @@ +import arrow import json import logging import os import requests +import sys +import emission.core.get_database as edb +import emission.storage.decorations.analysis_timeseries_queries as esda import emission.storage.decorations.user_queries as esdu +import emission.storage.timeseries.timequery as estt import emission.net.ext_service.push.notify_usage as pnu STUDY_CONFIG = os.getenv('STUDY_CONFIG', "stage-program") + +def users_without_recent_user_input(uuid_list, recent_user_input_threshold=None): + if recent_user_input_threshold is None: + logging.debug("No recent_user_input_threshold provided, returning all users") + return uuid_list + now = arrow.now() + tq = estt.TimeQuery( + "data.start_ts", + now.shift(days=-recent_user_input_threshold).int_timestamp, + now.int_timestamp + ) + filtered_uuids = [] + for user_id in uuid_list: + trips = esda.get_entries(esda.CONFIRMED_TRIP_KEY, user_id, tq) + for trip in trips: + # If the trip's user_input is blank, it will be an empty dict {} which is falsy. + # A slight caveat to this is that if the trip is partially labeled (i.e. they + # labeled 'Mode' but not 'Purpose'), it will be non-empty and will be considered + # the same as if it was fully labeled. + # I think this is fine because if a user has partially labeled a trip, they have + # already seen it and bugging them again is not likely to help. + if not trip['data']['user_input']: # empty user_input is {} which is falsy + logging.debug(f"User {user_id} has trip with no user input: {trip['_id']}") + filtered_uuids.append(user_id) + break + return filtered_uuids + + +def bin_users_by_lang(uuid_list, langs, lang_key='phone_lang'): + uuids_by_lang = {lang: [] for lang in langs} + for user_id in uuid_list: + user_profile = edb.get_profile_db().find_one({'user_id': user_id}) + user_lang = user_profile.get(lang_key) if user_profile else None + logging.debug(f"User {user_id} has phone language {user_lang}") + if user_lang not in uuids_by_lang: + logging.debug(f"{user_lang} was not one of the provided langs, defaulting to en") + user_lang = "en" + uuids_by_lang[user_lang].append(user_id) + return uuids_by_lang + + if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) logging.debug(f"STUDY_CONFIG is {STUDY_CONFIG}") @@ -20,23 +66,48 @@ if r.status_code != 200: logging.debug(f"Unable to download study config, status code: {r.status_code}") sys.exit(1) - else: - dynamic_config = json.loads(r.text) - logging.debug(f"Successfully downloaded config with version {dynamic_config['version']} "\ - f"for {dynamic_config['intro']['translated_text']['en']['deployment_name']} "\ - f"and data collection URL {dynamic_config['server']['connectUrl']}") - - if "reminderSchemes" in dynamic_config: - logging.debug("Found flexible notification configuration, skipping server-side push") - else: - uuid_list = esdu.get_all_uuids() - json_data = { - "title": "Trip labels requested", - "message": "Please label your trips for the day" - } - response = pnu.send_visible_notification_to_users(uuid_list, - json_data["title"], - json_data["message"], - json_data, - dev = False) + + dynamic_config = json.loads(r.text) + logging.debug(f"Successfully downloaded config with version {dynamic_config['version']} "\ + f"for {dynamic_config['intro']['translated_text']['en']['deployment_name']} "\ + f"and data collection URL {dynamic_config['server']['connectUrl']}") + + if "reminderSchemes" in dynamic_config: + logging.debug("Found flexible notification configuration, skipping server-side push") + sys.exit(0) + + # get push notification config (if not present in dynamic_config, use default) + push_config = dynamic_config.get('push_notifications', { + "title": { + "en": "Trip labels requested", + "es": "Etiquetas de viaje solicitadas", + }, + "message": { + "en": "Please label your recent trips", + "es": "Por favor etiquete sus viajes recientes", + }, + "recent_user_input_threshold": 7, # past week + }) + + # filter users based on recent user input and bin by language + filtered_uuids = users_without_recent_user_input( + esdu.get_all_uuids(), + push_config.get('recent_user_input_threshold') + ) + filtered_uuids_by_lang = bin_users_by_lang(filtered_uuids, push_config['title'].keys()) + # for each language, send a push notification to the selected users in that language + for lang, uuids_to_notify in filtered_uuids_by_lang.items(): + if len(uuids_to_notify) == 0: + logging.debug(f"No users to notify in lang {lang}") + continue + logging.debug(f"Sending push notifications to {len(uuids_to_notify)} users in lang {lang}") + json_data = { + "title": push_config["title"][lang], + "message": push_config["message"][lang], + } + response = pnu.send_visible_notification_to_users(uuids_to_notify, + json_data["title"], + json_data["message"], + json_data, + dev = False)