From a2095f4562957797b1d5d6b73a530620485ac5f6 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Wed, 13 Mar 2024 13:10:37 -0400 Subject: [PATCH] feat: Add url column for open response questions [PT-181870149] This adds an url column to each open response question that points to the portal dashboard so that researchers can listen to audio responses. --- query-creator/create-query/steps/aws.js | 41 +-- .../create-query/tests/unit/test-handler.js | 235 +++++++++--------- 2 files changed, 143 insertions(+), 133 deletions(-) diff --git a/query-creator/create-query/steps/aws.js b/query-creator/create-query/steps/aws.js index 68cd780..582743d 100644 --- a/query-creator/create-query/steps/aws.js +++ b/query-creator/create-query/steps/aws.js @@ -140,6 +140,24 @@ const getColumnsForQuestion = (questionId, question, denormalizedResource, authD const columnPrefix = `res_${activityIndex}_${questionId}`; + const modelUrl = [`CONCAT(`, + `'${process.env.PORTAL_REPORT_URL}`, + `?auth-domain=${encodeURIComponent(authDomain)}`, + `&firebase-app=${process.env.FIREBASE_APP}`, + `&sourceKey=${sourceKey}`, + `&iframeQuestionId=${questionId}`, + `&class=${encodeURIComponent(`${authDomain}/api/v1/classes/`)}',`, + ` CAST(${learnersAndAnswersTable}.class_id AS VARCHAR), `, + `'&offering=${encodeURIComponent(`${authDomain}/api/v1/offerings/`)}',`, + ` CAST(${learnersAndAnswersTable}.offering_id AS VARCHAR), `, + `'&studentId=',`, + ` CAST(${learnersAndAnswersTable}.user_id AS VARCHAR), `, + `'&answersSourceKey=', `, + ` ${learnersAndAnswersTable}.source_key['${questionId}']`, + `)` + ].join("") + const conditionalModelUrl = `CASE WHEN ${learnersAndAnswersTable}.kv1['${questionId}'] IS NULL THEN '' ELSE ${modelUrl} END`; + switch (type) { case "image_question": columns.push({name: `${columnPrefix}_image_url`, @@ -160,6 +178,9 @@ const getColumnsForQuestion = (questionId, question, denormalizedResource, authD columns.push({name: `${columnPrefix}_text`, value: filterNoAnswerJSON, header: promptHeader}); + columns.push({name: `${columnPrefix}_url`, + value: conditionalModelUrl, + header: promptHeader}); break; case "multiple_choice": let questionHasCorrectAnswer = false; @@ -181,26 +202,6 @@ const getColumnsForQuestion = (questionId, question, denormalizedResource, authD columns.push({name: `${columnPrefix}_json`, value: `${learnersAndAnswersTable}.kv1['${questionId}']`, header: promptHeader}); - - const modelUrl = [`CONCAT(`, - `'${process.env.PORTAL_REPORT_URL}`, - `?auth-domain=${encodeURIComponent(authDomain)}`, - `&firebase-app=${process.env.FIREBASE_APP}`, - `&sourceKey=${sourceKey}`, - `&iframeQuestionId=${questionId}`, - `&class=${encodeURIComponent(`${authDomain}/api/v1/classes/`)}',`, - ` CAST(${learnersAndAnswersTable}.class_id AS VARCHAR), `, - `'&offering=${encodeURIComponent(`${authDomain}/api/v1/offerings/`)}',`, - ` CAST(${learnersAndAnswersTable}.offering_id AS VARCHAR), `, - `'&studentId=',`, - ` CAST(${learnersAndAnswersTable}.user_id AS VARCHAR), `, - `'&answersSourceKey=', `, - ` ${learnersAndAnswersTable}.source_key['${questionId}']`, - `)` - ].join("") - - const conditionalModelUrl = `CASE WHEN ${learnersAndAnswersTable}.kv1['${questionId}'] IS NULL THEN '' ELSE ${modelUrl} END`; - columns.push({name: `${columnPrefix}_url`, value: conditionalModelUrl, header: promptHeader}); diff --git a/query-creator/create-query/tests/unit/test-handler.js b/query-creator/create-query/tests/unit/test-handler.js index 2895a22..8fd4c1a 100644 --- a/query-creator/create-query/tests/unit/test-handler.js +++ b/query-creator/create-query/tests/unit/test-handler.js @@ -163,62 +163,62 @@ describe('Query creation', function () { activities_2 AS (SELECT *, cardinality(questions) AS num_questions FROM "report-service"."activity_structure" WHERE structure_id = 'ABCDEFGHI'), grouped_answers_1 AS ( - SELECT l.run_remote_endpoint remote_endpoint, map_agg(a.question_id, a.answer) kv1, map_agg(a.question_id, a.submitted) submitted, map_agg(a.question_id, a.source_key) source_key - FROM "report-service"."partitioned_answers" a - INNER JOIN "report-service"."learners" l - ON (l.query_id = '123456789' AND l.run_remote_endpoint = a.remote_endpoint) - WHERE a.escaped_url = 'https---authoring-staging-concord-org-activities-000000' - GROUP BY l.run_remote_endpoint), + SELECT l.run_remote_endpoint remote_endpoint, map_agg(a.question_id, a.answer) kv1, map_agg(a.question_id, a.submitted) submitted, map_agg(a.question_id, a.source_key) source_key + FROM "report-service"."partitioned_answers" a + INNER JOIN "report-service"."learners" l + ON (l.query_id = '123456789' AND l.run_remote_endpoint = a.remote_endpoint) + WHERE a.escaped_url = 'https---authoring-staging-concord-org-activities-000000' + GROUP BY l.run_remote_endpoint), grouped_answers_2 AS ( - SELECT l.run_remote_endpoint remote_endpoint, map_agg(a.question_id, a.answer) kv1, map_agg(a.question_id, a.submitted) submitted, map_agg(a.question_id, a.source_key) source_key - FROM "report-service"."partitioned_answers" a - INNER JOIN "report-service"."learners" l - ON (l.query_id = 'ABCDEFGHI' AND l.run_remote_endpoint = a.remote_endpoint) - WHERE a.escaped_url = 'https---authoring-staging-concord-org-activities-000001' - GROUP BY l.run_remote_endpoint), + SELECT l.run_remote_endpoint remote_endpoint, map_agg(a.question_id, a.answer) kv1, map_agg(a.question_id, a.submitted) submitted, map_agg(a.question_id, a.source_key) source_key + FROM "report-service"."partitioned_answers" a + INNER JOIN "report-service"."learners" l + ON (l.query_id = 'ABCDEFGHI' AND l.run_remote_endpoint = a.remote_endpoint) + WHERE a.escaped_url = 'https---authoring-staging-concord-org-activities-000001' + GROUP BY l.run_remote_endpoint), learners_and_answers_1 AS ( SELECT run_remote_endpoint remote_endpoint, runnable_url as resource_url, learner_id, student_id, user_id, offering_id, student_name, username, school, class, class_id, permission_forms, last_run, teachers, grouped_answers_1.kv1 kv1, grouped_answers_1.submitted submitted, grouped_answers_1.source_key source_key, - IF (kv1 is null, 0, cardinality(array_intersect(map_keys(kv1),map_keys(activities_1.questions)))) num_answers, - cardinality(filter(map_values(activities_1.questions), x->x.required=TRUE)) num_required_questions, - IF (submitted is null, 0, cardinality(filter(map_values(submitted), x->x=TRUE))) num_required_answers - FROM "report-service"."learners" l - LEFT JOIN activities_1 ON 1=1 -- activities may be empty so we can't fully join them and they don't have any common columns with learners thus the 1=1 - LEFT JOIN grouped_answers_1 - ON l.run_remote_endpoint = grouped_answers_1.remote_endpoint - WHERE l.query_id = '123456789'), + IF (kv1 is null, 0, cardinality(array_intersect(map_keys(kv1),map_keys(activities_1.questions)))) num_answers, + cardinality(filter(map_values(activities_1.questions), x->x.required=TRUE)) num_required_questions, + IF (submitted is null, 0, cardinality(filter(map_values(submitted), x->x=TRUE))) num_required_answers + FROM "report-service"."learners" l + LEFT JOIN activities_1 ON 1=1 -- activities may be empty so we can't fully join them and they don't have any common columns with learners thus the 1=1 + LEFT JOIN grouped_answers_1 + ON l.run_remote_endpoint = grouped_answers_1.remote_endpoint + WHERE l.query_id = '123456789'), learners_and_answers_2 AS ( SELECT run_remote_endpoint remote_endpoint, runnable_url as resource_url, learner_id, student_id, user_id, offering_id, student_name, username, school, class, class_id, permission_forms, last_run, teachers, grouped_answers_2.kv1 kv1, grouped_answers_2.submitted submitted, grouped_answers_2.source_key source_key, - IF (kv1 is null, 0, cardinality(array_intersect(map_keys(kv1),map_keys(activities_2.questions)))) num_answers, - cardinality(filter(map_values(activities_2.questions), x->x.required=TRUE)) num_required_questions, - IF (submitted is null, 0, cardinality(filter(map_values(submitted), x->x=TRUE))) num_required_answers - FROM "report-service"."learners" l - LEFT JOIN activities_2 ON 1=1 -- activities may be empty so we can't fully join them and they don't have any common columns with learners thus the 1=1 - LEFT JOIN grouped_answers_2 - ON l.run_remote_endpoint = grouped_answers_2.remote_endpoint - WHERE l.query_id = 'ABCDEFGHI'), + IF (kv1 is null, 0, cardinality(array_intersect(map_keys(kv1),map_keys(activities_2.questions)))) num_answers, + cardinality(filter(map_values(activities_2.questions), x->x.required=TRUE)) num_required_questions, + IF (submitted is null, 0, cardinality(filter(map_values(submitted), x->x=TRUE))) num_required_answers + FROM "report-service"."learners" l + LEFT JOIN activities_2 ON 1=1 -- activities may be empty so we can't fully join them and they don't have any common columns with learners thus the 1=1 + LEFT JOIN grouped_answers_2 + ON l.run_remote_endpoint = grouped_answers_2.remote_endpoint + WHERE l.query_id = 'ABCDEFGHI'), unique_user_class AS (SELECT class_id, user_id, - arbitrary(student_id) as student_id, - arbitrary(student_name) as student_name, - arbitrary(username) as username, - arbitrary(school) as school, - arbitrary(class) as class, - arbitrary(permission_forms) as permission_forms, - -- We could just select arbitrary(teachers) here and then do the transform in the main query - array_join(transform(arbitrary(teachers), teacher -> teacher.user_id), ',') AS teacher_user_ids, - array_join(transform(arbitrary(teachers), teacher -> teacher.name), ',') AS teacher_names, - array_join(transform(arbitrary(teachers), teacher -> teacher.district), ',') AS teacher_districts, - array_join(transform(arbitrary(teachers), teacher -> teacher.state), ',') AS teacher_states, - array_join(transform(arbitrary(teachers), teacher -> teacher.email), ',') AS teacher_emails - FROM "report-service"."learners" l - WHERE l.query_id IN ('123456789', 'ABCDEFGHI') - GROUP BY class_id, user_id), + arbitrary(student_id) as student_id, + arbitrary(student_name) as student_name, + arbitrary(username) as username, + arbitrary(school) as school, + arbitrary(class) as class, + arbitrary(permission_forms) as permission_forms, + -- We could just select arbitrary(teachers) here and then do the transform in the main query + array_join(transform(arbitrary(teachers), teacher -> teacher.user_id), ',') AS teacher_user_ids, + array_join(transform(arbitrary(teachers), teacher -> teacher.name), ',') AS teacher_names, + array_join(transform(arbitrary(teachers), teacher -> teacher.district), ',') AS teacher_districts, + array_join(transform(arbitrary(teachers), teacher -> teacher.state), ',') AS teacher_states, + array_join(transform(arbitrary(teachers), teacher -> teacher.email), ',') AS teacher_emails + FROM "report-service"."learners" l + WHERE l.query_id IN ('123456789', 'ABCDEFGHI') + GROUP BY class_id, user_id), one_row_table_for_join as (SELECT null AS empty) - SELECT - 'Prompt' AS student_id, + SELECT + 'Prompt' AS student_id, null AS user_id, null AS student_name, null AS username, @@ -257,7 +257,9 @@ describe('Query creation', function () { activities_1.questions['multiple_choice_03000'].prompt AS res_1_multiple_choice_03000_choice, null AS res_1_multiple_choice_03000_submitted, activities_1.questions['open_response_11111'].prompt AS res_1_open_response_11111_text, + activities_1.questions['open_response_11111'].prompt AS res_1_open_response_11111_url, activities_1.questions['open_response_22222'].prompt AS res_1_open_response_22222_text, + activities_1.questions['open_response_22222'].prompt AS res_1_open_response_22222_url, null AS res_1_open_response_22222_submitted, activities_1.questions['image_question_33333'].prompt AS res_1_image_question_33333_image_url, activities_1.questions['image_question_33333'].prompt AS res_1_image_question_33333_text, @@ -267,6 +269,7 @@ describe('Query creation', function () { activities_1.questions['image_question_44444'].prompt AS res_1_image_question_44444_answer, null AS res_1_image_question_44444_submitted, activities_1.questions['managed_interactive_55555'].prompt AS res_1_managed_interactive_55555_text, + activities_1.questions['managed_interactive_55555'].prompt AS res_1_managed_interactive_55555_url, activities_1.questions['managed_interactive_66666'].prompt AS res_1_managed_interactive_66666_choice, activities_1.questions['managed_interactive_77777'].prompt AS res_1_managed_interactive_77777_image_url, activities_1.questions['managed_interactive_77777'].prompt AS res_1_managed_interactive_77777_text, @@ -276,14 +279,14 @@ describe('Query creation', function () { activities_1.questions['managed_interactive_99999'].prompt AS res_1_managed_interactive_99999_json, activities_2.questions['managed_interactive_88888'].prompt AS res_2_managed_interactive_88888_json, activities_2.questions['managed_interactive_88888'].prompt AS res_2_managed_interactive_88888_url - FROM one_row_table_for_join - LEFT JOIN activities_1 ON 1=1 + FROM one_row_table_for_join + LEFT JOIN activities_1 ON 1=1 LEFT JOIN activities_2 ON 1=1 UNION ALL - SELECT - 'Correct answer' AS student_id, + SELECT + 'Correct answer' AS student_id, null AS user_id, null AS student_name, null AS username, @@ -322,7 +325,9 @@ describe('Query creation', function () { activities_1.questions['multiple_choice_03000'].correctAnswer AS res_1_multiple_choice_03000_choice, null AS res_1_multiple_choice_03000_submitted, null AS res_1_open_response_11111_text, + null AS res_1_open_response_11111_url, null AS res_1_open_response_22222_text, + null AS res_1_open_response_22222_url, null AS res_1_open_response_22222_submitted, null AS res_1_image_question_33333_image_url, null AS res_1_image_question_33333_text, @@ -332,6 +337,7 @@ describe('Query creation', function () { null AS res_1_image_question_44444_answer, null AS res_1_image_question_44444_submitted, null AS res_1_managed_interactive_55555_text, + null AS res_1_managed_interactive_55555_url, activities_1.questions['managed_interactive_66666'].correctAnswer AS res_1_managed_interactive_66666_choice, null AS res_1_managed_interactive_77777_image_url, null AS res_1_managed_interactive_77777_text, @@ -341,75 +347,78 @@ describe('Query creation', function () { null AS res_1_managed_interactive_99999_json, null AS res_2_managed_interactive_88888_json, null AS res_2_managed_interactive_88888_url - FROM one_row_table_for_join - LEFT JOIN activities_1 ON 1=1 + FROM one_row_table_for_join + LEFT JOIN activities_1 ON 1=1 LEFT JOIN activities_2 ON 1=1 UNION ALL - SELECT - unique_user_class.student_id, - unique_user_class.user_id, - unique_user_class.student_name, - unique_user_class.username, - unique_user_class.school, - unique_user_class.class, - unique_user_class.class_id, - unique_user_class.permission_forms, - unique_user_class.teacher_user_ids, - unique_user_class.teacher_names, - unique_user_class.teacher_districts, - unique_user_class.teacher_states, - unique_user_class.teacher_emails, - 'test activity' AS res_1_name, - learners_and_answers_1.learner_id AS res_1_learner_id, - learners_and_answers_1.remote_endpoint AS res_1_remote_endpoint, - learners_and_answers_1.resource_url AS res_1_resource_url, - learners_and_answers_1.last_run AS res_1_last_run, - activities_1.num_questions AS res_1_total_num_questions, - learners_and_answers_1.num_answers AS res_1_total_num_answers, - round(100.0 * learners_and_answers_1.num_answers / activities_1.num_questions, 1) AS res_1_total_percent_complete, - learners_and_answers_1.num_required_questions AS res_1_num_required_questions, - learners_and_answers_1.num_required_answers AS res_1_num_required_answers, - 'test activity 2' AS res_2_name, - learners_and_answers_2.learner_id AS res_2_learner_id, - learners_and_answers_2.remote_endpoint AS res_2_remote_endpoint, - learners_and_answers_2.resource_url AS res_2_resource_url, - learners_and_answers_2.last_run AS res_2_last_run, - activities_2.num_questions AS res_2_total_num_questions, - learners_and_answers_2.num_answers AS res_2_total_num_answers, - round(100.0 * learners_and_answers_2.num_answers / activities_2.num_questions, 1) AS res_2_total_percent_complete, - learners_and_answers_2.num_required_questions AS res_2_num_required_questions, - learners_and_answers_2.num_required_answers AS res_2_num_required_answers, - array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['multiple_choice_00000'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['multiple_choice_00000'][x].content, IF(activities_1.choices['multiple_choice_00000'][x].correct,' (correct)',' (wrong)'))),', ') AS res_1_multiple_choice_00000_choice, - array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['multiple_choice_01000'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['multiple_choice_01000'][x].content, '')),', ') AS res_1_multiple_choice_01000_choice, - array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['multiple_choice_02000'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['multiple_choice_02000'][x].content, IF(activities_1.choices['multiple_choice_02000'][x].correct,' (correct)',' (wrong)'))),', ') AS res_1_multiple_choice_02000_choice, - array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['multiple_choice_03000'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['multiple_choice_03000'][x].content, IF(activities_1.choices['multiple_choice_03000'][x].correct,' (correct)',' (wrong)'))),', ') AS res_1_multiple_choice_03000_choice, - COALESCE(learners_and_answers_1.submitted['multiple_choice_03000'], false) AS res_1_multiple_choice_03000_submitted, - if(starts_with(learners_and_answers_1.kv1['open_response_11111'], '"{\\"mode\\":\\"report\\"'), '', learners_and_answers_1.kv1['open_response_11111']) AS res_1_open_response_11111_text, - if(starts_with(learners_and_answers_1.kv1['open_response_22222'], '"{\\"mode\\":\\"report\\"'), '', learners_and_answers_1.kv1['open_response_22222']) AS res_1_open_response_22222_text, - COALESCE(learners_and_answers_1.submitted['open_response_22222'], false) AS res_1_open_response_22222_submitted, - json_extract_scalar(learners_and_answers_1.kv1['image_question_33333'], '$.image_url') AS res_1_image_question_33333_image_url, - json_extract_scalar(learners_and_answers_1.kv1['image_question_33333'], '$.text') AS res_1_image_question_33333_text, - learners_and_answers_1.kv1['image_question_33333'] AS res_1_image_question_33333_answer, - json_extract_scalar(learners_and_answers_1.kv1['image_question_44444'], '$.image_url') AS res_1_image_question_44444_image_url, - json_extract_scalar(learners_and_answers_1.kv1['image_question_44444'], '$.text') AS res_1_image_question_44444_text, - learners_and_answers_1.kv1['image_question_44444'] AS res_1_image_question_44444_answer, - COALESCE(learners_and_answers_1.submitted['image_question_44444'], false) AS res_1_image_question_44444_submitted, - if(starts_with(learners_and_answers_1.kv1['managed_interactive_55555'], '"{\\"mode\\":\\"report\\"'), '', learners_and_answers_1.kv1['managed_interactive_55555']) AS res_1_managed_interactive_55555_text, - array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['managed_interactive_66666'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['managed_interactive_66666'][x].content, IF(activities_1.choices['managed_interactive_66666'][x].correct,' (correct)',' (wrong)'))),', ') AS res_1_managed_interactive_66666_choice, - json_extract_scalar(learners_and_answers_1.kv1['managed_interactive_77777'], '$.image_url') AS res_1_managed_interactive_77777_image_url, - json_extract_scalar(learners_and_answers_1.kv1['managed_interactive_77777'], '$.text') AS res_1_managed_interactive_77777_text, - learners_and_answers_1.kv1['managed_interactive_77777'] AS res_1_managed_interactive_77777_answer, - learners_and_answers_1.kv1['managed_interactive_88888'] AS res_1_managed_interactive_88888_json, - CASE WHEN learners_and_answers_1.kv1['managed_interactive_88888'] IS NULL THEN '' ELSE CONCAT('https://portal-report.test?auth-domain=fake-auth-domain&firebase-app=report-service-test&sourceKey=fake-source-key&iframeQuestionId=managed_interactive_88888&class=fake-auth-domain%2Fapi%2Fv1%2Fclasses%2F', CAST(learners_and_answers_1.class_id AS VARCHAR), '&offering=fake-auth-domain%2Fapi%2Fv1%2Fofferings%2F', CAST(learners_and_answers_1.offering_id AS VARCHAR), '&studentId=', CAST(learners_and_answers_1.user_id AS VARCHAR), '&answersSourceKey=', learners_and_answers_1.source_key['managed_interactive_88888']) END AS res_1_managed_interactive_88888_url, - learners_and_answers_1.['managed_interactive_99999'] AS res_1_managed_interactive_99999_json, - learners_and_answers_2.kv1['managed_interactive_88888'] AS res_2_managed_interactive_88888_json, - CASE WHEN learners_and_answers_2.kv1['managed_interactive_88888'] IS NULL THEN '' ELSE CONCAT('https://portal-report.test?auth-domain=fake-auth-domain&firebase-app=report-service-test&sourceKey=fake-source-key&iframeQuestionId=managed_interactive_88888&class=fake-auth-domain%2Fapi%2Fv1%2Fclasses%2F', CAST(learners_and_answers_2.class_id AS VARCHAR), '&offering=fake-auth-domain%2Fapi%2Fv1%2Fofferings%2F', CAST(learners_and_answers_2.offering_id AS VARCHAR), '&studentId=', CAST(learners_and_answers_2.user_id AS VARCHAR), '&answersSourceKey=', learners_and_answers_2.source_key['managed_interactive_88888']) END AS res_2_managed_interactive_88888_url - FROM unique_user_class - LEFT JOIN activities_1 ON 1=1 -- activities may be empty so we can't fully join them and they don't have any common columns with unique_user_class thus the 1=1 + SELECT + unique_user_class.student_id, + unique_user_class.user_id, + unique_user_class.student_name, + unique_user_class.username, + unique_user_class.school, + unique_user_class.class, + unique_user_class.class_id, + unique_user_class.permission_forms, + unique_user_class.teacher_user_ids, + unique_user_class.teacher_names, + unique_user_class.teacher_districts, + unique_user_class.teacher_states, + unique_user_class.teacher_emails, + 'test activity' AS res_1_name, + learners_and_answers_1.learner_id AS res_1_learner_id, + learners_and_answers_1.remote_endpoint AS res_1_remote_endpoint, + learners_and_answers_1.resource_url AS res_1_resource_url, + learners_and_answers_1.last_run AS res_1_last_run, + activities_1.num_questions AS res_1_total_num_questions, + learners_and_answers_1.num_answers AS res_1_total_num_answers, + round(100.0 * learners_and_answers_1.num_answers / activities_1.num_questions, 1) AS res_1_total_percent_complete, + learners_and_answers_1.num_required_questions AS res_1_num_required_questions, + learners_and_answers_1.num_required_answers AS res_1_num_required_answers, + 'test activity 2' AS res_2_name, + learners_and_answers_2.learner_id AS res_2_learner_id, + learners_and_answers_2.remote_endpoint AS res_2_remote_endpoint, + learners_and_answers_2.resource_url AS res_2_resource_url, + learners_and_answers_2.last_run AS res_2_last_run, + activities_2.num_questions AS res_2_total_num_questions, + learners_and_answers_2.num_answers AS res_2_total_num_answers, + round(100.0 * learners_and_answers_2.num_answers / activities_2.num_questions, 1) AS res_2_total_percent_complete, + learners_and_answers_2.num_required_questions AS res_2_num_required_questions, + learners_and_answers_2.num_required_answers AS res_2_num_required_answers, + array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['multiple_choice_00000'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['multiple_choice_00000'][x].content, IF(activities_1.choices['multiple_choice_00000'][x].correct,' (correct)',' (wrong)'))),', ') AS res_1_multiple_choice_00000_choice, + array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['multiple_choice_01000'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['multiple_choice_01000'][x].content, '')),', ') AS res_1_multiple_choice_01000_choice, + array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['multiple_choice_02000'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['multiple_choice_02000'][x].content, IF(activities_1.choices['multiple_choice_02000'][x].correct,' (correct)',' (wrong)'))),', ') AS res_1_multiple_choice_02000_choice, + array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['multiple_choice_03000'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['multiple_choice_03000'][x].content, IF(activities_1.choices['multiple_choice_03000'][x].correct,' (correct)',' (wrong)'))),', ') AS res_1_multiple_choice_03000_choice, + COALESCE(learners_and_answers_1.submitted['multiple_choice_03000'], false) AS res_1_multiple_choice_03000_submitted, + if(starts_with(learners_and_answers_1.kv1['open_response_11111'], '"{\\"mode\\":\\"report\\"'), '', learners_and_answers_1.kv1['open_response_11111']) AS res_1_open_response_11111_text, + CASE WHEN learners_and_answers_1.kv1['open_response_11111'] IS NULL THEN '' ELSE CONCAT('https://portal-report.test?auth-domain=fake-auth-domain&firebase-app=report-service-test&sourceKey=fake-source-key&iframeQuestionId=open_response_11111&class=fake-auth-domain%2Fapi%2Fv1%2Fclasses%2F', CAST(learners_and_answers_1.class_id AS VARCHAR), '&offering=fake-auth-domain%2Fapi%2Fv1%2Fofferings%2F', CAST(learners_and_answers_1.offering_id AS VARCHAR), '&studentId=', CAST(learners_and_answers_1.user_id AS VARCHAR), '&answersSourceKey=', learners_and_answers_1.source_key['open_response_11111']) END AS res_1_open_response_11111_url, + if(starts_with(learners_and_answers_1.kv1['open_response_22222'], '"{\\"mode\\":\\"report\\"'), '', learners_and_answers_1.kv1['open_response_22222']) AS res_1_open_response_22222_text, + CASE WHEN learners_and_answers_1.kv1['open_response_22222'] IS NULL THEN '' ELSE CONCAT('https://portal-report.test?auth-domain=fake-auth-domain&firebase-app=report-service-test&sourceKey=fake-source-key&iframeQuestionId=open_response_22222&class=fake-auth-domain%2Fapi%2Fv1%2Fclasses%2F', CAST(learners_and_answers_1.class_id AS VARCHAR), '&offering=fake-auth-domain%2Fapi%2Fv1%2Fofferings%2F', CAST(learners_and_answers_1.offering_id AS VARCHAR), '&studentId=', CAST(learners_and_answers_1.user_id AS VARCHAR), '&answersSourceKey=', learners_and_answers_1.source_key['open_response_22222']) END AS res_1_open_response_22222_url, + COALESCE(learners_and_answers_1.submitted['open_response_22222'], false) AS res_1_open_response_22222_submitted, + json_extract_scalar(learners_and_answers_1.kv1['image_question_33333'], '$.image_url') AS res_1_image_question_33333_image_url, + json_extract_scalar(learners_and_answers_1.kv1['image_question_33333'], '$.text') AS res_1_image_question_33333_text, + learners_and_answers_1.kv1['image_question_33333'] AS res_1_image_question_33333_answer, + json_extract_scalar(learners_and_answers_1.kv1['image_question_44444'], '$.image_url') AS res_1_image_question_44444_image_url, + json_extract_scalar(learners_and_answers_1.kv1['image_question_44444'], '$.text') AS res_1_image_question_44444_text, + learners_and_answers_1.kv1['image_question_44444'] AS res_1_image_question_44444_answer, + COALESCE(learners_and_answers_1.submitted['image_question_44444'], false) AS res_1_image_question_44444_submitted, + if(starts_with(learners_and_answers_1.kv1['managed_interactive_55555'], '"{\\"mode\\":\\"report\\"'), '', learners_and_answers_1.kv1['managed_interactive_55555']) AS res_1_managed_interactive_55555_text, + CASE WHEN learners_and_answers_1.kv1['managed_interactive_55555'] IS NULL THEN '' ELSE CONCAT('https://portal-report.test?auth-domain=fake-auth-domain&firebase-app=report-service-test&sourceKey=fake-source-key&iframeQuestionId=managed_interactive_55555&class=fake-auth-domain%2Fapi%2Fv1%2Fclasses%2F', CAST(learners_and_answers_1.class_id AS VARCHAR), '&offering=fake-auth-domain%2Fapi%2Fv1%2Fofferings%2F', CAST(learners_and_answers_1.offering_id AS VARCHAR), '&studentId=', CAST(learners_and_answers_1.user_id AS VARCHAR), '&answersSourceKey=', learners_and_answers_1.source_key['managed_interactive_55555']) END AS res_1_managed_interactive_55555_url, + array_join(transform(CAST(json_extract(learners_and_answers_1.kv1['managed_interactive_66666'],'$.choice_ids') AS ARRAY(VARCHAR)), x -> CONCAT(activities_1.choices['managed_interactive_66666'][x].content, IF(activities_1.choices['managed_interactive_66666'][x].correct,' (correct)',' (wrong)'))),', ') AS res_1_managed_interactive_66666_choice, + json_extract_scalar(learners_and_answers_1.kv1['managed_interactive_77777'], '$.image_url') AS res_1_managed_interactive_77777_image_url, + json_extract_scalar(learners_and_answers_1.kv1['managed_interactive_77777'], '$.text') AS res_1_managed_interactive_77777_text, + learners_and_answers_1.kv1['managed_interactive_77777'] AS res_1_managed_interactive_77777_answer, + learners_and_answers_1.kv1['managed_interactive_88888'] AS res_1_managed_interactive_88888_json, + CASE WHEN learners_and_answers_1.kv1['managed_interactive_88888'] IS NULL THEN '' ELSE CONCAT('https://portal-report.test?auth-domain=fake-auth-domain&firebase-app=report-service-test&sourceKey=fake-source-key&iframeQuestionId=managed_interactive_88888&class=fake-auth-domain%2Fapi%2Fv1%2Fclasses%2F', CAST(learners_and_answers_1.class_id AS VARCHAR), '&offering=fake-auth-domain%2Fapi%2Fv1%2Fofferings%2F', CAST(learners_and_answers_1.offering_id AS VARCHAR), '&studentId=', CAST(learners_and_answers_1.user_id AS VARCHAR), '&answersSourceKey=', learners_and_answers_1.source_key['managed_interactive_88888']) END AS res_1_managed_interactive_88888_url, + learners_and_answers_1.['managed_interactive_99999'] AS res_1_managed_interactive_99999_json, + learners_and_answers_2.kv1['managed_interactive_88888'] AS res_2_managed_interactive_88888_json, + CASE WHEN learners_and_answers_2.kv1['managed_interactive_88888'] IS NULL THEN '' ELSE CONCAT('https://portal-report.test?auth-domain=fake-auth-domain&firebase-app=report-service-test&sourceKey=fake-source-key&iframeQuestionId=managed_interactive_88888&class=fake-auth-domain%2Fapi%2Fv1%2Fclasses%2F', CAST(learners_and_answers_2.class_id AS VARCHAR), '&offering=fake-auth-domain%2Fapi%2Fv1%2Fofferings%2F', CAST(learners_and_answers_2.offering_id AS VARCHAR), '&studentId=', CAST(learners_and_answers_2.user_id AS VARCHAR), '&answersSourceKey=', learners_and_answers_2.source_key['managed_interactive_88888']) END AS res_2_managed_interactive_88888_url + FROM unique_user_class + LEFT JOIN activities_1 ON 1=1 -- activities may be empty so we can't fully join them and they don't have any common columns with unique_user_class thus the 1=1 LEFT JOIN activities_2 ON 1=1 -- activities may be empty so we can't fully join them and they don't have any common columns with unique_user_class thus the 1=1 - LEFT JOIN learners_and_answers_1 ON unique_user_class.user_id = learners_and_answers_1.user_id AND unique_user_class.class_id = learners_and_answers_1.class_id + LEFT JOIN learners_and_answers_1 ON unique_user_class.user_id = learners_and_answers_1.user_id AND unique_user_class.class_id = learners_and_answers_1.class_id LEFT JOIN learners_and_answers_2 ON unique_user_class.user_id = learners_and_answers_2.user_id AND unique_user_class.class_id = learners_and_answers_2.class_id ORDER BY class NULLS FIRST, username