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

Eliminated need for csv files with student majors and course lists

parent ae11cbe1
No related branches found
No related tags found
No related merge requests found
README-images/assginment-statistics.png

574 KiB

README-images/assignment-statistics.png

728 KiB

README-images/selecting-a-course.png

314 KiB | W: | H:

README-images/selecting-a-course.png

500 KiB | W: | H:

README-images/selecting-a-course.png
README-images/selecting-a-course.png
README-images/selecting-a-course.png
README-images/selecting-a-course.png
  • 2-up
  • Swipe
  • Onion skin
README-images/selecting-an-assignment.png

677 KiB | W: | H:

README-images/selecting-an-assignment.png

1.01 MiB | W: | H:

README-images/selecting-an-assignment.png
README-images/selecting-an-assignment.png
README-images/selecting-an-assignment.png
README-images/selecting-an-assignment.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -41,44 +41,13 @@ it between the opening- and closing-quotes in `config.py`.
### 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 substitution for
*semester*) and rename it with the course number, such as `csce231.csv`.
Repeat for each of your courses that you want to analyze.
You no longer need to manually retrieve students' majors from MyRed. The program
now retrieves students' majors from directory.unl.edu.
### 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).
You no longer need to maintain a rosters file. The program now retrieves your
courses from Canvas for a specified semester.
## Run the program
......@@ -86,23 +55,35 @@ 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.
You will be prompted to confirm the program's best-guess of the semester you
want to analyze. Confirm the semester or select a different semester. You will
then be presented with a list of the courses that you are associated with for
that semester. (Note that this is now based on Canvas courses, so if you
combined courses then it will only present the course(s) as it/they appear in
Canvas.) 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.
Please wait patiently at ths step. To determine each student's major, the
program must access Canvas to determine their login name and then must access
the UNL directory to determine their major(s). Canvas queries in particular can
take longer than you might like. There is a progress bar to reassure you that
the program is, well, progressing.
After the students' majors have been retrieved, you'll then be presented with an
assignment group, and after you select 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 take 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.)
After you select the assignment, you'll be provided the statistics for it. (Again,
please be patient, as the Canvas queries can take a few seconds.)
![Getting assignment statistics](README-images/assginment-statistics.png)
![Getting assignment statistics](README-images/assignment-statistics.png)
You will then be given then option to select another assignment from the same
course. If you choose not to do so, then you will be given the option to select
a different course.
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
......@@ -111,16 +92,36 @@ 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.
## Notes
- The program will provide statistics for *all* computing majors, explicitly
stating when there are no students in a particular computing major in the
course. The program will also provide statistics for non-computing majors
represented in the course: even though this is not required for OAT, you may
find it interesting. (Due to students taking multiple majors, the numbers of
students may exceed the number of students in the course.) The program does
*not* report statistics for computing minors.
- For majors with particularly long names (*e.g.*, "Criminology and Criminal
Justice"), the formatting may be a little off.
- The program only works at the assignment level. If you are making an OAT
assessment with a specific quiz/exam question (and the quiz/exam is a Canvas
quiz), let me know and I'll add question-level functionality.
- **I *will* be adding the option to break a course out by sections
(effectively undoing course merges). This is principally for the capstone
program so that I can separate-out students in their first capstone year from
those in their second capstone year. Perhaps it will benefit you, too.**
## 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.
a hair over 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`
then defines our three computing majors as well business majors (so that the
program lumps Raikes students in business majors with non-Raikes students in
business majors).
- `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
......
import csv
import io
import datetime
import statistics
import sys
from typing import List, Dict, KeysView, Optional
from typing import List, Optional, Dict, Set
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
def create_semester_filter() -> str:
today = datetime.date.today()
year: int = today.year
month: int = today.month
semester: str
# Normalize month to term start, recognizing the assessment may not happen until the month following the term
if month == 1:
year -= 1
month = 8
semester = 'Fall'
elif month < 6:
month = 1
semester = 'Spring'
elif month < 9:
month = 5
semester = 'Summer'
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
month = 8
semester = 'Fall'
answer = input(f'Analyze grades for {semester} {year}? [Y/n] ')
if answer != '' and answer[0].upper() == 'N':
year = 0
while year == 0:
try:
answer = input('Enter year: ')
year = int(answer)
except ValueError:
print(f'\t"{answer}" is not a valid response; '
f'please enter the year as an integer (e.g., {datetime.date.today().year}).')
semester = select_from_list(['Spring', 'Summer', 'Fall'], 'semester')
if semester == 'Spring':
month = 1
elif semester == 'Summer':
month = 5
else:
month = 8
return f'1{str(year)[-2:]}{month}' # introducing a y2.1k problem
def print_statistics(assignment: CanvasAssignment, students: Dict[str, List[CanvasUser]]) -> None:
def print_statistics_for_some_majors(assignment: CanvasAssignment, majors: Set[Major],
major_partitions: Dict[Major, Set[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]
for major in majors:
scores: List[float] = [assignment.get_score(student) for student in major_partitions[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}%')
print(f'{major.name:27}students:{len(major_partitions[major]):>3} '
f'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}')
print(f'{major.name:27}students:{len(major_partitions[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())
def print_statistics(assignment: CanvasAssignment, major_partitions: Dict[Major, Set[CanvasUser]]) -> None:
print(f'Statistics for {assignment}:')
computing_majors: Set[Major] = {major for major in Major.majors if major.is_computing_major}
non_computing_majors: Set[Major] = {major for major in major_partitions.keys() if not major.is_computing_major}
print_statistics_for_some_majors(assignment, computing_majors, major_partitions)
print_statistics_for_some_majors(assignment, non_computing_majors, major_partitions)
def assess_assignments(course: CanvasCourse, major_partitions: Dict[Major, Set[CanvasUser]]) -> None:
print('You may now select the assignments to assess.')
answer: str = 'Yes'
while answer == '' or answer[0].upper() != 'N':
assignment_group: CanvasAssignmentGroup = \
select_from_list(canvas_course.get_assignment_groups(), 'assignment group')
select_from_list(course.get_assignment_groups(), 'assignment group')
oat_assignment: CanvasAssignment = select_from_list(assignment_group.get_assignments(), 'assignment')
print_statistics(oat_assignment, major_partition)
print_statistics(oat_assignment, major_partitions)
print()
answer = input('Assess additional assignments? [Y/n] ')
def assess_course(course: CanvasCourse) -> None:
print(f'Assessing {course}.')
students: List[CanvasUser] = course.get_students()
print(f'Retrieved {len(students)} students.')
major_partitions: Dict[Major, Set[CanvasUser]] = {}
print('Retrieving majors ', end='')
sys.stdout.flush()
# Force all computing majors to be in the dictionary
for major in filter(lambda m: m.is_computing_major, Major.majors):
major_partitions[major] = set()
# Add students to the dictionary, as well as the non-computing majors represented in the course
for student in students:
print('.', end='')
sys.stdout.flush()
student_majors: Set[Major] = Major.get_student_majors(student.get_username())
for major in student_majors:
if major not in major_partitions.keys():
major_partitions[major] = set()
major_partitions[major].add(student)
print()
print(f'There are {len(major_partitions)} majors among the students '
f'(some students may have more than one major).')
assess_assignments(course, major_partitions)
print()
if __name__ == '__main__':
semester_filter = create_semester_filter()
courses: List[CanvasCourse] = CanvasCourse.get_all_courses(semester_filter)
course_selection: Optional[CanvasCourse] = select_from_list(courses, 'course', none_is_option=True)
while course_selection is not None:
assess_course(course_selection)
print()
course: Optional[Course] = select_from_list(list(semester.courses), 'course', none_is_option=True)
course_selection = select_from_list(courses, 'course', none_is_option=True)
......@@ -937,6 +937,17 @@ class CanvasCourseSection:
class CanvasCourse:
canvas_course: Course
@staticmethod
def get_all_courses(semester_filter: str = '') -> List["CanvasCourse"]:
canvas_courses: Iterable[Course] = CanvasSession.get_session().get_courses()
courses: List[CanvasCourse]
if semester_filter == '':
courses = [CanvasCourse(course.id) for course in canvas_courses]
else:
courses = [CanvasCourse(course.id) for course in canvas_courses if semester_filter in course.course_code]
return courses
def __init__(self, course_id: int):
self.canvas_course = CanvasSession.get_session().get_course(course_id)
......
......@@ -2,6 +2,7 @@ import datetime
import json
import re
import ssl
import sys
from typing import Dict, List, Any, Optional, TypeVar
from urllib import request
from urllib.error import HTTPError
......@@ -20,6 +21,7 @@ def select_from_list(choices: List[ChoiceType], choice_name: str, none_is_option
if none_is_option:
print('0)\tNone of the above'.expandtabs(4))
selection = input('Enter selection: ')
sys.stdout.flush()
try:
selection_value = int(selection)
if none_is_option and selection_value == 0:
......
......@@ -7,11 +7,11 @@ class Major:
majors: Set["Major"] = set()
def __init__(self, name: str, alternate_names: Set[str] = None,
abbreviations: Optional[Set[str]] = None, is_cse_major: bool = True):
self.name: str = name
abbreviations: Optional[Set[str]] = None, is_computing_major: bool = True):
self._name: str = name
self.alternate_names: Set[str] = alternate_names if alternate_names is not None else set()
self.abbreviations: Set[str] = abbreviations if abbreviations is not None else set()
self.is_cse_major: bool = is_cse_major
self._is_computing_major: bool = is_computing_major
Major.majors.add(self)
@classmethod
......@@ -20,7 +20,7 @@ class Major:
if candidate:
return candidate[0]
else:
return Major(name, is_cse_major=False)
return Major(name, is_computing_major=False)
@staticmethod
def get_student_majors(login: str) -> Set["Major"]:
......@@ -31,14 +31,33 @@ class Major:
majors: Optional[Set[str]] = student_data['unlSISMajor']
return {Major.get_major(major) for major in majors} if majors is not None else Major.get_major('None')
@property
def name(self) -> str:
return self._name
@property
def is_computing_major(self) -> bool:
return self._is_computing_major
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return f'Name: "{self.name}";\tAlternate Names: {self.alternate_names if self.alternate_names else "{}"};\t' \
f'CSE Major: {self.is_cse_major};\tAbbreviations: {self.abbreviations if self.abbreviations else "{}"}'
f'CSE Major: {self._is_computing_major};\t' \
f'Abbreviations: {self.abbreviations if self.abbreviations else "{}"}'
def __eq__(self, other: "Major") -> bool:
return self.name == other.name
def __ne__(self, other: "Major") -> bool:
return not self.__eq__(other)
def __hash__(self) -> int:
return hash(self.name)
# Computing Majors
Major('Computer Science', alternate_names={'Computer Science (Raikes)'},
abbreviations={'COMP-BS', 'COMP-BA', 'COMP-MAJ', 'CMPS-BSCS',
'JECS-BS', 'JECS-BA', 'JECS-MAJ', 'JECS-BSCS'}) # double-check Raikes BSCS
......@@ -46,3 +65,35 @@ Major('Computer Engineering', alternate_names={'Computer Engineering (Raikes)'},
abbreviations={'CENG-BSCP', 'JECE-BSCP'})
Major('Software Engineering', alternate_names={'Software Engineering (Raikes)'},
abbreviations={'SOFT-BSSE', 'JESE-BSSE'})
# Raikes Non-Computing Majors (to capture their alternate names)
Major('Accounting', alternate_names={'Accounting (Raikes)'},
is_computing_major=False,
abbreviations={'ACCG-BSBA', 'ACCG-MAJ', 'JEAC-BSBA', 'JEAC-MAJ'})
Major('Actuarial Science', alternate_names={'Actuarial Science (Raikes)'}, is_computing_major=False,
abbreviations={'AACTS-BA', 'AACTS-BS', 'AACTS-MAJ', 'ACTS-BSBA', 'ACTS-MAJ', 'JEAS-BSBA', 'JEAS-MAJ'})
Major('Business Administration', is_computing_major=False,
alternate_names={'Business Administration with Accounting Emphasis', 'Business Administration (Raikes)'},
abbreviations={'BAAC-BSBA', 'BAAC-MAJ', 'BLNK-BSBA', 'BSAD-BSBA', 'BSAD-MAJ', 'JEBA-BSBA', 'JEBA-MAJ'})
Major('Economics', alternate_names={'Economics (Raikes)'}, is_computing_major=False,
abbreviations={'ECON-BSBA', 'ECON-MAJ', 'JEEC-BSBA', 'JEEC-MAJ'})
Major('Finance', is_computing_major=False,
alternate_names={'Finance (Raikes)',
'Finance (Banking & Financial Institutions)',
'Finance (Banking & Financial Institutions) (Raikes)',
'Finance (CFA-Investments)', 'Finance (CFA-Investments) (Raikes)',
'Finance (Risk Management & Insurance)', 'Finance (Risk Management & Insurance) (Raikes)'},
abbreviations={'FINA-BSBA', 'FINA-MAJ', 'JEFN-BSBA', 'JEFN-MAJ',
'FINB-BSBA', 'FINB-MAJ', 'FINI-BSBA', 'FINI-MAJ', 'FINR-BSBA', 'FINR-MAJ',
'JEFB-BSBA', 'JEFB-MAJ', 'JEFI-BSBA', 'JEFI-MAJ', 'JEFR-BSBA', 'JEFR-MAJ'})
Major('International Business', alternate_names={'International Business (Raikes)'}, is_computing_major=False,
abbreviations={'IBUS-BSBA', 'IBUS-MAJ', 'JEIB-BSBA', 'JEIB-MAJ'})
Major('Marketing', alternate_names={'Marketing (Raikes)'}, is_computing_major=False,
abbreviations={'MRKT-BSBA', 'MRKT-MAJ', 'JEMK-BSBA', 'JEMK-MAJ'})
Major('Management', alternate_names={'Management (Raikes)'}, is_computing_major=False,
abbreviations={'MNGT-BSBA', 'MNGT-MAJ', 'JEMN-BSBA', 'JEMN-MAJ'})
# Thanks to being able to access directory.unl.edu, I think the abbreviations are now unnecessary
# But if that ever changes, see these "Data Dictionary" pages for the (almost) comprehensive list of abbreviations:
# https://registrar.unl.edu/epm/dd/ep_wf_cpp_v1.shtml/
# https://registrar.unl.edu/data-dictionary-academic-plan/
# 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
SEMESTER,SUBDIRECTORY,COURSE 1,FILENAME 1,CANVAS ID 1,COURSE 2,FILENAME 2,CANVAS ID 2,COURSE 3,FILENAME 3,CANVAS ID 3,COURSE 4,FILENAME 4,CANVAS ID 4,COURSE 5,FILENAME 5,CANVAS ID 5,COURSE 6,FILENAME 6,CANVAS ID 6,COURSE 7,FILENAME 7,CANVAS ID 7,COURSE 8,FILENAME 8,CANVAS ID 8,COURSE 9,FILENAME 9,CANVAS ID 9
Spring 2021,spring2021,SOFT 161,soft161.csv,105546,CSCE 231,csce231.csv,100808,CSCE 487,csce487.csv,100787,CSCE 489,csce489.csv,100787,CSCE 493,csce493.csv,100787,CSCE 493A,csce493a.csv,100787,SOFT 404,soft 404.csv,100787,,,,,,
Fall 2020,fall2020,SOFT 160,soft160.csv,94598,CSCE 231,csce231.csv,89472,SOFT 360,soft360.csv,94606,CSCE 486,csce486.csv,89523,CSCE 488,csce488.csv,89523,SOFT 403,soft403.csv,89523,CSCE 493,csce493.csv,89523,CSCE 493A,csce493a.csv,89523,,,
\ No newline at end of file
*.csv
\ No newline at end of file
from typing import Set, Iterator, FrozenSet
class Semester:
def __init__(self, name: str, subdirectory: str):
self._name: str = name
self._subdirectory: str = subdirectory
self._courses: Set["Course"] = set()
def add_course(self, course: "Course") -> None:
self._courses.add(course)
@property
def subdirectory(self) -> str:
return self._subdirectory
@property
def courses(self) -> FrozenSet["Course"]:
return frozenset(self._courses)
def __iter__(self) -> Iterator["Course"]:
return iter(self._courses)
def __str__(self) -> str:
return self._name
class Course:
def __init__(self, name: str, canvas_course_id: str, roster_file: str):
self._name: str = name
self._canvas_course_id = canvas_course_id
self._roster_file: str = f'{roster_file}.csv' if roster_file[-4:] != '.csv' else roster_file
@property
def canvas_course_id(self) -> str:
return self._canvas_course_id
@property
def roster_file(self) -> str:
return self._roster_file
def __str__(self) -> str:
return self._name
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment