Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Better Course Offering List #21

Merged
merged 3 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from sqlalchemy.ext.associationproxy import association_proxy

from server import app
from server.utils.date import parse_ISO8601

db = SQLAlchemy(app=app)

Expand Down Expand Up @@ -43,11 +44,16 @@
canvas_id = db.Column(db.String(255), nullable=False, index=True, unique=True)
name = db.Column(db.String(255), nullable=False)
code = db.Column(db.String(255), nullable=False)
start_at = db.Column(db.String(255), nullable=False)

exams = db.relationship('Exam', uselist=True, cascade='all, delete-orphan',
order_by='Exam.display_name',
backref=backref('offering', uselist=False, single_parent=True))

@property
def start_at_date(self):
return parse_ISO8601(self.start_at)

Check warning on line 55 in server/models.py

View check run for this annotation

Codecov / codecov/patch

server/models.py#L55

Added line #L55 was not covered by tests

def __repr__(self):
return '<Offering {}>'.format(self.name)

Expand Down
30 changes: 23 additions & 7 deletions server/services/canvas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from canvasapi import Canvas
from canvasapi.user import User
from canvasapi.course import Course
from canvasapi.enrollment import Enrollment

from server import app
from server.models import Offering
Expand Down Expand Up @@ -38,7 +37,9 @@
return not (not c) and \
hasattr(c, 'id') and \
hasattr(c, 'name') and \
hasattr(c, 'course_code')
hasattr(c, 'course_code') and \
hasattr(c, 'start_at') and \
hasattr(c, 'start_at_date')


def get_user_courses_categorized(user: FakeUser | User) \
Expand All @@ -55,19 +56,34 @@
student_courses.add(c)
else:
other.add(c)

# a course should not appear in more than one category
student_courses -= set(staff_courses)
other = other - set(staff_courses) - set(student_courses)
# sorted by course name
staff_courses: list[FakeCourse | Course] = sorted(staff_courses, key=lambda c: c.name)
student_courses: list[FakeCourse | Course] = sorted(student_courses, key=lambda c: c.name)
other: list[FakeCourse | Course] = sorted(other, key=lambda c: c.name)

# convert to list because order matters
staff_courses: list[FakeCourse | Course] = list(staff_courses)
student_courses: list[FakeCourse | Course] = list(student_courses)
other: list[FakeCourse | Course] = list(other)

Check warning on line 67 in server/services/canvas/__init__.py

View check run for this annotation

Codecov / codecov/patch

server/services/canvas/__init__.py#L65-L67

Added lines #L65 - L67 were not covered by tests

# sorted by start_at_date DESC and then by name ASC
def _sort_courses(courses: list[FakeCourse | Course]):

Check warning on line 70 in server/services/canvas/__init__.py

View check run for this annotation

Codecov / codecov/patch

server/services/canvas/__init__.py#L70

Added line #L70 was not covered by tests
# Cannot do courses.sort(key=lambda c: (c.start_at_date, c.name))
# String or Datetime object cannot be negated to reverse the order
courses.sort(key=lambda c: c.name)
courses.sort(key=lambda c: c.start_at_date, reverse=True)

Check warning on line 74 in server/services/canvas/__init__.py

View check run for this annotation

Codecov / codecov/patch

server/services/canvas/__init__.py#L73-L74

Added lines #L73 - L74 were not covered by tests

_sort_courses(staff_courses)
_sort_courses(student_courses)
_sort_courses(other)

Check warning on line 78 in server/services/canvas/__init__.py

View check run for this annotation

Codecov / codecov/patch

server/services/canvas/__init__.py#L76-L78

Added lines #L76 - L78 were not covered by tests

return list(staff_courses), list(student_courses), list(other)


def api_course_to_model(course: Course | FakeCourse) -> Offering:
return Offering(
canvas_id=course.id,
name=course.name,
code=course.course_code
code=course.course_code,
start_at=course.start_at,
)
7 changes: 7 additions & 0 deletions server/services/canvas/fake_canvas.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations
from datetime import datetime

from server.services.canvas.fake_data import FAKE_USERS, FAKE_COURSES, FAKE_ENROLLMENTS


Expand Down Expand Up @@ -37,6 +39,11 @@
self.course_code = FAKE_COURSES[str(canvas_id)]['course_code']
self.sis_course_id = FAKE_COURSES[str(canvas_id)]['sis_course_id']
self.enrollments = enrollments
# canvasapi.course.Course.start_at is a n ISO8601 date string
# canvasapi.course.Course.start_at_date is a datetime.datetime object
# so, we do the same here
self.start_at = FAKE_COURSES[str(canvas_id)]['start_at']
self.start_at_date = datetime.strptime(self.start_at, '%Y-%m-%dT%H:%M:%SZ')

Check warning on line 46 in server/services/canvas/fake_canvas.py

View check run for this annotation

Codecov / codecov/patch

server/services/canvas/fake_canvas.py#L45-L46

Added lines #L45 - L46 were not covered by tests

def get_users(self, *, enrollment_type) -> list[FakeUser]:
users = []
Expand Down
12 changes: 8 additions & 4 deletions server/services/canvas/fake_data/fake_courses.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@
"id": 1234567,
"name": "Introduction to Software Engineering (Fall 2023)",
"sis_course_id": "CRS:COMPSCI-169A-2023-D",
"course_code": "COMPSCI 169A"
"course_code": "COMPSCI 169A",
"start_at": "2023-08-16T07:00:00Z"
},
"2345678": {
"id": 2345678,
"name": "Introduction to the Internet: Architecture and Protocols (Fall 2022)",
"sis_course_id": "",
"course_code": "COMPSCI 168"
"course_code": "COMPSCI 168",
"start_at": "2022-08-16T07:00:00Z"
},
"3456789": {
"id": 3456789,
"name": "Introduction to Computer Security (Fall 2022)",
"sis_course_id": "",
"course_code": "COMPSCI 161"
"course_code": "COMPSCI 161",
"start_at": "2022-08-16T07:00:00Z"
},
"4567890": {
"id": 4567890,
"name": "Computer Architecture (Fall 2022)",
"sis_course_id": "",
"course_code": "COMPSCI 150"
"course_code": "COMPSCI 150",
"start_at": "2022-08-16T07:00:00Z"
}
}
5 changes: 4 additions & 1 deletion server/templates/select_offering.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ul class="mdl-list">
{% for offering in staff_offerings %}
<li class="mdl-list__item">
<span style="margin-right: 10px; color: #424242; font-weight: bold;"><small>{{ offering.start_at_date.strftime('%Y-%m') }}</small></span>
<a class="mdl-list__item-primary-content" href="{{ url_for('offering', offering=offering) }}">
{{ offering.name }}
</a>
Expand All @@ -18,8 +19,9 @@
<ul class="mdl-list">
{% for offering in student_offerings %}
<li class="mdl-list__item">
<span style="margin-right: 10px; color: #424242; font-weight: bold;"><small>{{ offering.start_at_date.strftime('%Y-%m') }}</small></span>
<a class="mdl-list__item-primary-content" href="{{ url_for('offering', offering=offering) }}">
{{ offering.name }}
{{ offering.name }}
</a>
</li>
{% endfor %}
Expand All @@ -28,6 +30,7 @@
<ul class="mdl-list">
{% for offering in other_offerings %}
<li class="mdl-list__item">
<span style="margin-right: 10px; color: #424242; font-weight: bold;"><small>{{ offering.start_at_date.strftime('%Y-%m') }}</small></span>
{{ offering.name }}
</li>
{% endfor %}
Expand Down
Empty file added server/utils/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions server/utils/date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from datetime import datetime


def parse_ISO8601(s: str) -> datetime:
"""
Parse an ISO8601 date string and return a datetime.datetime object.

datetime.fromisoformat cannot properly parse ISO 8601 format until 3.11
so we use datetime.strptime instead
in 3.11 you can do: datetime.fromisoformat(s)
"""
return datetime.strptime(s, '%Y-%m-%dT%H:%M:%SZ')

Check warning on line 12 in server/utils/date.py

View check run for this annotation

Codecov / codecov/patch

server/utils/date.py#L12

Added line #L12 was not covered by tests
12 changes: 8 additions & 4 deletions tests/fixtures/offering.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@
"id": 1,
"canvas_id": "1234567",
"name": "Introduction to Software Engineering (Fall 2023)",
"code": "COMPSCI 169A-LEC-001"
"code": "COMPSCI 169A-LEC-001",
"start_at": "2023-08-16T00:00:00Z"
},
{
"id": 2,
"canvas_id": "2345678",
"name": "Introduction to the Internet: Architecture and Protocols (Fall 2022)",
"code": "COMPSCI 168-LEC-001"
"code": "COMPSCI 168-LEC-001",
"start_at": "2022-08-16T00:00:00Z"
},
{
"id": 3,
"canvas_id": "3456789",
"name": "Introduction to Computer Security (Fall 2022)",
"code": "COMPSCI 161-LEC-001"
"code": "COMPSCI 161-LEC-001",
"start_at": "2022-08-16T00:00:00Z"
},
{
"id": 4,
"canvas_id": "4567890",
"name": "Computer Architecture (Fall 2022)",
"code": "COMPSCI 150-LEC-001"
"code": "COMPSCI 150-LEC-001",
"start_at": "2022-08-16T00:00:00Z"
}
]
}
Expand Down