Skip to content
Snippets Groups Projects
Commit 68975743 authored by Christopher Bohn's avatar Christopher Bohn :thinking:
Browse files

Initial commit

parents
Branches
No related tags found
No related merge requests found
Showing
with 368 additions and 0 deletions
# Project-specific
config.py
# Mac file finder metadata
.DS_Store
# Windows file metadata
._*
# Thumbnail image caches
Thumbs.db
ethumbs.db
# MS Office temporary file
~*
# Emacs backup file
*~
# Common
[Bb]in/
[Bb]uild/
[Oo]bj/
[Oo]ut/
[Tt]mp/
[Xx]86/
[Ii][Aa]32/
[Xx]64/
[Xx]86_64/
[Xx]86-64/
[Aa]rm
[Aa]32
[Tt]32
[Aa]64
*.tmp
*.bak
*.bk
*.swp
# Python files
*.pyc
*.pyo
__pycache__/
# JetBrains (IntelliJ IDEA, PyCharm, etc) files
.idea/
cmake-build-*/
*.iml
*.iws
*.ipr
venv/
README-images/MyRed-class-roster.png

42.1 KiB

README-images/assginment-statistics.png

574 KiB

README-images/canvas-account-settings.png

105 KiB

README-images/canvas-course-number.png

63.4 KiB

README-images/canvas-token-details.png

174 KiB

README-images/config-py.png

38 KiB

README-images/course-data.png

61.8 KiB

README-images/not-the-same-package.png

118 KiB

README-images/selecting-a-course.png

314 KiB

README-images/selecting-an-assignment.png

677 KiB

README.md 0 → 100644
# Analyze Grades
This program will produce statistics, by major, for selected assignments in
selected courses.
## Preparation
### Dependencies
You need Python. I think Python 3.6 and newer will work fine.
You need the `canvasapi` module. Run
```
pip install canvasapi
```
*Note*: I'm pretty sure this is **not** the same module that Chris Bourke
demonstrated a couple of years ago.
![Not the same package](README-images/not-the-same-package.png)
### Getting a Canvas API Key
To interface with Canvas, you need a Canvas API Key. Log into Canvas and click
on the *Account* icon in the left gutter. In the resulting menu, click on
*Settings*.
![Go to your Canvas account settings](README-images/canvas-account-settings.png)
On your account settings page, scroll down until you see the `+ New Access
Token` button. Click on that button. Fill out the fields in the popup window
and click on `Generate Token`. You will then get a new popup window with the
token's details. ***DO NOT close this window yet***.
![Canvas API Key details](README-images/canvas-token-details.png)
Copy the `config-example.py` file to `config.py` and open `config.py` for
editing. Delete the text *Your Canvas API Key goes here* (leave the opening-
and closing-quotes). Copy the token from your browser's popup window and paste
it between the opening- and closing-quotes in `config.py`.
![Copy/Paste your Canvas API Key into config.py](README-images/config-py.png)
### Getting Students' Majors
I don't currently know of a public API to pull data from MyRed. Maybe there's
an unadvertised API that ITS can provide us, but I threw this code together in
a couple of hours and it's easier for now to use what we have available.
Log into MyRed and navigate to the [Faculty -> Class Roster page](https://myred.nebraska.edu/psc/myred/NBL/HRMS/s/WEBLIB_DSHBOARD.ISCRIPT1.FieldFormula.IScript_GETPAGE?faculty1&path=faculty1.main.cr).
On the right-side under *Filters*, select the course whose roster you're
going to download. Click on the `Apply Filters` button. Then click on the
*Download* link.
![Downloading class rosters](README-images/MyRed-class-roster.png)
Do this for each of your courses that you're going to analyze. You should be
fine pulling just the lecture sections. If you combined multiple sections into
a single Canvas course, then you can do the same for your MyRed rosters. Run
```
dos2unix download.csv
```
to remove unwanted unprintable characters. Then move the csv file into the
`rosters/semester` subdirectory (making the appropriate substituion for
*semester*) and rename it with the course number, such as `csce231.csv`.
Repeat for each of your courses that you want to analyze.
### Edit `rosters.csv`
Open `rosters/rosters.csv` for editing. After the header row, each row
corresponds to a semester. In the first column, give a plaintext name for the
semester, and in the second column specify the subdirectory that contains the
MyRed rosters for that semester. Every three columns thereafter correspond to
a specific course: a plaintext name for the course, the filename of the csv
file with the course's MyRed roster, and the course's Canvas ID.
![Obtaining the Canvas Course ID](README-images/canvas-course-number.png)
![Placing the Canvas Course ID in rosters.csv](README-images/course-data.png)
If you use Excel to edit `rosters.csv`, pass rosters.csv` through `dos2unix`
when you're finished (even on Mac, Excel sometimes adds an unprintable Unicode
character at the start of csv files that can confuse my code).
## Run the program
Start the program with
```
python analyze_grades.py
```
Select the appropriate semester. You will then be presented with a list of the
courses for that semester that you included in `rosters.csv`. Select one.
![Select the course to analyze](README-images/selecting-a-course.png)
You'll then be presented with an assignment group, and after you selet one,
you'll be presented with a list of assignments in that group. Select one.
![Select the assignment to analyze](README-images/selecting-an-assignment.png)
After you select the assignment, you'll be provided the statistics for it. (Be
patient, as the Canvas queries can a few seconds.) You will then be looped back
to selecting a course. (Yes, I know there's an obvious usability optimization
here to stick with one course until you're done with it -- again, I threw this
together in a couple of hours.)
![Getting assignment statistics](README-images/assginment-statistics.png)
Right now this program will only analyze a full assignment. If you're using a
particular question off of an exam or quiz, you're going to have to do that
analysis manually for now. I know how to get data from individual Canvas quiz
questions -- so if we want to use this program going forward then I can make
that happen, but for my immediate purposes it wasn't necessary so I didn't add
that code.
## For the curious, the code
- `analyze_grades.py` contains the `main` function and is pretty simple code,
under 100 lines. I hope it's readable enough that I don't need to describe
it.
- `config.py` you've already met.
- `majors.py` defines a data structure to hold information about majors and
then defines our three majors.
- `semester.py` defines the `Course` and `Semester` classes for
`analyze_grades.py`
- `common_functions.py` has a few utility functions that I made for another
project. I only use one of them here.
- `api/canvas_classes.py` is also from another project; it has a slew of
classes that use the `canvasapi` package.
\ No newline at end of file
import csv
import io
import statistics
import sys
from typing import List, Dict, KeysView, Optional
from api.canvas_classes import CanvasAssignment, CanvasAssignmentGroup, CanvasCourse, CanvasUser
from common_functions import select_from_list
from majors import Major
from semester import *
rosters: str = 'rosters/rosters.csv'
def load_semester() -> Semester:
selected_semester: Dict[str, str]
with open(rosters, mode='r') as csv_file:
csv_reader: csv.DictReader = csv.DictReader(csv_file)
candidate_semester_names: List[str] = []
candidate_semesters: List[Dict[str, str]] = []
for candidate_semester in csv_reader:
candidate_semester_names.append(candidate_semester['SEMESTER'])
candidate_semesters.append(candidate_semester)
selection: str = select_from_list(candidate_semester_names, 'semester')
selected_semester = [s for s in candidate_semesters if s['SEMESTER'] == selection][0]
desired_semester: Semester = Semester(selected_semester['SEMESTER'], selected_semester['SUBDIRECTORY'])
course_number: int = 1
more_courses_remain: bool = True
semester_keys: KeysView[str] = selected_semester.keys()
while more_courses_remain:
course_key: str = f'COURSE {course_number}'
file_key: str = f'FILENAME {course_number}'
canvas_id_key: str = f'CANVAS ID {course_number}'
if course_key not in semester_keys:
more_courses_remain = False
elif selected_semester[course_key] == '':
more_courses_remain = False
else:
desired_semester.add_course(Course(selected_semester[course_key],
selected_semester[canvas_id_key], selected_semester[file_key]))
course_number += 1
return desired_semester
def load_roster(filename: str) -> List[Dict[str, str]]:
file = open(filename, mode='r')
file_string: str = ''
for line in file: # MyRed csv files have blank leading line
file_string = file_string if line == '\n' else f'{file_string}{line}'
file.close()
return [student for student in csv.DictReader(io.StringIO(file_string))]
def partition_into_majors(class_roster: List[Dict[str, str]], canvas_students: List[CanvasUser]) -> \
Dict[str, List[CanvasUser]]:
all_majors_students: Dict[str, List[CanvasUser]] = {}
for major in Major.majors:
print(f'\tProcessing {major.name} students...', end='')
sys.stdout.flush()
major_students: List[CanvasUser] = []
for student in class_roster:
student_majors: Set[str] = set(student['CPP'].split(', '))
if len(student_majors.intersection(major.abbreviations)) > 0:
possible_canvas_student: List[CanvasUser] = \
[s for s in canvas_students if s.get_nuid() == int(student['EMPLID'])]
if len(possible_canvas_student) > 0:
major_students.append(possible_canvas_student[0])
all_majors_students[major.name] = major_students
print(f'{len(all_majors_students[major.name])} students')
return all_majors_students
def print_statistics(assignment: CanvasAssignment, students: Dict[str, List[CanvasUser]]) -> None:
points_possible: float = assignment.get_points_possible()
for major in students.keys():
scores: List[float] = [assignment.get_score(student) for student in students[major]
if assignment.get_score(student) is not None]
try:
average_score: float = statistics.mean(scores)
scaled_average_score: float = 100 * average_score / points_possible
print(f'{major:27}students:{len(students[major]):>3} scaled mean score: {scaled_average_score:.2f}%')
except statistics.StatisticsError as exception:
print(f'{major:27}students:{len(students[major]):>3} no mean score computed: {exception}')
if __name__ == '__main__':
semester: Semester = load_semester()
course: Optional[Course] = select_from_list(list(semester.courses), 'course', none_is_option=True)
while course is not None:
print(f'Processing {course} for {semester}...')
roster: List[Dict[str, str]] = load_roster(f'rosters/{semester.subdirectory}/{course.roster_file}')
canvas_course: CanvasCourse = CanvasCourse(int(course.canvas_course_id))
major_partition: Dict[str, List[CanvasUser]] = partition_into_majors(roster, canvas_course.get_students())
assignment_group: CanvasAssignmentGroup = \
select_from_list(canvas_course.get_assignment_groups(), 'assignment group')
oat_assignment: CanvasAssignment = select_from_list(assignment_group.get_assignments(), 'assignment')
print_statistics(oat_assignment, major_partition)
print()
course: Optional[Course] = select_from_list(list(semester.courses), 'course', none_is_option=True)
This diff is collapsed.
import datetime
import re
from typing import List, Optional, TypeVar
ChoiceType = TypeVar("ChoiceType")
def select_from_list(choices: List[ChoiceType], choice_name: str, none_is_option: bool = False) -> ChoiceType:
query_user = True
return_value: ChoiceType = None
while query_user:
print(f'Choose the {choice_name} from this list:')
number_of_choices = len(choices)
for i in range(number_of_choices):
print(f'{i + 1})\t{choices[i]}'.expandtabs(4))
if none_is_option:
print('0)\tNone of the above'.expandtabs(4))
selection = input('Enter selection: ')
try:
selection_value = int(selection)
if none_is_option and selection_value == 0:
return_value = None
query_user = False
elif selection_value < 1 or selection_value > number_of_choices:
raise ValueError('Selection out of range.')
else:
return_value = choices[selection_value - 1]
query_user = False
except ValueError:
print(f'\tSelection must be an integer between {0 if none_is_option else 1} '
f'and {number_of_choices}, inclusive!')
return return_value
def strip_html(text: Optional[str]) -> Optional[str]:
if text is not None:
return re.sub(re.compile('<.*?>'), '', text)
else:
return None
def semester_stamp() -> str:
today = datetime.date.today()
year: int = today.year
month: int = today.month
# Normalize month to semester start
if month < 5:
month = 1
elif month < 8:
month = 5
else:
month = 8
return f'{year}-0{month}'
class Config:
# Canvas API configuration
canvas_url = 'https://canvas.unl.edu/'
canvas_api_key = 'Your Canvas API Key goes here'
from typing import Set
class Major:
majors: Set["Major"] = set()
def __init__(self, name: str, abbreviations: Set[str]):
self.name: str = name
self.abbreviations: Set[str] = abbreviations
Major.majors.add(self)
Major('Computer Science', {'COMP-BS', 'COMP-BA', 'COMP-MAJ', 'COMP-BSCS'}) # we don't yet have BSCS but I expect we soon will
Major('Computer Engineering', {'CENG-BSCP'})
Major('Software Engineering', {'SOFT-BSSE'})
# Rosters directory
* Edit `rosters.csv` so that each row corresponds to a semester. In the first
column, give a plaintext name for the semester, and in the second column
specify the subdirectory that will contain the MyRed rosters for that
semester. Every three columns thereafter correspond to a specific course: a
plaintext name for the course, the filename of the csv file with the
course's MyRed roster, and the course's Canvas ID.
![Obtaining the Canvas Course ID](../README-images/canvas-course-number.png)
![Placing the Canvas Course ID in rosters.csv](../README-images/course-data.png)
If you use Excel to edit `rosters.csv`, run `dos2unix rosters.csv` when
you're finished (Excel sometimes adds an unprintable Unicode character at
the start of csv files that can confuse my code.)
* [Download course rosters from MyRed](https://myred.nebraska.edu/psc/myred/NBL/HRMS/s/WEBLIB_DSHBOARD.ISCRIPT1.FieldFormula.IScript_GETPAGE?faculty1&path=faculty1.main.cr)
and place them into the appropriate subdirectory. If you combined multiple
sections into a single Canvas course, combine the rosters into a single csv
file. For good measure, pass the csv files through `dos2unix`. MyRed might
not add that Unicode character, but better safe than sorry.
\ No newline at end of file
*.csv
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment