def lab_tests(request): """Get lab tests for a given license number.""" # Authenticate the user. claims = authenticate_request(request) if not claims: return Response({'error': True, 'message': AUTH_ERROR}, status=403) _, project_id = google.auth.default() license_number = request.query_params.get('license') org_id = request.query_params.get('org_id') version_id = request.query_params.get('version_id') if not license_number or not org_id: message = 'Parameters `license` and `org_id` are required.' return Response({'error': True, 'message': message}, status=403) # Initialize Metrc. track = initialize_metrc(project_id, license_number, version_id) # Get lab results. if request.method == 'GET': # FIXME: Get a longer list of lab results from Firestore objs = track.get_lab_results(uid='185436', license_number=license_number) data = [obj.to_dict() for obj in objs] return Response({'data': data}, content_type='application/json')
def organization_team(request, organization_id=None, user_id=None): """Get team member data for an organization, given an authenticated request from a member of the organization. """ claims = authenticate_request(request) print('Claims:', claims) try: if organization_id in claims['team']: organization_data = get_document( f'organizations/{organization_id}') team = organization_data['team'] team_members = [] if user_id: team = [user_id] for uid in team: team_member = get_document(f'users/{uid}') team_members.append(team_member) return Response({'data': team_members}, content_type='application/json') else: message = 'You are not a member of the requested organization.' return Response({'error': True, 'message': message}, status=403) except KeyError: message = 'You are not a member of any teams. Try authenticating.' return Response({'error': True, 'message': message}, status=401)
def transfers(request): """Get, update, and delete transfers for a given license number.""" # Authenticate the user. claims = authenticate_request(request) if not claims: return Response({'error': True, 'message': AUTH_ERROR}, status=403) # Get the parameters. _, project_id = google.auth.default() license_number = request.query_params.get('license') org_id = request.query_params.get('org_id') version_id = request.query_params.get('version_id') if not license_number or not org_id: message = 'Parameters `license` and `org_id` are required.' return Response({'error': True, 'message': message}, status=403) # Initialize Metrc. track = initialize_metrc(project_id, license_number, version_id) # Get data. if request.method == 'GET': # TODO: Add filters # uid='' # transfer_type='incoming' # license_number='' # start='' # end='' # FIXME: Get transfers by specified date range? Or get longer list from Firestore. objs = track.get_transfers(license_number=license_number, start='2021-06-04', end='2021-06-05') data = [obj.to_dict() for obj in objs] print('Retrieved the data:', data) return Response({'data': data}, content_type='application/json')
def packages(request): """Get, update, and delete packages for a given license number.""" # Authenticate the user. claims = authenticate_request(request) if not claims: return Response({'error': True, 'message': AUTH_ERROR}, status=403) # Get the parameters. _, project_id = google.auth.default() license_number = request.query_params.get('license') org_id = request.query_params.get('org_id') version_id = request.query_params.get('version_id') if not license_number or not org_id: message = 'Parameters `license` and `org_id` are required.' return Response({'error': True, 'message': message}, status=403) # Initialize Metrc. track = initialize_metrc(project_id, license_number, version_id) # Get data. if request.method == 'GET': # FIXME: Get a longer list of packages from Firestore? objs = track.get_packages(license_number=license_number, start='2021-06-04', end='2021-06-05') try: data = [obj.to_dict() for obj in objs] except TypeError: data = [objs.to_dict()] return Response({'data': data}, content_type='application/json')
def release_coas(request): """Release certificates of analysis to the client.""" # Authenticate the user. claims = authenticate_request(request) # FIXME: Get `org_id` org_id = None if claims.get('user') is None: message = 'Authentication failed.' return Response({'success': False, 'data': message}, status=401) # Restrict approving certificates to QA and owners. qa = claims.get('qa', []) owner = claims.get('owner', []) if org_id not in owner and org_id not in qa: message = f'Your must be an owner or quality assurance manager of this organization for this operation.' return Response({'error': True, 'message': message}, status=403) # Get the samples. posted_data = loads(request.body.decode('utf-8')) sample_ids = posted_data['sample_ids'] # Update the certificate_status in Firestore. # Send (email and/or text) to the client's recipients. return NotImplementedError
def post_coas(request): """Post certificates of analysis to the state traceability system.""" # Authenticate the user. claims = authenticate_request(request) # FIXME: Get `org_id` org_id = None if claims.get('user') is None: message = 'Authentication failed.' return Response({'success': False, 'data': message}, status=401) # Restrict approving certificates to QA and owners. qa = claims.get('qa', []) owner = claims.get('owner', []) if org_id not in owner and org_id not in qa: message = f'Your must be an owner or quality assurance manager of this organization for this operation.' return Response({'error': True, 'message': message}, status=403) # Get sample IDs. posted_data = loads(request.body.decode('utf-8')) sample_ids = posted_data['sample_ids'] # Format data for API requests. # Post certificates 1 by 1. return NotImplementedError
def items(request): """Get, update, and delete items for a given license number.""" # Authenticate the user. claims = authenticate_request(request) if not claims: return Response({'error': True, 'message': AUTH_ERROR}, status=403) # Get the parameters. _, project_id = google.auth.default() license_number = request.query_params.get('license') org_id = request.query_params.get('org_id') version_id = request.query_params.get('version_id') if not license_number or not org_id: message = 'Parameters `license` and `org_id` are required.' return Response({'error': True, 'message': message}, status=403) # Initialize Metrc. track = initialize_metrc(project_id, license_number, version_id) # Get data. if request.method == 'GET': objs = track.get_items(license_number=license_number, uid='243821') data = [obj.to_dict() for obj in objs] print('Retrieved the data:', data) return Response({'data': data}, content_type='application/json')
def employees(request): """Get a licenses employees from Metrc. Args: request (HTTPRequest): A `djangorestframework` request. """ # Authenticate the user. claims = authenticate_request(request) if not claims: message = 'Authentication failed. Please use the console or provide a valid API key.' return Response({'error': True, 'message': message}, status=403) _, project_id = google.auth.default() license_number = request.query_params.get('name') # Optional: Figure out how to pre-initialize a Metrc client. # Get Vendor API key using secret manager. # FIXME: Determine where to store project_id, secret_id, and version_id. vendor_api_key = access_secret_version(project_id=project_id, secret_id='metrc_vendor_api_key', version_id='1') # TODO: Get user API key using secret manager. user_api_key = access_secret_version(project_id=project_id, secret_id=f'{license_number}_secret', version_id='1') # Create a Metrc client. track = authorize(vendor_api_key, user_api_key) # Make a request to the Metrc API. data = track.get_employees(license_number=license_number) # Return the requested data. return Response({'data': data}, content_type='application/json')
def users(request): """Get, update, or create user's data.""" try: # Authenticate the user. claims = authenticate_request(request) print('User claims:', claims) uid = claims['uid'] # Get the user's data. if request.method == 'GET': user_data = get_document(f'users/{uid}') response = {'success': True, 'data': user_data} return Response(response, content_type='application/json') # Edit user data if a 'POST' request. post_data = loads(request.body.decode('utf-8')) update_document(f'users/{uid}', post_data) create_log( ref=f'users/{uid}/logs', claims=claims, action='Updated user data.', log_type='users', key='user_data', changes=[post_data] ) return Response({'success': True}, content_type='application/json') except: return Response( {'success': False}, content_type='application/json', status=status.HTTP_500_INTERNAL_SERVER_ERROR )
def users(request): """Get, update, or create user's data.""" print('Request to users endpoint!') try: # Authenticate the user. claims = authenticate_request(request) # Get user data. if request.method == 'GET': user_data = get_document(f'users/{claims["uid"]}') return Response(user_data, content_type='application/json') # Edit user data. if request.method == 'POST': # Get the user's ID. post_data = loads(request.body.decode('utf-8')) uid = claims['uid'] post_data['uid'] = uid # Update the user's data, create a log, and return the data. try: update_document(f'users/{uid}', post_data) create_log(ref=f'users/{uid}/logs', claims=claims, action='Updated user data.', log_type='users', key='user_data', changes=[post_data]) return Response(post_data, content_type='application/json') except: # Create the user's data, create a log, and return the data. user_email = post_data['email'] user = { 'email': user_email, 'created_at': utils.get_timestamp(), 'uid': post_data['uid'], 'photo_url': f'https://robohash.org/${user_email}?set=set5', } update_document(f'users/{uid}', post_data) create_log(f'users/{uid}/logs', claims, 'Created new user.', 'users', 'user_data', [post_data]) return Response(user, content_type='application/json') except: # Return a server error. return Response({'success': False}, content_type='application/json', status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def employees(request): """Get employees for a given license number.""" # Authenticate the user. claims = authenticate_request(request) if not claims: return Response({'error': True, 'message': AUTH_ERROR}, status=403) _, project_id = google.auth.default() license_number = request.query_params.get('license') version_id = request.query_params.get('version_id') # Initialize Metrc. track = initialize_metrc(project_id, license_number, version_id) # Make a request to the Metrc API. objs = track.get_employees(license_number=license_number) data = [obj.to_dict() for obj in objs] # Return the requested data. return Response({'data': data}, content_type='application/json')
def analytics(request): """Get cannabis analytics.""" # Authenticate the user. claims = authenticate_request(request) try: uid = claims['uid'] except KeyError: message = 'Your request was not authenticated. Ensure that you have a valid session or API key.' return Response({'error': True, 'message': message}, status=401) # GET pre-defined analytics. if request.method == 'GET': return Response({'success': True, 'data': {'pre_existing_analytics': True}}, status=200) # POST request to get analytics given inputs. elif request.method == 'POST': posted_data = loads(request.body.decode('utf-8')) print('Posted data:', posted_data) return Response({'success': True, 'data': {'analytics': posted_data}}, status=200)
def delete_license(request, *args, **argv): #pylint: disable=unused-argument """Delete a license from an organization's licenses.""" # Authenticate the user. _, project_id = google.auth.default() user_claims = authenticate_request(request) data = loads(request.body.decode('utf-8')) deletion_reason = data.get('deletion_reason', 'No deletion reason.') license_number = request.query_params.get('license') org_id = request.query_params.get('license') if not license_number or not org_id: message = 'Parameters `license` and `org_id` are required.' return Response({'error': True, 'message': message}, status=403) # Delete the license data and redact the secret. doc = get_document(f'organizations/{org_id}') existing_licenses = doc['licenses'] licenses = [] for license_data in existing_licenses: license_number = license_data['license_number'] if license_data['license_number'] != license_number: licenses.append(license_data) else: add_secret_version( project_id, license_data['user_api_key_secret']['secret_id'], 'redacted' ) doc['licenses'] = licenses update_document(f'organizations/{org_id}', doc) # Create a log. create_log( ref=f'organizations/{org_id}/logs', claims=user_claims, action='License deleted.', log_type='traceability', key='delete_license', changes=[license_number, deletion_reason] ) return JsonResponse({'status': 'success', 'message': 'License deleted.'})
def logs(request, log_id=None): """Get and create logs.""" # Initialize and authenticate. model_id = log_id model_type = 'logs' model_type_singular = 'log' claims = authenticate_request(request) try: claims['uid'] #pylint: disable=pointless-statement owner = claims.get('owner', []) team = claims.get('team', []) quality_control = claims.get('qc', []) authorized_ids = owner + team + quality_control except KeyError: message = 'Your request was not authenticated. Ensure that you have a valid session or API key.' return Response({'error': True, 'message': message}, status=401) # Authorize that the user can work with the data. organization_id = request.query_params.get('organization_id') if organization_id not in authorized_ids: message = f'Your must be an owner, quality assurance, or a team member of this organization to manage {model_type}.' return Response({'error': True, 'message': message}, status=403) # GET data. if request.method == 'GET': docs = get_objects(request, authorized_ids, organization_id, model_id, model_type) return Response({'success': True, 'data': docs}, status=200) # POST data. elif request.method == 'POST': data = update_object(request, claims, model_type, model_type_singular, organization_id) if data: return Response({'success': True, 'data': data}, status=200) # Return an error message if post fails. message = 'Data not recognized. Please post either a singular object or an array of objects.' return Response({'error': True, 'message': message}, status=400)
def inventory(request, inventory_id=None): """Get, create, or update inventory.""" # Initialize. model_id = inventory_id model_type = 'inventory' model_type_singular = 'item' # Authenticate the user. claims = authenticate_request(request) # FIXME: Get `org_id` org_id = None if claims.get('user') is None: message = 'Authentication failed.' return Response({'success': False, 'data': message}, status=401) # GET data. if request.method == 'GET': docs = get_objects(request, claims, org_id, model_id, model_type) return Response({'success': True, 'data': docs}, status=200) # POST data. elif request.method == 'POST': data = update_object(request, claims, model_type, model_type_singular, org_id) if data: return Response({'success': True, 'data': data}, status=200) else: message = 'Data not recognized. Please post either a singular object or an array of objects.' return Response({'error': True, 'message': message}, status=400) # DELETE data. elif request.method == 'DELETE': success = delete_object(request, claims, model_id, model_type, model_type_singular, org_id) if not success: message = f'Your must be an owner or quality assurance to delete {model_type}.' return Response({'error': True, 'message': message}, status=403) return Response({'success': True, 'data': []}, status=200)
def approve_coas(request): """Approve certificates of analysis for release after they have been reviewed.""" # Authenticate the user. claims = authenticate_request(request) # FIXME: Get `org_id` org_id = None if claims.get('user') is None: message = 'Authentication failed.' return Response({'success': False, 'data': message}, status=401) # Restrict approving certificates to QA and owners. qa = claims.get('qa', []) owner = claims.get('owner', []) if org_id not in owner and org_id not in qa: message = f'Your must be an owner or quality assurance manager of this organization for this operation.' return Response({'error': True, 'message': message}, status=403) # Require pin. uid = claims['uid'] post_data = loads(request.body.decode('utf-8')) pin = post_data['pin'] message = f'{pin}:{uid}' app_secret = get_document('admin/api')['app_secret_key'] code = sha256_hmac(app_secret, message) verified_claims = get_document(f'admin/api/pin_hmacs/{code}') if not verified_claims: return JsonResponse({'error': True, 'message': 'Invalid pin.'}) elif verified_claims.get('uid') != uid: return JsonResponse({'error': True, 'message': 'Invalid pin.'}) # Call generate_coas # - Make sure to fill-in approvers signature. # Update the sample's certificate_status. return NotImplementedError
def review_coas(request): """Review certificates of analysis so that they can be approved and released.""" # Authenticate the user. claims = authenticate_request(request) # FIXME: Get `org_id` org_id = None if claims.get('user') is None: message = 'Authentication failed.' return Response({'success': False, 'data': message}, status=401) # Require pin. error_response = verify_user_pin(request) print(error_response) if error_response.status_code != 200: return error_response # Call generate_coas # - Make sure to fill-in reviewers signature. # Update the sample's certificate_status. return NotImplementedError
def join_organization(request): """Send the owner of an organization a request for a user to join.""" # Identify the user. claims = authenticate_request(request) uid = claims['uid'] user_email = claims['email'] post_data = loads(request.body.decode('utf-8')) organization = post_data.get('organization') # Return an error if the organization doesn't exist. query = {'key': 'organization', 'operation': '==', 'value': organization} organizations = get_collection('organizations', filters=[query]) if not organizations: message = 'Organization does not exist. Please check the organization name and try again.' return Response({'success': False, 'message': message}, status=400) # Send the owner an email requesting to add the user to the organization's team. org_email = organizations[0]['email'] text = f"A user with the email address {user_email} would like to join your organization, \ {organization}. Do you want to add this user to your organization's team? Please \ reply YES or NO to confirm." paragraphs = [] # TODO: Generate confirm, decline, and unsubscribe links with HMACs from user's uid and owner's uid. user_hmac = '' owner_hmac = '' # Optional: Find new home's for endpoints in api and cannlytics_website confirm_link = f'https://console.cannlytics.com/api/organizations/confirm?hash={owner_hmac}&member={user_hmac}' decline_link = f'https://console.cannlytics.com/api/organizations/decline?hash={owner_hmac}&member={user_hmac}' unsubscribe_link = f'https://console.cannlytics.com/api/unsubscribe?hash={owner_hmac}' # html_message = render_to_string('templates/console/emails/action_email_template.html', { # 'recipient': org_email, # 'paragraphs': paragraphs, # 'primary_action': 'Confirm', # 'primary_link': confirm_link, # 'secondary_action': 'Decline', # 'secondary_link': decline_link, # 'unsubscribe_link': unsubscribe_link, # }) # TODO: Skip sending email if owner is unsubscribed. # send_mail( # subject="Request to join your organization's team.", # message=text, # from_email=DEFAULT_FROM_EMAIL, # recipient_list=LIST_OF_EMAIL_RECIPIENTS, # fail_silently=False, # html_message=html_message # ) # Create activity logs. # create_log(f'users/{uid}/logs', claims, 'Requested to join an organization.', 'users', 'user_data', [post_data]) # create_log(f'organization/{uid}/logs', claims, 'Request from a user to join the organization.', 'organizations', 'organization_data', [post_data]) message = f'Request to join {organization} sent to the owner.' return Response({ 'success': True, 'message': message }, content_type='application/json')
def create_coas(request): """Generate certificates of analysis.""" # Authenticate the user. claims = authenticate_request(request) # FIXME: Get `org_id` org_id = None if claims.get('user') is None: message = 'Authentication failed.' return Response({'success': False, 'data': message}, status=401) # Require pin. error_response = verify_user_pin(request) print(error_response) if error_response.status_code != 200: return error_response # Get posted samples. posted_data = loads(request.body.decode('utf-8')) sample_ids = posted_data['sample_ids'] # Create certificates for each sample. data = [] for sample_id in sample_ids: # Get the sample data. sample_data = get_document( f'organizations/{org_id}/samples/{sample_id}') # Get the results for each sample. If there are no results, # then get the measurements for each sample and calculate # the results for each sample. Add a empty dictionary if missing everything. sample_results = get_collection(f'organization/{org_id}/results', order_by='updated_at', desc=True, filters=[{ 'key': 'sample_id', 'operation': '==', 'value': sample_id }]) if not sample_results: sample_results = calculate_results(request) if not sample_results: sample_results = [{}] # Define the certificate context. context = {**sample_data, **sample_results[0]} # Get the certificate template. template_name = sample_data.get('coa_template_ref', DEFAULT_TEMPLATE) # FIXME: # Create the PDF, keeping the data. # Efficiency gain: Keep the template in /tmp so they don't have # to be downloaded each iteration. certificate = generate_coas( context, coa_template=template_name, # output_pages=pages, # limits=limits ) data.append(certificate) # Return list of certificate data. return Response({'data': data}, status=200)
def organizations(request, organization_id=None, type='lab'): """Get, create, or update organizations. E.g. ``` organization = { 'owner': [], 'name': '', 'license': '', 'type': '', 'team': [], 'support': '', } ``` """ # Get endpoint variables. model_type = 'organizations' _, project_id = google.auth.default() claims = authenticate_request(request) uid = claims['uid'] print('User request to organizations:', uid) # Get organization(s). if request.method == 'GET': # Get organization_id parameter if organization_id: print('Query organizations by ID:', organization_id) data = get_document(f'{model_type}/{organization_id}') print('Found data:', data) if not data: message = 'No organization exists with the given ID.' return Response({ 'error': True, 'message': message }, status=404) elif data['public']: return Response({'data': data}, status=200) elif uid not in data['team']: message = 'This is a private organization and you are not a team member. Request to join before continuing.' return Response({ 'error': True, 'message': message }, status=400) else: return Response({'data': data}, status=200) # TODO: Get query parameters. keyword = request.query_params.get('name') if keyword: print('Query by name:', keyword) query = {'key': 'name', 'operation': '==', 'value': keyword} docs = get_collection(model_type, filters=[query]) return Response({'data': docs}, status=200) # Get all of a user's organizations else: query = { 'key': 'team', 'operation': 'array_contains', 'value': uid } docs = get_collection(model_type, filters=[query]) return Response({'data': docs}, status=200) # Optional: Get list of other organizations. # Check if user is in organization's team, otherwise, # only return publically available information. # Optional: Try to get facility data from Metrc. # facilities = track.get_facilities() # Create or update an organization. elif request.method == 'POST': # Update an organization with the posted data if there is an ID. data = loads(request.body.decode('utf-8')) if organization_id: # Return an error if the organization already exists # and the user is not part of the organization's team. doc = get_document(f'{model_type}/{organization_id}') if not doc: message = 'No data exists for the given ID.' return Response({ 'error': True, 'message': message }, status=400) organization_id = doc['uid'] team_list = claims.get('team', []) owner_list = claims.get('owner', []) if uid not in team_list and organization_id not in owner_list: message = 'You do not currently belong to this organization. Request to join before continuing.' return Response({ 'error': True, 'message': message }, status=400) # If an organization already exists, then only the owner # can edit the organization's team. if uid != doc['owner']: data['team'] = doc['team'] # Store posted API keys as secrets. # FIXME: Update licenses if they are being edited. new_licenses = data.get('licenses') if new_licenses: licenses = doc.get('licenses', []) for license_data in new_licenses: license_number = license_data['license_number'] secret_id = f'{license_number}_secret' try: create_secret(project_id, secret_id, license_data['user_api_key']) except: pass secret = add_secret_version(project_id, secret_id, license_data['user_api_key']) version_id = secret.split('/')[-1] license_data['user_api_key_secret'] = { 'project_id': project_id, 'secret_id': secret_id, 'version_id': version_id, } del license_data['user_api_key'] licenses.append(license_data) doc['licenses'] = licenses # Create organization if it doesn't exist # All organizations have a unique `organization_id`. else: doc = {} organization_id = slugify(data['name']) doc['organization_id'] = organization_id doc['team'] = [uid] doc['owner'] = uid # Identify created organization type. doc['type'] = type # All organizations start with the standard data models. # FIXME: Remove data models that have permissions # if the user does not have sufficient claims. data_models = get_collection('public/state/data_models') for data_model in data_models: key = data_model['key'] update_document( f'{model_type}/{organization_id}/data_models/{key}', data_model) # Create or update the organization in Firestore. entry = {**doc, **data} print('Entry:', entry) update_document(f'{model_type}/{organization_id}', entry) # FIXME: # On organization creation, the creating user get custom claims. update_custom_claims(uid, claims={ 'owner': organization_id, 'team': organization_id }) # TODO: Owners can add other users to the team and # the receiving user then gets the claims. # team: [organization_id, ...] # Create activity log. changes = [data] create_log(f'{model_type}/{uid}/logs', claims=claims, action='Updated organization data.', log_type=model_type, key=f'{model_type}_data', changes=changes) return Response({ 'data': entry, 'success': True }, content_type='application/json') elif request.method == 'DELETE': # TODO: Only user's with organization_id in owner claim can delete the organization. return Response({'error': 'not_implemented'}, content_type='application/json')