diff --git a/cd2_views/create_user_activity_view_canvas.sql b/cd2_views/create_user_activity_view_canvas.sql new file mode 100644 index 0000000..6e040a6 --- /dev/null +++ b/cd2_views/create_user_activity_view_canvas.sql @@ -0,0 +1,131 @@ +-- user_activity view — canvas database (primary Canvas instance) +-- +-- In CD2, everything is identified by user_id (as in, Canvas ID), so there will be +-- unique activity per user_id. We can't assume that a single user_id will map to +-- a single integration_id (PUID) or sis_user_id (student number), however. +-- We have not 'squished' any of these into single lines, so, e.g., if one user_id +-- maps to two integration_ids, there will be two rows with the same activity. +-- In other words, there will be one row per unique user_id x integration_id +-- x sis_user_id (x workflow_state) combination. +-- +-- Almost every user_id has two pseudonyms entries, one with an integration_id etc., +-- and one that is quite empty. We have filtered out any pseudonyms with no integration_id, +-- as these lines will be useless when only searching on integration_id. +-- But *all* non-null integration_ids have been kept in, even those with atypical +-- formats, such as 12345ABCD_INC12345. +-- +-- Note there's also no filtering here based on the workflow_state of the entry in +-- pseudonyms or users, though both can be, e.g., 'deleted'. +-- +-- From Claude: +-- Run against the "canvas" CD2 database, connected as the CD2 database owner +-- (the db user created for this database in setup/prepare_aurora_db.py), so +-- pg-deps-management's view-dependency hooks pick up the view as owned by a +-- role sync_table already knows how to drop/recreate on schema change. + +CREATE OR REPLACE VIEW canvas.user_activity AS +-- I don't actually expect much, if any, aggregation to happen here, +-- as I haven't seen multiple pseudonyms with the same user_id, integration_id, +-- sis_user_id, AND workflow_state. The point of this subquery is to +-- pair up user_ids (which will have all the associated CD2 activity) +-- with any/all integration_ids linked (the same is happening with sis_user_ids +-- and workflow_states, but those are not the main goal). +WITH pseudonym_agg AS ( + SELECT + user_id, + integration_id, + sis_user_id, + workflow_state, + MAX(last_login_at) AS last_login_at, + MAX(last_request_at) AS last_request_at, + SUM(login_count) AS login_count + FROM canvas.pseudonyms + WHERE integration_id IS NOT NULL + GROUP BY user_id, integration_id, sis_user_id, workflow_state +), +enrollment_agg AS ( + SELECT + user_id, + COUNT(*) AS enrolment_count_total, + -- active enrolments != classes currently being taken + -- I believe it's all enrolments, past and present, that they student didn't drop + -- or otherwise get removed from. I believe completed courses are active. + COUNT(*) FILTER (WHERE workflow_state = 'active') AS enrolment_count_active + FROM canvas.enrollments + GROUP BY user_id +), +submission_agg AS ( + SELECT + user_id, + COUNT(*) AS submission_count, + MAX(submitted_at) AS last_submission_at + FROM canvas.submissions + WHERE submitted_at IS NOT NULL + AND workflow_state IN ('submitted', 'graded') + GROUP BY user_id +), + +-- Note that quiz_submissions is basically a subset of submissions: +-- all quiz_submission entries should have a corresponding entry in submissions. +-- I say 'basically' because I've seen different created_at dates for +-- the 'same' entry -- probably they are created, empty, before the student +-- actually does anything, and those times are not necessarily the same, +-- meaning one *can* exist without the other. +quiz_submission_agg AS ( + SELECT + user_id, + COUNT(*) AS quiz_submission_count, + MAX(finished_at) AS last_quiz_submission_at + FROM canvas.quiz_submissions + WHERE workflow_state = 'complete' + GROUP BY user_id +), +discussion_entry_agg AS ( + SELECT + user_id, + COUNT(*) AS discussion_entry_count, + MAX(created_at) AS last_discussion_entry_at + FROM canvas.discussion_entries + WHERE workflow_state = 'active' + GROUP BY user_id +), +conversation_message_agg AS ( + SELECT + author_id AS user_id, + COUNT(*) AS conversation_message_count, + MAX(created_at) AS last_conversation_message_at + FROM canvas.conversation_messages + -- messages don't have workflow_states + GROUP BY author_id +) +SELECT + pa.integration_id AS sis_integration_id, + pa.sis_user_id, + pa.user_id, + pa.workflow_state AS pseudonym_workflow_state, + u.merged_into_user_id, + pa.last_login_at, + pa.last_request_at, + COALESCE(pa.login_count, 0) AS login_count, + COALESCE(ea.enrolment_count_active, 0) AS enrolment_count_active, + COALESCE(ea.enrolment_count_total, 0) AS enrolment_count_total, + COALESCE(sa.submission_count, 0) AS submission_count, + sa.last_submission_at, + COALESCE(qa.quiz_submission_count, 0) AS quiz_submission_count, + qa.last_quiz_submission_at, + COALESCE(da.discussion_entry_count, 0) AS discussion_entry_count, + da.last_discussion_entry_at, + COALESCE(ca.conversation_message_count, 0) AS conversation_message_count, + ca.last_conversation_message_at +FROM pseudonym_agg pa +LEFT JOIN canvas.users u ON u.id = pa.user_id +LEFT JOIN enrollment_agg ea ON ea.user_id = pa.user_id +LEFT JOIN submission_agg sa ON sa.user_id = pa.user_id +LEFT JOIN quiz_submission_agg qa ON qa.user_id = pa.user_id +LEFT JOIN discussion_entry_agg da ON da.user_id = pa.user_id +LEFT JOIN conversation_message_agg ca ON ca.user_id = pa.user_id; +ORDER BY sis_integration_id + +-- If running against catalog instead, change athena to athena_catalog +GRANT SELECT ON canvas.user_activity TO athena; +GRANT SELECT ON canvas.user_activity TO ubc_it_easd_cache diff --git a/cd2_views/create_user_activity_view_canvas.txt b/cd2_views/create_user_activity_view_canvas.txt new file mode 100644 index 0000000..a2fe909 --- /dev/null +++ b/cd2_views/create_user_activity_view_canvas.txt @@ -0,0 +1,20 @@ +Hi EASD folks, + +The view has been created. Here are a couple notes on what the data covers: +- This view has one row per unique sis_integration_id (PUID) x sis_user_id (student number) x user_id (Canvas user ID) x pseudonyms_workflow_state combination. The latter is the status of the entry in our CD2 pseudonyms table. +- As we anticipate your searching to be by PUID, we have filtered to only rows with non-null PUIDs. All other fields may be null. +- The view name is user_activity, and the role name is ubc_it_easd_cache. +- The stats presented per user cover: + - logins and requests + - enrolments + - Note that 'active' enrolments are not just the student's enrolments for the current semester, but generally represent all enrolments, past and present, where the student did not drop the course or was otherwise removed. + - submissions + - This covers assignments, quizzes, etc. + - quiz_submissions + - This should be a subset of the above. This may not cover everything that we might think of as quizzes, as Canvas has 'Classic' and 'New' quizzes and typically only counts the former as quizzes. But all of these will be included in submissions above. + - discussion_entries + - conversation_messages + - This covers direct messages sent via Canvas's messaging system, which are often announcements sent from the instructor to all students. + +Don't hesitate to reach out if there's anything else I can clarify about the data! Best, +Anna \ No newline at end of file