################################################################################ ################################################################################ ## Canvas user page view retrieval ## ## ## ## Fill in the three variables directly below this header, then run with py ## ## **Use at your own risk. Please try changes in Test/Beta before Prod.** ## ## ## ## 2025 Christopher M. Casey (cmcasey79@hotmail.com) ## ################################################################################ ################################################################################ script_version='2025.03.11.00002' # Script mode setup # set as '' for deault operation, 'unattended' to run without prompts using the info below, 'verbose' for more detailed screen progress output, 'silent' to run without screen output, or 'silentunattended' or 'verboseunattended' to combine the options. mode='' # Canvas API token setup # Key should be omitted for versions sent to its, will be downoloaded from server later on if last three characters of script_version is 'its' and left blank here canvas_api_token='' # Canvas subdomain setup # set this variable as your subdomain of instructure.com (ex: umich) canvas_subdomain='' # Canvas vanity domain setup # set these variable as your relevant vanity domain(s) if you have them, otherwise, leave it as '' canvas_production_vanity_domain='' canvas_beta_vanity_domain='' canvas_test_vanity_domain='' # Canvas account id setup # set this variable as the account/subaccount id you'd like to work in. canvas_account_id='' # Canvas environment setup # set this variable as production, test, beta or leave as '' to prompt if not running in unattended mode canvas_environment='' ################################################################################ ################################################################################ ## No changes to below code should be necessary. ## ################################################################################ ################################################################################ import socket import requests import urllib3.exceptions import json import datetime import csv import collections import re scriptlog='' #Added to suppress warnings requests.packages.urllib3.disable_warnings(category=urllib3.exceptions.InsecureRequestWarning) #Requests call with retry on server error def requestswithretry(retries=3, backoff_factor=0.3, status_forcelist=(500, 502, 504), session=None, ): session = session or requests.Session() retry = requests.packages.urllib3.util.retry.Retry( total=retries, read=retries, connect=retries, backoff_factor=backoff_factor, status_forcelist=status_forcelist, allowed_methods=frozenset({'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT', 'TRACE', 'POST'}) ) adapter = requests.adapters.HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) return session # Paginated API call to Canvas # Returns a tuple including list of all items from all pages of a paginated API call and the http status code and reason returned from the call(s) # Last updated 2025-03-07 def canvas_get_allpages(url, headers): if not 'per_page=' in url: url=url+('&' if '?' in url else '?')+'per_page=100' return_data=[] return_key=None repeat=True while repeat: canvas_request=requestswithretry().get(url,headers=headers) if canvas_request.status_code!=200: return_data=None repeat=False else: canvas_request_responsedata=canvas_request.json() # if Canvas returned a single item dictionary instead of a list, try using the value of that dictionary as the list if type(canvas_request_responsedata) is dict: if len(canvas_request_responsedata.keys())==1: return_key=list(canvas_request_responsedata.keys())[0] canvas_request_responsedata=next(iter(canvas_request_responsedata.values())) else: return_data=canvas_request_responsedata repeat=False if type(canvas_request_responsedata) is list: # if a list was returned, add it to the existing list return_data+=canvas_request_responsedata url=canvas_request.links.get('current',{}).get('url',url) last_url=canvas_request.links.get('last',{}).get('url','') # if not on the last page of results, set up retrieval of next page if (url != last_url) and ('next' in canvas_request.links.keys()): url=canvas_request.links['next']['url'] else: repeat=False return_data_namedtuple = collections.namedtuple('return_data_namedtuple', ['data', 'reason', 'status_code']) return return_data_namedtuple(return_data if (return_key==None) else {return_key:return_data},str(url)+': '+str(canvas_request.reason),canvas_request.status_code) # Prompt and/or validate canvas envornment information. Canvas_environment is a dict which will be populated by the function. # Prompts and/or validates the working environment, domains, api token, account_id. # Only validates the working environment by default, but an enviromnet list in the form of ['production','beta','test'] can be passed if validation of additional environments is needed. # Returns true if everything was properly validated, false if some information could not be valitated (signaling the script should end) # Last updated 2025-03-07. def canvas_validate_environment(canvas_environment, validate_environments=None): global scriptlog if mode[0:6]!='silent': print('--------------------------------------------------------------------------------\n') if mode[-10:]=='unattended': scriptlog+='--------------------------------------------------------------------------------\n\n' # Validate Canvas environment selection and domains domain_validated=False while not domain_validated: # Prompt for Canvas environment selection env_selection=False while (not env_selection): if mode[-10:]=='unattended' or canvas_environment['working_environment']!='': env=canvas_environment['working_environment'][0] else: env=input('Which Canvas environment would you like to work in: [P]roduction, [T]est, or [B]eta? ') env_selection=True if env.lower()=='p': canvas_environment['working_environment']='production' elif env.lower()=='t': canvas_environment['working_environment']='test' elif env.lower()=='b': canvas_environment['working_environment']='beta' else: if mode[-10:]=='unattended': print('Invalid canvas_environment selection.') else: print('Invalid selection, please try again.') env_selection=False canvas_environment['working_environment']='' if validate_environments==None: validate_environments=[canvas_environment['working_environment']] if canvas_environment['subdomain']=='' and mode[-10:]!='unattended': canvas_environment['subdomain']=input('Please enter your instructure.com subdomain (ex: umich): ') for environment in validate_environments: if canvas_environment['domains'][environment]=='': if mode[-10:]!='unattended': canvas_environment['domains'][environment]=input(f'Please enter your {environment} Canvas vanity domain (hit enter/return if you do not have one): ') if canvas_environment['domains'][environment]=='': canvas_environment['domains'][environment]=f'{canvas_environment['subdomain']}{'.'+environment if (environment!='' and environment!='production') else ''}.instructure.com' domain_validated=True for environment in validate_environments: environment_validated=False url=f'https://{canvas_environment['domains'][environment]}' if mode[0:6]!='silent': print(url) try: r = requests.get(url) except requests.exceptions.RequestException as e: print(f'{datetime.datetime.now().isoformat()}: Connection error {e}') scriptlog+=f'{datetime.datetime.now().isoformat()}: Connection error {e}\n' else: if r.status_code==200: if mode[0:6]!='silent': print(f'{datetime.datetime.now().isoformat()}: Connection test to {canvas_environment['domains'][environment]} successful, proceeding...') if mode[-10:]=='unattended': scriptlog+=f'{datetime.datetime.now().isoformat()}: Connection test to {canvas_environment['domains'][environment]} successful, proceeding...\n' environment_validated=True elif r.status_code==401: if mode[0:6]!='silent': print(f'{datetime.datetime.now().isoformat()}: Connection test to {canvas_environment['domains'][environment]} successful but not validated, vanity domain or authentication may be needed, proceeding...') if mode[-10:]=='unattended': scriptlog+=f'{datetime.datetime.now().isoformat()}: Connection test to {canvas_environment['domains'][environment]} successful but not validated, vanity domain or authentication may be needed, proceeding...\n' environment_validated=True else: print(f'{datetime.datetime.now().isoformat()}: Error {r.status_code} - connection to {canvas_environment['domains'][environment]} failed.') if mode[-10:]=='unattended': scriptlog+=f'{datetime.datetime.now().isoformat()}: Error {r.status_code} - connection to {canvas_environment['domains'][environment]} failed.\n' if not environment_validated: if canvas_environment['domains'][environment].split('.')[0]==canvas_environment['subdomain']: canvas_environment['subdomain']='' canvas_environment['domains'][environment]='' domain_validated=domain_validated and environment_validated domain_validated=domain_validated or mode[-10:]=='unattended' # Validate API Token apitoken_validated=False while (not apitoken_validated) and (canvas_environment['domains'][environment]!=''): # Prompt for API Token if not given if canvas_environment['api_token']=='' and mode[-10:]!='unattended': canvas_environment['api_token']=str(input('Please enter your Canvas administrative API token: ')) #Set HTTP headers for API calls url=f'https://{canvas_environment['domains'][canvas_environment['working_environment']]}/api/v1/accounts' canvas_accounts = canvas_get_allpages(url,headers={'Authorization' : f'Bearer {canvas_environment['api_token']}'}) if canvas_accounts.status_code==200: canvas_environment['url_headers']={'Authorization' : f'Bearer {canvas_environment['api_token']}'} canvas_accounts_dict={} for canvas_account in canvas_accounts.data: canvas_accounts_dict[canvas_account['id']]=canvas_account url=f'https://{canvas_environment['domains'][canvas_environment['working_environment']]}/api/v1/accounts/{canvas_account['id']}/sub_accounts?recursive=true' canvas_subaccounts=canvas_get_allpages(url,canvas_environment['url_headers']) if canvas_subaccounts.status_code==200: for canvas_subaccount in canvas_subaccounts.data: canvas_accounts_dict[canvas_subaccount['id']]=canvas_subaccount if mode[0:6]!='silent': print(f'{datetime.datetime.now().isoformat()}: API token validated, proceeding...') if mode[-10:]=='unattended': scriptlog+=f'{datetime.datetime.now().isoformat()}: API token validated, proceeding...\n' apitoken_validated=True else: print(f'{datetime.datetime.now().isoformat()}: API token validation failed. Token or domain settings are incorrect.') if mode[-10:]=='unattended': scriptlog+=f'{datetime.datetime.now().isoformat()}: API token validation failed. Token or domain settings are incorrect.\n' canvas_environment['api_token']='' canvas_environment['url_headers']='' if mode[-10:]=='unattended': apitoken_validated=True # Prompt for Canvas Account ID # Get account list from Canvas account_id_validated=False if canvas_environment['domains'][canvas_environment['working_environment']]!='': all_accounts_ids_validated=True all_root_account_ids=set() for account in (set(canvas_environment['account_ids'].keys())-set(['root'])): account_id_validated=False while (not account_id_validated and all_accounts_ids_validated): if canvas_environment['account_ids'][account]=='' and mode[-10:]=='unattended': canvas_environment['account_ids'][account]=canvas_environment['account_ids']['root'] account_id_validated=True else: # Prompt for ID selection if canvas_environment['account_ids'][account]=='' and mode[-10:]!='unattended': canvas_environment['account_ids'][account]=input(f'What is the id number for the {account} account: # or [L]ist)? ') if str(canvas_environment['account_ids'][account]).lower()=='l': for account_id in canvas_accounts_dict.keys(): print(str(account_id)+': '+canvas_accounts_dict[account_id]['name']) canvas_environment['account_ids'][account]=input(f'What is the id number for the {account} account? ') if (type(canvas_environment['account_ids'][account])==int or (type(canvas_environment['account_ids'][account])==str and (canvas_environment['account_ids'][account].isdigit()))) and (int(canvas_environment['account_ids'][account]) in canvas_accounts_dict.keys()): canvas_environment['account_ids'][account]=int(canvas_environment['account_ids'][account]) if mode[0:6]!='silent': print(f'{datetime.datetime.now().isoformat()}: {account} account selection validated (account_id: {canvas_environment['account_ids'][account]}, account_name: {canvas_accounts_dict[canvas_environment['account_ids'][account]]['name']}), proceeding...') if mode[-10:]=='unattended': scriptlog+=f'{datetime.datetime.now().isoformat()}: {account} account selection validated (account_id: {canvas_environment['account_ids'][account]}, account_name: {canvas_accounts_dict[canvas_environment['account_ids'][account]]['name']}), proceeding...\n' account_id_validated=True account_root_id=canvas_environment['account_ids'][account] while canvas_accounts_dict[account_root_id]['parent_account_id']!=None: account_root_id=canvas_accounts_dict[account_root_id]['parent_account_id'] all_root_account_ids.add(account_root_id) else: print(f'{datetime.datetime.now().isoformat()}: invalid {account} account selection (account_id: {canvas_environment['account_ids'][account]}).') if mode[-10:]=='unattended':scriptlog+=f'{datetime.datetime.now().isoformat()}: invalid {account} account selection (account_id: {canvas_environment['account_ids'][account]}).\n' canvas_environment['account_ids'][account]='' if mode[-10:]=='unattended': account_id_validated=True if canvas_environment['account_ids'][account]=='': all_accounts_ids_validated=False canvas_environment['account_ids']['root']=min(all_root_account_ids) if len(all_root_account_ids)==1: if mode[0:6]!='silent': print(f'{datetime.datetime.now().isoformat()}: root account selection validated (account_id: {canvas_environment['account_ids']['root']}, account_name: {canvas_accounts_dict[canvas_environment['account_ids']['root']]['name']}), proceeding...') if mode[-10:]=='unattended': scriptlog+=f'{datetime.datetime.now().isoformat()}: root account selection validated (account_id: {canvas_environment['account_ids']['root']}, account_name: {canvas_accounts_dict[canvas_environment['account_ids']['root']]['name']}), proceeding...\n' else: print('Specified accounts belong to different root accounts, unable to continue...') if mode[-10:]=='unattended': scriptlog+='Specified accounts belong to different root accounts, unable to continue...\n' all_accounts_ids_validated=False if canvas_environment['domains'][canvas_environment['working_environment']]!='': url=f'https://{canvas_environment['domains'][canvas_environment['working_environment']]}/api/v1/users/self' r=requestswithretry().get(url,headers=canvas_environment['url_headers']) if r.status_code==200: canvas_user=r.json() if mode[0:6]!='silent': print(f'Running script as {canvas_user['name']}\n login_id: {canvas_user['login_id']}\n sis_user_id: {canvas_user['sis_user_id']}\n email: {canvas_user['email']}') if mode[-10:]=='unattended': scriptlog+=f'Running script as {canvas_user['name']}\n login_id: {canvas_user['login_id']}\n sis_user_id: {canvas_user['sis_user_id']}\n email: {canvas_user['email']}\n\n' else: print(f'{datetime.datetime.now().isoformat()}: Could not retrieve script user infomration.') if mode[-10:]=='unattended': scriptlog+=f'{datetime.datetime.now().isoformat()}: Could not retrieve script user infomration..\n' if mode[0:6]!='silent': print('\n--------------------------------------------------------------------------------\n') if mode[-10:]=='unattended': scriptlog+='\n--------------------------------------------------------------------------------\n\n' return (canvas_environment['working_environment']!='') and (canvas_environment['domains'][canvas_environment['working_environment']]!='') and (canvas_environment['subdomain']!='') and (canvas_environment['api_token']!='') and all_accounts_ids_validated and domain_validated; def main(): global scriptlog, canvas_api_token, canvas_subdomain, canvas_environment, canvas_production_vanity_domain, canvas_test_vanity_domain, canvas_beta_vanity_domain, canvas_account_id hostname=socket.gethostname() IPAddr=socket.gethostbyname(hostname) print(f'Running script version {script_version} on {hostname}, {IPAddr}') scriptlog+=f'Running script version {script_version} on {hostname}, {IPAddr}\n' start_time = datetime.datetime.now() canvas_environment = {'api_token':str(canvas_api_token), 'url_headers':{'Authorization' : 'Bearer '+str(canvas_api_token)},'subdomain':str(canvas_subdomain), 'working_environment':canvas_environment, 'domains':{'production':canvas_production_vanity_domain, 'test':canvas_test_vanity_domain, 'beta':canvas_beta_vanity_domain}, 'account_ids':{'root':'', 'working':canvas_account_id}} if (not canvas_validate_environment(canvas_environment)): print('Canvas environment validation failed. Exiting Script.') scriptlog+='Canvas environment validation failed. Exiting Script.\n' else: canvas_target_user_id=input('Please enter the canvas_user_id of the user which you wish to see the page view history: ') url=f'https://{canvas_environment['domains'][canvas_environment['working_environment']]}/api/v1/users/{canvas_target_user_id}' canvas_target_user_request=canvas_get_allpages(url,canvas_environment['url_headers']) if canvas_target_user_request.status_code!=200: print(f'{datetime.datetime.now().isoformat()}: Could not target canvas user infomration.') else: print(f'Retrieving pageviews for {canvas_target_user_request.data['name']}\n login_id: {canvas_target_user_request.data['login_id']}\n sis_user_id: {canvas_target_user_request.data['sis_user_id']}\n email: {canvas_target_user_request.data['email']}\n') pageview_start_datetime_localstring=input('Enter start date, formatted yyyy-mm-dd: ') while re.match(r'\d{4}-[0-2][1-9]-[0-3][1-9]',pageview_start_datetime_localstring)==None: pageview_start_datetime_localstring=input('Enter start date, formatted yyyy-mm-dd: ') pageview_start_datetime=datetime.datetime.fromisoformat(pageview_start_datetime_localstring).astimezone() pageview_start_datetime_zulustring=pageview_start_datetime.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z") print('Start date/time zulustring:', pageview_start_datetime_zulustring,'\n') pageview_end_datetime_localstring=input('Enter end date, formatted yyyy-mm-dd: ') while re.match(r'\d{4}-[0-2][1-9]-[0-3][1-9]',pageview_end_datetime_localstring)==None: pageview_end_datetime_localstring=input('Enter end date, formatted yyyy-mm-dd: ') pageview_end_datetime=datetime.datetime.fromisoformat(pageview_end_datetime_localstring+'T23:59:59').astimezone() pageview_end_datetime_zulustring=pageview_end_datetime.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z") print('End_date/time zulustring:', pageview_end_datetime_zulustring,'\n') if pageview_start_datetime>=pageview_end_datetime: print('Error: start date/time must be before end date/time') else: start_time = datetime.datetime.now() url=f'https://{canvas_environment['domains'][canvas_environment['working_environment']]}/api/v1/users/{canvas_target_user_id}/page_views?start_time={pageview_start_datetime_zulustring}&end_time={pageview_end_datetime_zulustring}' canvas_user_page_views_response=canvas_get_allpages(url,canvas_environment['url_headers']) print('User had',len(canvas_user_page_views_response.data),'total page views in the specified period\n') if canvas_target_user_request.status_code==200 and len(canvas_user_page_views_response.data)>0: with open('pageviews-all.csv', 'w', newline='') as output_file: writer = csv.DictWriter(output_file, canvas_user_page_views_response.data[0].keys()) writer.writeheader() writer.writerows(canvas_user_page_views_response.data) canvas_user_participation_pageviews = [pageview for pageview in canvas_user_page_views_response.data if pageview['participated']==True] print('User had',len(canvas_user_participation_pageviews),'partipation page views in the specified period\n') with open('pageviews-participations.csv', 'w', newline='') as output_file: writer = csv.DictWriter(output_file, canvas_user_page_views_response.data[0].keys()) writer.writeheader() if len(canvas_user_participation_pageviews)>0: writer.writerows(canvas_user_participation_pageviews) print(f'Finished! Run time: {datetime.datetime.now() - start_time}') scriptlog+=(f'\nFinished! Run time: {datetime.datetime.now() - start_time}\n') if __name__=='__main__': main()