Skip to content

Commit

Permalink
New JSON renderer that handles datetime objects
Browse files Browse the repository at this point in the history
Based on the default json serializer from pyramid but it formats datetime
objects as isoformat().

It special cases datetimes without datetime information and assumes they
are in UTC.

This commit introduces this seralizer as a new one on top of the default
`json` as `json_iso_utc` an starts using it on the assignments API.

Short term we'll use this serializer in all dashboard related routes and
longer term we'll replace all uses of json by this seralizer and then will
make it the default.
  • Loading branch information
marcospri committed Oct 4, 2024
1 parent 12a9307 commit 582d81b
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 13 deletions.
4 changes: 4 additions & 0 deletions lms/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from sqlalchemy.exc import IntegrityError

from lms.config import configure
from lms.renderers import json_iso_utc


def configure_jinja2_assets(config):
Expand Down Expand Up @@ -35,6 +36,9 @@ def create_app(global_config, **settings): # noqa: ARG001
config.include("pyramid_jinja2")
config.include("pyramid_services")

# Add our own json renderer that handles datetime objects.
config.add_renderer("json_iso_utc", json_iso_utc())

# Use pyramid_tm's explicit transaction manager.
#
# This means that trying to access a request's transaction after pyramid_tm
Expand Down
23 changes: 23 additions & 0 deletions lms/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from datetime import UTC, datetime

from pyramid.renderers import JSON


def json_iso_utc():
"""Return a JSON renderer that formats dates as `isoformat`.
This renderer assumes datetimes without tz info are in UTC and
includes that in the datetime objects so the resulting string
includes tz information.
"""

renderer = JSON()

def _datetime_adapter(obj: datetime, _request) -> str:
if not obj.tzinfo:
obj = obj.replace(tzinfo=UTC)
return obj.isoformat()

renderer.add_adapter(datetime, _datetime_adapter)

return renderer
12 changes: 6 additions & 6 deletions lms/views/dashboard/api/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def __init__(self, request) -> None:
@view_config(
route_name="api.dashboard.assignments",
request_method="GET",
renderer="json",
renderer="json_iso_utc",
permission=Permissions.DASHBOARD_VIEW,
schema=ListAssignmentsSchema,
)
Expand All @@ -85,7 +85,7 @@ def assignments(self) -> APIAssignments:
APIAssignment(
id=assignment.id,
title=assignment.title,
created=assignment.created.isoformat(),
created=assignment.created,
)
for assignment in assignments
],
Expand All @@ -95,15 +95,15 @@ def assignments(self) -> APIAssignments:
@view_config(
route_name="api.dashboard.assignment",
request_method="GET",
renderer="json",
renderer="json_iso_utc",
permission=Permissions.DASHBOARD_VIEW,
)
def assignment(self) -> APIAssignment:
assignment = self.dashboard_service.get_request_assignment(self.request)
api_assignment = APIAssignment(
id=assignment.id,
title=assignment.title,
created=assignment.created.isoformat(),
created=assignment.created,
course=APICourse(
id=assignment.course.id,
title=assignment.course.lms_name,
Expand All @@ -128,7 +128,7 @@ def assignment(self) -> APIAssignment:
@view_config(
route_name="api.dashboard.course.assignments.metrics",
request_method="GET",
renderer="json",
renderer="json_iso_utc",
permission=Permissions.DASHBOARD_VIEW,
schema=AssignmentsMetricsSchema,
)
Expand Down Expand Up @@ -191,7 +191,7 @@ def course_assignments_metrics(self) -> APIAssignments:
APIAssignment(
id=assignment.id,
title=assignment.title,
created=assignment.created.isoformat(),
created=assignment.created,
course=api_course,
annotation_metrics=metrics,
)
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/lms/renderers_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from datetime import UTC, datetime
from unittest.mock import sentinel
from zoneinfo import ZoneInfo

import pytest

from lms.renderers import json_iso_utc


@pytest.mark.parametrize(
"time,expected",
[
# No timezone, UTC is assumed
(datetime(2024, 1, 1), "2024-01-01T00:00:00+00:00"),
# UTC, UTC is left intact
(datetime(2024, 1, 1, tzinfo=UTC), "2024-01-01T00:00:00+00:00"),
# Non-UTC, timezone is also left intact
(
datetime(2024, 1, 1, tzinfo=ZoneInfo("Europe/Madrid")),
"2024-01-01T00:00:00+01:00",
),
],
)
def test_json_iso_utc(time, expected):
assert (
json_iso_utc()(sentinel.info)({"time": time}, {})
== f"""{{"time": "{expected}"}}"""
)
14 changes: 7 additions & 7 deletions tests/unit/lms/views/dashboard/api/assignment_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_get_assignments(
)
assert response == {
"assignments": [
{"id": a.id, "title": a.title, "created": a.created.isoformat()}
{"id": a.id, "title": a.title, "created": a.created}
for a in assignments
],
"pagination": sentinel.pagination,
Expand Down Expand Up @@ -69,7 +69,7 @@ def test_assignment(
assert response == {
"id": assignment.id,
"title": assignment.title,
"created": assignment.created.isoformat(),
"created": assignment.created,
"course": {"id": assignment.course.id, "title": assignment.course.lms_name},
}

Expand All @@ -96,7 +96,7 @@ def test_assignment_with_auto_grading(
assert response == {
"id": assignment.id,
"title": assignment.title,
"created": assignment.created.isoformat(),
"created": assignment.created,
"course": {"id": assignment.course.id, "title": assignment.course.lms_name},
"groups": [],
"auto_grading_config": {
Expand Down Expand Up @@ -125,7 +125,7 @@ def test_assignment_with_groups(
assert response == {
"id": assignment.id,
"title": assignment.title,
"created": assignment.created.isoformat(),
"created": assignment.created,
"course": {"id": assignment.course.id, "title": assignment.course.lms_name},
"groups": [
{"h_authority_provided_id": g.authority_provided_id, "name": g.lms_name}
Expand All @@ -152,7 +152,7 @@ def test_assignment_with_sections(
assert response == {
"id": assignment.id,
"title": assignment.title,
"created": assignment.created.isoformat(),
"created": assignment.created,
"course": {"id": assignment.course.id, "title": assignment.course.lms_name},
"sections": [
{"h_authority_provided_id": g.authority_provided_id, "name": g.lms_name}
Expand Down Expand Up @@ -210,7 +210,7 @@ def test_course_assignments(
{
"id": assignment.id,
"title": assignment.title,
"created": assignment.created.isoformat(),
"created": assignment.created,
"course": {
"id": course.id,
"title": course.lms_name,
Expand All @@ -224,7 +224,7 @@ def test_course_assignments(
{
"id": assignment_with_no_annos.id,
"title": assignment_with_no_annos.title,
"created": assignment_with_no_annos.created.isoformat(),
"created": assignment_with_no_annos.created,
"course": {
"id": course.id,
"title": course.lms_name,
Expand Down

0 comments on commit 582d81b

Please sign in to comment.