def manage_projects(): result = None is_active = request.args.get('is_active', None) if request.method == 'POST': model = {x: request.form[x] for x in request.form} project_id = model.pop('project_id') model['is_microsetta'] = model.get('is_microsetta', '') == 'true' model['bank_samples'] = model.get('bank_samples', '') == 'true' model = _translate_nones(model, False) if project_id.isdigit(): # update (put) an existing project action = "update" status, api_output = APIRequest.put('{}/{}'.format( API_PROJECTS_URL, project_id), json=model) else: # create (post) a new project action = "create" status, api_output = APIRequest.post(API_PROJECTS_URL, json=model) # if api post or put failed if status >= 400: result = {'error_message': f'Unable to {action} project.'} # end if post # if the above work (if any) didn't produce an error message, return # the projects list if result is None: _, result = _get_projects(include_stats=True, is_active=is_active) return render_template('manage_projects.html', **build_login_variables(), result=result), 200
def submit_daklapack_order(): error_msg_key = "error_message" def return_error(msg): return render_template('submit_daklapack_order.html', **build_login_variables(), error_message=msg) status, dak_articles_output = APIRequest.get( '/api/admin/daklapack_articles') if status >= 400: return return_error("Unable to load daklapack articles list.") status, projects_output = _get_projects(include_stats=False, is_active=True) if status >= 400: return return_error(projects_output[error_msg_key]) return render_template( 'submit_daklapack_order.html', **build_login_variables(), error_message=None, dummy_status=DUMMY_SELECT_TEXT, dak_articles=dak_articles_output, contact_phone_number=SERVER_CONFIG["order_contact_phone"], projects=projects_output['projects'])
def _fetch_barcode_metadata(sample_barcode): """Query the private API to obtain per-sample metadata Parameters ---------- sample_barcode : str The barcode to request Returns ------- dict The survey responses associated with the sample barcode dict or None Any error information associated with the retreival. If an error is observed, the survey responses should not be considered valid. """ errors = None status, response = APIRequest.get( '/api/admin/metadata/samples/%s/surveys/' % sample_barcode) if status != 200: errors = { "barcode": sample_barcode, "error": str(status) + " from api" } return response, errors
def _fetch_survey_template(template_id, ids): """Fetch the survey structure to get full multi-choice detail Parameters ---------- template_id : int The survey template ID to fetch ids : dict An account and source ID to use Returns ------- dict The survey structure as returned from the private API dict or None Any error information associated with the retreival. If an error is observed, the survey responses should not be considered valid. """ errors = None ids['template_id'] = template_id url = ("/api/accounts/%(account_id)s/sources/%(source_id)s/" "survey_templates/%(template_id)d?language_tag=en-US") status, response = APIRequest.get(url % ids) if status != 200: errors = {"ids": ids, "error": str(status) + " from api"} return response, errors
def new_kits(): _, result = _get_projects(include_stats=False, is_active=True) projects = result.get('projects') if request.method == 'GET': return render_template('create_kits.html', error_message=result.get('error_message'), projects=projects, **build_login_variables()) elif request.method == 'POST': num_kits = int(request.form['num_kits']) num_samples = int(request.form['num_samples']) prefix = request.form['prefix'] selected_project_ids = request.form.getlist('project_ids') payload = { 'number_of_kits': num_kits, 'number_of_samples': num_samples, 'project_ids': selected_project_ids } if prefix: payload['kit_id_prefix'] = prefix status, result = APIRequest.post('/api/admin/create/kits', json=payload) if status != 201: return render_template('create_kits.html', error_message='Failed to create kits', projects=projects, **build_login_variables()) # StringIO/BytesIO based off https://stackoverflow.com/a/45111660 buf = io.StringIO() payload = io.BytesIO() # explicitly expand out the barcode detail kits = pd.DataFrame(result['created']) for i in range(num_samples): kits['barcode_%d' % (i + 1)] = [ r['sample_barcodes'][i] for _, r in kits.iterrows() ] kits.drop(columns='sample_barcodes', inplace=True) kits.to_csv(buf, sep=',', index=False, header=True) payload.write(buf.getvalue().encode('utf-8')) payload.seek(0) buf.close() stamp = datetime.now().strftime('%d%b%Y-%H%M') fname = f'kits-{stamp}.csv' return send_file(payload, as_attachment=True, attachment_filename=fname, mimetype='text/csv')
def search_result(): query = request.form['search_term'] barcode_result, barcode_status = APIRequest.get('/.../scan/%s' % query) name_result, name_status = APIRequest.get('/.../name/%s' % query) kitid_result, kitid_status = APIRequest.get('/.../kitid/%s' % query) error = False status = 200 if barcode_status == 200: result = barcode_result elif name_status == 200: result = name_result elif kitid_status == 200: result = kitid_result else: error = True status = 404 result = {'message': 'Nothing was found.'} return render_template('search_result.html', **build_login_variables(), result=result, error=error), status
def _get_projects(include_stats, is_active): projects_uri = API_PROJECTS_URL + f"?include_stats={include_stats}" if is_active is not None: projects_uri += f"&is_active={is_active}" status, projects_output = APIRequest.get(projects_uri) if status >= 400: result = { 'error_message': f"Unable to load project list: " f"{projects_uri}" } else: cleaned_projects = [_translate_nones(x, True) for x in projects_output] # if we're not using full project stats, sort # alphabetically by project name if not include_stats: cleaned_projects = sorted(cleaned_projects, key=lambda k: k['project_name']) result = {'projects': cleaned_projects} return status, result
def _search(resource=None): if request.method == 'GET': return render_template('search.html', **build_login_variables()) elif request.method == 'POST': query = request.form['search_%s' % resource] status, result = APIRequest.get('/api/admin/search/%s/%s' % (resource, query)) if status == 404: result = {'error_message': "Query not found"} return render_template('search_result.html', **build_login_variables(), result=result), 200 elif status == 200: return render_template('search_result.html', **build_login_variables(), resource=resource, result=result), 200 else: return result
def _get_by_sample_barcode(sample_barcodes, strip_sampleid, projects): payload = {'sample_barcodes': sample_barcodes} status, result = APIRequest.post('/api/admin/account_barcode_summary?' 'strip_sampleid=%s' % str(strip_sampleid), json=payload) if status == 200: if result['partial_result'] is True: unprocessed_barcodes = result['unprocessed_barcodes'] else: unprocessed_barcodes = None resource = pd.DataFrame(result['samples']) order = [ 'sampleid', 'project', 'account-email', 'source-email', 'source-type', 'site-sampled', 'sample-status', 'sample-received', 'ffq-taken', 'ffq-complete', 'vioscreen_username' ] order.extend(sorted(set(resource.columns) - set(order))) resource = resource[order] if unprocessed_barcodes: return render_template('per_sample_summary.html', resource=resource, projects=projects, error_message="Too many barcodes. S" "erver processed only" " the first 1000.", **build_login_variables()) else: return render_template('per_sample_summary.html', resource=resource, projects=projects, **build_login_variables()) else: return render_template('per_sample_summary.html', resource=None, projects=projects, error_message=result, **build_login_variables())
def post_submit_daklapack_order(): def return_error(msg): return render_template('submit_daklapack_order.html', **build_login_variables(), error_message=msg) error_message = success_submissions = failure_submissions = headers = None expected_headers = [ "firstName", "lastName", "address1", "insertion", "address2", "postalCode", "city", "state", "country", "countryCode" ] # get required fields; cast where expected by api phone_number = request.form['contact_phone_number'] project_ids_list = list(map(int, request.form.getlist('projects'))) dak_article_code = request.form['dak_article_code'] article_quantity = int(request.form['quantity']) file = request.files['addresses_file'] # get optional fields or defaults planned_send_str = request.form.get('planned_send_date') planned_send_date = planned_send_str if planned_send_str else None description = request.form.get('description') fedex_ref_1 = request.form.get('fedex_ref_1') fedex_ref_2 = request.form.get('fedex_ref_2') fedex_ref_3 = request.form.get('fedex_ref_3') try: # NB: import everything as a string so that zip codes beginning with # zero (e.g., 06710) don't get silently cast to numbers if file.filename.endswith('xls'): addresses_df = pd.read_excel(file, dtype=str) elif file.filename.endswith('xlsx'): addresses_df = pd.read_excel(file, engine='openpyxl', dtype=str) else: raise ValueError(f"Unrecognized extension on putative excel " f"filename: {file.filename}") headers = list(addresses_df.columns) except Exception as e: # noqa return return_error('Could not parse addresses file') if headers != expected_headers: return return_error(f"Received column names {headers} do " f"not match expected column names" f" {expected_headers}") # add (same) contact phone number to every address addresses_df['phone'] = phone_number addresses_df = addresses_df.fillna("") temp_dict = addresses_df.to_dict(orient='index') addresses_list = [temp_dict[n] for n in range(len(temp_dict))] status, post_output = APIRequest.post('/api/admin/daklapack_orders', json={ "project_ids": project_ids_list, "article_code": dak_article_code, "quantity": article_quantity, "addresses": addresses_list, "planned_send_date": planned_send_date, "description": description, "fedex_ref_1": fedex_ref_1, "fedex_ref_2": fedex_ref_2, "fedex_ref_3": fedex_ref_3 }) # if the post failed, keep track of the error so it can be displayed if status != 200: error_message = post_output else: order_submissions = post_output["order_submissions"] success_submissions = [ x for x in order_submissions if x["order_success"] ] failure_submissions = [ x for x in order_submissions if not x["order_success"] ] return render_template('submit_daklapack_order.html', **build_login_variables(), error_message=error_message, success_submissions=success_submissions, failure_submissions=failure_submissions)
def _scan_post_update_info(sample_barcode, technician_notes, sample_status, action, issue_type, template, received_type, recorded_type): ### # Bugfix Part 1 for duplicate emails being sent. Theory is that client is # out of sync due to hitting back button after a scan has changed # state. # Can't test if client is up to date without ETags, so for right now, # we just validate whether or not they should send an email, duplicating # the client log. (This can still break with multiple admin clients, # but that is unlikely at the moment.) latest_status = None # TODO: Replace this with ETags! status, result = APIRequest.get('/api/admin/search/samples/%s' % sample_barcode) if result['latest_scan']: latest_status = result['latest_scan']['sample_status'] ### # Do the actual update status, response = APIRequest.post('/api/admin/scan/%s' % sample_barcode, json={ "sample_status": sample_status, "technician_notes": technician_notes }) # if the update failed, keep track of the error so it can be displayed if status != 201: update_error = response return _scan_get(sample_barcode, update_error) else: update_error = None # If we're not supposed to send an email, go back to GET if action != "send_email": return _scan_get(sample_barcode, update_error) ### # Bugfix Part 2 for duplicate emails being sent. if sample_status == latest_status: # This is what we'll hit if javascript thinks it's updating status # but is out of sync with the database. update_error = "Ignoring Send Email, sample_status would " \ "not have been updated (Displayed page was out of " \ "sync)" return _scan_get(sample_barcode, update_error) ### # This is what we'll hit if there are no email templates to send for # the new sample status (or if we screw up javascript side :D ) if template is None: update_error = "Cannot Send Email: No Issue Type Specified " \ "(or no issue types available)" return _scan_get(sample_barcode, update_error) # Otherwise, send out an email to the end user status, response = APIRequest.post('/api/admin/email', json={ "issue_type": issue_type, "template": template, "template_args": { "sample_barcode": sample_barcode, "recorded_type": recorded_type, "received_type": received_type } }) # if the email failed to send, keep track of the error # so it can be displayed if status != 200: update_error = response else: update_error = None return _scan_get(sample_barcode, update_error)
def _scan_get(sample_barcode, update_error): # If there is no sample_barcode in the GET # they still need to enter one in the box, so show empty page if sample_barcode is None: return render_template('scan.html', **build_login_variables()) # Assuming there is a sample barcode, grab that sample's information status, result = APIRequest.get('/api/admin/search/samples/%s' % sample_barcode) # If we successfully grab it, show the page to the user if status == 200: # Process result in python because its easier than jinja2. status_warning = _check_sample_status(result) # check the latest scan to find the default sample_status for form latest_status = DUMMY_SELECT_TEXT if result['latest_scan']: latest_status = result['latest_scan']['sample_status'] account = result.get('account') events = [] if account: event_status, event_result = APIRequest.get( '/api/admin/events/accounts/%s' % account['id']) if event_status != 200: raise Exception("Couldn't pull event history") events = event_result return render_template('scan.html', **build_login_variables(), barcode_info=result["barcode_info"], projects_info=result['projects_info'], scans_info=result['scans_info'], latest_status=latest_status, dummy_status=DUMMY_SELECT_TEXT, status_options=STATUS_OPTIONS, send_email=session.get( SEND_EMAIL_CHECKBOX_DEFAULT_NAME, True), sample_info=result['sample'], extended_info=result, status_warning=status_warning, update_error=update_error, received_type_dropdown=RECEIVED_TYPE_DROPDOWN, source=result['source'], events=events) elif status == 401: # If we fail due to unauthorized, need the user to log in again return redirect('/logout') elif status == 404: # If we fail due to not found, need to tell the user to pick a diff # barcode return render_template('scan.html', **build_login_variables(), search_error="Barcode %s Not Found" % sample_barcode, update_error=update_error, received_type_dropdown=RECEIVED_TYPE_DROPDOWN) else: raise BadRequest()
def per_sample_summary(): # get a list of all projects in the system _, result = _get_projects(include_stats=False, is_active=True) projects = result.get('projects') # filter out any projects that don't belong to Microsetta projects = [x for x in projects if x['is_microsetta'] is True] # build a list of dictionaries with just the project id and the project # name. projects = [{ 'project_name': x['project_name'], 'project_id': x['project_id'] } for x in projects] # determine if user wants sample ids stripped strip_sampleid = request.form.get('strip_sampleid', 'off') strip_sampleid = strip_sampleid.lower() == 'on' if request.method == 'GET': # If user arrived via GET then they are either here w/out # querying and they simply need the default webpage, or they are # querying with either a list of barcodes, or with a project id. # look for both parameters to determine which state we are in. sample_barcode = request.args.get('sample_barcode') project_id = request.args.get('project_id') if sample_barcode is None and project_id is None: # user just wants the default page. return render_template('per_sample_summary.html', resource=None, projects=projects, **build_login_variables()) if project_id is not None: # user wants to get summaries on all samples in a project. payload = {'project_id': project_id} status, result = APIRequest.post( '/api/admin/account_barcode_summa' 'ry?strip_sampleid=False', json=payload) if status == 200: if result['partial_result'] is True: unprocessed_barcodes = result['unprocessed_barcodes'] else: unprocessed_barcodes = None resource = pd.DataFrame(result['samples']) order = [ 'sampleid', 'project', 'account-email', 'source-email', 'source-type', 'site-sampled', 'sample-status', 'sample-received', 'ffq-taken', 'ffq-complete', 'vioscreen_username' ] order.extend(sorted(set(resource.columns) - set(order))) resource = resource[order] if unprocessed_barcodes: return render_template('per_sample_summary.html', resource=resource, projects=projects, error_message="Too many barcodes. S" "erver processed only" " the first 1000.", **build_login_variables()) else: return render_template('per_sample_summary.html', resource=resource, projects=projects, **build_login_variables()) else: return render_template('per_sample_summary.html', resource=None, projects=projects, error_message=result, **build_login_variables()) # if we are here then the user is querying using barcodes and we # simply need to set up the query below to perform. sample_barcodes = [ sample_barcode, ] else: # assume POST, since there are only two methods defined in route. # if we are here, it is because the user is querying using an uploaded # file containing sample names. sample_barcodes, err = upload_util.parse_request_csv_col( request, 'file', 'sample_name') if err is not None: # there was an error. abort early. return render_template('per_sample_summary.html', resource=None, projects=projects, **build_login_variables(), search_error=[{ 'error': err }]) # perform the main query. payload = {'sample_barcodes': sample_barcodes} status, result = APIRequest.post('/api/admin/account_barcode_summary?stri' 'p_sampleid=%s' % str(strip_sampleid), json=payload) if status == 200: if result['partial_result'] is True: unprocessed_barcodes = result['unprocessed_barcodes'] else: unprocessed_barcodes = None resource = pd.DataFrame(result['samples']) order = [ 'sampleid', 'project', 'account-email', 'source-email', 'source-type', 'site-sampled', 'sample-status', 'sample-received', 'ffq-taken', 'ffq-complete', 'vioscreen_username' ] order.extend(sorted(set(resource.columns) - set(order))) resource = resource[order] if unprocessed_barcodes: return render_template('per_sample_summary.html', resource=resource, projects=projects, error_message="Too many barcodes. S" "erver processed only" " the first 1000.", **build_login_variables()) else: return render_template('per_sample_summary.html', resource=resource, projects=projects, **build_login_variables()) else: return render_template('per_sample_summary.html', resource=None, projects=projects, error_message=result, **build_login_variables())
def email_stats(): _, result = _get_projects(include_stats=False, is_active=True) projects = result.get('projects') if request.method == 'GET': project = request.args.get('project', None) email = request.args.get('email') if email is None: # They want to search for emails, show them the search dialog return render_template("email_stats_pulldown.html", **build_login_variables(), resource=None, search_error=None, projects=projects) emails = [ email, ] elif request.method == 'POST': project = request.form.get('project', None) emails, upload_err = upload_util.parse_request_csv_col( request, 'file', 'email') if upload_err is not None: return render_template('email_stats_pulldown.html', **build_login_variables(), resource=None, search_error=[{ 'error': upload_err }], projects=projects) else: raise BadRequest() if project == "": project = None # de-duplicate emails = list({e.lower() for e in emails}) status, result = APIRequest.post('/api/admin/account_email_summary', json={ "emails": emails, "project": project }) if status != 200: return render_template('email_stats_pulldown.html', search_error=[{ 'error': result }], resource=None, **build_login_variables(), projects=projects) # At a minimum, our table will display these columns. # We may show additional info depending on what comes back from the request base_data_template = { 'email': 'XXX', 'summary': 'XXX', 'account_id': 'XXX', 'creation_time': 'XXX', 'kit_name': 'XXX', 'project': 'XXX', 'unclaimed-samples-in-kit': 0, 'never-scanned': 0, 'sample-is-valid': 0, 'no-associated-source': 0, 'no-registered-account': 0, 'no-collection-info': 0, 'sample-has-inconsistencies': 0, 'received-unknown-validity': 0 } df = pd.DataFrame([base_data_template] + result) df = df.drop(0) # remove the template row numeric_cols = [ "unclaimed-samples-in-kit", "never-scanned", "sample-is-valid", "no-associated-source", "no-registered-account", "no-collection-info", "sample-has-inconsistencies", "received-unknown-validity" ] df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric) df[numeric_cols] = df[numeric_cols].fillna(0) def urlify_account_id(id_): if pd.isnull(id_): return "No associated account" else: ui_endpoint = SERVER_CONFIG['ui_endpoint'] account_url = f"{ui_endpoint}/accounts/{id_}" return f'<a target="_blank" href="{account_url}">{id_}</a>' # see https://stackoverflow.com/questions/20035518/insert-a-link-inside-a-pandas-table # noqa df['account_id'] = df["account_id"].apply(urlify_account_id) return render_template("email_stats_pulldown.html", search_error=None, resource=df, **build_login_variables(), projects=projects)