def main(args): skip_count = 0 new_or_updated_count = 0 matched_count = 0 with open(args.file, 'r') as csv_file: sites_reader = csv.DictReader(csv_file) hpo_dao = HPODao() site_dao = SiteDao() existing_site_map = { site.googleGroup: site for site in site_dao.get_all() } with site_dao.session() as session: for row in sites_reader: site = _site_from_row(row, hpo_dao) if site is None: skip_count += 1 continue changed = _upsert_site(site, existing_site_map.get(site.googleGroup), site_dao, session, args.dry_run) if changed: new_or_updated_count += 1 else: matched_count += 1 logging.info( 'Done%s. %d skipped, %d sites new/updated, %d sites not changed.', ' (dry run)' if args.dry_run else '', skip_count, new_or_updated_count, matched_count)
class SiteImporter(CsvImporter): def __init__(self): args = parser.parse_args() self.organization_dao = OrganizationDao() self.stub_geocoding = args.stub_geocoding self.ACTIVE = SiteStatus.ACTIVE self.status_exception_list = ['hpo-site-walgreensphoenix'] self.instance = args.instance self.creds_file = args.creds_file self.new_sites_list = [] self.project = None if args.project: self.project = args.project if self.project in ENV_LIST: self.environment = ' ' + self.project.split('-')[-1].upper() else: self.environment = ' ' + ENV_TEST.split('-')[-1].upper() super(SiteImporter, self).__init__('site', SiteDao(), 'siteId', 'googleGroup', [ SITE_ORGANIZATION_ID_COLUMN, SITE_SITE_ID_COLUMN, SITE_SITE_COLUMN, SITE_STATUS_COLUMN + self.environment, ENROLLING_STATUS_COLUMN + self.environment, DIGITAL_SCHEDULING_STATUS_COLUMN + self.environment ]) def run(self, filename, dry_run): super(SiteImporter, self).run(filename, dry_run) insert_participants = False if not dry_run: if self.environment: current_env = ENV_STABLE if self.environment.strip() == 'STABLE' and len( self.new_sites_list) > 0: from googleapiclient.discovery import build logging.info( 'Starting reboot of app instances to insert new test participants' ) service = build('appengine', 'v1', cache_discovery=False) request = service.apps().services().versions().list( appsId=current_env, servicesId='default') versions = request.execute() for version in versions['versions']: if version['servingStatus'] == 'SERVING': _id = version['id'] request = service.apps().services().versions( ).instances().list(servicesId='default', versionsId=_id, appsId=current_env) instances = request.execute() try: for instance in instances['instances']: sha = instance['name'].split('/')[-1] delete_instance = service.apps().services( ).versions().instances().delete( appsId=current_env, servicesId='default', versionsId=_id, instancesId=sha) response = delete_instance.execute() if response['done']: insert_participants = True logging.info( 'Reboot of instance: %s in stable complete.', instance['name']) else: logging.warn( 'Not able to reboot instance on server, Error: %s', response) except KeyError: logging.warn('No running instance for %s', version['name']) if insert_participants: logging.info('Starting import of test participants.') self._insert_new_participants(self.new_sites_list) def delete_sql_statement(self, session, str_list): sql = """ DELETE FROM site WHERE site_id IN ({str_list}) AND NOT EXISTS( SELECT * FROM participant WHERE site_id = site.site_id) AND NOT EXISTS( SELECT * FROM participant_summary WHERE site_id = site.site_id OR physical_measurements_finalized_site_id = site.site_id OR physical_measurements_created_site_id = site.site_id OR biospecimen_source_site_id = site.site_id OR biospecimen_collected_site_id = site.site_id OR biospecimen_processed_site_id = site.site_id OR biospecimen_finalized_site_id = site.site_id ) AND NOT EXISTS( SELECT * FROM participant_history WHERE site_id = site.site_id) AND NOT EXISTS( SELECT * FROM physical_measurements WHERE created_site_id = site.site_id OR finalized_site_id = site.site_id) AND NOT EXISTS( SELECT * FROM biobank_order WHERE finalized_site_id = site.site_id OR source_site_id = site.site_id OR collected_site_id = site.site_id OR processed_site_id = site.site_id ) """.format(str_list=str_list) session.execute(sql) def _cleanup_old_entities(self, session, row_list, dry_run): log_prefix = '(dry run) ' if dry_run else '' self.site_dao = SiteDao() existing_sites = set(site.googleGroup for site in self.site_dao.get_all()) site_group_list_from_sheet = [ str(row[SITE_SITE_ID_COLUMN].lower()) for row in row_list ] sites_to_remove = existing_sites - set(site_group_list_from_sheet) if sites_to_remove: site_id_list = [] for site in sites_to_remove: logging.info( log_prefix + 'Deleting old Site no longer in Google sheet: %s', site) old_site = self.site_dao.get_by_google_group(site) if old_site and old_site.isObsolete != ObsoleteStatus.OBSOLETE: site_id_list.append(old_site.siteId) self.deletion_count += 1 elif old_site and old_site.isObsolete == ObsoleteStatus.OBSOLETE: logging.info( 'Not attempting to delete site [%s] with existing obsolete status', old_site.googleGroup) if site_id_list and not dry_run: str_list = ','.join([str(i) for i in site_id_list]) logging.info(log_prefix + 'Marking old site as obsolete : %s', old_site) sql = """ UPDATE site SET is_obsolete = 1 WHERE site_id in ({site_id_list})""".format(site_id_list=str_list) session.execute(sql) self.site_dao._invalidate_cache() # Try to delete old sites. self.delete_sql_statement(session, str_list) def _insert_new_participants(self, entity): num_participants = 0 participants = { 'zip_code': '20001', 'date_of_birth': '1933-3-3', 'gender_identity': 'GenderIdentity_Woman', 'withdrawalStatus': 'NOT_WITHDRAWN', 'suspensionStatus': 'NOT_SUSPENDED' } client = Client('rdr/v1', False, self.creds_file, self.instance) client_log.setLevel(logging.WARN) questionnaire_to_questions, consent_questionnaire_id_and_version = _setup_questionnaires( client) consent_questions = questionnaire_to_questions[ consent_questionnaire_id_and_version] for site in entity: for participant, v in enumerate(range(1, 21), 1): num_participants += 1 participant = participants participant.update( {'last_name': site.googleGroup.split('-')[-1]}) participant.update({'first_name': 'Participant {}'.format(v)}) participant.update({'site': site.googleGroup}) import_participant(participant, client, consent_questionnaire_id_and_version, questionnaire_to_questions, consent_questions, num_participants) logging.info('%d participants imported.' % num_participants) def _entity_from_row(self, row): google_group = row[SITE_SITE_ID_COLUMN].lower() organization = self.organization_dao.get_by_external_id( row[SITE_ORGANIZATION_ID_COLUMN].upper()) if organization is None: logging.warn('Invalid organization ID %s importing site %s', row[SITE_ORGANIZATION_ID_COLUMN].upper(), google_group) self.errors.append( 'Invalid organization ID {} importing site {}'.format( row[SITE_ORGANIZATION_ID_COLUMN].upper(), google_group)) return None launch_date = None launch_date_str = row.get(SITE_LAUNCH_DATE_COLUMN) if launch_date_str: try: launch_date = parse(launch_date_str).date() except ValueError: logging.warn('Invalid launch date %s for site %s', launch_date_str, google_group) self.errors.append('Invalid launch date {} for site {}'.format( launch_date_str, google_group)) return None name = row[SITE_SITE_COLUMN] mayolink_client_number = None mayolink_client_number_str = row.get( SITE_MAYOLINK_CLIENT_NUMBER_COLUMN) if mayolink_client_number_str: try: mayolink_client_number = int(mayolink_client_number_str) except ValueError: logging.warn('Invalid Mayolink Client # %s for site %s', mayolink_client_number_str, google_group) self.errors.append( 'Invalid Mayolink Client # {} for site {}'.format( mayolink_client_number_str, google_group)) return None notes = row.get(SITE_NOTES_COLUMN) notes_es = row.get(SITE_NOTES_COLUMN_ES) try: site_status = SiteStatus(row[SITE_STATUS_COLUMN + self.environment].upper()) except TypeError: logging.warn('Invalid site status %s for site %s', row[SITE_STATUS_COLUMN + self.environment], google_group) self.errors.append('Invalid site status {} for site {}'.format( row[SITE_STATUS_COLUMN + self.environment], google_group)) return None try: enrolling_status = EnrollingStatus(row[ENROLLING_STATUS_COLUMN + self.environment].upper()) except TypeError: logging.warn('Invalid enrollment site status %s for site %s', row[ENROLLING_STATUS_COLUMN + self.environment], google_group) self.errors.append( 'Invalid enrollment site status {} for site {}'.format( row[ENROLLING_STATUS_COLUMN + self.environment], google_group)) directions = row.get(SITE_DIRECTIONS_COLUMN) physical_location_name = row.get(SITE_PHYSICAL_LOCATION_NAME_COLUMN) address_1 = row.get(SITE_ADDRESS_1_COLUMN) address_2 = row.get(SITE_ADDRESS_2_COLUMN) city = row.get(SITE_CITY_COLUMN) state = row.get(SITE_STATE_COLUMN) zip_code = row.get(SITE_ZIP_COLUMN) phone = row.get(SITE_PHONE_COLUMN) admin_email_addresses = row.get(SITE_ADMIN_EMAIL_ADDRESSES_COLUMN) link = row.get(SITE_LINK_COLUMN) digital_scheduling_status = DigitalSchedulingStatus( row[DIGITAL_SCHEDULING_STATUS_COLUMN + self.environment].upper()) schedule_instructions = row.get(SCHEDULING_INSTRUCTIONS) schedule_instructions_es = row.get(SCHEDULING_INSTRUCTIONS_ES) return Site(siteName=name, googleGroup=google_group, mayolinkClientNumber=mayolink_client_number, organizationId=organization.organizationId, hpoId=organization.hpoId, siteStatus=site_status, enrollingStatus=enrolling_status, digitalSchedulingStatus=digital_scheduling_status, scheduleInstructions=schedule_instructions, scheduleInstructions_ES=schedule_instructions_es, launchDate=launch_date, notes=notes, notes_ES=notes_es, directions=directions, physicalLocationName=physical_location_name, address1=address_1, address2=address_2, city=city, state=state, zipCode=zip_code, phoneNumber=phone, adminEmails=admin_email_addresses, link=link) def _update_entity(self, entity, existing_entity, session, dry_run): self._populate_lat_lng_and_time_zone(entity, existing_entity) if entity.siteStatus == self.ACTIVE and (entity.latitude == None or entity.longitude == None): self.errors.append( 'Skipped active site without geocoding: {}'.format( entity.googleGroup)) return None, True return super(SiteImporter, self)._update_entity(entity, existing_entity, session, dry_run) def _insert_entity(self, entity, existing_map, session, dry_run): self._populate_lat_lng_and_time_zone(entity, None) if entity.siteStatus == self.ACTIVE and (entity.latitude == None or entity.longitude == None): self.errors.append( 'Skipped active site without geocoding: {}'.format( entity.googleGroup)) return False self.new_sites_list.append(entity) super(SiteImporter, self)._insert_entity(entity, existing_map, session, dry_run) def _populate_lat_lng_and_time_zone(self, site, existing_site): if site.address1 and site.city and site.state: if existing_site: if (existing_site.address1 == site.address1 and existing_site.city == site.city and existing_site.state == site.state and existing_site.latitude is not None and existing_site.longitude is not None and existing_site.timeZoneId is not None): # Address didn't change, use the existing lat/lng and time zone. site.latitude = existing_site.latitude site.longitude = existing_site.longitude site.timeZoneId = existing_site.timeZoneId return if self.stub_geocoding: # Set dummy latitude and longitude when importing sites locally / on a CircleCI box. site.latitude = 32.176 site.longitude = -110.93 site.timeZoneId = 'America/Phoenix' else: latitude, longitude = self._get_lat_long_for_site( site.address1, site.city, site.state) site.latitude = latitude site.longitude = longitude if latitude and longitude: site.timeZoneId = self._get_time_zone(latitude, longitude) else: if site.googleGroup not in self.status_exception_list: if site.siteStatus == self.ACTIVE: self.errors.append( 'Active site must have valid address. Site: {}, Group: {}' .format(site.siteName, site.googleGroup)) def _get_lat_long_for_site(self, address_1, city, state): self.full_address = address_1 + ' ' + city + ' ' + state try: self.api_key = os.environ.get('API_KEY') self.gmaps = googlemaps.Client(key=self.api_key) try: geocode_result = self.gmaps.geocode(address_1 + '' + city + ' ' + state)[0] except IndexError: self.errors.append( 'Bad address for {}, could not geocode.'.format( self.full_address)) return None, None if geocode_result: geometry = geocode_result.get('geometry') if geometry: location = geometry.get('location') if location: latitude = location.get('lat') longitude = location.get('lng') return latitude, longitude else: logging.warn('Can not find lat/long for %s', self.full_address) self.errors.append('Can not find lat/long for {}'.format( self.full_address)) return None, None else: logging.warn('Geocode results failed for %s.', self.full_address) self.errors.append('Geocode results failed for {}'.format( self.full_address)) return None, None except ValueError as e: logging.exception('Invalid geocode key: %s. ERROR: %s', self.api_key, e) self.errors.append('Invalid geocode key: {}. ERROR: {}'.format( self.api_key, e)) return None, None except IndexError as e: logging.exception( 'Geocoding failure Check that address is correct. ERROR: %s', e) self.errors.append( 'Geocoding failured Check that address is correct. ERROR: {}'. format(self.api_key, e)) return None, None def _get_time_zone(self, latitude, longitude): time_zone = self.gmaps.timezone(location=(latitude, longitude)) if time_zone['status'] == 'OK': time_zone_id = time_zone['timeZoneId'] return time_zone_id else: logging.info('can not retrieve time zone from %s', self.full_address) self.errors.append('Can not retrieve time zone from {}'.format( self.full_address)) return None
class HierarchyContentApiTest(FlaskTestBase): def setUp(self): super(HierarchyContentApiTest, self).setUp(with_data=False) hpo_dao = HPODao() hpo_dao.insert( HPO(hpoId=UNSET_HPO_ID, name='UNSET', displayName='Unset', organizationType=OrganizationType.UNSET, resourceId='h123456')) hpo_dao.insert( HPO(hpoId=PITT_HPO_ID, name='PITT', displayName='Pittsburgh', organizationType=OrganizationType.HPO, resourceId='h123457')) hpo_dao.insert( HPO(hpoId=AZ_HPO_ID, name='AZ_TUCSON', displayName='Arizona', organizationType=OrganizationType.HPO, resourceId='h123458')) self.site_dao = SiteDao() self.org_dao = OrganizationDao() def test_create_new_hpo(self): self._setup_data() request_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e51f", 'meta': { 'versionId': '1' }, "extension": [{ "url": "http://all-of-us.org/fhir/sites/awardee-type", "valueString": "DV" }], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/awardee-id", "value": "TEST_HPO_NAME" }], "active": True, "type": [{ "coding": [{ "code": "AWARDEE", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Test new HPO display name" } self.send_put('organization/hierarchy', request_data=request_json) result = self.send_get('Awardee/TEST_HPO_NAME') self.assertEquals( _make_awardee_resource('TEST_HPO_NAME', 'Test new HPO display name', 'DV'), result) def test_update_existing_hpo(self): self._setup_data() request_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e51f", 'meta': { 'versionId': '2' }, "extension": [{ "url": "http://all-of-us.org/fhir/sites/awardee-type", "valueString": "DV" }], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/awardee-id", "value": "PITT" }], "active": True, "type": [{ "coding": [{ "code": "AWARDEE", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Test update HPO display name" } self.send_put('organization/hierarchy', request_data=request_json) result = self.send_get('Awardee/PITT') self.assertEqual(result['displayName'], 'Test update HPO display name') self.assertEqual(result['type'], 'DV') def test_create_new_organization(self): self._setup_data() request_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e51f", 'meta': { 'versionId': '1' }, "extension": [], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/organization-id", "value": "TEST_NEW_ORG" }], "active": True, "type": [{ "coding": [{ "code": "ORGANIZATION", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Test create organization display name", "partOf": { "reference": "Organization/h123457" } } result_before = self.send_get('Awardee/PITT') self.assertEqual(2, len(result_before['organizations'])) self.send_put('organization/hierarchy', request_data=request_json) result_after = self.send_get('Awardee/PITT') self.assertEqual(3, len(result_after['organizations'])) self.assertIn( { 'displayName': 'Test create organization display name', 'id': 'TEST_NEW_ORG' }, result_after['organizations']) def test_update_existing_organization(self): self._setup_data() request_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e51f", 'meta': { 'versionId': '2' }, "extension": [], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/organization-id", "value": "AARDVARK_ORG" }], "active": True, "type": [{ "coding": [{ "code": "ORGANIZATION", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Test update organization display name", "partOf": { "reference": "Organization/h123457" } } result_before = self.send_get('Awardee/PITT') self.assertEqual(2, len(result_before['organizations'])) self.send_put('organization/hierarchy', request_data=request_json) result_after = self.send_get('Awardee/PITT') self.assertEqual(2, len(result_after['organizations'])) self.assertIn( { 'displayName': 'Test update organization display name', 'id': 'AARDVARK_ORG' }, result_after['organizations']) @mock.patch( 'dao.organization_hierarchy_sync_dao.OrganizationHierarchySyncDao.' '_get_lat_long_for_site') @mock.patch( 'dao.organization_hierarchy_sync_dao.OrganizationHierarchySyncDao.' '_get_time_zone') def test_create_new_site(self, time_zone, lat_long): self._setup_data() lat_long.return_value = 100, 110 time_zone.return_value = 'America/Los_Angeles' request_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e51f", 'meta': { 'versionId': '1' }, "extension": [{ "url": "http://all-of-us.org/fhir/sites/enrollmentStatusActive", "valueBoolean": True }, { "url": "http://all-of-us.org/fhir/sites/digitalSchedulingStatusActive", "valueBoolean": True }, { "url": "http://all-of-us.org/fhir/sites/schedulingStatusActive", "valueBoolean": True }, { "url": "http://all-of-us.org/fhir/sites/notes", "valueString": "This is a note about an organization" }, { "url": "http://all-of-us.org/fhir/sites/schedulingInstructions", "valueString": "Please schedule appointments up to a week before intended date." }, { "url": "http://all-of-us.org/fhir/sites/anticipatedLaunchDate", "valueDate": "07-02-2010" }, { "url": "http://all-of-us.org/fhir/sites/locationName", "valueString": "Thompson Building" }, { "url": "http://all-of-us.org/fhir/sites/directions", "valueString": "Exit 95 N and make a left onto Fake Street" }], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/site-id", "value": "hpo-site-awesome-testing" }, { "system": "http://all-of-us.org/fhir/sites/mayo-link-identifier", "value": "123456" }, { "system": "http://all-of-us.org/fhir/sites/google-group-identifier", "value": "Awesome Genomics Testing" }], "active": True, "type": [{ "coding": [{ "code": "SITE", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Awesome Genomics Testing", "partOf": { "reference": "Organization/o123457" }, "address": [{ "line": ["1855 4th Street", "AAC5/6"], "city": "San Francisco", "state": "CA", "postalCode": "94158" }], "contact": [{ "telecom": [{ "system": "phone", "value": "7031234567" }] }, { "telecom": [{ "system": "email", "value": "*****@*****.**" }] }, { "telecom": [{ "system": "url", "value": "http://awesome-genomic-testing.com" }] }] } self.send_put('organization/hierarchy', request_data=request_json) self.send_get('Awardee/PITT') existing_map = { entity.googleGroup: entity for entity in self.site_dao.get_all() } existing_entity = existing_map.get('hpo-site-awesome-testing') self.assertEqual(existing_entity.adminEmails, '*****@*****.**') self.assertEqual(existing_entity.siteStatus, SiteStatus('ACTIVE')) self.assertEqual(existing_entity.isObsolete, None) self.assertEqual(existing_entity.city, 'San Francisco') self.assertEqual(existing_entity.googleGroup, 'hpo-site-awesome-testing') self.assertEqual(existing_entity.state, 'CA') self.assertEqual(existing_entity.digitalSchedulingStatus, DigitalSchedulingStatus('ACTIVE')) self.assertEqual(existing_entity.mayolinkClientNumber, 123456) self.assertEqual(existing_entity.address1, '1855 4th Street') self.assertEqual(existing_entity.address2, 'AAC5/6') self.assertEqual(existing_entity.zipCode, '94158') self.assertEqual(existing_entity.directions, 'Exit 95 N and make a left onto Fake Street') self.assertEqual(existing_entity.notes, 'This is a note about an organization') self.assertEqual(existing_entity.enrollingStatus, EnrollingStatus('ACTIVE')) self.assertEqual( existing_entity.scheduleInstructions, 'Please schedule appointments up to a week before intended date.') self.assertEqual(existing_entity.physicalLocationName, 'Thompson Building') self.assertEqual(existing_entity.link, 'http://awesome-genomic-testing.com') self.assertEqual(existing_entity.launchDate, datetime.date(2010, 7, 2)) self.assertEqual(existing_entity.phoneNumber, '7031234567') @mock.patch( 'dao.organization_hierarchy_sync_dao.OrganizationHierarchySyncDao.' '_get_lat_long_for_site') @mock.patch( 'dao.organization_hierarchy_sync_dao.OrganizationHierarchySyncDao.' '_get_time_zone') def test_update_existing_site(self, time_zone, lat_long): self._setup_data() lat_long.return_value = 100, 110 time_zone.return_value = 'America/Los_Angeles' request_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e51f", 'meta': { 'versionId': '2' }, "extension": [{ "url": "http://all-of-us.org/fhir/sites/enrollmentStatusActive", "valueBoolean": True }, { "url": "http://all-of-us.org/fhir/sites/digitalSchedulingStatusActive", "valueBoolean": False }, { "url": "http://all-of-us.org/fhir/sites/schedulingStatusActive", "valueBoolean": True }, { "url": "http://all-of-us.org/fhir/sites/notes", "valueString": "This is a note about an organization" }, { "url": "http://all-of-us.org/fhir/sites/schedulingInstructions", "valueString": "Please schedule appointments up to a week before intended date." }, { "url": "http://all-of-us.org/fhir/sites/anticipatedLaunchDate", "valueDate": "07-02-2010" }, { "url": "http://all-of-us.org/fhir/sites/locationName", "valueString": "Thompson Building" }, { "url": "http://all-of-us.org/fhir/sites/directions", "valueString": "Exit 95 N and make a left onto Fake Street" }], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/site-id", "value": "hpo-site-1" }, { "system": "http://all-of-us.org/fhir/sites/mayo-link-identifier", "value": "123456" }, { "system": "http://all-of-us.org/fhir/sites/google-group-identifier", "value": "Awesome Genomics Testing" }], "active": True, "type": [{ "coding": [{ "code": "SITE", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Awesome Genomics Testing", "partOf": { "reference": "Organization/o123456" }, "address": [{ "line": ["1855 4th Street", "AAC5/6"], "city": "San Francisco", "state": "CA", "postalCode": "94158" }], "contact": [{ "telecom": [{ "system": "phone", "value": "7031234567" }] }, { "telecom": [{ "system": "email", "value": "*****@*****.**" }] }, { "telecom": [{ "system": "url", "value": "http://awesome-genomic-testing.com" }] }] } self.send_put('organization/hierarchy', request_data=request_json) existing_map = { entity.googleGroup: entity for entity in self.site_dao.get_all() } existing_entity = existing_map.get('hpo-site-1') self.assertEqual(existing_entity.adminEmails, '*****@*****.**') self.assertEqual(existing_entity.siteStatus, SiteStatus('ACTIVE')) self.assertEqual(existing_entity.isObsolete, None) self.assertEqual(existing_entity.city, 'San Francisco') self.assertEqual(existing_entity.googleGroup, 'hpo-site-1') self.assertEqual(existing_entity.state, 'CA') self.assertEqual(existing_entity.digitalSchedulingStatus, DigitalSchedulingStatus('INACTIVE')) self.assertEqual(existing_entity.mayolinkClientNumber, 123456) self.assertEqual(existing_entity.address1, '1855 4th Street') self.assertEqual(existing_entity.address2, 'AAC5/6') self.assertEqual(existing_entity.zipCode, '94158') self.assertEqual(existing_entity.directions, 'Exit 95 N and make a left onto Fake Street') self.assertEqual(existing_entity.notes, 'This is a note about an organization') self.assertEqual(existing_entity.enrollingStatus, EnrollingStatus('ACTIVE')) self.assertEqual( existing_entity.scheduleInstructions, 'Please schedule appointments up to a week before intended date.') self.assertEqual(existing_entity.physicalLocationName, 'Thompson Building') self.assertEqual(existing_entity.link, 'http://awesome-genomic-testing.com') self.assertEqual(existing_entity.launchDate, datetime.date(2010, 7, 2)) self.assertEqual(existing_entity.phoneNumber, '7031234567') @mock.patch( 'dao.organization_hierarchy_sync_dao.OrganizationHierarchySyncDao.' '_get_lat_long_for_site') @mock.patch( 'dao.organization_hierarchy_sync_dao.OrganizationHierarchySyncDao.' '_get_time_zone') def test_create_hpo_org_site(self, time_zone, lat_long): hpo_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e51f", 'meta': { 'versionId': '1' }, "extension": [{ "url": "http://all-of-us.org/fhir/sites/awardee-type", "valueString": "DV" }], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/awardee-id", "value": "TEST_HPO_NAME" }], "active": True, "type": [{ "coding": [{ "code": "AWARDEE", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Test new HPO display name" } self.send_put('organization/hierarchy', request_data=hpo_json) org_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e123", 'meta': { 'versionId': '1' }, "extension": [], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/organization-id", "value": "TEST_NEW_ORG" }], "active": True, "type": [{ "coding": [{ "code": "ORGANIZATION", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Test create organization display name", "partOf": { "reference": "Organization/a893282c-2717-4a20-b276-d5c9c2c0e51f" } } self.send_put('organization/hierarchy', request_data=org_json) lat_long.return_value = 100, 110 time_zone.return_value = 'America/Los_Angeles' site_json = { "resourceType": "Organization", "id": "a893282c-2717-4a20-b276-d5c9c2c0e234", 'meta': { 'versionId': '1' }, "extension": [{ "url": "http://all-of-us.org/fhir/sites/enrollmentStatusActive", "valueBoolean": True }, { "url": "http://all-of-us.org/fhir/sites/digitalSchedulingStatusActive", "valueBoolean": True }, { "url": "http://all-of-us.org/fhir/sites/schedulingStatusActive", "valueBoolean": True }, { "url": "http://all-of-us.org/fhir/sites/notes", "valueString": "This is a note about an organization" }, { "url": "http://all-of-us.org/fhir/sites/schedulingInstructions", "valueString": "Please schedule appointments up to a week before intended date." }, { "url": "http://all-of-us.org/fhir/sites/anticipatedLaunchDate", "valueDate": "07-02-2010" }, { "url": "http://all-of-us.org/fhir/sites/locationName", "valueString": "Thompson Building" }, { "url": "http://all-of-us.org/fhir/sites/directions", "valueString": "Exit 95 N and make a left onto Fake Street" }], "identifier": [{ "system": "http://all-of-us.org/fhir/sites/site-id", "value": "hpo-site-awesome-testing" }, { "system": "http://all-of-us.org/fhir/sites/mayo-link-identifier", "value": "123456" }, { "system": "http://all-of-us.org/fhir/sites/google-group-identifier", "value": "Awesome Genomics Testing" }], "active": True, "type": [{ "coding": [{ "code": "SITE", "system": "http://all-of-us.org/fhir/sites/type" }] }], "name": "Awesome Genomics Testing", "partOf": { "reference": "Organization/a893282c-2717-4a20-b276-d5c9c2c0e123" }, "address": [{ "line": ["1855 4th Street", "AAC5/6"], "city": "San Francisco", "state": "CA", "postalCode": "94158" }], "contact": [{ "telecom": [{ "system": "phone", "value": "7031234567" }] }, { "telecom": [{ "system": "email", "value": "*****@*****.**" }] }, { "telecom": [{ "system": "url", "value": "http://awesome-genomic-testing.com" }] }] } self.send_put('organization/hierarchy', request_data=site_json) result = self.send_get('Awardee/TEST_HPO_NAME') self.assertEqual( { u'displayName': u'Test new HPO display name', u'type': u'DV', u'id': u'TEST_HPO_NAME', u'organizations': [{ u'displayName': u'Test create organization display name', u'id': u'TEST_NEW_ORG', u'sites': [{ u'mayolinkClientNumber': 123456, u'timeZoneId': u'America/Los_Angeles', u'displayName': u'Awesome Genomics Testing', u'notes': u'This is a note about an organization', u'launchDate': u'2010-07-02', u'notesEs': u'', u'enrollingStatus': u'ACTIVE', u'longitude': 110.0, u'schedulingInstructions': u'Please schedule appointments up ' u'to a week before intended date.', u'latitude': 100.0, u'physicalLocationName': u'Thompson Building', u'phoneNumber': u'7031234567', u'siteStatus': u'ACTIVE', u'address': { u'postalCode': u'94158', u'city': u'San Francisco', u'line': [u'1855 4th Street', u'AAC5/6'], u'state': u'CA' }, u'directions': u'Exit 95 N and make a left onto Fake Street', u'link': u'http://awesome-genomic-testing.com', u'id': u'hpo-site-awesome-testing', u'adminEmails': [u'*****@*****.**'], u'digitalSchedulingStatus': u'ACTIVE' }] }] }, result) def _setup_data(self): organization_dao = OrganizationDao() site_dao = SiteDao() org_1 = organization_dao.insert( Organization(externalId='ORG_1', displayName='Organization 1', hpoId=PITT_HPO_ID, resourceId='o123456')) organization_dao.insert( Organization(externalId='AARDVARK_ORG', displayName='Aardvarks Rock', hpoId=PITT_HPO_ID, resourceId='o123457')) site_dao.insert( Site(siteName='Site 1', googleGroup='hpo-site-1', mayolinkClientNumber=123456, organizationId=org_1.organizationId, siteStatus=SiteStatus.ACTIVE, enrollingStatus=EnrollingStatus.ACTIVE, launchDate=datetime.datetime(2016, 1, 1), notes='notes', latitude=12.1, longitude=13.1, directions='directions', physicalLocationName='locationName', address1='address1', address2='address2', city='Austin', state='TX', zipCode='78751', phoneNumber='555-555-5555', adminEmails='[email protected], [email protected]', link='http://www.example.com')) site_dao.insert( Site(siteName='Zebras Rock', googleGroup='aaaaaaa', organizationId=org_1.organizationId, enrollingStatus=EnrollingStatus.INACTIVE, siteStatus=SiteStatus.INACTIVE))
class OrganizationHierarchySyncDao(BaseDao): def __init__(self): super(OrganizationHierarchySyncDao, self).__init__(HPO) self.hpo_dao = HPODao() self.organization_dao = OrganizationDao() self.site_dao = SiteDao() def from_client_json(self, resource_json, id_=None, expected_version=None, client_id=None): # pylint: disable=unused-argument try: fhir_org = lib_fhir.fhirclient_3_0_0.models.organization.Organization( resource_json) except FHIRValidationError: raise BadRequest('Invalid FHIR format in payload data.') if not fhir_org.meta or not fhir_org.meta.versionId: raise BadRequest('No versionId info found in payload data.') try: fhir_org.version = int(fhir_org.meta.versionId) except ValueError: raise BadRequest('Invalid versionId in payload data.') return fhir_org def to_client_json(self, hierarchy_org_obj): return hierarchy_org_obj.as_json() def get_etag(self, id_, pid): # pylint: disable=unused-argument return None def update(self, hierarchy_org_obj): obj_type = self._get_type(hierarchy_org_obj) operation_funcs = { 'AWARDEE': self._update_awardee, 'ORGANIZATION': self._update_organization, 'SITE': self._update_site } if obj_type not in operation_funcs: raise BadRequest('No awardee-type info found in payload data.') operation_funcs[obj_type](hierarchy_org_obj) def _update_awardee(self, hierarchy_org_obj): if hierarchy_org_obj.id is None: raise BadRequest('No id found in payload data.') awardee_id = self._get_value_from_identifier( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'awardee-id') if awardee_id is None: raise BadRequest( 'No organization-identifier info found in payload data.') is_obsolete = ObsoleteStatus( 'OBSOLETE') if not hierarchy_org_obj.active else None awardee_type = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'awardee-type') try: organization_type = OrganizationType(awardee_type) if organization_type == OrganizationType.UNSET: organization_type = None except TypeError: raise BadRequest( 'Invalid organization type {} for awardee {}'.format( awardee_type, awardee_id)) entity = HPO(name=awardee_id.upper(), displayName=hierarchy_org_obj.name, organizationType=organization_type, isObsolete=is_obsolete, resourceId=hierarchy_org_obj.id) existing_map = { entity.name: entity for entity in self.hpo_dao.get_all() } existing_entity = existing_map.get(entity.name) with self.hpo_dao.session() as session: if existing_entity: hpo_id = existing_entity.hpoId new_dict = entity.asdict() new_dict['hpoId'] = None existing_dict = existing_entity.asdict() existing_dict['hpoId'] = None if existing_dict == new_dict: logging.info('Not updating {}.'.format(new_dict['name'])) else: existing_entity.displayName = entity.displayName existing_entity.organizationType = entity.organizationType existing_entity.isObsolete = entity.isObsolete existing_entity.resourceId = entity.resourceId self.hpo_dao.update_with_session(session, existing_entity) else: entity.hpoId = len(existing_map) hpo_id = entity.hpoId self.hpo_dao.insert_with_session(session, entity) bq_hpo_update_by_id(hpo_id) def _update_organization(self, hierarchy_org_obj): if hierarchy_org_obj.id is None: raise BadRequest('No id found in payload data.') organization_id = self._get_value_from_identifier( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'organization-id') if organization_id is None: raise BadRequest( 'No organization-identifier info found in payload data.') is_obsolete = ObsoleteStatus( 'OBSOLETE') if not hierarchy_org_obj.active else None resource_id = self._get_reference(hierarchy_org_obj) hpo = self.hpo_dao.get_by_resource_id(resource_id) if hpo is None: raise BadRequest( 'Invalid partOf reference {} importing organization {}'.format( resource_id, organization_id)) entity = Organization(externalId=organization_id.upper(), displayName=hierarchy_org_obj.name, hpoId=hpo.hpoId, isObsolete=is_obsolete, resourceId=hierarchy_org_obj.id) existing_map = { entity.externalId: entity for entity in self.organization_dao.get_all() } existing_entity = existing_map.get(entity.externalId) with self.organization_dao.session() as session: if existing_entity: new_dict = entity.asdict() new_dict['organizationId'] = None existing_dict = existing_entity.asdict() existing_dict['organizationId'] = None if existing_dict == new_dict: logging.info('Not updating {}.'.format( new_dict['externalId'])) else: existing_entity.displayName = entity.displayName existing_entity.hpoId = entity.hpoId existing_entity.isObsolete = entity.isObsolete existing_entity.resourceId = entity.resourceId self.organization_dao.update_with_session( session, existing_entity) else: self.organization_dao.insert_with_session(session, entity) org_id = self.organization_dao.get_by_external_id( organization_id.upper()).organizationId bq_organization_update_by_id(org_id) def _update_site(self, hierarchy_org_obj): if hierarchy_org_obj.id is None: raise BadRequest('No id found in payload data.') google_group = self._get_value_from_identifier( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'site-id') if google_group is None: raise BadRequest( 'No organization-identifier info found in payload data.') google_group = google_group.lower() is_obsolete = ObsoleteStatus( 'OBSOLETE') if not hierarchy_org_obj.active else None resource_id = self._get_reference(hierarchy_org_obj) organization = self.organization_dao.get_by_resource_id(resource_id) if organization is None: raise BadRequest( 'Invalid partOf reference {} importing site {}'.format( resource_id, google_group)) launch_date = None launch_date_str = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'anticipatedLaunchDate', 'valueDate') if launch_date_str: try: launch_date = parse(launch_date_str).date() except ValueError: raise BadRequest('Invalid launch date {} for site {}'.format( launch_date_str, google_group)) name = hierarchy_org_obj.name mayolink_client_number = None mayolink_client_number_str = self._get_value_from_identifier( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'mayo-link-identifier') if mayolink_client_number_str: try: mayolink_client_number = int(mayolink_client_number_str) except ValueError: raise BadRequest( 'Invalid Mayolink Client # {} for site {}'.format( mayolink_client_number_str, google_group)) notes = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'notes') site_status_bool = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'schedulingStatusActive', 'valueBoolean') try: site_status = SiteStatus( 'ACTIVE' if site_status_bool else 'INACTIVE') except TypeError: raise BadRequest('Invalid site status {} for site {}'.format( site_status, google_group)) enrolling_status_bool = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'enrollmentStatusActive', 'valueBoolean') try: enrolling_status = EnrollingStatus( 'ACTIVE' if enrolling_status_bool else 'INACTIVE') except TypeError: raise BadRequest( 'Invalid enrollment site status {} for site {}'.format( enrolling_status_bool, google_group)) digital_scheduling_bool = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'digitalSchedulingStatusActive', 'valueBoolean') try: digital_scheduling_status = DigitalSchedulingStatus( 'ACTIVE' if digital_scheduling_bool else 'INACTIVE') except TypeError: raise BadRequest( 'Invalid digital scheduling status {} for site {}'.format( digital_scheduling_bool, google_group)) directions = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'directions') physical_location_name = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'locationName') address_1, address_2, city, state, zip_code = self._get_address( hierarchy_org_obj) phone = self._get_contact_point(hierarchy_org_obj, 'phone') admin_email_addresses = self._get_contact_point( hierarchy_org_obj, 'email') link = self._get_contact_point(hierarchy_org_obj, 'url') schedule_instructions = self._get_value_from_extention( hierarchy_org_obj, HIERARCHY_CONTENT_SYSTEM_PREFIX + 'schedulingInstructions') entity = Site(siteName=name, googleGroup=google_group, mayolinkClientNumber=mayolink_client_number, organizationId=organization.organizationId, hpoId=organization.hpoId, siteStatus=site_status, enrollingStatus=enrolling_status, digitalSchedulingStatus=digital_scheduling_status, scheduleInstructions=schedule_instructions, scheduleInstructions_ES='', launchDate=launch_date, notes=notes, notes_ES='', directions=directions, physicalLocationName=physical_location_name, address1=address_1, address2=address_2, city=city, state=state, zipCode=zip_code, phoneNumber=phone, adminEmails=admin_email_addresses, link=link, isObsolete=is_obsolete, resourceId=hierarchy_org_obj.id) existing_map = { entity.googleGroup: entity for entity in self.site_dao.get_all() } existing_entity = existing_map.get(entity.googleGroup) with self.site_dao.session() as session: if existing_entity: self._populate_lat_lng_and_time_zone(entity, existing_entity) if entity.siteStatus == SiteStatus.ACTIVE and \ (entity.latitude is None or entity.longitude is None): raise BadRequest( 'Active site without geocoding: {}'.format( entity.googleGroup)) new_dict = entity.asdict() new_dict['siteId'] = None existing_dict = existing_entity.asdict() existing_dict['siteId'] = None if existing_dict == new_dict: logging.info('Not updating {}.'.format( new_dict['googleGroup'])) else: for k, v in entity.asdict().iteritems(): if k != 'siteId' and k != 'googleGroup': setattr(existing_entity, k, v) self.site_dao.update_with_session(session, existing_entity) else: self._populate_lat_lng_and_time_zone(entity, None) if entity.siteStatus == SiteStatus.ACTIVE and \ (entity.latitude is None or entity.longitude is None): raise BadRequest( 'Active site without geocoding: {}'.format( entity.googleGroup)) self.site_dao.insert_with_session(session, entity) site_id = self.site_dao.get_by_google_group(google_group).siteId bq_site_update_by_id(site_id) def _get_type(self, hierarchy_org_obj): obj_type = None type_arr = hierarchy_org_obj.type for type_item in type_arr: code_arr = type_item.coding for code_item in code_arr: if code_item.system == HIERARCHY_CONTENT_SYSTEM_PREFIX + 'type': obj_type = code_item.code break return obj_type def _get_value_from_identifier(self, hierarchy_org_obj, system): identifier_arr = hierarchy_org_obj.identifier for identifier in identifier_arr: if identifier.system == system: return identifier.value else: return None def _get_value_from_extention(self, hierarchy_org_obj, url, value_key='valueString'): extension_arr = hierarchy_org_obj.extension for extension in extension_arr: if extension.url == url: ext_json = extension.as_json() return ext_json[value_key] else: return None def _get_contact_point(self, hierarchy_org_obj, code): contact_arr = hierarchy_org_obj.contact for contact in contact_arr: telecom_arr = contact.telecom for telecom in telecom_arr: if telecom.system == code: return telecom.value else: return None def _get_address(self, hierarchy_org_obj): address = hierarchy_org_obj.address[0] address_1 = address.line[0] if len(address.line) > 0 else '' address_2 = address.line[1] if len(address.line) > 1 else '' city = address.city state = address.state postal_code = address.postalCode return address_1, address_2, city, state, postal_code def _get_reference(self, hierarchy_org_obj): try: return hierarchy_org_obj.partOf.reference.split('/')[1] except IndexError: return None def _populate_lat_lng_and_time_zone(self, site, existing_site): if site.address1 and site.city and site.state: if existing_site: if (existing_site.address1 == site.address1 and existing_site.city == site.city and existing_site.state == site.state and existing_site.latitude is not None and existing_site.longitude is not None and existing_site.timeZoneId is not None): # Address didn't change, use the existing lat/lng and time zone. site.latitude = existing_site.latitude site.longitude = existing_site.longitude site.timeZoneId = existing_site.timeZoneId return latitude, longitude = self._get_lat_long_for_site( site.address1, site.city, site.state) site.latitude = latitude site.longitude = longitude if latitude and longitude: site.timeZoneId = self._get_time_zone(latitude, longitude) else: if site.googleGroup not in self.status_exception_list: if site.siteStatus == self.ACTIVE: logging.warn( 'Active site must have valid address. Site: {}, Group: {}' .format(site.siteName, site.googleGroup)) def _get_lat_long_for_site(self, address_1, city, state): self.full_address = address_1 + ' ' + city + ' ' + state try: self.api_key = os.environ.get('API_KEY') self.gmaps = googlemaps.Client(key=self.api_key) try: geocode_result = self.gmaps.geocode(address_1 + '' + city + ' ' + state)[0] except IndexError: logging.warn('Bad address for {}, could not geocode.'.format( self.full_address)) return None, None if geocode_result: geometry = geocode_result.get('geometry') if geometry: location = geometry.get('location') if location: latitude = location.get('lat') longitude = location.get('lng') return latitude, longitude else: logging.warn('Can not find lat/long for %s', self.full_address) return None, None else: logging.warn('Geocode results failed for %s.', self.full_address) return None, None except ValueError as e: logging.exception('Invalid geocode key: %s. ERROR: %s', self.api_key, e) return None, None except IndexError as e: logging.exception( 'Geocoding failure Check that address is correct. ERROR: %s', e) return None, None def _get_time_zone(self, latitude, longitude): time_zone = self.gmaps.timezone(location=(latitude, longitude)) if time_zone['status'] == 'OK': time_zone_id = time_zone['timeZoneId'] return time_zone_id else: logging.info('can not retrieve time zone from %s', self.full_address) return None