def set_user_task_completed(ut_id, correlation_id=None): utils.validate_uuid(ut_id) # check that user_task exists result = get_user_task(ut_id, correlation_id) if len(result) == 0: errorjson = { "user_task_id": ut_id, "correlation_id": str(correlation_id) } raise utils.ObjectDoesNotExistError("user task does not exist", errorjson) if result[0]["project_task_status"] not in ["planned", "testing"]: updated_rows_count = execute_non_query( sql_q.UPDATE_USER_TASK_STATUS, ( "complete", str(utils.now_with_tz()), str(ut_id), ), correlation_id, ) assert ( updated_rows_count == 1 ), f"Failed to update status of user task {ut_id}; updated_rows_count: {updated_rows_count}"
def from_json(cls, ugm_json, correlation_id): """ Creates new object as specified by JSON. Checks that attributes are present but does not check for referential integrity (ie that user and group exist) :param ugm_json: MUST contain: user_id, user_group_id, may OPTIONALLY include: id, created, modified :param correlation_id: :return: new ugm object """ try: user_id = utils.validate_uuid(ugm_json["user_id"]) user_group_id = utils.validate_uuid(ugm_json["user_group_id"]) except utils.DetailedValueError as err: # uuids are not valid err.add_correlation_id(correlation_id) raise err except KeyError as err: # mandatory data not present error_json = { "parameter": err.args[0], "correlation_id": str(correlation_id), } raise utils.DetailedValueError( "mandatory data missing", error_json ) from err ugm = cls(user_id, user_group_id, ugm_json, correlation_id) return ugm
def __init__(self, response_dict, correlation_id=None): self.survey_id = response_dict.pop("survey_id", None) self.response_id = response_dict.pop("response_id", None) self.project_task_id = str( utils.validate_uuid(response_dict.pop("project_task_id", None))) self.anon_project_specific_user_id = str( utils.validate_uuid( response_dict.pop("anon_project_specific_user_id", None))) self.anon_user_task_id = str( utils.validate_uuid(response_dict.pop("anon_user_task_id", None))) for required_parameter_name, value in [ ("survey_id", self.survey_id), ("response_id", self.response_id), ]: if not value: raise utils.DetailedValueError( f"Required parameter {required_parameter_name} not present in body of call", details={ "response_dict": response_dict, "correlation_id": correlation_id, }, ) self.response_dict = response_dict self.ddb_client = Dynamodb( stack_name=const.STACK_NAME, correlation_id=correlation_id, ) self.correlation_id = correlation_id
def list_user_tasks_by_user(user_id, correlation_id=None): try: user_id = utils.validate_uuid(user_id) except utils.DetailedValueError: raise # check that user exists try: user_result = get_user_by_id(user_id, correlation_id)[0] except IndexError: errorjson = {"user_id": user_id, "correlation_id": str(correlation_id)} raise utils.ObjectDoesNotExistError("user does not exist", errorjson) result = execute_query(sql_q.LIST_USER_TASKS_SQL, (str(user_id), ), correlation_id) # add url field to each user_task in result edited_result = list() for ut in result: user_task = UserTask(correlation_id=correlation_id) user_task.from_dict(ut_dict=ut) user_task.id = ut["user_task_id"] user_task.first_name = user_result["first_name"] user_task.last_name = user_result["last_name"] user_task.email = user_result["email"] ut["url"] = user_task.calculate_url() del ut["base_url"] del ut["external_task_id"] del ut["user_specific_url"] del ut["user_task_url"] del ut["anon_project_specific_user_id"] del ut["anonymise_url"] edited_result.append(ut) return edited_result
def _create_user_task_validate_mandatory_data(self): for param in ["user_id", "project_task_id", "consented"]: if self.__dict__[param] is None: errorjson = { "parameter": param, "correlation_id": str(self._correlation_id), } raise utils.DetailedValueError("mandatory data missing", details=errorjson) try: utils.validate_uuid(self.user_id) utils.validate_uuid(self.project_task_id) utils.validate_utc_datetime(self.consented) except utils.DetailedValueError as err: err.add_correlation_id(self._correlation_id) raise err
def main(self, project_task_id=None): self.project_task_id = project_task_id if self.project_task_id is None: self.project_task_id = utils.validate_uuid( input( "Please enter the project_task_id of the completed user tasks we will be creating:" ).strip()) for anon_psu_id in self.anon_ids: self.create_user_task_(anon_psu_id)
def get_user_by_any_anon_id(anon_id, correlation_id=None): try: anon_id = utils.validate_uuid(anon_id) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err user_json = execute_query(sql_q.GET_USER_BY_ANY_ANON_ID_SQL, ((str(anon_id), ), ) * 2, correlation_id) return append_calculated_properties_to_list(user_json)
def get_user_by_id(user_id, correlation_id=None): try: user_id = utils.validate_uuid(user_id) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err sql_where_clause = " WHERE id = %s" user_json = execute_query(sql_q.BASE_USER_SELECT_SQL + sql_where_clause, (str(user_id), ), correlation_id) return append_calculated_properties_to_list(user_json)
def main(response_dataset, account, survey_id): logger = utils.get_logger() with open(response_dataset) as csvfile: reader = csv.DictReader(csvfile) row_counter = 0 for row in reader: row_counter += 1 logger.info(f"Working on row {row_counter}") try: response_id = row.pop("ResponseId") event_time = row.pop("RecordedDate") anon_project_specific_user_id = row.pop( "anon_project_specific_user_id") anon_user_task_id = row.pop("anon_user_task_id") except KeyError as exc: raise utils.DetailedValueError( f"Mandatory {exc} data not found in source event", details={ "row": row, }, ) try: utils.validate_uuid(anon_project_specific_user_id) except utils.DetailedValueError: logger.warning( f"Could not process row because anon_project_specific_user_id is not valid: {anon_project_specific_user_id}" ) continue emulated_event = user_task_completed_event_template.copy() emulated_event["time"] = event_time emulated_event["detail"] = { "anon_project_specific_user_id": anon_project_specific_user_id, "anon_user_task_id": anon_user_task_id, "response_id": f"{survey_id}-{response_id}", "account": account, } resp.put_task_response(emulated_event, {})
def list_user_projects(user_id, correlation_id): try: user_id = utils.validate_uuid(user_id) except utils.DetailedValueError: raise # check that user exists result = get_user_by_id(user_id, correlation_id) if len(result) == 0: errorjson = {"user_id": user_id, "correlation_id": str(correlation_id)} raise utils.ObjectDoesNotExistError("user does not exist", errorjson) return execute_query(LIST_USER_PROJECTS_SQL, (str(user_id), ), correlation_id)
def redirect_to_user_interview_task(event, context): """ Updates user task url in response to user_interview_task events posted by Qualtrics """ detail_type = event["detail-type"] assert (detail_type == "user_interview_task" ), f"Unexpected detail-type: {detail_type}" event_detail = event["detail"] correlation_id = event["id"] try: anon_user_task_id = utils.validate_uuid( event_detail.pop("anon_user_task_id")) except KeyError as exc: raise utils.DetailedValueError( f"Mandatory {exc} data not found in source event", details={ "event": event, }, ) ssm_client = SsmClient() vcs_param = ssm_client.get_parameter(name="video-call-system") ut_base_url = vcs_param["base-url"] user_task_url = f"{ut_base_url}?response_id={event_detail['response_id']}" ut_id = anon_user_task_id_2_user_task_id(anon_user_task_id, correlation_id=correlation_id) updated_rows_count = execute_non_query( sql_q.UPDATE_USER_TASK_URL, ( user_task_url, str(utils.now_with_tz()), str(ut_id), ), correlation_id, ) assert ( updated_rows_count == 1 ), f"Failed to update url of user task {ut_id}; updated_rows_count: {updated_rows_count}" body = { "user_task_id": ut_id, "user_task_url": user_task_url, } return {"statusCode": HTTPStatus.OK, "body": json.dumps(body)}
def get_by_id(cls, user_group_id, correlation_id): try: user_group_id = validate_uuid(user_group_id) except DetailedValueError as err: err.add_correlation_id(correlation_id) raise err sql_where_clause = " WHERE id = %s" ug_json = execute_query( sql_q.USER_GROUP_BASE_SELECT_SQL + sql_where_clause, (str(user_group_id), ), correlation_id, ) if len(ug_json) > 0: ug_json = ug_json[0] return cls.from_json(ug_json, correlation_id) else: return None
def get_personal_link_api(event, context): valid_accounts = ["cambridge", "thisinstitute"] logger = event["logger"] correlation_id = event["correlation_id"] params = event["queryStringParameters"] project_task_id = params.get('project_task_id') # validate params try: survey_id = params["survey_id"] anon_project_specific_user_id = str( utils.validate_uuid(params["anon_project_specific_user_id"])) if (account := params["account"]) not in valid_accounts: raise utils.DetailedValueError( f'Account {account} is not supported. Valid values are {",".join(valid_accounts)}', details={"params": params}, ) except KeyError as err: raise utils.DetailedValueError(f"Mandatory {err} data not provided", details={"params": params}) logger.info( "API call", extra={ "anon_project_specific_user_id": anon_project_specific_user_id, "correlation_id": correlation_id, "survey_id": survey_id, "project_task_id": project_task_id, }, ) plm = PersonalLinkManager( survey_id=survey_id, anon_project_specific_user_id=anon_project_specific_user_id, account=account, project_task_id=project_task_id, ) return { "statusCode": HTTPStatus.OK, "body": json.dumps({"personal_link": plm.get_personal_link()}), }
def get_project_status_for_user_api(event, context): logger = event["logger"] correlation_id = event["correlation_id"] params = event["queryStringParameters"] user_id = str(utils.validate_uuid(params["user_id"])) try: demo = json.loads(params.get("demo", "false")) include_non_visible_data = json.loads( params.get("include_non_visible_data", "false")) except json.decoder.JSONDecodeError: raise utils.DetailedValueError( "The parameters 'demo' and 'include_non_visible_data', if present, must be either 'true' or 'false' " "(lowercase)", {}, ) logger.info( "API call", extra={ "user_id": user_id, "correlation_id": correlation_id, "event": event }, ) if user_id == "760f4e4d-4a3b-4671-8ceb-129d81f9d9ca": raise ValueError("Deliberate error raised to test error handling") return { "statusCode": HTTPStatus.OK, "body": json.dumps( get_project_status_for_user( user_id=user_id, demo=demo, correlation_id=correlation_id, include_non_visible_data=include_non_visible_data, )), }
def check_no_testers_in_data(self): users_in_both_groups = list() users_in_test_group = list() users_in_user_group = list() user_ids_in_input_file = list() unique_user_ids_in_input_file = set() this_users = list() with open(self.input_filename) as csv_f: reader = csv.DictReader(csv_f) for row in reader: user_id = row.get(self.user_id_column) if user_id is None: anon_project_specific_user_id = row.get( self.anon_project_specific_user_id_column) try: utils.validate_uuid(anon_project_specific_user_id) except utils.DetailedValueError: self.logger.warning( f'anon_project_specific_user_id "{anon_project_specific_user_id}" not valid; row skipped' ) continue user = pg_utils.execute_query( sql_q.GET_USER_BY_ANON_PROJECT_SPECIFIC_USER_ID_SQL, [str(anon_project_specific_user_id)], )[0] user_id = user["id"] else: try: utils.validate_uuid(user_id) except utils.DetailedValueError: self.logger.warning( f'User id "{user_id}" not valid; row skipped') continue user = pg_utils.execute_query( f"{sql_q.BASE_USER_SELECT_SQL} WHERE id = %s", [str(user_id)], )[0] self.user_table.add_row([ user["first_name"], user["last_name"], user["email"], ]) user_ids_in_input_file.append(user_id) unique_user_ids_in_input_file.add(user_id) if (user_id in self.test_group_ids) and (user_id in self.user_group_ids): self.logger.warning( f"User {user_id} is both in user group and in test group for this project task" ) users_in_both_groups.append(user_id) elif user_id in self.test_group_ids: self.logger.error( f"User {user_id} is a tester; they are not in user this project task" ) users_in_test_group.append(user_id) elif user_id in self.user_group_ids: users_in_user_group.append(user_id) user = u.get_user_by_id(user_id)[0] user_email = user["email"] if user_email: if "@thisinstitute.cam.ac.uk" in user_email: self.logger.error( f"User {user_id} is a member of THIS") this_users.append(user_id) else: print("user_group_ids:") pprint(self.user_group_ids) print("\ntest_group_ids:") pprint(self.test_group_ids) err_message = ( f"User id {user_id} (anon_project_specific_user_id: {anon_project_specific_user_id}) could not be " f"found on user group or test group of project task {self.project_task_id}. Are " f"you sure you entered the correct project task id?") raise ValueError(err_message) number_of_rows_with_duplicate_user_ids = len( user_ids_in_input_file) - len(unique_user_ids_in_input_file) return ( users_in_both_groups, users_in_test_group, users_in_user_group, user_ids_in_input_file, this_users, number_of_rows_with_duplicate_user_ids, )
def create_user_project(up_json, correlation_id, do_nothing_if_exists=False): """ Inserts new UserProject row in thiscovery db Args: up_json: must contain user_id and project_id; may optionally include id, created, status, anon_project_specific_user_id correlation_id: do_nothing_if_exists: Returns: """ # extract mandatory data from json try: user_id = utils.validate_uuid( up_json["user_id"]) # all public id are uuids project_id = utils.validate_uuid(up_json["project_id"]) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err except KeyError as err: errorjson = { "parameter": err.args[0], "correlation_id": str(correlation_id) } raise utils.DetailedValueError("mandatory data missing", errorjson) from err # now process optional json data optional_fields_name_default_and_validator = [ ("anon_project_specific_user_id", str(uuid.uuid4()), utils.validate_uuid), ("created", str(utils.now_with_tz()), utils.validate_utc_datetime), ("status", DEFAULT_STATUS, validate_status), ] for ( variable_name, default_value, validating_func, ) in optional_fields_name_default_and_validator: if variable_name in up_json: try: globals()[variable_name] = validating_func( up_json[variable_name] ) # https://stackoverflow.com/a/4687672 except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err else: globals()[variable_name] = default_value # id shadows builtin function, so treat if separately (using globals() approach above would overwrite that function) if "id" in up_json: try: id = utils.validate_uuid(up_json["id"]) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err else: id = str(uuid.uuid4()) # check external account does not already exist existing = get_existing_user_project_id(user_id, project_id, correlation_id) if len(existing) > 0: if do_nothing_if_exists: return existing[0] else: errorjson = { "user_id": user_id, "project_id": project_id, "correlation_id": str(correlation_id), } raise utils.DuplicateInsertError("user_project already exists", errorjson) # lookup user id (needed for insert) for user uuid (supplied in json) result = get_user_by_id(user_id, correlation_id) if len(result) == 0: errorjson = {"user_id": user_id, "correlation_id": str(correlation_id)} raise utils.ObjectDoesNotExistError("user does not exist", errorjson) execute_non_query( CREATE_USER_PROJECT_SQL, ( id, created, created, user_id, project_id, status, anon_project_specific_user_id, ), correlation_id, ) new_user_project = { "id": id, "created": created, "modified": created, "user_id": user_id, "project_id": project_id, "status": status, "anon_project_specific_user_id": anon_project_specific_user_id, } return new_user_project
def create_user_external_account(uea_json, correlation_id): # json MUST contain: external_system_id, user_id, external_user_id # json may OPTIONALLY include: id, created, status # extract mandatory data from json try: external_system_id = utils.validate_uuid( uea_json["external_system_id"]) user_id = utils.validate_uuid(uea_json["user_id"]) external_user_id = uea_json["external_user_id"] except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err except KeyError as err: errorjson = { "parameter": err.args[0], "correlation_id": str(correlation_id) } raise utils.DetailedValueError("mandatory data missing", errorjson) from err # now process optional json data if "id" in uea_json: try: id = utils.validate_uuid(uea_json["id"]) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err else: id = str(uuid.uuid4()) if "created" in uea_json: try: created = utils.validate_utc_datetime(uea_json["created"]) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err else: created = str(utils.now_with_tz()) if "status" in uea_json: try: status = validate_status(uea_json["status"]) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err else: status = DEFAULT_STATUS # check external account does not already exist existing = check_user_id_and_external_account(user_id, external_system_id, correlation_id) if len(existing) > 0: errorjson = { "user_id": user_id, "external_system_id": external_system_id, "correlation_id": str(correlation_id), } raise utils.DuplicateInsertError( "user_external_account already exists", errorjson) # lookup user id (needed for insert) for user uuid (supplied in json) existing_user = get_user_by_id(user_id, correlation_id) if len(existing_user) == 0: errorjson = {"user_id": user_id, "correlation_id": str(correlation_id)} raise utils.ObjectDoesNotExistError("user does not exist", errorjson) execute_non_query( CREATE_USER_EXTERNAL_ACCOUNT_SQL, (id, created, created, external_system_id, user_id, external_user_id, status), correlation_id, ) new_user_external_account = { "id": id, "created": created, "modified": created, "external_system_id": external_system_id, "user_id": user_id, "external_user_id": external_user_id, "status": status, } return new_user_external_account
def create_user(user_json, correlation_id): # json MUST contain: email, first_name, last_name, status # json may OPTIONALLY include: id, title, created, auth0_id # note that users will always be created with email_address_verified = false # extract mandatory data from json try: email = user_json["email"].lower().strip() first_name = user_json["first_name"].strip() last_name = user_json["last_name"].strip() status = validate_status(user_json["status"]) country_code = user_json["country_code"].strip() country_utils.get_country_name(country_code) # looking up the name is a way of validating the code - an invalid code will raise an error except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err # now process optional json data if "id" in user_json: try: id = utils.validate_uuid(user_json["id"]) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err else: id = str(uuid.uuid4()) if "created" in user_json: try: created = utils.validate_utc_datetime(user_json["created"]) except utils.DetailedValueError as err: err.add_correlation_id(correlation_id) raise err else: created = str(utils.now_with_tz()) if "auth0_id" in user_json: auth0_id = user_json["auth0_id"] else: auth0_id = None if "title" in user_json: title = user_json["title"] else: title = None existing_user = get_user_by_id(id, correlation_id) if len(existing_user) > 0: errorjson = {"id": id, "correlation_id": str(correlation_id)} raise utils.DuplicateInsertError("user already exists", errorjson) params = ( id, created, created, email, title, first_name, last_name, country_code, auth0_id, status, ) execute_non_query(sql_q.CREATE_USER_SQL, params, correlation_id) new_user = { "id": id, "created": created, "modified": created, "email": email, "title": title, "first_name": first_name, "last_name": last_name, "auth0_id": auth0_id, "crm_id": None, "country_code": country_code, "status": status, } new_user = append_calculated_properties(new_user) return new_user