for writing King

profileChenJunLong
cs1064-f19-project6-version1.zip

canvas_analyzer.py

""" Project 6 Canvas Analyzer CS 1064 Introduction to Programming in Python Spring 2018 Access the Canvas Learning Management System and process learning analytics. Edit this file to implement the project. To test your current solution, run the `test_my_solution.py` file. Refer to the instructions on Canvas for more information. "I have neither given nor received help on this assignment." author: YOUR NAME HERE """ __version__ = 7 # 1) main # 2) print_user_info # 3) filter_available_courses # 4) print_courses # 5) get_course_ids # 6) choose_course # 7) summarize_points # 8) summarize_groups # 9) plot_scores # 10) plot_grade_trends # Keep any function tests inside this IF statement to ensure # that your `test_my_solution.py` does not execute it. if __name__ == "__main__": main('hermione') # main('ron') # main('harry') # https://community.canvaslms.com/docs/DOC-10806-4214724194 # main('YOUR OWN CANVAS TOKEN (You know, if you want)')

canvas_requests.py

""" Canvas Module for accessing the Canvas API. Provides a simple interface over Requests, and equipped with a simple cache for testing and development purposes. You should not modify this file, because the instructor will be using their own local version anyway. @author: acbart """ __version__ = 7 import requests import os import json import sqlite3 import sys import re def get_user(user_id): return get("users/self/profile", user_id) def get_courses(user_id): return get("courses", user_id) def get_submissions(user_id, course_id): submissions = get("courses/{}/students/submissions".format(course_id), user_id) groups = get("courses/{}/assignment_groups".format(course_id), user_id) group_map = {g['id']: g for g in groups} for submission in submissions: assignment_group_id = submission['assignment']['assignment_group_id'] submission['assignment']['group'] = group_map[assignment_group_id].copy() return submissions # Make sure we are using the right Python version. if not sys.version_info >= (3, 0): raise Exception("This code is expected to be run in Python 3.x") # Special Exception class for checking purposes class CanvasException(Exception): pass # Change for different institution BASE_URL = 'https://vt.instructure.com/api/v1/' # Connect to local SQLite database (the cache) DATABASE_NAME = 'sample_canvas_data.db' if not os.access(DATABASE_NAME, os.F_OK): raise CanvasException(('Error! Could not find a "{0}" file. ' 'Make sure that there is a \"{0}\" in the same ' 'directory as "{1}.py"! Spelling is very ' 'important here.').format(DATABASE_NAME, __file__)) if not os.access(DATABASE_NAME, os.R_OK): raise CanvasException(('Error! Could not read the "{0}" file. ' 'Make sure that it readable by changing its ' 'permissions. You may need to get help from ' 'your instructor.').format(DATABASE_NAME, __file__)) DATABASE = sqlite3.connect(DATABASE_NAME) # Preload the list of available users in the cache USERS = DATABASE.execute("""SELECT name FROM users""") USERS = [u.lower() for u, in USERS] # Responses are in the database as JSON data DATABASE.text_factory = lambda x: json.loads(x.decode('utf-8')) def get(url, user): ''' Accesses the Canvas API to return data, or from the local cache. Params: url (str): The URL endpoint to access user (str): The User (e.g., 'hermione') or API token Returns: dict or list: Depending on what URL is accessed, will return a list of all the results or just a single dictionary ''' # Confirm types if not isinstance(url, str): raise TypeError("The URL must be a string.") if not isinstance(user, str): raise TypeError("The user token must be a string.") # If a special user, then return the cached result rows = _get_via_cache(url, user) if rows: return rows[0] # Otherwise, get via the requests module return _get_via_requests(url, user) def _normalize_url(url): ''' Normalizes a URL to remove extra trailing slashes, and converts to lowercase Params: url (str): The URL endpoint to normalize Returns: str: The normalized URL endpoint ''' if url.endswith('/'): url = url[:-1] return url.lower() def _get_via_cache(url, user): ''' Retrieves the given user's result for that URL from the local cache. Params: url (str): The URL endpoint to look up in the cache user (str): One of the users in the cache Returns: dict or list: Depending on what URL is accessed, will return a list of all the results or just a single dictionary ''' # Normalize URL and user to find them in the cache normalized_user = user.lower() normalized_url = _normalize_url(url) if user.lower() in USERS: # Perform the query selection rows = DATABASE.execute("""SELECT response FROM responses WHERE url=? AND user=?""", (normalized_url, normalized_user)) return [r for r, in rows] return False def _get_via_requests(url, token): # Provide token and increase number of results returned to maximum full_url = BASE_URL + url parameters = {} parameters['access_token'] = token parameters['per_page'] = 100 if re.match("courses/(\d\d+)/students/submissions", url): parameters["include[]"] = "assignment" final_result = [] # Loop until we get every page of results while True: # Make the actual request response = requests.get(full_url, parameters) if response.status_code == 404: exception = ("Canvas URL not found for URL '{}'").format(url) raise CanvasException(exception) json_data = response.json() # Inspect the results, return any dictionaries directly if isinstance(json_data, dict): if 'errors' in json_data: errors = json_data['errors'] if errors: error_message = errors[0]['message'] if error_message == 'Invalid access token.': exception= ("Invalid access token '{}' for " "URL '{}'. Did you spell the name right?").format(token, url) else: exception= ("Canvas error '{}' for " "URL '{}'").format(error_message, url) else: exception = ("General canvas error for " "URL '{}'").format(url) raise CanvasException(exception) return json_data # Otherwise, start building up our list final_result.extend(json_data) # Check if there's another page of data if 'next' in response.links: # Now we'll go onto the next page full_url = response.links['next']['url'] else: # No more pages, stop here return final_result

flow_diagram.png

sample_canvas_data.db

sample_output.html

CS-1064 Final Project Sample Output

Version: 7

DO NOT MODIFY THIS FILE!

This file represents example output of the plots. You may use it to see what your output should look like. The test_my_solution.py file will use this to test your program's output. The grader will use their own version, so there is no point to modifying this file.

Note: The User and Course at the top of each section are the parameters used to control which user and course that section corresponds to. You should not include them in your output.

User: hermione Course: 52 (Introduction to Programming in Python) User: hermione Course: 15 (Potions) User: hermione Course: 23 (Defense Against the Dark Arts) User: hermione Course: 34 (Transfiguration) User: harry Course: 52 (Introduction to Programming in Python) User: harry Course: 15 (Potions) User: harry Course: 23 (Defense Against the Dark Arts) User: ron Course: 52 (Introduction to Programming in Python) User: ron Course: 15 (Potions) User: ron Course: 23 (Defense Against the Dark Arts)

sample_output.txt

# Version: 7 # DO NOT MODIFY THIS FILE! # This file represents example output. # You may use it to see what your output should look like. # The `test_my_solution.py` file will use this to test your program's output. # The grader will use their own version, so there is no point to modifying # this file. # # Note: The User and Course at the top of each section are the parameters # used to control which user and course that section corresponds to. # You should not include them in your output. # Note: Plots are embedded via their lists of data. You will need to create # actual plots, and you should not print out their lists. Refer to the # sample_output.html file to see the actual generated output. ################################ # User: hermione # Course: 52 # print_user_info Name: Hermione Granger Title: Student Primary Email: [email protected] Bio: Interested in Magic, Learning, and House Elf Rights # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 34, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Transfiguration'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts 34 : Transfiguration # get_course_ids [52, 15, 23, 34] # choose_course 52 # summarize_points Points possible so far: 13145.0 Points obtained: 11175 Current grade: 85 # summarize_groups * Class Activities : 78 * Discussion Forums : 80 * Learning Quizzes : 88 * Programming Problems : 83 # plot_scores [90.0, 90.0, 0.0, 83.33, 90.0, 0.0, 85.71, 90.0, 75.0, 83.33, 80.0, 75.0, 85.71, 75.0, 80.0, 85.71, 80.0, 75.0, 66.67, 90.0, 50.0, 93.33, 83.33, 85.71, 85.71, 80.0, 90.0, 75.0, 83.33, 80.0, 80.0, 87.5, 85.71, 66.67, 83.33, 80.0, 90.0, 90.0, 90.0, 80.0, 85.71, 80.0, 88.89, 75.0, 80.0, 83.33, 50.0, 90.48, 83.33, 80.0, 90.0, 66.67, 75.0, 88.89, 75.0, 83.33, 50.0, 80.0, 83.33, 88.24, 87.5, 80.0, 90.91, 75.0, 92.86, 66.67, 80.0, 85.71, 66.67, 75.0, 88.89, 75.0, 87.5, 66.67, 80.0, 66.67, 85.71, 90.0, 80.0, 90.0, 85.71, 85.71, 85.71, 90.0, 88.89, 88.89, 80.0, 80.0, 80.0] # plot_grade_trends ['2017-08-30 16:20:00', '2017-08-30 16:20:00', '2017-08-30 16:20:00', '2017-09-01 16:20:00', '2017-09-01 16:20:00', '2017-09-01 16:20:00', '2017-09-06 16:20:00', '2017-09-06 16:20:00', '2017-09-08 16:20:00', '2017-09-08 16:20:00', '2017-09-11 16:20:00', '2017-09-11 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-18 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-25 16:20:00', '2017-09-25 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-29 16:20:00', '2017-09-29 16:20:00', '2017-10-02 16:20:00', '2017-10-02 16:20:00', '2017-10-02 16:20:00', '2017-10-04 16:20:00', '2017-10-04 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-11 16:20:00', '2017-10-11 16:20:00', '2017-10-11 16:20:00', '2017-10-15 16:00:00', '2017-10-16 16:20:00', '2017-10-16 16:20:00', '2017-10-16 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-20 16:20:00', '2017-10-20 16:20:00', '2017-10-20 16:20:00', '2017-10-23 16:20:00', '2017-10-23 16:20:00', '2017-10-23 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-06 17:20:00', '2017-11-06 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-10 17:20:00', '2017-11-10 17:20:00', '2017-11-13 17:20:00', '2017-11-13 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-17 17:20:00', '2017-11-17 17:20:00', '2017-11-17 17:20:00', '2017-11-27 17:20:00', '2017-11-27 17:20:00', '2017-12-15 17:20:00', '2017-12-15 17:20:00', '2017-12-15 17:20:00'] [1.16, 2.31, 2.31, 2.95, 4.11, 4.11, 4.88, 5.34, 5.73, 5.98, 6.5, 6.65, 7.42, 7.81, 8.01, 8.32, 8.53, 8.91, 9.17, 10.32, 10.37, 11.27, 11.92, 13.07, 13.84, 14.05, 15.2, 15.59, 16.23, 16.74, 16.74, 16.95, 18.75, 19.52, 19.77, 20.42, 20.62, 21.78, 22.93, 24.09, 24.6, 25.37, 25.58, 26.61, 26.99, 27.5, 28.15, 28.2, 30.64, 31.28, 31.54, 31.74, 32.51, 33.54, 34.69, 34.95, 35.34, 36.36, 36.75, 38.03, 38.29, 40.86, 41.5, 41.55, 42.58, 43.09, 43.35, 45.27, 46.17, 46.38, 46.58, 47.87, 48.25, 48.9, 49.15, 50.82, 51.08, 52.36, 52.52, 53.54, 54.06, 54.83, 56.11, 56.21, 56.73, 57.11, 58.14, 58.53, 58.68, 59.58, 59.84, 60.61, 61.25, 61.45, 61.56, 62.33, 63.48, 64.0, 65.15, 65.92, 66.69, 67.46, 68.62, 69.65, 70.67, 71.19, 71.7, 71.91, 75.76, 82.18, 89.88] [1.16, 2.31, 2.31, 2.95, 4.11, 4.11, 4.88, 5.34, 5.73, 5.98, 6.5, 6.65, 7.42, 7.81, 8.01, 8.32, 8.53, 8.91, 9.17, 10.32, 10.37, 11.27, 11.92, 13.07, 13.84, 14.05, 15.2, 15.59, 16.23, 16.74, 16.74, 16.95, 18.75, 19.52, 19.77, 20.42, 20.62, 21.78, 22.93, 24.09, 24.6, 25.37, 25.58, 26.61, 26.99, 27.5, 28.15, 28.2, 30.64, 31.28, 31.28, 31.48, 31.48, 31.48, 32.64, 32.9, 33.28, 34.31, 34.69, 34.69, 34.95, 34.95, 34.95, 35.0, 35.0, 35.52, 35.77, 37.7, 38.6, 38.8, 38.8, 40.09, 40.47, 40.47, 40.47, 42.14, 42.4, 42.4, 42.4, 42.4, 42.91, 43.68, 43.68, 43.79, 43.79, 44.17, 45.2, 45.58, 45.58, 46.48, 46.74, 46.74, 46.74, 46.94, 47.05, 47.82, 48.97, 49.49, 50.64, 51.41, 52.18, 52.95, 54.11, 55.14, 56.16, 56.68, 57.19, 57.4, 57.4, 57.4, 57.4] [1.28, 2.57, 2.62, 3.39, 4.67, 4.8, 5.7, 6.21, 6.73, 7.04, 7.68, 7.88, 8.78, 9.3, 9.55, 9.91, 10.17, 10.68, 11.07, 12.35, 12.46, 13.42, 14.19, 15.54, 16.44, 16.69, 17.98, 18.49, 19.26, 19.9, 19.9, 20.16, 22.21, 23.11, 23.5, 24.27, 24.52, 25.81, 27.09, 28.38, 29.02, 29.92, 30.17, 31.33, 31.84, 32.49, 33.26, 33.36, 36.06, 36.83, 37.08, 37.34, 38.11, 39.14, 40.42, 40.81, 41.32, 42.48, 42.99, 44.27, 44.58, 47.15, 47.79, 47.89, 48.92, 49.56, 49.87, 52.05, 53.08, 53.34, 53.54, 54.96, 55.47, 56.11, 56.37, 58.17, 58.55, 59.84, 59.99, 61.02, 61.66, 62.56, 63.84, 64.0, 64.51, 65.02, 66.18, 66.69, 66.85, 67.87, 68.26, 69.03, 69.67, 69.93, 70.08, 70.98, 72.27, 72.91, 74.19, 75.09, 75.99, 76.89, 78.17, 79.33, 80.48, 81.12, 81.77, 82.02, 85.88, 92.3, 100.0] ################################ # User: hermione # Course: 15 # print_user_info Name: Hermione Granger Title: Student Primary Email: [email protected] Bio: Interested in Magic, Learning, and House Elf Rights # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 34, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Transfiguration'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts 34 : Transfiguration # get_course_ids [52, 15, 23, 34] # choose_course 15 # summarize_points Points possible so far: 6600 Points obtained: 6100 Current grade: 92 # summarize_groups * Practical : 93 * Project : 94 * Quiz : 90 # plot_scores [90.0, 90.0, 90.0, 90.0, 90.0, 93.33, 95.0, 95.0, 90.0, 96.67] # plot_grade_trends ['2017-08-30 16:00:00', '2017-09-08 16:00:00', '2017-09-18 16:00:00', '2017-09-26 16:00:00', '2017-10-05 16:00:00', '2017-10-13 16:00:00', '2017-10-23 16:00:00', '2017-11-01 16:00:00', '2017-11-10 16:00:00', '2017-11-17 16:00:00', '2017-11-27 16:00:00', '2017-12-06 16:00:00', '2017-12-15 16:00:00', '2017-12-25 16:00:00'] [4.86, 12.16, 14.59, 21.89, 24.32, 34.41, 37.12, 42.25, 47.39, 49.82, 57.66, 65.77, 68.47, 95.5] [4.86, 12.16, 14.59, 21.89, 24.32, 34.41, 34.41, 39.55, 44.68, 47.12, 54.95, 54.95, 54.95, 54.95] [5.41, 13.51, 16.22, 24.32, 27.03, 37.84, 40.54, 45.95, 51.35, 54.05, 62.16, 70.27, 72.97, 100.0] ################################ # User: hermione # Course: 23 # print_user_info Name: Hermione Granger Title: Student Primary Email: [email protected] Bio: Interested in Magic, Learning, and House Elf Rights # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 34, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Transfiguration'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts 34 : Transfiguration # get_course_ids [52, 15, 23, 34] # choose_course 23 # summarize_points Points possible so far: 13875 Points obtained: 12630 Current grade: 91 # summarize_groups * Lab : 91 * Project : 91 * Quiz : 90 # plot_scores [90.0, 90.0, 80.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 93.33, 90.0, 90.0, 90.0, 90.0, 91.67, 95.0, 90.0, 90.0, 96.67, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 93.33, 95.0, 90.0, 90.0, 95.0] # plot_grade_trends ['2017-08-24 16:00:00', '2017-08-25 16:00:00', '2017-08-29 16:00:00', '2017-08-31 16:00:00', '2017-09-04 16:00:00', '2017-09-05 16:00:00', '2017-09-08 16:00:00', '2017-09-11 16:00:00', '2017-09-13 16:00:00', '2017-09-15 16:00:00', '2017-09-18 16:00:00', '2017-09-20 16:00:00', '2017-09-22 16:00:00', '2017-09-25 16:00:00', '2017-09-28 16:00:00', '2017-09-29 16:00:00', '2017-10-03 16:00:00', '2017-10-05 16:00:00', '2017-10-09 16:00:00', '2017-10-10 16:00:00', '2017-10-13 16:00:00', '2017-10-16 16:00:00', '2017-10-18 16:00:00', '2017-10-20 16:00:00', '2017-10-23 16:00:00', '2017-10-25 16:00:00', '2017-10-27 16:00:00', '2017-10-30 16:00:00', '2017-11-02 16:00:00', '2017-11-03 16:00:00', '2017-11-07 16:00:00', '2017-11-09 16:00:00', '2017-11-13 16:00:00', '2017-11-14 16:00:00', '2017-11-17 16:00:00', '2017-11-20 16:00:00', '2017-11-22 16:00:00', '2017-11-24 16:00:00', '2017-11-27 16:00:00', '2017-11-29 16:00:00', '2017-12-01 16:00:00', '2017-12-04 16:00:00', '2017-12-07 16:00:00', '2017-12-08 16:00:00', '2017-12-12 16:00:00', '2017-12-14 16:00:00', '2017-12-18 16:00:00', '2017-12-19 16:00:00', '2017-12-22 16:00:00', '2017-12-25 16:00:00'] [0.6, 1.8, 2.34, 2.94, 6.55, 7.75, 8.95, 9.55, 10.16, 10.76, 11.96, 15.57, 16.17, 17.1, 18.91, 19.51, 20.71, 24.32, 31.67, 32.94, 33.54, 34.74, 38.62, 39.22, 39.82, 41.02, 41.69, 42.9, 43.5, 44.7, 46.04, 46.64, 50.24, 51.45, 52.05, 52.65, 54.52, 55.79, 56.39, 57.06, 57.66, 58.93, 59.6, 60.27, 60.94, 62.27, 63.27, 81.09, 94.45, 94.45] [0.6, 1.8, 2.34, 2.94, 6.55, 7.75, 8.95, 9.55, 10.16, 10.76, 11.96, 15.57, 16.17, 17.1, 18.91, 19.51, 20.71, 24.32, 31.67, 32.94, 33.54, 34.74, 38.62, 39.22, 39.82, 41.02, 41.02, 42.23, 42.83, 44.03, 44.03, 44.63, 48.24, 49.44, 50.04, 50.65, 52.52, 53.79, 54.39, 54.39, 54.99, 56.26, 56.26, 56.26, 56.26, 56.26, 56.26, 56.26, 56.26, 56.26] [0.67, 2.0, 2.67, 3.34, 7.35, 8.69, 10.02, 10.69, 11.36, 12.03, 13.36, 17.37, 18.04, 19.04, 21.05, 21.71, 23.05, 27.06, 35.08, 36.41, 37.08, 38.42, 42.43, 43.1, 43.76, 45.1, 45.77, 47.1, 47.77, 49.11, 50.45, 51.11, 55.12, 56.46, 57.13, 57.8, 59.8, 61.14, 61.8, 62.47, 63.14, 64.48, 65.14, 65.81, 66.48, 67.82, 68.82, 86.64, 100.0, 100.0] ################################ # User: hermione # Course: 34 # print_user_info Name: Hermione Granger Title: Student Primary Email: [email protected] Bio: Interested in Magic, Learning, and House Elf Rights # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 34, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Transfiguration'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts 34 : Transfiguration # get_course_ids [52, 15, 23, 34] # choose_course 34 # summarize_points Points possible so far: 17700 Points obtained: 16220 Current grade: 92 # summarize_groups * Lab : 91 * Project : 93 * Quiz : 91 # plot_scores [92.0, 90.0, 93.33, 90.0, 90.0, 90.0, 90.0, 95.0, 90.0, 95.0, 93.33, 90.0, 95.0, 96.67, 90.0, 90.0, 90.0, 80.0, 93.33, 90.0, 93.33, 90.0, 86.67, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 93.33, 90.0, 90.0, 90.0, 95.0] # plot_grade_trends ['2017-08-24 16:00:00', '2017-08-25 16:00:00', '2017-08-29 16:00:00', '2017-08-31 16:00:00', '2017-09-04 16:00:00', '2017-09-06 16:00:00', '2017-09-08 16:00:00', '2017-09-11 16:00:00', '2017-09-13 16:00:00', '2017-09-15 16:00:00', '2017-09-18 16:00:00', '2017-09-21 16:00:00', '2017-09-22 16:00:00', '2017-09-26 16:00:00', '2017-09-28 16:00:00', '2017-10-02 16:00:00', '2017-10-04 16:00:00', '2017-10-06 16:00:00', '2017-10-09 16:00:00', '2017-10-11 16:00:00', '2017-10-13 16:00:00', '2017-10-16 16:00:00', '2017-10-19 16:00:00', '2017-10-20 16:00:00', '2017-10-24 16:00:00', '2017-10-27 16:00:00', '2017-10-30 16:00:00', '2017-11-01 16:00:00', '2017-11-03 16:00:00', '2017-11-06 16:00:00', '2017-11-08 16:00:00', '2017-11-10 16:00:00', '2017-11-13 16:00:00', '2017-11-16 16:00:00', '2017-11-17 16:00:00', '2017-11-21 16:00:00', '2017-11-24 16:00:00', '2017-11-27 16:00:00', '2017-11-29 16:00:00', '2017-12-01 16:00:00', '2017-12-04 16:00:00', '2017-12-06 16:00:00', '2017-12-08 16:00:00', '2017-12-11 16:00:00', '2017-12-14 16:00:00', '2017-12-18 16:00:00', '2017-12-19 16:00:00', '2017-12-22 16:00:00', '2017-12-25 16:00:00'] [2.95, 3.24, 5.94, 6.23, 7.96, 8.25, 8.54, 10.37, 12.1, 13.93, 16.63, 18.36, 20.19, 22.99, 24.72, 26.45, 29.05, 29.82, 31.17, 32.91, 35.6, 36.47, 37.72, 39.65, 42.25, 43.21, 44.65, 46.39, 49.28, 49.57, 49.86, 50.14, 50.43, 51.3, 51.88, 54.57, 54.86, 56.6, 57.56, 58.52, 59.39, 62.28, 64.11, 66.04, 67.96, 69.89, 72.78, 75.99, 95.25] [2.95, 3.24, 5.94, 6.23, 7.96, 8.25, 8.54, 10.37, 12.1, 13.93, 16.63, 18.36, 20.19, 22.99, 24.72, 26.45, 29.05, 29.82, 31.17, 32.91, 35.6, 36.47, 37.72, 37.72, 40.32, 40.32, 40.32, 42.05, 42.05, 42.34, 42.63, 42.92, 43.21, 44.08, 44.65, 47.35, 47.64, 49.37, 49.37, 49.37, 50.24, 50.24, 52.07, 52.07, 52.07, 52.07, 52.07, 52.07, 52.07] [3.21, 3.53, 6.42, 6.74, 8.67, 8.99, 9.31, 11.24, 13.16, 15.09, 17.98, 19.9, 21.83, 24.72, 26.65, 28.57, 31.46, 32.42, 33.87, 35.79, 38.68, 39.65, 41.09, 43.02, 45.91, 46.87, 48.31, 50.24, 53.13, 53.45, 53.77, 54.09, 54.41, 55.38, 56.02, 58.91, 59.23, 61.16, 62.12, 63.08, 64.04, 66.93, 68.86, 70.79, 72.71, 74.64, 77.53, 80.74, 100.0] ################################ # User: harry # Course: 52 # print_user_info Name: Harry Potter Title: Student Primary Email: [email protected] Bio: Quidditch, Defense Against the Dark Arts # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts # get_course_ids [52, 15, 23] # choose_course 52 # summarize_points Points possible so far: 13585.0 Points obtained: 9265 Current grade: 68 # summarize_groups * Class Activities : 66 * Discussion Forums : 63 * Learning Quizzes : 70 * Programming Problems : 67 # plot_scores [70.0, 60.0, 0.0, 66.67, 70.0, 0.0, 71.43, 70.0, 50.0, 83.33, 80.0, 75.0, 71.43, 75.0, 60.0, 71.43, 60.0, 50.0, 33.33, 70.0, 50.0, 66.67, 83.33, 57.14, 71.43, 60.0, 60.0, 50.0, 66.67, 60.0, 60.0, 81.25, 71.43, 66.67, 66.67, 80.0, 70.0, 70.0, 90.0, 60.0, 71.43, 80.0, 66.67, 50.0, 60.0, 66.67, 50.0, 90.48, 66.67, 60.0, 60.0, 66.67, 75.0, 70.0, 66.67, 75.0, 77.78, 75.0, 55.0, 60.0, 75.0, 60.0, 66.67, 75.0, 80.0, 50.0, 72.73, 60.0, 50.0, 60.0, 66.67, 75.0, 66.67, 75.0, 75.0, 66.67, 50.0, 33.33, 75.0, 60.0, 71.43, 80.0, 60.0, 70.0, 71.43, 57.14, 70.0, 55.56, 66.67, 60.0, 60.0, 60.0] # plot_grade_trends ['2017-08-30 16:20:00', '2017-08-30 16:20:00', '2017-08-30 16:20:00', '2017-09-01 16:20:00', '2017-09-01 16:20:00', '2017-09-01 16:20:00', '2017-09-06 16:20:00', '2017-09-06 16:20:00', '2017-09-08 16:20:00', '2017-09-08 16:20:00', '2017-09-11 16:20:00', '2017-09-11 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-18 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-25 16:20:00', '2017-09-25 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-29 16:20:00', '2017-09-29 16:20:00', '2017-10-02 16:20:00', '2017-10-02 16:20:00', '2017-10-02 16:20:00', '2017-10-04 16:20:00', '2017-10-04 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-11 16:20:00', '2017-10-11 16:20:00', '2017-10-11 16:20:00', '2017-10-15 16:00:00', '2017-10-16 16:20:00', '2017-10-16 16:20:00', '2017-10-16 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-20 16:20:00', '2017-10-20 16:20:00', '2017-10-20 16:20:00', '2017-10-23 16:20:00', '2017-10-23 16:20:00', '2017-10-23 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-06 17:20:00', '2017-11-06 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-10 17:20:00', '2017-11-10 17:20:00', '2017-11-13 17:20:00', '2017-11-13 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-17 17:20:00', '2017-11-17 17:20:00', '2017-11-17 17:20:00', '2017-11-27 17:20:00', '2017-11-27 17:20:00', '2017-12-15 17:20:00', '2017-12-15 17:20:00', '2017-12-15 17:20:00'] [0.9, 1.67, 1.67, 2.18, 3.08, 3.08, 3.72, 4.08, 4.34, 4.6, 5.11, 5.26, 5.91, 6.29, 6.45, 6.7, 6.86, 7.11, 7.24, 8.14, 8.19, 8.83, 9.48, 10.25, 10.89, 11.04, 11.81, 12.07, 12.58, 12.97, 12.97, 13.12, 14.79, 15.43, 15.69, 16.2, 16.41, 17.31, 18.21, 19.36, 19.75, 20.39, 20.6, 21.37, 21.62, 22.01, 22.52, 22.57, 25.01, 25.53, 25.68, 25.83, 26.35, 27.12, 28.02, 28.27, 28.66, 29.56, 29.94, 31.23, 31.54, 32.95, 33.33, 33.44, 34.21, 34.59, 34.8, 36.98, 37.75, 37.96, 38.06, 39.09, 39.6, 39.98, 40.11, 41.91, 42.3, 43.07, 43.17, 43.94, 44.58, 45.48, 46.76, 46.87, 47.25, 47.64, 48.41, 48.66, 48.72, 49.49, 49.87, 50.64, 51.28, 51.44, 51.59, 52.23, 53.26, 53.65, 54.55, 55.44, 56.09, 56.6, 57.5, 58.14, 58.91, 59.3, 59.68, 59.84, 63.69, 70.11, 77.81] [0.9, 1.67, 1.67, 2.18, 3.08, 3.08, 3.72, 4.08, 4.34, 4.6, 5.11, 5.26, 5.91, 6.29, 6.45, 6.7, 6.86, 7.11, 7.24, 8.14, 8.19, 8.83, 9.48, 10.25, 10.89, 11.04, 11.81, 12.07, 12.58, 12.97, 12.97, 13.12, 14.79, 15.43, 15.69, 16.2, 16.41, 17.31, 18.21, 19.36, 19.75, 20.39, 20.6, 21.37, 21.62, 22.01, 22.52, 22.57, 25.01, 25.53, 25.68, 25.83, 26.35, 27.12, 28.02, 28.27, 28.66, 29.56, 29.94, 29.94, 29.94, 31.36, 31.74, 31.74, 32.51, 32.9, 33.1, 33.1, 33.87, 34.08, 34.18, 35.21, 35.21, 35.59, 35.72, 35.72, 35.72, 36.49, 36.59, 37.37, 37.37, 37.37, 37.37, 37.47, 37.85, 38.24, 39.01, 39.27, 39.32, 40.09, 40.09, 40.09, 40.09, 40.24, 40.24, 40.88, 41.91, 42.3, 43.19, 43.19, 43.84, 44.35, 45.25, 45.89, 46.66, 47.05, 47.43, 47.59, 47.59, 47.59, 47.59] [1.28, 2.57, 2.62, 3.39, 4.67, 4.8, 5.7, 6.21, 6.73, 7.04, 7.68, 7.88, 8.78, 9.3, 9.55, 9.91, 10.17, 10.68, 11.07, 12.35, 12.46, 13.42, 14.19, 15.54, 16.44, 16.69, 17.98, 18.49, 19.26, 19.9, 19.9, 20.16, 22.21, 23.11, 23.5, 24.27, 24.52, 25.81, 27.09, 28.38, 29.02, 29.92, 30.17, 31.33, 31.84, 32.49, 33.26, 33.36, 36.06, 36.83, 37.08, 37.34, 38.11, 39.14, 40.42, 40.81, 41.32, 42.48, 42.99, 44.27, 44.58, 47.15, 47.79, 47.89, 48.92, 49.56, 49.87, 52.05, 53.08, 53.34, 53.54, 54.96, 55.47, 56.11, 56.37, 58.17, 58.55, 59.84, 59.99, 61.02, 61.66, 62.56, 63.84, 64.0, 64.51, 65.02, 66.18, 66.69, 66.85, 67.87, 68.26, 69.03, 69.67, 69.93, 70.08, 70.98, 72.27, 72.91, 74.19, 75.09, 75.99, 76.89, 78.17, 79.33, 80.48, 81.12, 81.77, 82.02, 85.88, 92.3, 100.0] ################################ # User: harry # Course: 15 # print_user_info Name: Harry Potter Title: Student Primary Email: [email protected] Bio: Quidditch, Defense Against the Dark Arts # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts # get_course_ids [52, 15, 23] # choose_course 15 # summarize_points Points possible so far: 6600 Points obtained: 4570 Current grade: 69 # summarize_groups * Practical : 53 * Project : 70 * Quiz : 76 # plot_scores [70.0, 83.33, 70.0, 76.67, 80.0, 53.33, 70.0, 70.0, 60.0, 73.33] # plot_grade_trends ['2017-08-30 16:00:00', '2017-09-08 16:00:00', '2017-09-18 16:00:00', '2017-09-26 16:00:00', '2017-10-05 16:00:00', '2017-10-13 16:00:00', '2017-10-23 16:00:00', '2017-11-01 16:00:00', '2017-11-10 16:00:00', '2017-11-17 16:00:00', '2017-11-27 16:00:00', '2017-12-06 16:00:00', '2017-12-15 16:00:00', '2017-12-25 16:00:00'] [3.78, 10.54, 12.43, 18.65, 20.81, 26.58, 28.47, 33.87, 39.28, 41.17, 46.04, 51.98, 54.68, 81.71] [3.78, 10.54, 12.43, 18.65, 20.81, 26.58, 28.47, 28.47, 28.47, 30.36, 35.23, 41.17, 41.17, 41.17] [5.41, 13.51, 16.22, 24.32, 27.03, 37.84, 40.54, 45.95, 51.35, 54.05, 62.16, 70.27, 72.97, 100.0] ################################ # User: harry # Course: 23 # print_user_info Name: Harry Potter Title: Student Primary Email: [email protected] Bio: Quidditch, Defense Against the Dark Arts # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts # get_course_ids [52, 15, 23] # choose_course 23 # summarize_points Points possible so far: 12075 Points obtained: 8745 Current grade: 72 # summarize_groups * Lab : 71 * Project : 74 * Quiz : 70 # plot_scores [60.0, 80.0, 60.0, 70.0, 73.33, 75.0, 65.0, 50.0, 70.0, 80.0, 50.0, 60.0, 70.0, 73.33, 73.33, 80.0, 75.0, 86.67, 78.33, 75.0, 80.0, 80.0, 85.0, 55.0, 70.0, 70.0, 75.0, 70.0, 70.0, 70.0, 70.0, 70.0, 70.0, 70.0] # plot_grade_trends ['2017-08-24 16:00:00', '2017-08-25 16:00:00', '2017-08-29 16:00:00', '2017-08-31 16:00:00', '2017-09-04 16:00:00', '2017-09-05 16:00:00', '2017-09-08 16:00:00', '2017-09-11 16:00:00', '2017-09-13 16:00:00', '2017-09-15 16:00:00', '2017-09-18 16:00:00', '2017-09-20 16:00:00', '2017-09-22 16:00:00', '2017-09-25 16:00:00', '2017-09-28 16:00:00', '2017-09-29 16:00:00', '2017-10-03 16:00:00', '2017-10-05 16:00:00', '2017-10-09 16:00:00', '2017-10-10 16:00:00', '2017-10-13 16:00:00', '2017-10-16 16:00:00', '2017-10-18 16:00:00', '2017-10-20 16:00:00', '2017-10-23 16:00:00', '2017-10-25 16:00:00', '2017-10-27 16:00:00', '2017-10-30 16:00:00', '2017-11-02 16:00:00', '2017-11-03 16:00:00', '2017-11-07 16:00:00', '2017-11-09 16:00:00', '2017-11-13 16:00:00', '2017-11-14 16:00:00', '2017-11-17 16:00:00', '2017-11-20 16:00:00', '2017-11-22 16:00:00', '2017-11-24 16:00:00', '2017-11-27 16:00:00', '2017-11-29 16:00:00', '2017-12-01 16:00:00', '2017-12-04 16:00:00', '2017-12-07 16:00:00', '2017-12-08 16:00:00', '2017-12-12 16:00:00', '2017-12-14 16:00:00', '2017-12-18 16:00:00', '2017-12-19 16:00:00', '2017-12-22 16:00:00', '2017-12-25 16:00:00'] [0.4, 1.47, 1.87, 2.34, 5.28, 6.28, 7.15, 7.48, 7.95, 8.49, 9.15, 11.56, 12.03, 12.76, 14.23, 14.77, 15.77, 19.24, 25.52, 26.53, 27.19, 28.26, 32.27, 32.94, 33.47, 34.81, 35.48, 36.61, 37.28, 38.62, 39.35, 39.82, 42.63, 43.63, 44.1, 44.57, 45.97, 46.9, 47.37, 47.84, 48.31, 49.64, 50.31, 50.98, 51.65, 52.98, 53.99, 71.8, 85.17, 85.17] [0.4, 1.47, 1.87, 2.34, 5.28, 6.28, 7.15, 7.48, 7.95, 8.49, 9.15, 11.56, 12.03, 12.76, 14.23, 14.77, 15.77, 19.24, 25.52, 26.53, 26.53, 27.59, 27.59, 27.59, 28.13, 28.13, 28.13, 29.27, 29.27, 29.27, 30.0, 30.47, 33.27, 34.28, 34.74, 35.21, 36.61, 37.55, 38.02, 38.49, 38.95, 38.95, 38.95, 38.95, 38.95, 38.95, 38.95, 38.95, 38.95, 38.95] [0.67, 2.0, 2.67, 3.34, 7.35, 8.69, 10.02, 10.69, 11.36, 12.03, 13.36, 17.37, 18.04, 19.04, 21.05, 21.71, 23.05, 27.06, 35.08, 36.41, 37.08, 38.42, 42.43, 43.1, 43.76, 45.1, 45.77, 47.1, 47.77, 49.11, 50.45, 51.11, 55.12, 56.46, 57.13, 57.8, 59.8, 61.14, 61.8, 62.47, 63.14, 64.48, 65.14, 65.81, 66.48, 67.82, 68.82, 86.64, 100.0, 100.0] ################################ # User: ron # Course: 52 # print_user_info Name: Ron Weasley Title: Student Primary Email: [email protected] Bio: I'll do this later # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts # get_course_ids [52, 15, 23] # choose_course 52 # summarize_points Points possible so far: 13475.0 Points obtained: 8785 Current grade: 65 # summarize_groups * Class Activities : 64 * Discussion Forums : 35 * Learning Quizzes : 71 * Programming Problems : 58 # plot_scores [80.0, 90.0, 0.0, 83.33, 80.0, 0.0, 57.14, 90.0, 50.0, 83.33, 80.0, 75.0, 71.43, 75.0, 60.0, 85.71, 80.0, 75.0, 66.67, 80.0, 50.0, 66.67, 66.67, 76.19, 71.43, 80.0, 60.0, 25.0, 66.67, 80.0, 60.0, 75.0, 0.0, 0.0, 0.0, 0.0, 80.0, 80.0, 80.0, 100.0, 85.71, 40.0, 77.78, 75.0, 80.0, 50.0, 50.0, 83.33, 80.0, 0.0, 50.0, 40.0, 33.33, 75.0, 100.0, 75.0, 70.0, 66.67, 80.0, 100.0, 80.0, 83.33, 75.0, 75.0, 81.82, 75.0, 40.0, 85.71, 0.0, 62.5, 60.0, 71.43, 60.0, 66.67, 75.0, 50.0, 77.78, 50.0, 66.67, 75.0, 66.67, 50.0, 60.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 85.71, 100.0, 60.0, 77.78, 80.0, 80.0] # plot_grade_trends ['2017-08-30 16:20:00', '2017-08-30 16:20:00', '2017-08-30 16:20:00', '2017-09-01 16:20:00', '2017-09-01 16:20:00', '2017-09-01 16:20:00', '2017-09-06 16:20:00', '2017-09-06 16:20:00', '2017-09-08 16:20:00', '2017-09-08 16:20:00', '2017-09-11 16:20:00', '2017-09-11 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-13 16:20:00', '2017-09-18 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-20 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-22 16:20:00', '2017-09-25 16:20:00', '2017-09-25 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-27 16:20:00', '2017-09-29 16:20:00', '2017-09-29 16:20:00', '2017-10-02 16:20:00', '2017-10-02 16:20:00', '2017-10-02 16:20:00', '2017-10-04 16:20:00', '2017-10-04 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-06 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-09 16:20:00', '2017-10-11 16:20:00', '2017-10-11 16:20:00', '2017-10-11 16:20:00', '2017-10-15 16:00:00', '2017-10-16 16:20:00', '2017-10-16 16:20:00', '2017-10-16 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-18 16:20:00', '2017-10-20 16:20:00', '2017-10-20 16:20:00', '2017-10-20 16:20:00', '2017-10-23 16:20:00', '2017-10-23 16:20:00', '2017-10-23 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-25 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-27 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-10-30 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-01 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-03 16:20:00', '2017-11-06 17:20:00', '2017-11-06 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-08 17:20:00', '2017-11-10 17:20:00', '2017-11-10 17:20:00', '2017-11-13 17:20:00', '2017-11-13 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-15 17:20:00', '2017-11-17 17:20:00', '2017-11-17 17:20:00', '2017-11-17 17:20:00', '2017-11-27 17:20:00', '2017-11-27 17:20:00', '2017-12-15 17:20:00', '2017-12-15 17:20:00', '2017-12-15 17:20:00'] [1.03, 2.18, 2.18, 2.82, 3.85, 3.85, 4.37, 4.83, 5.08, 5.34, 5.86, 6.01, 6.65, 7.04, 7.19, 7.5, 7.7, 8.09, 8.35, 9.37, 9.42, 10.07, 10.58, 11.61, 12.25, 12.46, 13.23, 13.35, 13.87, 14.38, 14.38, 14.54, 16.08, 16.08, 16.08, 16.08, 16.08, 17.1, 18.13, 19.16, 19.8, 20.57, 20.67, 21.57, 21.96, 22.47, 22.86, 22.91, 25.6, 26.25, 26.45, 26.45, 27.22, 27.73, 28.25, 28.38, 28.76, 29.92, 30.3, 31.2, 31.41, 33.98, 34.49, 34.59, 35.62, 36.13, 36.39, 38.57, 39.34, 39.6, 39.75, 40.91, 41.29, 41.55, 41.81, 43.35, 43.73, 45.02, 45.02, 45.66, 46.05, 46.69, 47.46, 47.56, 47.95, 48.2, 49.1, 49.36, 49.46, 50.23, 50.49, 50.87, 51.26, 51.26, 51.26, 51.26, 51.26, 51.26, 51.26, 52.03, 52.93, 53.83, 54.6, 55.75, 56.65, 57.16, 57.68, 57.94, 61.79, 68.21, 75.91] [1.03, 2.18, 2.18, 2.82, 3.85, 3.85, 4.37, 4.83, 5.08, 5.34, 5.86, 6.01, 6.65, 7.04, 7.19, 7.5, 7.7, 8.09, 8.35, 9.37, 9.42, 10.07, 10.58, 11.61, 12.25, 12.46, 13.23, 13.35, 13.87, 14.38, 14.38, 14.54, 16.08, 16.08, 16.08, 16.08, 16.08, 17.1, 18.13, 19.16, 19.8, 20.57, 20.67, 21.57, 21.96, 22.47, 22.86, 22.91, 22.91, 23.55, 23.75, 23.75, 23.75, 24.27, 24.78, 24.91, 25.3, 26.45, 26.84, 27.73, 27.94, 27.94, 28.45, 28.45, 29.48, 29.99, 30.25, 30.25, 31.02, 31.02, 31.18, 32.33, 32.72, 32.97, 32.97, 34.51, 34.51, 34.51, 34.51, 35.16, 35.54, 36.18, 36.95, 37.06, 37.44, 37.7, 38.6, 38.85, 38.96, 39.73, 39.98, 40.37, 40.76, 40.76, 40.76, 40.76, 40.76, 40.76, 40.76, 41.53, 42.42, 42.42, 43.19, 43.19, 44.09, 44.61, 45.12, 45.12, 45.12, 45.12, 45.12] [1.28, 2.57, 2.62, 3.39, 4.67, 4.8, 5.7, 6.21, 6.73, 7.04, 7.68, 7.88, 8.78, 9.3, 9.55, 9.91, 10.17, 10.68, 11.07, 12.35, 12.46, 13.42, 14.19, 15.54, 16.44, 16.69, 17.98, 18.49, 19.26, 19.9, 19.9, 20.16, 22.21, 23.11, 23.5, 24.27, 24.52, 25.81, 27.09, 28.38, 29.02, 29.92, 30.17, 31.33, 31.84, 32.49, 33.26, 33.36, 36.06, 36.83, 37.08, 37.34, 38.11, 39.14, 40.42, 40.81, 41.32, 42.48, 42.99, 44.27, 44.58, 47.15, 47.79, 47.89, 48.92, 49.56, 49.87, 52.05, 53.08, 53.34, 53.54, 54.96, 55.47, 56.11, 56.37, 58.17, 58.55, 59.84, 59.99, 61.02, 61.66, 62.56, 63.84, 64.0, 64.51, 65.02, 66.18, 66.69, 66.85, 67.87, 68.26, 69.03, 69.67, 69.93, 70.08, 70.98, 72.27, 72.91, 74.19, 75.09, 75.99, 76.89, 78.17, 79.33, 80.48, 81.12, 81.77, 82.02, 85.88, 92.3, 100.0] ################################ # User: ron # Course: 15 # print_user_info Name: Ron Weasley Title: Student Primary Email: [email protected] Bio: I'll do this later # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts # get_course_ids [52, 15, 23] # choose_course 15 # summarize_points Points possible so far: 6000 Points obtained: 4500 Current grade: 75 # summarize_groups * Practical : 70 * Project : 76 * Quiz : 76 # plot_scores [90.0, 63.33, 70.0, 60.0, 70.0, 70.0, 80.0, 100.0, 90.0] # plot_grade_trends ['2017-08-30 16:00:00', '2017-09-08 16:00:00', '2017-09-18 16:00:00', '2017-09-26 16:00:00', '2017-10-05 16:00:00', '2017-10-13 16:00:00', '2017-10-23 16:00:00', '2017-11-01 16:00:00', '2017-11-10 16:00:00', '2017-11-17 16:00:00', '2017-11-27 16:00:00', '2017-12-06 16:00:00', '2017-12-15 16:00:00', '2017-12-25 16:00:00'] [4.86, 10.0, 11.89, 16.76, 18.65, 26.22, 28.92, 34.32, 38.65, 41.35, 48.65, 56.76, 59.46, 86.49] [4.86, 10.0, 11.89, 16.76, 18.65, 26.22, 26.22, 26.22, 30.54, 33.24, 40.54, 40.54, 40.54, 40.54] [5.41, 13.51, 16.22, 24.32, 27.03, 37.84, 40.54, 45.95, 51.35, 54.05, 62.16, 70.27, 72.97, 100.0] ################################ # User: ron # Course: 23 # print_user_info Name: Ron Weasley Title: Student Primary Email: [email protected] Bio: I'll do this later # filter_available_courses [{'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 52, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Introduction to Programming in Python'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 15, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Potions'}, {'workflow_state': 'available', 'end_at': '2017-12-24T17:06:00Z', 'id': 23, 'start_at': '2017-08-21T17:06:00Z', 'name': 'Defense Against the Dark Arts'}] # print_courses 52 : Introduction to Programming in Python 15 : Potions 23 : Defense Against the Dark Arts # get_course_ids [52, 15, 23] # choose_course 23 # summarize_points Points possible so far: 12825 Points obtained: 9795 Current grade: 76 # summarize_groups * Lab : 71 * Project : 84 * Quiz : 67 # plot_scores [80.0, 80.0, 80.0, 80.0, 100.0, 45.0, 70.0, 60.0, 90.0, 80.0, 70.0, 73.33, 0.0, 40.0, 80.0, 0.0, 75.0, 80.0, 73.33, 85.0, 65.0, 100.0, 90.0, 100.0, 70.0, 50.0, 80.0, 80.0, 85.0, 100.0, 86.67, 60.0, 40.0, 80.0, 70.0, 80.0] # plot_grade_trends ['2017-08-24 16:00:00', '2017-08-25 16:00:00', '2017-08-29 16:00:00', '2017-08-31 16:00:00', '2017-09-04 16:00:00', '2017-09-05 16:00:00', '2017-09-08 16:00:00', '2017-09-11 16:00:00', '2017-09-13 16:00:00', '2017-09-15 16:00:00', '2017-09-18 16:00:00', '2017-09-20 16:00:00', '2017-09-22 16:00:00', '2017-09-25 16:00:00', '2017-09-28 16:00:00', '2017-09-29 16:00:00', '2017-10-03 16:00:00', '2017-10-05 16:00:00', '2017-10-09 16:00:00', '2017-10-10 16:00:00', '2017-10-13 16:00:00', '2017-10-16 16:00:00', '2017-10-18 16:00:00', '2017-10-20 16:00:00', '2017-10-23 16:00:00', '2017-10-25 16:00:00', '2017-10-27 16:00:00', '2017-10-30 16:00:00', '2017-11-02 16:00:00', '2017-11-03 16:00:00', '2017-11-07 16:00:00', '2017-11-09 16:00:00', '2017-11-13 16:00:00', '2017-11-14 16:00:00', '2017-11-17 16:00:00', '2017-11-20 16:00:00', '2017-11-22 16:00:00', '2017-11-24 16:00:00', '2017-11-27 16:00:00', '2017-11-29 16:00:00', '2017-12-01 16:00:00', '2017-12-04 16:00:00', '2017-12-07 16:00:00', '2017-12-08 16:00:00', '2017-12-12 16:00:00', '2017-12-14 16:00:00', '2017-12-18 16:00:00', '2017-12-19 16:00:00', '2017-12-22 16:00:00', '2017-12-25 16:00:00'] [0.53, 1.6, 2.14, 2.67, 6.68, 7.28, 8.22, 8.62, 9.22, 9.76, 10.69, 13.63, 13.63, 14.03, 15.63, 15.63, 16.64, 19.84, 25.72, 26.86, 27.53, 28.4, 32.41, 33.01, 33.67, 35.01, 35.48, 36.15, 36.68, 37.75, 38.89, 39.55, 43.03, 43.83, 44.1, 44.63, 46.64, 47.97, 48.64, 49.11, 49.78, 51.11, 51.65, 52.32, 52.98, 54.32, 55.32, 73.14, 86.5, 86.5] [0.53, 1.6, 2.14, 2.67, 6.68, 7.28, 8.22, 8.62, 9.22, 9.76, 10.69, 13.63, 13.63, 14.03, 15.63, 15.63, 16.64, 19.84, 25.72, 26.86, 26.86, 27.73, 31.74, 32.34, 33.01, 33.01, 33.47, 34.14, 34.68, 35.75, 36.88, 37.55, 41.02, 41.83, 42.09, 42.63, 42.63, 42.63, 42.63, 43.1, 43.1, 43.1, 43.63, 43.63, 43.63, 43.63, 43.63, 43.63, 43.63, 43.63] [0.67, 2.0, 2.67, 3.34, 7.35, 8.69, 10.02, 10.69, 11.36, 12.03, 13.36, 17.37, 18.04, 19.04, 21.05, 21.71, 23.05, 27.06, 35.08, 36.41, 37.08, 38.42, 42.43, 43.1, 43.76, 45.1, 45.77, 47.1, 47.77, 49.11, 50.45, 51.11, 55.12, 56.46, 57.13, 57.8, 59.8, 61.14, 61.8, 62.47, 63.14, 64.48, 65.14, 65.81, 66.48, 67.82, 68.82, 86.64, 100.0, 100.0]

test_my_solution.py

''' This file is used to test your implementation of `canvas_analyzer.py` for project 6 (Canvas Analyzer). You should not modify this file, because the instructor will be running your code against the original version anyway. @author: acbart ''' #%% Imports __version__ = 7 import unittest from unittest.mock import patch, mock_open, MagicMock import sys import os import io import bisect import string import textwrap import traceback import pprint from hashlib import md5 import inspect import re import ast import copy from datetime import datetime, timedelta import types #%% Top-level constants __file__ = inspect.getfile(lambda:None) # Settings FINAL_MODE = False FULL_TRACE = False STRICT_VERSIONS = True # Value checking tolerance TOLERANCE = 2 TIME_TOLERANCE = timedelta(seconds=31) # Display options MAXIMUM_VALUE_LENGTH = 120 # File validation STUDENT_FILENAME = "canvas_analyzer.py" SAMPLE_OUTPUT_FILENAME = "sample_output.txt" SAMPLE_OUTPUT_HASH = "197688191079b4fca95b00091886d4df" CANVAS_PY_HASH = "7099e31181b55d2a789e8a4dbab1a2de" CONGRATULATIONS = """CONGRATULATIONS! Your solution passes all the unit tests. Clean and organize your code, so that it looks nice. Check the style guide for recommendations on organization. Then, submit on Web-CAT. Outstanding work on writing all that code :) """ #%% Mocks class MockPlt: ''' Mock MatPlotLib library that can be used to capture plot data. ''' def __init__(self): self._reset_plots() def show(self, **kwargs): self.plots.append(self.active_plot) self._reset_plot() def unshown_plots(self): return self.active_plot['data'] def __repr__(self): return repr(self.plots) def __str__(self): return str(self.plots) def _reset_plots(self): self.plots = [] self._reset_plot() def _reset_plot(self): self.active_plot = {'data': [], 'xlabel': None, 'ylabel': None, 'title': None, 'legend': False} def hist(self, data, **kwargs): label = kwargs.get('label', None) self.active_plot['data'].append({'type': 'Histogram', 'values': data, 'label': label}) def plot(self, xs, ys=None, **kwargs): label = kwargs.get('label', None) if ys == None: self.active_plot['data'].append({'type': 'Line', 'x': range(len(xs)), 'y': xs, 'label': label}) else: self.active_plot['data'].append({'type': 'Line', 'x': xs, 'y': ys, 'label': label}) def scatter(self, xs, ys, **kwargs): label = kwargs.get('label', None) self.active_plot['data'].append({'type': 'Scatter', 'x': xs, 'y': ys, 'label': label}) def xlabel(self, label, **kwargs): self.active_plot['xlabel'] = label def title(self, label, **kwargs): self.active_plot['title'] = label def ylabel(self, label, **kwargs): self.active_plot['ylabel'] = label def legend(self, **kwargs): self.active_plot['legend'] = True def _add_to_module(self, module): for name, value in self._generate_patches().items(): setattr(module, name, value) def _generate_patches(self): def dummy(**kwargs): pass return dict(hist=self.hist, plot=self.plot, scatter=self.scatter, show=self.show, xlabel=self.xlabel, ylabel=self.ylabel, title=self.title, legend=self.legend, xticks=dummy, yticks=dummy, autoscale=dummy, axhline=dummy, axhspan=dummy, axvline=dummy, axvspan=dummy, clf=dummy, cla=dummy, close=dummy, figlegend=dummy, figimage=dummy, suptitle=dummy, text=dummy, tick_params=dummy, ticklabel_format=dummy, tight_layout=dummy, xkcd=dummy, xlim=dummy, ylim=dummy, xscale=dummy, yscale=dummy) fake_module = types.ModuleType('matplotlib') fake_module.pyplot = types.ModuleType('pyplot') MOCKED_MODULES = { 'matplotlib': fake_module, 'matplotlib.pyplot': fake_module.pyplot, } mock_plt = MockPlt() mock_plt._add_to_module(fake_module.pyplot) #%% Helper Functions def call_safely(a_function, *args, prompter=None, matplotlib=False, example=None, **kwargs): ''' Call a given function, but patch all the dangerous built-ins. Params: a_function (function): The function to call. prompter (generator): A generator to supply Returns: function: The wrapped function, ready to be called (without parameters) ''' if prompter is None: def prompter(prompt=''): return '' parameters = copy.deepcopy(kwargs) mock_plt._reset_plots() @patch('sys.stdout', new_callable=io.StringIO) @patch('builtins.input', prompter) @patch('time.sleep', return_value=None) @patch('builtins.open', mock_open()) @patch('requests.get', side_effect=_requests_not_usable) @patch.dict(sys.modules, MOCKED_MODULES) def wrapped_function(r, sleep, stdout): input_failed = False try: result = a_function(*args, **kwargs) except StopIteration: input_failed = True result= None except Exception as e: if example is None: code = _demonstrate_call(a_function, parameters) else: code = example _raise_improved_error(e, code) if matplotlib: return result, stdout.getvalue(), input_failed, mock_plt else: return result, stdout.getvalue(), input_failed return wrapped_function() def _append_to_error(e, message): e.args = (e.args[0]+message,) return e def _demonstrate_complex_call(call, conditions): code = ".\n\nThe error above occurred when I ran:\n" code += indent4(call) code += "\nWhere:\n" defs = '' for key, value in conditions.items(): value = pprint.pformat(value, indent=2, compact=True) if len(value) >= MAXIMUM_VALUE_LENGTH: value = value[:MAXIMUM_VALUE_LENGTH-3] + "..." defs += "{} is {}\n".format(key, value) code += indent4(defs) return code def indent4(a_string): return textwrap.indent(a_string, ' '*4) def _run_example(*lines, **format_values): code = ".\n\nThe error above occurred when I ran:\n" code += indent4('\n'.join(lines)) return code.format(**format_values) class FormatPrinter(pprint.PrettyPrinter): def __init__(self, formats): if FINAL_MODE: super(FormatPrinter, self).__init__() else: super(FormatPrinter, self).__init__(compact=True) self.formats = formats def format(self, obj, ctx, maxlvl, lvl): if type(obj) in self.formats: return self.formats[type(obj)](obj), 1, 0 return pprint.PrettyPrinter.format(self, obj, ctx, maxlvl, lvl) fp = FormatPrinter({float: lambda f: "{:0.2f}".format(f).rstrip('0').rstrip('.'), int: "{:d}".format}) def _demonstrate_call(a_function, parameters): if not isinstance(a_function, str): a_function = a_function.__name__ defs = "" for key, value in parameters.items(): value = pprint.pformat(value, indent=2, compact=True) if len(value) >= MAXIMUM_VALUE_LENGTH: value = value[:MAXIMUM_VALUE_LENGTH-3] + "..." defs += "{} = {}\n".format(key, value) arguments = ", ".join(parameters.keys()) code = "{defs}{name}({arguments})".format(name=a_function, defs=defs, arguments=arguments) code = code.replace("{", "{{").replace("}", "}}") code = indent4(code) code = ".\n\nThe error above occurred when I ran:\n"+code return code def _raise_improved_error(e, code): if isinstance(e, KeyError): raise _copy_key_error(e, code) from None else: raise _append_to_error(e, code) def _copy_key_error(e, code): new_args = (repr(e.args[0])+code,) new_except = _KeyError(*new_args) new_except.__cause__ = e.__cause__ new_except.__traceback__ = e.__traceback__ new_except.__context__ = e.__context__ return new_except def _make_inputs(*input_list, repeat=None): ''' Helper function for creating mock user input. Params: input_list (list of str): The list of inputs to be returned Returns: function (str=>str): The mock input function that is returned, which will return the next element of input_list each time it is called. ''' generator = iter(input_list) def mock_input(prompt=''): print(prompt) try: return next(generator) except StopIteration as SI: if repeat is None: raise SI else: return repeat return mock_input #: A basic identify function that returns the given argument unchanged IDENTITY_FUNCTION = lambda x: x def _skip_unless_callable(module, function_name): ''' Helper function to test if the student has defined the relevant function. This is meant to be used as a decorator. Params: function_name (str): The name of the function to test in student_main. Returns: function (x=>x): Either the identity function (unchanged), or a unittest.skip function to let you skip over the function's tests. ''' if FINAL_MODE: return IDENTITY_FUNCTION msg = ("You have not defined `{0}` in {1} as a function." ).format(function_name, STUDENT_FILENAME) if hasattr(module, function_name): if callable(getattr(module, function_name)): return IDENTITY_FUNCTION return unittest.skip(msg) return unittest.skip(msg) # Prevent certain code constructs class ProjectRulesViolation(Exception): pass # No cheap built-ins def no_builtins_exception(name): def f(*args, **kwargs): raise ProjectRulesViolation('Error! You seem to have used a builtin function.') return f def _human_type(a_value): return {list: 'List', dict: 'Dictionary', int: 'Integer', str: 'String', float: 'Float', bool: 'Boolean' }.get(type(a_value), type(a_value)) def _clean_output(output): output = output.replace("{", "{{").replace( "}", "}}") return textwrap.indent(output, ' | ') punctuation_table = str.maketrans(string.punctuation, ' '*len(string.punctuation)) def _normalize_string(a_string, numeric_endings=False): # Lower case a_string = a_string.lower() # Remove trailing decimals (TODO: How awful!) if numeric_endings: a_string = re.sub(r"(\s*[0-9]+)\.[0-9]+(\s*)", r"\1\2", a_string) # Remove punctuation a_string = a_string.translate(punctuation_table) # Split lines lines = a_string.split("\n") normalized = [[piece for piece in line.split()] for line in lines] normalized = [[piece for piece in line if piece] for line in normalized if line] return sorted(normalized) _OUTPUT_DIFF_EXPLANATION = """\nI recommend you check the Output Diff above. Check each line above for these symbols: + means a line you need to REMOVE - means a line you need to ADD ? means a line you need to CHANGE ^ means a character you need to CHANGE""" class TestBase(unittest.TestCase): maxDiff = None def __init__(self, methodName='runTest'): super(TestBase, self).__init__(methodName) self.example = None def tearDown(self): self.example = None def make_feedback(self, msg, _call=None, **format_args): msg = msg.format(function=self.shortDescription(), **format_args) msg = "\n\nFeedback:\n"+ textwrap.indent(msg, ' ') if self.example is not None: msg = self.example + msg elif _call is not None: _call = _demonstrate_call(self.shortDescription(), _call) msg = _call.format()+msg return msg def call_safely(self, a_function, *args, prompter=None, matplotlib=False, **kwargs): ''' Wrapper around `call_safey` to automatically include the example parameter ''' return call_safely(a_function, *args, prompter=prompter, matplotlib=matplotlib, example=self.example, **kwargs) def assertSimilarStrings(self, first, second, msg): if _normalize_string(first) != _normalize_string(second): return self.assertEqual(first, second, msg) #msg = self._formatMessage(msg, '%s == %s' % (safe_repr(first), # safe_repr(second))) #return self.failureException(msg) class _KeyError(KeyError): def __str__(self): return BaseException.__str__(self) class TestSuccessHolder(unittest.TextTestResult): ''' Improved TextTestResult that records successes and removes errors that are not directly related to the students' code. ''' def __init__(self, stream, descriptions, verbosity): ''' Initialize the successes list ''' super(TestSuccessHolder, self).__init__(stream, descriptions, verbosity) self.successes = [] def addSuccess(self, test): ''' Add this passing test to the successes list ''' super(TestSuccessHolder, self).addSuccess(test) self.successes.append(test) def _is_relevant_tb_level(self, tb): ''' Determines if the give part of the traceback is relevant to the user. Returns: boolean: True means it is NOT relevant ''' # Are in verbose mode? if FULL_TRACE: return False filename, _, _, _ = traceback.extract_tb(tb, limit=1)[0] # Is the error in this test file? if filename == __file__: return True # Is the error related to a file in this directory? current_directory = os.path.dirname(os.path.realpath(__file__)) if filename.startswith(current_directory): return False # Is the error in a local file? if filename.startswith('.'): return False # Is the error in an absolute path? if not os.path.isabs(filename): return False # Okay, it's not a student related file return True #: Mapping TextTestResult's attributes to messages UNIT_TEST_MESSAGES = [('successes', 'Success!'), ('skipped', 'Skipped (function not defined)'), ('failures', 'Test failed'), ('errors', 'Test error (your code has an error!)'), ('unexpectedSuccesses', 'Unexpected success'), ('expectedFailures', 'Expected failure')] def _run_unit_tests(phases): runner = unittest.TextTestRunner(resultclass=TestSuccessHolder) success = True total_cases = 0 for number, (name, phase) in enumerate(phases): if not FINAL_MODE: print("#"*70) print("Testing Phase {}: {}".format(number, name)) print("Summary: ", end="") sys.stderr.flush() sys.stdout.flush() phase_suite = unittest.TestSuite() phase_suite.addTest(unittest.makeSuite(phase)) total_cases += phase_suite.countTestCases() result = runner.run(phase_suite) for UNIT_TEST_TYPE, MESSAGE in UNIT_TEST_MESSAGES: for case in getattr(result, UNIT_TEST_TYPE): if isinstance(case, tuple): case = case[0] print("\t", case.shortDescription()+":", MESSAGE) success = success and (result.wasSuccessful() and not result.skipped) if not FINAL_MODE and not success: break sys.stderr.flush() sys.stdout.flush() if success: print("") print(CONGRATULATIONS) def _requests_not_usable(): raise Exception("Requests is not available on Web-CAT. " "Your code attempted to access the actual Canvas site." "You should not do that on here.") def _validate_python_file(a_filename): # Dummy prompter avoids the most common student error # TODO: Extract this, make it more elegant. dummy_prompter = _make_inputs('52', '', repeat='') try: return call_safely(__import__, a_filename[:-3], prompter=dummy_prompter, matplotlib=True) except ImportError as e: if e.name == a_filename: message = ('Error! Could not find a "{1}" file. ' 'Make sure that there is a "{1}" in the same ' 'directory as "{0}"! Spelling is very ' 'important here.').format(__file__, a_filename) raise Exception(message) else: raise e except Exception as e: message = ("\n\nWhile importing your {0} file, I encountered an error." "\nTry running the file directly and debug the error." "\nMake sure you are not calling input() or a function that" "\n uses input() at the top-level." ).format(a_filename) raise _append_to_error(e, message) def _hash_file(a_filename): with open(a_filename, 'rb') as output_file: hashed= md5(output_file.read()).hexdigest() return hashed def group_by_value(ds, ys): result = {} for d, y in zip(ds, ys): if d not in result: result[d] = [] bisect.insort(result[d], y) return result #%% Check Python Version if sys.version_info <= (3, 0): raise Exception("This code is expected to be run in Python 3.x") #%% Student main file was created student_main, _, _, _ = _validate_python_file(STUDENT_FILENAME) #%% Student has not modified canvas.py inappropriately canvas_requests, _, _, _ = _validate_python_file("canvas_requests.py") if STRICT_VERSIONS and canvas_requests.__version__ != __version__: raise Exception(('The canvas module is not at the same version ({}) ' 'as the version of this unit test file ({}). Make sure ' 'you download the correct versions.' ).format(canvas_requests.__version__, __version__)) if CANVAS_PY_HASH and CANVAS_PY_HASH != _hash_file("canvas_requests.py"): raise Exception(('Error! Hash mismatch. ' 'Make sure that you did not modify your "canvas_requests.py" file.' 'You may want to redownload the file.')) #%% Student has not modified sample_output.txt if not os.access(SAMPLE_OUTPUT_FILENAME, os.F_OK): raise Exception(('Error! Could not find a "{0}" file. ' 'Make sure that there is a \"{0}\" in the same ' 'directory as "{1}.py"! Spelling is very ' 'important here.').format(SAMPLE_OUTPUT_FILENAME, __file__)) if not os.access(SAMPLE_OUTPUT_FILENAME, os.R_OK): raise Exception(('Error! Could not read the "{0}" file. ' 'Make sure that it readable by changing its ' 'permissions. You may need to get help from ' 'your instructor.').format(SAMPLE_OUTPUT_FILENAME, __file__)) if SAMPLE_OUTPUT_HASH and SAMPLE_OUTPUT_HASH != _hash_file(SAMPLE_OUTPUT_FILENAME): raise Exception(('Error! Hash mismatch. ' 'Make sure that you did not modify your "{0}" file.' 'You may want to redownload the file.') .format(SAMPLE_OUTPUT_FILENAME)) #%% Load sample_output.txt with open(SAMPLE_OUTPUT_FILENAME) as output_file: output = output_file.read() C = "#" sections = output.split(C*32) output_version = sections[0].split("\n", maxsplit=1)[0].split()[-1] if STRICT_VERSIONS and str(__version__) != output_version: raise Exception(('Your "{0}" is at a different version ' 'than these unit tests. You should make sure you are ' 'using the latest version of all these files ({1}).') .format(SAMPLE_OUTPUT_FILENAME, __version__)) reference = {} for section in sections[1:]: a_ref = {} user, course = section.split("\n")[1:3] rest = section.split("\n")[3:] user = user.rsplit()[-1] course = course.rsplit()[-1] for line in rest: if line.startswith(C): key = line[2:] a_ref[key] = [] else: a_ref[key].append(textwrap.dedent(line)) reference[(user, course)] = a_ref #%% Assignment-Specific Unit Tests #: List of (String phase name, TestBase) pairs class TestFunctions(TestBase): @_skip_unless_callable(student_main, 'print_user_info') def test_print_user_info(self): "print_user_info" for (user_id, course_id), data in reference.items(): self.example = _run_example("user = canvas_requests.get_user({user_id}!r)", "print_user_info(user)", user_id=user_id) user = canvas_requests.get_user(user_id) _, output, _ = self.call_safely(student_main.print_user_info, user) # Did they print? msg = self.make_feedback("{function} did not print anything.") self.assertNotEqual(output, "", msg=msg) # Did they print the essential components? msg = self.make_feedback("{function} did not print the correct value") expected = reference[(user_id, course_id)]["print_user_info"] expected_joined = '\n'.join(expected) msg = self.make_feedback("{function} did not print the correct value\n" "Expected to see the following lines:\n" "{expected}\n" "Instead only found:\n" "{output}\n", expected=indent4(expected_joined), output=indent4(output)) output = _normalize_string(output) for line in _normalize_string(expected_joined): self.assertIn(line, output, msg=msg) @_skip_unless_callable(student_main, 'filter_available_courses') def test_filter_available_courses(self): "filter_available_courses" for (user_id, course_id), data in reference.items(): self.example = _run_example("courses = canvas_requests.get_courses({user_id!r})", "filter_available_courses(courses)", user_id=user_id) courses = canvas_requests.get_courses(user_id) result, _, _ = call_safely(student_main.filter_available_courses, courses) # Did they return something? msg = self.make_feedback("{function} returned nothing (None).") self.assertIsNotNone(result, msg=msg) # Did they return a list? msg = self.make_feedback("{function} must return a List.\n" "Instead it has returned a {t}", t=_human_type(result)) self.assertIsInstance(result, list, msg=msg) # Was it empty? msg = self.make_feedback("{function} must return a non-empty List.\n" "Instead the list was empty") self.assertTrue(result, msg=msg) # Did they return a list of dictionaries? msg = self.make_feedback("{function} must return a List of Dictionaries.\n" "Instead it returned a List of {t}.", t=_human_type(result[0])) msgContains = self.make_feedback("{function} must return a list of Course dictionaries.\n" "But the course dictionaries were missing the 'id' field.") for index, item in enumerate(result): self.assertIsInstance(item, dict, msg=msg) self.assertIn("id", item, msg=msgContains) # Was the list correct? expected = reference[(user_id, course_id)]["filter_available_courses"][0] expected = ast.literal_eval(expected) expected.sort(key=lambda c: c['id']) result.sort(key=lambda c: c['id']) msg = self.make_feedback("{function} did not return the right list.") self.assertSequenceEqual(expected, result, msg=msg) class TestCourseHandling(TestBase): @_skip_unless_callable(student_main, 'print_courses') def test_print_courses(self): "print_courses" for (user_id, course_id), data in reference.items(): self.example = _run_example("courses = canvas_requests.get_courses({user_id!r})", "courses = filter_available_courses(courses)", "print_courses(courses)", user_id=user_id) courses = canvas_requests.get_courses(user_id) courses, _, _ = call_safely(student_main.filter_available_courses, courses) _, output, _ = call_safely(student_main.print_courses, courses) # Did they print? msg = self.make_feedback("{function} did not print anything.") self.assertNotEqual(output, "", msg=msg) # Did they print the essential components? msg = self.make_feedback("{function} did not print the correct value", user=user) expected = reference[(user_id, course_id)]["print_courses"] expected_joined = '\n'.join(expected) msg = self.make_feedback("{function} did not print the correct value\n" "Expected to see the following lines:\n" "{expected}\n" "Instead only found:\n" "{output}\n", expected=indent4(expected_joined), output=indent4(output)) output = _normalize_string(output) for line in _normalize_string(expected_joined): self.assertIn(line, output, msg=msg) @_skip_unless_callable(student_main, 'get_course_ids') def test_get_course_ids(self): "get_course_ids" for (user_id, course_id), data in reference.items(): self.example = _run_example("courses = canvas_requests.get_courses({user_id!r})", "courses = filter_available_courses(courses)", "get_course_ids(courses)", user_id=user_id) courses = canvas_requests.get_courses(user_id) courses, _, _ = call_safely(student_main.filter_available_courses, courses) result, _, _ = call_safely(student_main.get_course_ids, courses) # Did they return something? msg = self.make_feedback("{function} returned nothing (None).") self.assertIsNotNone(result, msg=msg) # Did they return a list? msg = self.make_feedback("{function} must return a List.\n" "Instead it has returned a {t}", t=_human_type(result)) self.assertIsInstance(result, list, msg=msg) # Was it empty? msg = self.make_feedback("{function} must return a non-empty List.\n" "Instead the list was empty") self.assertTrue(result, msg=msg) # Did they return a list of dictionaries? msg = self.make_feedback("{function} must return a List of Integers.\n" "Instead it returned a List of {t}.", t=_human_type(result[0])) for index, item in enumerate(result): self.assertIsInstance(item, int, msg=msg) # Was the list correct? expected = reference[(user_id, course_id)]["get_course_ids"][0] expected = ast.literal_eval(expected) expected.sort() result.sort() msg = self.make_feedback("{function} did not return the right list.") self.assertSequenceEqual(expected, result, msg=msg) class TestCourseInput(TestBase): @_skip_unless_callable(student_main, 'choose_course') def test_choose_course(self): "choose_course" for (user_id, course_id), data in reference.items(): self.example = _run_example("courses = canvas_requests.get_courses({user_id!r})", "courses = filter_available_courses(courses)", "course_ids = get_course_ids(courses)", "# User intends to type in 10000, -1, and {course_id}", "choose_course(courses)", user_id=user_id, course_id=course_id) courses = canvas_requests.get_courses(user_id) courses, _, _ = call_safely(student_main.filter_available_courses, courses) course_ids, _, _ = call_safely(student_main.get_course_ids, courses) # TODO: Hard mode, include a non-integer input_values= ["10000", "-1", course_id] inputs = _make_inputs(*input_values) result, _, input_failed = call_safely(student_main.choose_course, course_ids, prompter=inputs) # Did the input fail? msg = self.make_feedback("{function} did not stop accepting valid inputs.\n" "Maybe an infinite loop happened?") self.assertFalse(input_failed, msg=msg) # Did they return something? msg = self.make_feedback("{function} returned nothing (None).") self.assertIsNotNone(result, msg=msg) # Did they return an integer? msg = self.make_feedback("{function} must return an Integer.\n" "Instead it has returned a {t}", t=_human_type(result)) self.assertIsInstance(result, int, msg=msg) # Did they return the RIGHT integer? expected = reference[(user_id, course_id)]["choose_course"][0] expected = ast.literal_eval(expected) msg = self.make_feedback("{function} did not return the right value\n" " when I entered the inputs {inputs}.\n" "It should have returned {expected}.\n" "Instead it returned {actual}.", inputs=', '.join(input_values), expected=expected, actual=result) self.assertEqual(expected, result, msg=msg) class TestSubmissions(TestBase): @_skip_unless_callable(student_main, 'summarize_points') def test_summarize_points(self): "summarize_points" for (user_id, course_id), data in reference.items(): self.example = _run_example("submissions = canvas_requests.get_submissions({user_id!r}, {course_id!r})", "summarize_points(submissions)", user_id=user_id, course_id=course_id) submissions = canvas_requests.get_submissions(user_id, course_id) _, output, _ = call_safely(student_main.summarize_points, submissions) # Did they print? msg = self.make_feedback("{function} did not print anything") self.assertNotEqual(output, "", msg=msg) # Did they print the essential components? expected = reference[(user_id, course_id)]["summarize_points"] expected_joined = '\n'.join(expected) msg = self.make_feedback("{function} did not print the correct value\n" "Expected to see the following lines:\n" "{expected}\n" "Instead only found:\n" "{output}\n", expected=indent4(expected_joined), output=indent4(output)) output = _normalize_string(output, numeric_endings=True) for line in _normalize_string('\n'.join(expected), numeric_endings=True): self.assertIn(line, output, msg=msg) @_skip_unless_callable(student_main, 'summarize_groups') def test_summarize_groups(self): "summarize_groups" for (user_id, course_id), data in reference.items(): self.example = _run_example("submissions = canvas_requests.get_submissions({user_id!r}, {course_id!r})", "summarize_groups(submissions)", user_id=user_id, course_id=course_id) submissions = canvas_requests.get_submissions(user_id, course_id) _, output, _ = call_safely(student_main.summarize_groups, submissions) # Did they print? msg = self.make_feedback("{function} did not print anything\n") self.assertNotEqual(output, "", msg=msg) # Did they print the essential components? expected = reference[(user_id, course_id)]["summarize_groups"] expected_joined = '\n'.join(expected) msg = self.make_feedback("{function} did not print the correct value\n" "Expected to see the following:\n" "{expected}\n" "Instead only found:\n" "{output}\n" "Order does not matter, but the words and numbers do.\n" "It's okay to include categories without any graded submissions.", expected=indent4(expected_joined), output=indent4(output)) output = _normalize_string(output, numeric_endings=True) for line in _normalize_string(expected_joined, numeric_endings=True): self.assertIn(line, output, msg=msg) @_skip_unless_callable(student_main, 'plot_scores') def test_plot_scores(self): "plot_scores" for (user_id, course_id), data in reference.items(): self.example = _run_example("submissions = canvas_requests.get_submissions({user_id!r}, {course_id!r})", "plot_scores(submissions)", user_id=user_id, course_id=course_id) submissions = canvas_requests.get_submissions(user_id, course_id) _, output, _, mpl = call_safely(student_main.plot_scores, submissions, matplotlib=True) # Did they forget to plot something? msg = self.make_feedback("{function} has an unshown plot\n" "Make sure you call plt.show().") self.assertFalse(mpl.unshown_plots(), msg=msg) # Do they have no plots? msg = self.make_feedback("{function} is not plotting anything\n") self.assertTrue(mpl.plots, msg=msg) # Do they have too many plots? msg = self.make_feedback("{function} has more than one plot\n") self.assertEqual(len(mpl.plots), 1, msg=msg) # Do they have any data? plot = mpl.plots[0] plot_data = plot['data'] msg = self.make_feedback("{function} is not plotting any data\n") self.assertTrue(plot_data, msg=msg) # Do they have too much data? msg = self.make_feedback("{function} is plotting too many things\n") self.assertEqual(len(plot_data), 1, msg=msg) # Did they make the right kind of plot? histogram = plot_data[0] msg = self.make_feedback("{function} did not produce a Histogram\n") self.assertEqual(histogram['type'], 'Histogram', msg=msg) # Did they plot the right amount of data? expected = reference[(user_id, course_id)]["plot_scores"][0] expected = ast.literal_eval(expected) msg = self.make_feedback("{function} did not plot the right number of things\n" +"You should only plot assignments with points possible\n" +"and with a score that is not None.") self.assertEqual(len(histogram['values']), len(expected), msg=msg) # Did they plot the actual right data actual = sorted(histogram['values']) expected.sort() msg = self.make_feedback("{function} did not plot the right data\n" +" Expected: "+fp.pformat(expected) +"\n Actual: "+fp.pformat(actual)) for i, (student, correct) in enumerate(zip(actual, expected)): self.assertAlmostEqual(student, correct, TOLERANCE, msg=msg) # Did they have titles? msg = self.make_feedback('{function} did not have the right title ("Distribution of Grades")\n') self.assertIsNotNone(plot['title'], msg=msg) self.assertSimilarStrings(plot['title'], "Distribution of Grades", msg=msg) msg = self.make_feedback('{function} did not have the right xlabel ("Grades")\n') self.assertIsNotNone(plot['xlabel'], msg=msg) self.assertSimilarStrings(plot['xlabel'], "Grades", msg=msg) msg = self.make_feedback('{function} did not have the right ylabel ("Number of Assignments").\n') self.assertIsNotNone(plot['ylabel'], msg=msg) self.assertSimilarStrings(plot['ylabel'], "Number of Assignments", msg=msg) @_skip_unless_callable(student_main, 'plot_grade_trends') def test_plot_grade_trends(self): "plot_grade_trends" for (user_id, course_id), data in reference.items(): self.example = _run_example("submissions = canvas_requests.get_submissions({user_id!r}, {course_id!r})", "plot_grade_trends(submissions)", user_id=user_id, course_id=course_id) submissions = canvas_requests.get_submissions(user_id, course_id) _, output, _, mpl = call_safely(student_main.plot_grade_trends, submissions, matplotlib=True) # Did they forget to plot something? msg = self.make_feedback("{function} has an unshown plot\n"+ "Make sure you call plt.show().") self.assertFalse(mpl.unshown_plots(), msg=msg) # Do they have no plots? msg = self.make_feedback("{function} is not plotting anything\n") self.assertTrue(mpl.plots, msg=msg) # Do they have too many plots? msg = self.make_feedback("{function} has more than one plot\n") self.assertEqual(len(mpl.plots), 1, msg=msg) # Do they have any data? plot = mpl.plots[0] plot_data = plot['data'] msg = self.make_feedback("{function} is not plotting any data\n") self.assertTrue(plot_data, msg=msg) # Extract the reference data expecteds = reference[(user_id, course_id)]["plot_grade_trends"] expecteds = [e for e in expecteds if e] dates, highs, lows, maxes = map(ast.literal_eval, expecteds) dates = [datetime.strptime(d, '%Y-%m-%d %H:%M:%S') for d in dates] expected_plots = {'highest': highs, 'lowest': lows, 'maximum': maxes} # Do they have the labels? plotted_labels = [l['label'] for l in plot_data if l['label']] msg = self.make_feedback("{function} has no labels on its plots\n") self.assertGreater(len(plotted_labels), 0, msg=msg) user_plots = {x['label'].lower(): x for x in plot_data if x['label']} msg = self.make_feedback("{function} has an unexpected label\n") for label in user_plots: self.assertIn(label.lower(), expected_plots, msg=msg) # Chck each plot in turn all_plotted = True for label, data in expected_plots.items(): if label not in user_plots: all_plotted = False continue actual = user_plots[label] msg = self.make_feedback("{function} did not produce a line plot for {label}\n", label=label) self.assertEqual(actual['type'], 'Line', msg=msg) # Assert y-axis length msg = self.make_feedback("{function} did not plot correctly\n" +"The {label} line plot had the wrong number of\n" +"elements on the Y-axis.\n" +"You should be plotting every single submission.", label=label) self.assertEqual(len(actual['y']), len(data), msg=msg) # Assert y-axis length msg = self.make_feedback("{function} did not plot correctly\n" +"The {label} line plot had the wrong number of\n" +"elements on the X-axis.\n" +"You should be plotting every single submission.", label=label) self.assertEqual(len(actual['x']), len(dates), msg=msg) # Assert x-axis values matched_xs = zip(sorted(actual['x']), sorted(dates)) for i, (s, c) in enumerate(matched_xs): msg = self.make_feedback("{function} has incorrect X axis values\n" +"You did not plot datetimes for the {label}.\n", label=label) self.assertIsInstance(s, datetime, msg=msg) msg = self.make_feedback("{function} did not plot correctly\n" +"You have plotted the wrong X data for the {label}\n" +"(position {i} in the list)", label=label, i=i) self.assertAlmostEqual(s, c, delta=TIME_TOLERANCE, msg=msg) # Assert y-axis values student_ys = group_by_value(actual['x'], actual['y']) expected_ys = group_by_value(dates, data) for d in dates: # Did they plot all the Y values for this date? msg = self.make_feedback("{function} has incorrect Y axis values\n" +"The {label} line has no values for the date {date}.", label=label,date=d) self.assertIn(d, student_ys, msg=msg) # Did they get the right number of Y values? s_ys = student_ys[d] c_ys = expected_ys[d] msg = self.make_feedback("{function} has incorrect Y axis values\n" +"The {label} line has the wrong number of values for date {date}.", label=label,date=d) self.assertEqual(len(c_ys), len(s_ys), msg=msg) msg = self.make_feedback("{function} has incorrect Y axis values\n" +"The {label} line has the wrong values for date {date}.", label=label,date=d) self.assertAlmostEqual(c_ys[-1], s_ys[-1], TOLERANCE, msg=msg) # Did they actually plot everything? msg = self.make_feedback("{function} does not have 3 labelled line plots\n") self.assertTrue(all_plotted, msg=msg) # Did they have titles? msg = self.make_feedback("{function} did not have the right title\n") self.assertIsNotNone(plot['title'], msg=msg) self.assertSimilarStrings(plot['title'], "Grade Trend", msg=msg) msg = self.make_feedback("{function} did not have the right ylabel\n") self.assertIsNotNone(plot['ylabel'], msg=msg) self.assertSimilarStrings(plot['ylabel'], "Grade", msg=msg) class TestMain(TestBase): @_skip_unless_callable(student_main, 'main') def test_main(self): "main" for (user_id, course_id), data in reference.items(): self.example = _run_example("# User types 10000, -1, {course_id}", "main({user_id!r})", user_id=user_id, course_id=course_id) input_values= ["10000", "-1", course_id] inputs = _make_inputs(*input_values) _,output,i,mpl = call_safely(student_main.main, user_id, prompter=inputs, matplotlib=True) # Did they terminate? msg = self.make_feedback("{function} did not stop accepting valid inputs.") self.assertFalse(i, msg=msg) # Did they print? msg = self.make_feedback("{function} did not print anything.") self.assertNotEqual(output, "", msg=msg) # Did they print the essential components? msg = self.make_feedback("{function} did not print the correct value") expected = reference[(user_id, course_id)] printing_functions = ['print_user_info', 'print_courses', 'summarize_points', 'summarize_groups'] printing_functions = [f for f in printing_functions if hasattr(student_main, f)] for function_name in printing_functions: expected_joined = '\n'.join(expected[function_name]) msg = self.make_feedback("{function} did not print the correct value\n" "Expected to see the following lines:\n" "{expected}\n" "Instead only found:\n" "{output}\n" "Perhaps the {inner} function was not called?", expected=indent4(expected_joined), output=indent4(output), inner=function_name) norm_out = _normalize_string(output, numeric_endings=True) for line in _normalize_string(expected_joined, numeric_endings=True): self.assertIn(line, norm_out, msg=msg) # Did they have plots? plotting_functions = ['plot_scores', 'plot_grade_trends'] plotting_functions = [f for f in plotting_functions if hasattr(student_main, f)] msg = self.make_feedback("{function} did not create the right number of plots.\n", "You were supposed to only need {plots}", plots=len(plotting_functions)) self.assertEqual(len(mpl.plots), len(plotting_functions), msg=msg) # Did they call all the functions they needed? all_functions = (printing_functions + plotting_functions + ['filter_available_courses', 'get_course_ids', 'choose_course', ]) main_declaration = inspect.getsource(student_main.main) all_nodes = ast.walk(ast.parse(main_declaration)) all_calls = [node.func.id for node in all_nodes if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)] for function_name in all_functions: if hasattr(student_main, function_name): msg = self.make_feedback("{function} did not seem to call {inner}.", inner=function_name) self.assertIn(function_name, all_calls, msg=msg) _TEST_PHASES = [("User Info and Course Filter", TestFunctions), ("Course Handling", TestCourseHandling), ("Course Input", TestCourseInput), ("Submissions", TestSubmissions), ("Main Function", TestMain)] #%% Run unit tests if __name__ == "__main__": _run_unit_tests(_TEST_PHASES)