def create_push_notification_tasks(): # we reuse the high level strategy from data processing celery tasks, see that documentation. expiry = (datetime.utcnow() + timedelta(minutes=5)).replace(second=30, microsecond=0) now = timezone.now() surveys, schedules, patient_ids = get_surveys_and_schedules(now) print(surveys) print(schedules) print(patient_ids) with make_error_sentry(sentry_type=SentryTypes.data_processing): if not check_firebase_instance(): print("Firebase is not configured, cannot queue notifications.") return # surveys and schedules are guaranteed to have the same keys, assembling the data structures # is a pain, so it is factored out. sorry, but not sorry. it was a mess. for fcm_token in surveys.keys(): print(f"Queueing up push notification for user {patient_ids[fcm_token]} for {surveys[fcm_token]}") safe_queue_push( args=[fcm_token, surveys[fcm_token], schedules[fcm_token]], max_retries=0, expires=expiry, task_track_started=True, task_publish_retry=False, retry=False, )
def render_edit_participant(participant: Participant, study: Study): # to reduce database queries we get all the data across 4 queries and then merge it together. # dicts of intervention id to intervention date string, and of field names to value # (this was quite slow previously) intervention_dates_map = { intervention_id: # this is the intervention's id, not the intervention_date's id. intervention_date.strftime(API_DATE_FORMAT) if isinstance( intervention_date, date) else "" for intervention_id, intervention_date in participant.intervention_dates.values_list("intervention_id", "date") } participant_fields_map = { name: value for name, value in participant.field_values.values_list( "field__field_name", "value") } # list of tuples of (intervention id, intervention name, intervention date) intervention_data = [ (intervention.id, intervention.name, intervention_dates_map.get(intervention.id, "")) for intervention in study.interventions.order_by("name") ] # list of tuples of field name, value. field_data = [(field_id, field_name, participant_fields_map.get(field_name, "")) for field_id, field_name in study.fields.order_by( "field_name").values_list('id', "field_name")] return render_template( 'edit_participant.html', participant=participant, study=study, intervention_data=intervention_data, field_values=field_data, push_notifications_enabled_for_ios=check_firebase_instance( require_ios=True), push_notifications_enabled_for_android=check_firebase_instance( require_android=True))
def view_study(study_id=None): study = Study.objects.get(pk=study_id) participants = study.participants.all() # creates dicts of Custom Fields and Interventions to be easily accessed in the template for p in participants: p.field_dict = {tag.field.field_name: tag.value for tag in p.field_values.all()} p.intervention_dict = {tag.intervention.name: tag.date for tag in p.intervention_dates.all()} return render_template( 'view_study.html', study=study, participants=participants, audio_survey_ids=study.get_survey_ids_and_object_ids('audio_survey'), image_survey_ids=study.get_survey_ids_and_object_ids('image_survey'), tracking_survey_ids=study.get_survey_ids_and_object_ids('tracking_survey'), # these need to be lists because they will be converted to json. study_fields=list(study.fields.all().values_list('field_name', flat=True)), interventions=list(study.interventions.all().values_list("name", flat=True)), page_location='study_landing', study_id=study_id, push_notifications_enabled=check_firebase_instance(require_android=True) or check_firebase_instance(require_ios=True), )
def render_edit_survey(survey_id=None): try: survey = Survey.objects.get(pk=survey_id) except Survey.DoesNotExist: return abort(404) return render_template( 'edit_survey.html', survey=survey.as_unpacked_native_python(), study=survey.study, allowed_studies=get_researcher_allowed_studies(), is_admin=researcher_is_an_admin(), domain_name=DOMAIN_NAME, # used in a Javascript alert, see survey-editor.js interventions_dict={ intervention.id: intervention.name for intervention in survey.study.interventions.all() }, weekly_timings=survey.weekly_timings(), relative_timings=survey.relative_timings(), absolute_timings=survey.absolute_timings(), push_notifications_enabled=check_firebase_instance(require_android=True) or \ check_firebase_instance(require_ios=True), today=localtime(timezone.now(), survey.study.timezone).strftime('%Y-%m-%d'), )
def celery_send_push_notification(fcm_token: str, survey_obj_ids: List[str], schedule_pks: List[int]): ''' Celery task that sends push notifications. Note that this list of pks may contain duplicates.''' patient_id = ParticipantFCMHistory.objects.filter(token=fcm_token) \ .values_list("participant__patient_id", flat=True).get() with make_error_sentry(sentry_type=SentryTypes.data_processing): if not check_firebase_instance(): print("Firebase credentials are not configured.") return # use the earliest timed schedule as our reference for the sent_time parameter. (why?) participant = Participant.objects.get(patient_id=patient_id) schedules = ScheduledEvent.objects.filter(pk__in=schedule_pks) reference_schedule = schedules.order_by("scheduled_time").first() survey_obj_ids = list(set(survey_obj_ids)) # already deduped; whatever. print(f"Sending push notification to {patient_id} for {survey_obj_ids}...") try: send_push_notification(participant, reference_schedule, survey_obj_ids, fcm_token) # error types are documented at firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode except UnregisteredError as e: # is an internal 404 http response, it means the token used was wrong. # mark the fcm history as out of date. return except QuotaExceededError: # limits are very high, this is effectively impossible, but it is possible, so we catch it. raise except ThirdPartyAuthError as e: failed_send_handler(participant, fcm_token, str(e), schedules) # This means the credentials used were wrong for the target app instance. This can occur # both with bad server credentials, and with bad device credentials. # We have only seen this error statement, error name is generic so there may be others. if str(e) != "Auth error from APNS or Web Push Service": raise return except ValueError as e: # This case occurs ever? is tested for in check_firebase_instance... weird race condition? # Error should be transient, and like all other cases we enqueue the next weekly surveys regardless. if "The default Firebase app does not exist" in str(e): enqueue_weekly_surveys(participant, schedules) return else: raise success_send_handler(participant, fcm_token, schedules)
def send_notification(): """ Sends a push notification to the participant, used for testing Expects a patient_id in the request body. """ print(check_firebase_instance()) message = messaging.Message( data={ 'type': 'fake', 'content': 'hello good sir', }, token=get_session_participant().get_fcm_token().token, ) response = messaging.send(message) print('Successfully sent notification message:', response) return '', 204