Teaching process automation

Optimising and automating course admin tasks




UNSW Design Next


Doménique van Gennip

My role

Automating admin tasks to save time

Simplifying and automating operational processes within large and complex university courses frees up time to spend on actual teaching and interactions with students.

I developed python code to aid our teaching of several large Engineering courses with 3100+ students per year. The software would download data from Moodle (UNSW’s learning management system) and use it to generate student lists for use in other systems. Its main function was to sync student enrolments into Microsoft Teams, which we used extensively to deliver lectures, classes, and course materials. Keeping Teams in sync with enrolments is important for a good student experience.

Over time, the software evolved into a library of functions to increasingly automate the many administration and data handling tasks involved in managing such large courses. As a small unit without in-house teaching admin support, this saved us significant time.


Time reductions realised after implementing the automation script.

Setting up course materials on Moodle and MS Teams ahead of term could now be completed in just a few hours, a significant reduction from a purely manual process. During term, it reduces daily manual handling of data (approx. 15 minutes) to effectively just starting the script.

It tied in with our efforts to further develop design teaching at UNSW Engineering, a task which focused in part on streamlining an existing large and complex course. Making sure administrative tasks are done on time, with minimal time spent, ensured our management of these courses could focus on bigger-picture tasks.

Also, I learnt more from the development than I ever would from doing repetitive admin tasks…

Find the code on GitHub

dvangennip/course_updater on GitHub

The linked repository has the full code, some basic instructions to get going, and a few examples.

Simple example

from course_updater import MoodleUpdater, TeamsUpdater

my_user_file = "empty for now, will become a reference to a filepath"

with MoodleUpdater(course_id, username, password) as mu:
	# let's download a list of students currently enrolled
	my_user_file = mu.get_users_csv()

with TeamsUpdater(my_user_file, username, password) as tu:
	# start by importing the freshly downloaded list

	# create a new team and add some channels
	team_id = tu.create_team('Robotics project')
	tu.create_channel(team_id, 'QnA forum')

	for x in range(1,9):
		tu.create_channel(team_id, f'Class {x}', channel_type='private')

	# add some staff as owners to the team
	tu.add_users_to_team(team_id, tu.user_stafflist, role='Owner')

	# find a subset of users based on search criteria, like group membership
	students = tu.find_users('group', 'Project - Robotics')
	# sync team membership for students
	#   the script will compare with who's already a member
	#   and only add/remove people when needed
	tu.update_team(team_id, students, role='Member')

Overview of features

My initial step was a simple python script that would load student data downloaded from Moodle, connect to MS Teams via powershell and use the data to add or remove students from private channels in MS Teams.

With each teaching term, I found time and ways to build on it:

  • Using the splinter library, the script can log in to Moodle and automatically download student data;
  • Squeeze more data out of what we import from Moodle, so that we can find students by group or grouping membership, their project, team, class, mentor, etc.
  • Output a list of all classed and students in csv format with additional data included, such as which staff is mentoring them, for various uses by upstream teaching support teams;
  • Morph into a more generic wrapper around MS Teams powershell commands, which made it much more flexible and powerful:
    • I could now create Teams, set up and change channels, sync team and channel membership based on Moodle groups and groupings, and even add icons to a Teams instance;
    • Odd Teams setups were suddenly much easier to recreate;
  • Generate a list of commonly used groups/groupings for use in Moodle. Such group membership can then feed back into the sync with MS Teams;
  • On Moodle, add sections to the course page, including setting up restrictions based on group membership. Handy when you’ve got to do 20+ of them;
  • Set up the Gradebook in Moodle where marks are kept and categorised. Again, handy when you’ve got five common categories, eleven project categories, and another eight for technical parts, all within one course;
  • A few more minor things, some of questionable return-on-time-investment (but nice to have anyway).
    • Did I mention colour-coded output in the terminal? It’s so nice :)

If given time, I would have made some parts even more generic and useful to a broader audience, and second, I would have liked to simplify the configuration required ahead of each term.

Before you ask…

… I mean, no one is asking but let’s pretend.

Why do you use python?'

Well, I’m familiar with python and I figured it would save time compared to learning powershell. Also, the latter wouldn’t keep up with more advanced features I hoped to add.

So, you use python to run powershell commands?

Uhuh… (I don’t like where this is going…)

Again, why? Isn’t that inviting trouble?

Eh… let me quote from one of the comments in the code:

class PowerShellWrapper:
	This is what happens when you know python and think you'll just call
	some powershell commands, only to realise when you're in knee-deep that
	the Teams login step requires the shell process to stay alive between calls
	and now you have to tame powershell somehow, making you wonder if simply
	learning powershell in the first place wouldn't have been a better bet...

	enter this wrapper class... squarely aimed at using the same shell for everything.
	handles powershell commands in the background, works kind of like the
	`pexpect` library, listening and returning only when it encounters the
	reappearing prompt or another string in the output that we want to stop at.

	very simple, very likely not to work with most edge cases.
	def __init__ (blah, blah):

The surprising bit is that it actually works really, really well.