Aulas API: En opdatering

Fordi jeg blev kontaktet af nogle flinke skoleansatte, der er i gang med at lette deres hverdag med AULA, har jeg opdateret mit Python-script med eksempler på, hvad man kan lave i AULA, uden rent faktisk at pege sin browser på AULA.

Det nye eksempel (eksempel 6) viser, hvordan man kan oprette en kalenderbegivenhed i AULA (de gamle eksempler nøjedes med at læse data fra systemet).

Og til de nysgerrige: Nej, man kan ikke (umiddelbart) oprette begivenheder med Javascript i “beskrivelsen”. Ja, man kan godt lave begivenheder med inline-css i beskrivelsen, så begivenheder ser ret specielle ud, rent visuelt.

# aula.py
# Author: Morten Helmstedt. E-mail: helmstedt@gmail.com
''' An example of how to log in to the Danish LMS Aula (https://aula.dk) and
extract data from the API. Could be further developed to also submit data and/or to
create your own web or terminal interface(s) for Aula.'''

# Imports
import requests					# Perform http/https requests
from bs4 import BeautifulSoup	# Parse HTML pages
import json						# Needed to print JSON API data

# User info
user = {
	'username': '',
	'password': ''
}

# Start requests session
session = requests.Session()
	
# Get login page
url = 'https://login.aula.dk/auth/login.php?type=unilogin'
response = session.get(url)

# Login is handled by a loop where each page is first parsed by BeautifulSoup.
# Then the destination of the form is saved as the next url to post to and all
# inputs are collected with special cases for the username and password input.
# Once the loop reaches the Aula front page the loop is exited. The loop has a
# maximum number of iterations to avoid an infinite loop if something changes
# with the Aula login.
counter = 0
success = False
while success == False and counter < 10:
	try:
		# Parse response using BeautifulSoup
		soup = BeautifulSoup(response.text, "lxml")
		# Get destination of form element (assumes only one)
		url = soup.form['action']	
		
		# If form has a destination, inputs are collected and names and values
		# for posting to form destination are saved to a dictionary called data
		if url:
			# Get all inputs from page
			inputs = soup.find_all('input')
			# Check whether page has inputs
			if inputs:
				# Create empty dictionary 
				data = {}
				# Loop through inputs
				for input in inputs:
					# Some inputs may have no names or values so a try/except
					# construction is used.
					try:
						# Login takes place in single input steps, which
						# is the reason for the if/elif construction
						# Save username if input is a username field
						if input['name'] == 'username':
							data[input['name']] = user['username']
						# Save password if input is a password field
						elif input['name'] == 'password':
							data[input['name']] = user['password']
						# For employees the login procedure has an additional field to select a role
						# If an employee needs to login in a parent role, this value needs to be changed
						elif input['name'] == 'selected-aktoer':
							data[input['name']] = "MEDARBEJDER_EKSTERN"
						# For all other inputs, save name and value of input
						else:
							data[input['name']] = input['value']
					# If input has no value, an error is caught but needs no handling
					# since inputs without values do not need to be posted to next
					# destination.
					except:
						pass
			# If there's data in the dictionary, it is submitted to the destination url
			if data:
				response = session.post(url, data=data)
			# If there's no data, just try to post to the destination without data
			else:
				response = session.post(url)
			# If the url of the response is the Aula front page, loop is exited
			if response.url == 'https://www.aula.dk:443/portal/':
				success = True
	# If some error occurs, try to just ignore it
	except:
		pass
	# One is added to counter each time the loop runs independent of outcome
	counter += 1

# Login succeeded without an HTTP error code and API requests can begin	
if success == True and response.status_code == 200:
	print("Login lykkedes")
	
	# All API requests go to the below url
	# Each request has a number of parameters, of which method is always included
	# Data is returned in JSON
	url = 'https://www.aula.dk/api/v12/'

	### First API request. This request must be run to generate correct correct cookies for subsequent requests. ###
	params = {
		'method': 'profiles.getProfilesByLogin'
		}
	# Perform request, convert to json and print on screen
	response_profile = session.get(url, params=params).json()
	print(json.dumps(response_profile, indent=4))
	
	### Second API request. This request must be run to generate correct correct cookies for subsequent requests. ###
	params = {
		'method': 'profiles.getProfileContext',
		'portalrole': 'guardian',	# 'guardian' for parents (or other guardians), 'employee' for employees
	}
	# Perform request, convert to json and print on screen
	response_profile_context = session.get(url, params=params).json()
	print(json.dumps(response_profile_context, indent=4))

	# Loop to get institutions and children associated with profile and save
	# them to lists
	institutions = []
	institution_profiles = []
	children = []
	for institution in response_profile_context['data']['institutions']:
		institutions.append(institution['institutionCode'])
		institution_profiles.append(institution['institutionProfileId'])
		for child in institution['children']:
			children.append(child['id'])
	
	children_and_institution_profiles = institution_profiles + children

	### Third example API request, uses data collected from second request ###
	params = {
		'method': 'notifications.getNotificationsForActiveProfile',
		'activeChildrenIds[]': children,
		'activeInstitutionCodes[]': institutions
	}
	
	# Perform request, convert to json and print on screen
	notifications_response = session.get(url, params=params).json()
	print(json.dumps(notifications_response, indent=4))
	
	### Fourth example API request, only succeeds when the third has been run before ###
	params = {
		'method': 'messaging.getThreads',
		'sortOn': 'date',
		'orderDirection': 'desc',
		'page': '0'
	}
	
	# Perform request, convert to json and print on screen
	response_threads = session.get(url, params=params).json()
	print(json.dumps(response_threads, indent=4))
	
	### Fifth example. getAllPosts uses a combination of children and instituion profiles. ###
	params = {
		'method': 'posts.getAllPosts',
		'parent': 'profile',
		'index': "0",
		'institutionProfileIds[]': children_and_institution_profiles,
		'limit': '10'
	}

	# Perform request, convert to json and print on screen
	response_threads = session.get(url, params=params).json()
	print(json.dumps(response_threads, indent=4))
	
	### Sixth example. Posting a calender event. ###
	params = (
		('method', 'calendar.createSimpleEvent'),
	)
	
	# Manually setting the cookie "profile_change". This probably has to do with posting as a parent.
	session.cookies['profile_change'] = '2'
	
	# Csrfp-token is manually added to session headers.
	session.headers['csrfp-token'] = session.cookies['Csrfp-Token']

	data = {
		'title': 'This is a test',
		'description': '<p>A really nice test.</p>',
		'startDateTime': '2021-05-18T14:30:00.0000+02:00',
		'endDateTime': '2021-05-18T15:00:00.0000+02:00',
		'startDate': '2021-05-17',
		'endDate': '2021-05-17',
		'startTime': '12:00:19',
		'endTime': '12:30:19',
		'id': '',
		'institutionCode': response_profile['data']['profiles'][0]['institutionProfiles'][0]['institutionCode'],
		'creatorInstProfileId': response_profile['data']['profiles'][0]['institutionProfiles'][0]['id'],
		'type': 'event',
		'allDay': False,
		'private': False,
		'primaryResource': {},
		'additionalLocations': [],
		'invitees': [],
		'invitedGroups': [],
		'invitedGroupIds': [],
		'invitedGroupHomes': [],
		'responseRequired': True,
		'responseDeadline': None,
		'resources': [],
		'attachments': [],
		'oldStartDateTime': '',
		'oldEndDateTime': '',
		'isEditEvent': False,
		'addToInstitutionCalendar': False,
		'hideInOwnCalendar': False,
		'inviteeIds': [],
		'additionalResources': [],
		'pattern': 'never',
		'occurenceLimit': 0,
		'weekdayMask': [
			False,
			False,
			False,
			False,
			False,
			False,
			False
		],
		'maxDate': None,
		'interval': 0,
		'lessonId': '',
		'noteToClass': '',
		'noteToSubstitute': '',
		'eventId': '',
		'isPrivate': False,
		'resourceIds': [],
		'additionalLocationIds': [],
		'additionalResourceIds': [],
		'attachmentIds': []
	}
	
	response_calendar = session.post(url, params=params, json=data).json()
	print(json.dumps(response_calendar, indent=4))

# Login failed for some unknown reason
else:
	print("Noget gik galt med login")

Et lille kig på Aulas API

Det her kodeeksempel er ret gammelt. Du kan med fordel kigge på Aulas API: En opdatering i stedet

Hvis du har børn i skolealderen, kender du måske Aula. Det har jeg, og derfor har jeg lavet en lille programmeringsøvelse, hvor jeg trækker data ud fra Aula’s API.

Det kan ikke rigtigt bruges til noget i nuværende form (jeg bruger heller ikke rigtig selv Aula til noget endnu), men senere kunne det være relevant at udvide med mulighed for at tilgå visse hyppigt brugte funktioner uden at logge ind på hjemmesiden, eller til at lave sit eget personlige Aula-interface.

Ift. mine andre hente-data-fra-API’er-øvelser, har jeg her gjort to ting, som jeg synes er smarte:

  1. Jeg bruger en “session” i Python-modulet requests. Det gør, at jeg ikke behøver at rode med, hvilke cookies, de enkelte trin i loginproceduren, har brug for. De gemmes og benyttes i stedet automatisk gennem trinnene.
  2. I stedet for at gentage en masse kode i hver enkelt trin i login, bruger jeg en løkke, der, selv finder formularer og viste og skjulte input-felter på de enkelte trin, udfylder dem og sender dem af sted.

Hvis du har lyst til at prøve det af, finder du koden her. Du kan finde flere API-forespørgsler ved at bruge din browsers udviklerværktøjer på Aulas side. Koden burde også nemt kunne bruges til at logge ind og hente data fra andre hjemmesider end Aula, det kræver blot et par småjusteringer.

# aula.py
# Author: Morten Helmstedt. E-mail: helmstedt@gmail.com
''' An example of how to log in to the Danish LMS Aula (https://aula.dk) and
extract data from the API. Could be further developed to also submit data and/or to
create your own web or terminal interface(s) for Aula.'''

# Imports
import requests					# Perform http/https requests
from bs4 import BeautifulSoup	# Parse HTML pages
import json						# Needed to print JSON API data

# User info
user = {
	'username': '',
	'password': ''
	}

# Start requests session
session = requests.Session()
	
# Get login page
url = 'https://login.aula.dk/auth/login.php?type=unilogin'
response = session.get(url)

# Login is handled by a loop where each page is first parsed by BeautifulSoup.
# Then the destination of the form is saved as the next url to post to and all
# inputs are collected with special cases for the username and password input.
# Once the loop reaches the Aula front page the loop is exited. The loop has a
# maximum number of iterations to avoid an infinite loop if something changes
# with the Aula login.
counter = 0
success = False
while success == False and counter < 10:
	try:
		# Parse response using BeautifulSoup
		soup = BeautifulSoup(response.text, "lxml")
		# Get destination of form element (assumes only one)
		url = soup.form['action']	
		
		# If form has a destination, inputs are collected and names and values
		# for posting to form destination are saved to a dictionary called data
		if url:
			# Get all inputs from page
			inputs = soup.find_all('input')
			# Check whether page has inputs
			if inputs:
				# Create empty dictionary 
				data = {}
				# Loop through inputs
				for input in inputs:
					# Some inputs may have no names or values so a try/except
					# construction is used.
					try:
						# Save username if input is a username field
						if input['name'] == 'username':
							data[input['name']] = user['username']
						# Save password if input is a password field
						elif input['name'] == 'password':
							data[input['name']] = user['password']
						# For all other inputs, save name and value of input
						else:
							data[input['name']] = input['value']
					# If input has no value, an error is caught but needs no handling
					# since inputs without values do not need to be posted to next
					# destination.
					except:
						pass
			# If there's data in the dictionary, it is submitted to the destination url
			if data:
				response = session.post(url, data=data)
			# If there's no data, just try to post to the destination without data
			else:
				response = session.post(url)
			# If the url of the response is the Aula front page, loop is exited
			if response.url == 'https://www.aula.dk:443/portal/':
				success = True
	# If some error occurs, try to just ignore it
	except:
		pass
	# One is added to counter each time the loop runs independent of outcome
	counter += 1

# Login succeeded without an HTTP error code and API requests can begin	
if success == True and response.status_code == 200:
	print("Login lykkedes")
	
	# All API requests go to the below url
	# Each request has a number of parameters, of which method is always included
	# Data is returned in JSON
	url = 'https://www.aula.dk/api/v12/'
	
	### First example API request ###
	params = {
		'method': 'profiles.getProfilesByLogin'
		}
	# Perform request, convert to json and print on screen
	response_profile = session.get(url, params=params).json()
	print(json.dumps(response_profile, indent=4))
	
	
	### Second example API request ###
	params = {
		'method': 'profiles.getProfileContext',
		'portalrole': 'guardian',
	}
	# Perform request, convert to json and print on screen
	response_profile_context = session.get(url, params=params).json()
	print(json.dumps(response_profile_context, indent=4))

	# Loop to get institutions and children associated with profile and save
	# them to lists
	institutions = []
	institution_profiles = []
	children = []
	for institution in response_profile_context['data']['institutions']:
		institutions.append(institution['institutionCode'])
		institution_profiles.append(institution['institutionProfileId'])
		for child in institution['children']:
			children.append(child['id'])
	
	children_and_institution_profiles = institution_profiles + children

	### Third example API request, uses data collected from second request ###
	params = {
		'method': 'notifications.getNotificationsForActiveProfile',
		'activeChildrenIds[]': children,
		'activeInstitutionCodes[]': institutions
	}
	
	# Perform request, convert to json and print on screen
	notifications_response = session.get(url, params=params).json()
	print(json.dumps(notifications_response, indent=4))
	
	### Fourth example API request, only succeeds when the third has been run before ###
	params = {
		'method': 'messaging.getThreads',
		'sortOn': 'date',
		'orderDirection': 'desc',
		'page': '0'
	}
	
	# Perform request, convert to json and print on screen
	response_threads = session.get(url, params=params).json()
	#print(json.dumps(response_threads, indent=4))
	
	### Fifth example. getAllPosts uses a combination of children and instituion profiles. ###
	params = {
		'method': 'posts.getAllPosts',
		'parent': 'profile',
		'index': "0",
		'institutionProfileIds[]': children_and_institution_profiles,
		'limit': '10'
	}

	# Perform request, convert to json and print on screen
	response_threads = session.get(url, params=params).json()
	print(json.dumps(response_threads, indent=4))
	
# Login failed for some unknown reason
else:
	print("Noget gik galt med login")