Skip to content

Commit

Permalink
Merge pull request #1556 from tl-its-umich-edu/1490-cron-remove-time-…
Browse files Browse the repository at this point in the history
…conversion

remove local-time fields, add time zone (iss. #1490, #1491)
  • Loading branch information
lsloan committed Jan 16, 2024
2 parents dca2734 + ca18d09 commit 582908d
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 63 deletions.
3 changes: 2 additions & 1 deletion assets/config/lti_config_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"custom_fields": {
"user_username": "$User.username",
"canvas_user_id": "$Canvas.user.id",
"canvas_course_id": "$Canvas.course.id"
"canvas_course_id": "$Canvas.course.id",
"person_address_timezone": "$Person.address.timezone"
},
"public_jwk_url": "%(base_url)s%(jwks_url_suffix)s",
"target_link_uri": "%(base_url)s%(launch_url_suffix)s",
Expand Down
7 changes: 5 additions & 2 deletions assets/src/__tests__/util/date.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ describe('calculateWeekOffset', () => {

describe('dateToMonthDay', () => {
it('takes as input a date time string and returns in {month}/{day} format', () => {
const date = new Date('2019-05-16T18:42:35+00:00')
const date = '2019-05-16T18:42:35+00:00'
expect(dateToMonthDay(date)).toEqual('5/16')

const date2 = new Date('2019-06-02T06:03:19+00:00')
const date2 = '2019-06-02T06:03:19+00:00'
expect(dateToMonthDay(date2)).toEqual('6/2')

const date3 = '2019-12-02T23:59:59-04:00'
expect(dateToMonthDay(date3)).toEqual('12/2')
})
})
2 changes: 1 addition & 1 deletion assets/src/hooks/useAssignmentData.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ query Assignment($courseId: ID!) {
assignments {
id
name
localDate
dueDate
pointsPossible
averageGrade
assignmentGroupId
Expand Down
8 changes: 4 additions & 4 deletions assets/src/util/assignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const sortAssignments = assignments => {

const assignmentsWithDueDates = initialSortedAssignments
.filter(a => a.week)
.sort((a, b) => new Date(a.localDate).getTime() - new Date(b.localDate).getTime())
.sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime())

const assignmentsWithoutDueDates = initialSortedAssignments
.filter(a => !a.week)
Expand All @@ -209,13 +209,13 @@ const createAssignmentFields = (
return sortAssignments(
assignments.map(a => {
const {
localDate,
dueDate,
pointsPossible,
assignmentGroupId,
currentUserSubmission
} = a

a.week = calculateWeekOffset(courseStartDate, localDate)
a.week = calculateWeekOffset(courseStartDate, dueDate)
a.percentOfFinalGrade = roundToXDecimals(
(
assignmentWeightConsideration
Expand All @@ -227,7 +227,7 @@ const createAssignmentFields = (
// filter out null values
a.graded = (currentUserSubmission !== null) && (currentUserSubmission.gradedDate !== null) && (currentUserSubmission.score !== null)
a.submitted = !!currentUserSubmission && !!currentUserSubmission.submittedAt
a.dueDateMonthDay = dateToMonthDay(localDate)
a.dueDateMonthDay = dueDate && dateToMonthDay(dueDate)
a.goalGrade = ''
a.goalGradeSetByUser = false
a.inputFocus = false
Expand Down
9 changes: 4 additions & 5 deletions assets/src/util/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ const calculateWeekOffset = (startDateTime, targetDateTime) => {
return Math.ceil((differenceInDays + 1) / 7)
}

// Return date as a formatted "month/day" string
// eg. 2022-06-23T23:59:00 to 6/23
const dateToMonthDay = date => {
const dateObj = new Date(date)
const [, month, day] = date.split('T')[0].split('-')

const month = dateObj.getMonth() + 1
const day = dateObj.getUTCDate()

return `${month}/${day}`
return `${Number(month)}/${Number(day)}`
}

export {
Expand Down
11 changes: 5 additions & 6 deletions config/cron_udp.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,7 @@
with assignment_info as
(
select
la.due_date as due_date,
timezone(%(time_zone)s, la.due_date AT TIME ZONE 'UTC') as local_date,
la.due_date AT TIME ZONE 'UTC' as due_date,
la.title as name,
cast(co.lms_int_id as BIGINT) as course_id,
cast(la_km.lms_int_id as BIGINT) as id,
Expand Down Expand Up @@ -228,7 +227,7 @@
lar.published_score as published_score,
lar.response_date as submitted_at,
lar.graded_date as graded_at,
timezone(:time_zone, lar.posted_at AT TIME ZONE 'UTC') as grade_posted_local_date,
lar.posted_at as grade_posted,
lar.grading_status as submission_workflow_state,
la.title as title,
lar.learner_activity_result_id as learner_activity_result_id,
Expand All @@ -255,7 +254,7 @@
(
case
when
(grade_posted_local_date is null or submission_workflow_state != 'graded')
(grade_posted is null or submission_workflow_state != 'graded')
then
null
else
Expand All @@ -264,7 +263,7 @@
) AS score,
submitted_at AS submitted_at,
graded_at AS graded_date,
grade_posted_local_date
grade_posted
from
submission
)
Expand All @@ -279,7 +278,7 @@
f.score::float,
f.submitted_at,
f.graded_date,
f.grade_posted_local_date,
f.grade_posted,
cast(f1.avg_score as float) as avg_score
from
all_assign_sub f join
Expand Down
15 changes: 9 additions & 6 deletions config/env_sample.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -100,25 +100,28 @@
# LTI is disabled by default
"ENABLE_LTI": false,
# LTI 1.3 configuration
# Each key in this object corresponds to an "iss" from the LTI protocol.
# The first key of LTI_CONFIG is the Canvas URL (production, beta, or test)
"LTI_CONFIG": {
"https://canvas.instructure.com": [
{
# The default set of variables for LTI validation
# requests without ID from this platform use this config by default
"default": true,
# LTI Dev Key from Canvas
# from Canvas: Developer Keys → value from Details column
"client_id": "17700000000000111",
# Allowed hosts for the following 3 URLs: canvas.instructure.com, canvas.beta.instructure.com, canvas.test.instructure.com
"auth_login_url": "https://canvas.instructure.com/api/lti/authorize_redirect",
"auth_token_url": "https://canvas.instructure.com/login/oauth2/token",
"key_set_url": "https://canvas.instructure.com/api/lti/security/jwks",
"key_set":null,
# Tools private key for LTI validation
# this tool's private key
"private_key_file": "/secrets/private.key",
# Tools public key for LTI validation
# this tool's public key
"public_key_file": "/secrets/public.key",
# Tools installation id in platform
"deployment_ids": ["27297:7db438071375c02373713c12c73869ff2f470b68"]
# Go to Canvas course where this tool installed, click on the setting button (a gear icon, similar to '⚙️'), click 'Deployment Id', copy it, and paste here
"deployment_ids": [
"9:0123456789abcdef0123456789abcdef01234567"
]
}
]
},
Expand Down
4 changes: 1 addition & 3 deletions dashboard/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,7 @@ def update_assignment(self):
# loop through multiple course ids
status += util_function(queries['assignment'],
'assignment',
{'course_ids': self.valid_locked_course_ids,
'time_zone': settings.TIME_ZONE})
{'course_ids': self.valid_locked_course_ids})

return status

Expand All @@ -487,7 +486,6 @@ def submission(self):
# filter out not released grades (submission_dim.posted_at date is not null) and partial grades (submission_dim.workflow_state != 'graded')
query_params = {
'course_ids': self.valid_locked_course_ids,
'time_zone': settings.TIME_ZONE,
'canvas_data_id_increment': settings.CANVAS_DATA_ID_INCREMENT,
}
Session = sessionmaker(bind=data_warehouse_engine)
Expand Down
10 changes: 10 additions & 0 deletions dashboard/graphql/objects.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytz
from graphene_django import DjangoObjectType
import graphene
from django.conf import settings
import numpy as np
import json

Expand Down Expand Up @@ -143,11 +145,19 @@ def resolve_median_grade(parent, info):
lambda submissions: AssignmentType._median_grade_lambda(parent, info, submissions)
)

def resolve_due_date(parent, info):
original_due_date = parent.due_date
if original_due_date is None:
return None
new_timezone = pytz.timezone(info.context.session.get('time_zone', settings.TIME_ZONE))
return original_due_date.astimezone(new_timezone)

class Meta:
model = Assignment
only_fields = (
'id', 'name', 'due_date', 'local_date', 'points_possible', 'course_id', 'assignment_group_id'
)

AssignmentGroupType.assignments = graphene.List(AssignmentType)
AssignmentGroupType.assignments = graphene.List(AssignmentType)

Expand Down
80 changes: 47 additions & 33 deletions dashboard/lti_new.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import logging
import random
import string
import urllib.parse
from collections import namedtuple
from typing import Any, Dict
from datetime import datetime
from typing import Dict
from typing import Union, Any

import django.contrib.auth
import django.contrib.auth
import pytz
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse
import logging, string, random
import urllib.parse
from datetime import datetime
from typing import Mapping, MutableSequence, Union, Any

import django.contrib.auth
from django.contrib.staticfiles import finders

from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import redirect
from django.urls import reverse
Expand Down Expand Up @@ -200,30 +197,45 @@ def short_user_role_list(roles):
def extract_launch_variables_for_tool_use(request, message_launch):
launch_data = message_launch.get_launch_data()
logger.debug(f'lti launch data {launch_data}')
custom_params = launch_data['https://purl.imsglobal.org/spec/lti/claim/custom']
logger.debug(f'lti_custom_param {custom_params}')
if not custom_params:
raise Exception(
f'You need to have custom parameters configured on your LTI Launch. Please see the LTI installation guide on the Github Wiki for more information.'
)
course_name = launch_data['https://purl.imsglobal.org/spec/lti/claim/context']['title']
roles = launch_data['https://purl.imsglobal.org/spec/lti/claim/roles']
username = custom_params['user_username']
course_id = custom_params['canvas_course_id']
canvas_course_long_id = canvas_id_to_incremented_id(course_id)
canvas_user_id = custom_params['canvas_user_id']
canvas_user_long_id = canvas_id_to_incremented_id(canvas_user_id)

if 'email' not in launch_data.keys():
logger.info('Possibility that LTI launch by Instructor/admin becoming Canvas Test Student')
error_message = 'Student view is not available for My Learning Analytics.'
logger.info('Possible LTI launch by instructor/admin becoming '
'Canvas Test Student')
error_message = ('Student view is not available for '
'My Learning Analytics.')
raise Exception(error_message)

email = launch_data['email']
first_name = launch_data['given_name']
last_name = launch_data['family_name']
full_name = launch_data['name']
course_name = launch_data[
'https://purl.imsglobal.org/spec/lti/claim/context']['title']
roles = launch_data['https://purl.imsglobal.org/spec/lti/claim/roles']

custom_params = launch_data[
'https://purl.imsglobal.org/spec/lti/claim/custom']
logger.debug(f'lti_custom_param {custom_params}')

# Add user to DB if not there; avoids Django redirection to login page
if not custom_params:
raise Exception('Custom parameters must be configured for LTI '
'launches. Please see the LTI installation guide on '
'the GitHub wiki for more information.')

username = custom_params['user_username']
course_id = custom_params['canvas_course_id']
canvas_user_id = custom_params['canvas_user_id']
time_zone = custom_params.get('person_address_timezone',
settings.TIME_ZONE).strip()

if time_zone not in pytz.all_timezones:
time_zone = settings.TIME_ZONE # default zone from `env.hjson`

request.session['time_zone'] = time_zone

canvas_course_long_id = canvas_id_to_incremented_id(course_id)
canvas_user_long_id = canvas_id_to_incremented_id(canvas_user_id)

# Add user to Django's `auth_user` DB table if not there; avoids Django redirection to login page
try:
user_obj = User.objects.get(username=username)
# update
Expand All @@ -236,12 +248,13 @@ def extract_launch_variables_for_tool_use(request, message_launch):
user_obj = User.objects.create_user(username=username, email=email, password=password, first_name=first_name,
last_name=last_name)

# Add username into the MyLA User table, since the data was not pulled in from cron job
# Add user to MyLA's `user` table,
# since data wasn't pulled from scheduled job
user_id = settings.CANVAS_DATA_ID_INCREMENT + int(canvas_user_id)
enrollment_qs = MylaUser.objects.filter(user_id=user_id)
enrollment_qs = MylaUser.objects.filter(user_id=user_id)
if enrollment_qs.exists():
enrollment_qs.update(sis_name=username)

user_obj.backend = 'django.contrib.auth.backends.ModelBackend'
django.contrib.auth.login(request, user_obj)
is_instructor = check_if_instructor(roles, username, course_id)
Expand All @@ -267,7 +280,7 @@ def login(request):
return lti_error(config)
target_link_uri = request.POST.get('target_link_uri', request.GET.get('target_link_uri'))
if not target_link_uri:
error_message = 'LTI Login failed due to missing "target_link_uri" param'
error_message = 'LTI login failed; missing "target_link_uri" param'
return lti_error(error_message)
CacheConfig = get_cache_config()
oidc_login = DjangoOIDCLogin(request, config, launch_data_storage=CacheConfig.launch_data_storage)
Expand All @@ -283,11 +296,12 @@ def launch(request):
CacheConfig = get_cache_config()
message_launch = ExtendedDjangoMessageLaunch(request, config, launch_data_storage=CacheConfig.launch_data_storage)
if not CacheConfig.is_dummy_cache:
# fetch platform's public key from cache instead of calling the API will speed up the launch process
# platform's public key from cache instead of calling API speeds up launch process
message_launch.set_public_key_caching(CacheConfig.launch_data_storage,
cache_lifetime=CacheConfig.cache_lifetime)
else:
logger.info('DummyCache is set up, recommended atleast to us Mysql DB cache for LTI advantage services')
logger.info('DummyCache is configured; MySQL DB cache '
'recommended for LTI Advantage services.')

try:
course_id = extract_launch_variables_for_tool_use(request, message_launch)
Expand Down
26 changes: 26 additions & 0 deletions dashboard/migrations/0027_remove_or_change_local_date_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.20 on 2023-12-11 19:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0026_course_date_created'),
]

operations = [
migrations.RemoveField(
model_name='assignment',
name='local_date',
),
migrations.RemoveField(
model_name='submission',
name='grade_posted_local_date',
),
migrations.AddField(
model_name='submission',
name='grade_posted',
field=models.DateTimeField(blank=True, null=True, verbose_name='Posted Grade DateTime'),
),
]
3 changes: 1 addition & 2 deletions dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ class Assignment(models.Model):
id = models.BigIntegerField(primary_key=True, verbose_name="Assignment Id")
name = models.CharField(max_length=255, verbose_name="Name", default='')
due_date = models.DateTimeField(blank=True, null=True, verbose_name="Due DateTime")
local_date = models.DateTimeField(blank=True, null=True, verbose_name="Local DateTime")
points_possible = models.FloatField(blank=True, null=True, verbose_name="Points Possible")
course_id = models.BigIntegerField(verbose_name="Course Id")
assignment_group_id = models.BigIntegerField(verbose_name="Assignment Group Id")
Expand Down Expand Up @@ -350,7 +349,7 @@ class Submission(models.Model):
score = models.FloatField(blank=True, null=True, verbose_name="Score")
graded_date = models.DateTimeField(blank=True, null=True, verbose_name="Graded DateTime")
# This is used for tracking of grade posted date and not used in Assignment view hence making it CharField
grade_posted_local_date = models.CharField(max_length=255,blank=True, null=True, verbose_name="Posted Grade in local DateTime")
grade_posted = models.DateTimeField(blank=True, null=True, verbose_name="Posted Grade DateTime")
avg_score = models.FloatField(blank=True, null=True, verbose_name="Average Grade")

def __str__(self):
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ pinax-eventlog==5.1.1 #no updates
pycryptodome==3.19.1
PyLTI1p3==2.0.0 #no further update Nov 2022)
hjson==3.1.0
pytz==2023.3.post1

0 comments on commit 582908d

Please sign in to comment.