diff --git a/analytics_data_api/insights_snowflake/mappers/performance.py b/analytics_data_api/insights_snowflake/mappers/performance.py new file mode 100644 index 00000000..0743f942 --- /dev/null +++ b/analytics_data_api/insights_snowflake/mappers/performance.py @@ -0,0 +1,97 @@ +"""Map Snowflake performance rows into the existing API response shapes.""" + +from analytics_data_api.v0.models import ProblemFirstLastResponseAnswerDistribution + +COURSE_PROBLEM_FIELDS = ( + 'module_id', + 'total_submissions', + 'correct_submissions', + 'part_ids', + 'created', +) +COURSE_PROBLEM_INTEGER_FIELDS = ( + 'total_submissions', + 'correct_submissions', +) + + +def _row_value(row, name): + """Return a row value from dictionary rows produced by the Snowflake client.""" + if name in row: + return row[name] + return row[name.upper()] + + +def _optional_row_value(row, name): + """Return a row value when present, otherwise None.""" + if name in row: + return row[name] + return row.get(name.upper()) + + +def _nullable_int(value): + """Return an integer value while preserving nulls.""" + if value is None: + return None + return int(value) + + +def _api_value(row, name): + """Return the API-compatible value for a Snowflake course problem field.""" + value = _row_value(row, name) + if name == 'part_ids': + return value.split(',') if value else [] + if name in COURSE_PROBLEM_INTEGER_FIELDS: + return int(value) + return value + + +def _answer_value(row): + """Return the Snowflake answer text using the existing API field name.""" + if 'answer_value_text' in row: + return row['answer_value_text'] + if 'ANSWER_VALUE_TEXT' in row: + return row['ANSWER_VALUE_TEXT'] + return _row_value(row, 'answer_value') + + +def _answer_sort_key(row): + """Sort answer rows so existing part grouping is deterministic.""" + variant = _optional_row_value(row, 'variant') + return ( + _row_value(row, 'part_id') or '', + _row_value(row, 'value_id') or '', + -1 if variant is None else int(variant), + ) + + +def map_course_problem_rows(rows): + """Map Snowflake course problem rows into existing API response dictionaries.""" + return [ + { + field: _api_value(row, field) + for field in COURSE_PROBLEM_FIELDS + } + for row in rows or [] + ] + + +def map_problem_answer_distribution_rows(rows): + """Map Snowflake answer distribution rows into unsaved model instances.""" + return [ + ProblemFirstLastResponseAnswerDistribution( + course_id=_row_value(row, 'course_id'), + module_id=_row_value(row, 'module_id'), + part_id=_row_value(row, 'part_id'), + correct=_row_value(row, 'correct'), + value_id=_row_value(row, 'value_id'), + answer_value=_answer_value(row), + variant=_nullable_int(_optional_row_value(row, 'variant')), + problem_display_name=_row_value(row, 'problem_display_name'), + question_text=_row_value(row, 'question_text'), + first_response_count=int(_row_value(row, 'first_response_count')), + last_response_count=int(_row_value(row, 'last_response_count')), + created=_row_value(row, 'created'), + ) + for row in sorted(rows or [], key=_answer_sort_key) + ] diff --git a/analytics_data_api/insights_snowflake/queries/performance.py b/analytics_data_api/insights_snowflake/queries/performance.py new file mode 100644 index 00000000..e6f463b6 --- /dev/null +++ b/analytics_data_api/insights_snowflake/queries/performance.py @@ -0,0 +1,50 @@ +"""Snowflake queries for performance metrics.""" + +from analytics_data_api.insights_snowflake.client import fetch_all, get_qualified_table_name + +PROBLEM_ANSWER_DISTRIBUTION_TABLE = 'PROBLEM_ANSWER_DISTRIBUTION' + + +def get_course_problem_rows(course_id): + """Return Snowflake rows for the course problems list.""" + table_name = get_qualified_table_name(PROBLEM_ANSWER_DISTRIBUTION_TABLE) + sql = """ +SELECT + module_id, + SUM(last_response_count) / COUNT(DISTINCT part_id) AS total_submissions, + SUM(CASE WHEN correct = 1 THEN last_response_count ELSE 0 END) / COUNT(DISTINCT part_id) + AS correct_submissions, + LISTAGG(DISTINCT part_id, ',') WITHIN GROUP (ORDER BY part_id) AS part_ids, + MAX(created) AS created +FROM {table_name} +WHERE course_id = %(course_id)s +GROUP BY module_id +ORDER BY module_id +""".format(table_name=table_name) + + return fetch_all(sql, {'course_id': course_id}) + + +def get_problem_answer_distribution_rows(problem_id): + """Return Snowflake rows for a problem answer distribution.""" + table_name = get_qualified_table_name(PROBLEM_ANSWER_DISTRIBUTION_TABLE) + sql = """ +SELECT + course_id, + module_id, + part_id, + correct, + value_id, + answer_value_text, + variant, + problem_display_name, + question_text, + first_response_count, + last_response_count, + created +FROM {table_name} +WHERE module_id = %(problem_id)s +ORDER BY part_id, value_id, variant +""".format(table_name=table_name) + + return fetch_all(sql, {'problem_id': problem_id}) diff --git a/analytics_data_api/insights_snowflake/service.py b/analytics_data_api/insights_snowflake/service.py index 5021f282..996ec85c 100644 --- a/analytics_data_api/insights_snowflake/service.py +++ b/analytics_data_api/insights_snowflake/service.py @@ -9,6 +9,10 @@ map_course_enrollment_location_rows, map_course_enrollment_mode_rows, ) +from analytics_data_api.insights_snowflake.mappers.performance import ( + map_course_problem_rows, + map_problem_answer_distribution_rows, +) from analytics_data_api.insights_snowflake.mappers.programs import map_program_metadata_rows from analytics_data_api.insights_snowflake.mappers.videos import map_course_video_rows, map_video_timeline_rows from analytics_data_api.insights_snowflake.queries.activity import get_course_activity_weekly_rows @@ -24,6 +28,10 @@ get_course_enrollment_location_rows, get_course_enrollment_mode_rows, ) +from analytics_data_api.insights_snowflake.queries.performance import ( + get_course_problem_rows, + get_problem_answer_distribution_rows, +) from analytics_data_api.insights_snowflake.queries.programs import get_program_metadata_rows from analytics_data_api.insights_snowflake.queries.videos import get_course_video_rows, get_video_timeline_rows @@ -97,3 +105,15 @@ def get_video_timeline(video_id): """Return video timeline metrics in the existing API response shape.""" rows = get_video_timeline_rows(video_id) return map_video_timeline_rows(rows) + + +def get_course_problems(course_id): + """Return course problems in the existing API response shape.""" + rows = get_course_problem_rows(course_id) + return map_course_problem_rows(rows) + + +def get_problem_answer_distribution(problem_id): + """Return problem answer distribution rows in the existing API response shape.""" + rows = get_problem_answer_distribution_rows(problem_id) + return map_problem_answer_distribution_rows(rows) diff --git a/analytics_data_api/tests/test_insights_snowflake.py b/analytics_data_api/tests/test_insights_snowflake.py index 1fda00f3..9ef276dc 100644 --- a/analytics_data_api/tests/test_insights_snowflake.py +++ b/analytics_data_api/tests/test_insights_snowflake.py @@ -17,6 +17,10 @@ map_course_enrollment_location_rows, map_course_enrollment_mode_rows, ) +from analytics_data_api.insights_snowflake.mappers.performance import ( + map_course_problem_rows, + map_problem_answer_distribution_rows, +) from analytics_data_api.insights_snowflake.mappers.programs import map_program_metadata_rows from analytics_data_api.insights_snowflake.mappers.videos import map_course_video_rows, map_video_timeline_rows from analytics_data_api.insights_snowflake.queries.activity import get_course_activity_weekly_rows @@ -41,6 +45,11 @@ get_course_enrollment_location_rows, get_course_enrollment_mode_rows, ) +from analytics_data_api.insights_snowflake.queries.performance import ( + PROBLEM_ANSWER_DISTRIBUTION_TABLE, + get_course_problem_rows, + get_problem_answer_distribution_rows, +) from analytics_data_api.insights_snowflake.queries.programs import ( COURSE_PROGRAM_METADATA_TABLE, get_program_metadata_rows, @@ -63,8 +72,10 @@ get_course_enrollment_gender, get_course_enrollment_location, get_course_enrollment_mode, + get_course_problems, get_course_summaries, get_course_videos, + get_problem_answer_distribution, get_program_metadata, get_video_timeline, ) @@ -411,6 +422,45 @@ def test_video_query_functions_use_expected_tables(self, mock_get_table_name, _m self.assertEqual(params, {'video_id': 'video-1'}) +class InsightsSnowflakePerformanceQueryTests(SimpleTestCase): + """Cover performance query construction with mocked Snowflake execution.""" + + @patch('analytics_data_api.insights_snowflake.queries.performance.fetch_all') + @patch( + 'analytics_data_api.insights_snowflake.queries.performance.get_qualified_table_name', + Mock(return_value='PROD.INSIGHTS.PROBLEM_ANSWER_DISTRIBUTION') + ) + def test_get_course_problem_rows_uses_expected_table(self, mock_fetch_all): + mock_fetch_all.return_value = [{'module_id': 'problem-1'}] + course_id = 'course-v1:edX+DemoX+Demo_Course' + + rows = get_course_problem_rows(course_id) + + self.assertEqual(rows, [{'module_id': 'problem-1'}]) + sql, params = mock_fetch_all.call_args[0] + self.assertIn('FROM PROD.INSIGHTS.PROBLEM_ANSWER_DISTRIBUTION', sql) + self.assertIn('LISTAGG(DISTINCT part_id', sql) + self.assertIn('WHERE course_id = %(course_id)s', sql) + self.assertEqual(params, {'course_id': course_id}) + + @patch('analytics_data_api.insights_snowflake.queries.performance.fetch_all') + @patch('analytics_data_api.insights_snowflake.queries.performance.get_qualified_table_name') + def test_get_problem_answer_distribution_rows_uses_expected_table(self, mock_get_table_name, _mock_fetch_all): + mock_get_table_name.return_value = 'PROD.INSIGHTS.PROBLEM_ANSWER_DISTRIBUTION' + problem_id = 'i4x://edX/DemoX/problem/Test' + + get_problem_answer_distribution_rows(problem_id) + + mock_get_table_name.assert_called_once_with(PROBLEM_ANSWER_DISTRIBUTION_TABLE) + sql, params = _mock_fetch_all.call_args[0] + self.assertIn('answer_value_text', sql) + self.assertIn('first_response_count', sql) + self.assertIn('last_response_count', sql) + self.assertIn('WHERE module_id = %(problem_id)s', sql) + self.assertIn('ORDER BY part_id, value_id, variant', sql) + self.assertEqual(params, {'problem_id': problem_id}) + + class InsightsSnowflakeActivityMapperTests(SimpleTestCase): """Cover Snowflake activity row mapping into the existing API shape.""" @@ -868,6 +918,62 @@ def test_map_video_timeline_rows(self): }]) +class InsightsSnowflakePerformanceMapperTests(SimpleTestCase): + """Cover Snowflake performance row mapping into the existing API shapes.""" + + def test_map_course_problem_rows_accepts_uppercase_snowflake_keys(self): + created = datetime.datetime(2014, 1, 2, tzinfo=datetime.timezone.utc) + rows = [{ + 'MODULE_ID': 'i4x://test/problem/1', + 'TOTAL_SUBMISSIONS': 150, + 'CORRECT_SUBMISSIONS': 50, + 'PART_IDS': 'part-1,part-2', + 'CREATED': created, + }] + + self.assertEqual(map_course_problem_rows(rows), [{ + 'module_id': 'i4x://test/problem/1', + 'total_submissions': 150, + 'correct_submissions': 50, + 'part_ids': ['part-1', 'part-2'], + 'created': created, + }]) + + def test_map_problem_answer_distribution_rows_returns_first_last_model_instances(self): + created = datetime.datetime(2014, 1, 2, tzinfo=datetime.timezone.utc) + rows = [{ + 'COURSE_ID': 'course-v1:edX+DemoX+Demo_Course', + 'MODULE_ID': 'i4x://test/problem/1', + 'PART_ID': 'part-1', + 'CORRECT': True, + 'VALUE_ID': 'choice-1', + 'ANSWER_VALUE_TEXT': 'Answer 1', + 'VARIANT': 123, + 'PROBLEM_DISPLAY_NAME': 'Test Problem', + 'QUESTION_TEXT': 'Question Text', + 'FIRST_RESPONSE_COUNT': 2, + 'LAST_RESPONSE_COUNT': 3, + 'CREATED': created, + }] + + mapped_rows = map_problem_answer_distribution_rows(rows) + + self.assertEqual(len(mapped_rows), 1) + mapped_row = mapped_rows[0] + self.assertEqual(mapped_row.course_id, 'course-v1:edX+DemoX+Demo_Course') + self.assertEqual(mapped_row.module_id, 'i4x://test/problem/1') + self.assertEqual(mapped_row.part_id, 'part-1') + self.assertTrue(mapped_row.correct) + self.assertEqual(mapped_row.value_id, 'choice-1') + self.assertEqual(mapped_row.answer_value, 'Answer 1') + self.assertEqual(mapped_row.variant, 123) + self.assertEqual(mapped_row.problem_display_name, 'Test Problem') + self.assertEqual(mapped_row.question_text, 'Question Text') + self.assertEqual(mapped_row.first_response_count, 2) + self.assertEqual(mapped_row.last_response_count, 3) + self.assertEqual(mapped_row.created, created) + + class InsightsSnowflakeServiceTests(SimpleTestCase): """Cover service orchestration without real Snowflake calls.""" @@ -984,6 +1090,32 @@ def test_get_video_timeline_calls_query_and_mapper(self, mock_get_rows, mock_map mock_get_rows.assert_called_once_with('video-1') mock_map_rows.assert_called_once_with(raw_rows) + @patch('analytics_data_api.insights_snowflake.service.map_course_problem_rows') + @patch('analytics_data_api.insights_snowflake.service.get_course_problem_rows') + def test_get_course_problems_calls_query_and_mapper(self, mock_get_rows, mock_map_rows): + raw_rows = [{'module_id': 'problem-1'}] + mapped_rows = [{'module_id': 'problem-1', 'total_submissions': 10}] + mock_get_rows.return_value = raw_rows + mock_map_rows.return_value = mapped_rows + + self.assertEqual(get_course_problems('course-v1:edX+DemoX+Demo_Course'), mapped_rows) + + mock_get_rows.assert_called_once_with('course-v1:edX+DemoX+Demo_Course') + mock_map_rows.assert_called_once_with(raw_rows) + + @patch('analytics_data_api.insights_snowflake.service.map_problem_answer_distribution_rows') + @patch('analytics_data_api.insights_snowflake.service.get_problem_answer_distribution_rows') + def test_get_problem_answer_distribution_calls_query_and_mapper(self, mock_get_rows, mock_map_rows): + raw_rows = [{'module_id': 'problem-1'}] + mapped_rows = [{'module_id': 'problem-1', 'last_response_count': 3}] + mock_get_rows.return_value = raw_rows + mock_map_rows.return_value = mapped_rows + + self.assertEqual(get_problem_answer_distribution('problem-1'), mapped_rows) + + mock_get_rows.assert_called_once_with('problem-1') + mock_map_rows.assert_called_once_with(raw_rows) + @patch('analytics_data_api.insights_snowflake.service.map_program_metadata_rows') @patch('analytics_data_api.insights_snowflake.service.get_program_metadata_rows') def test_get_program_metadata_calls_query_and_mapper(self, mock_get_rows, mock_map_rows): diff --git a/analytics_data_api/v0/tests/views/test_courses.py b/analytics_data_api/v0/tests/views/test_courses.py index 87d14e57..45657e6c 100644 --- a/analytics_data_api/v0/tests/views/test_courses.py +++ b/analytics_data_api/v0/tests/views/test_courses.py @@ -871,6 +871,10 @@ def test_get_with_intervals(self, course_id): @ddt.ddt @set_databases class CourseProblemsListViewTests(TestCaseWithAuthentication): + def tearDown(self): + thread_data.analyticsapi_database = getattr(settings, 'ANALYTICS_DATABASE', 'analytics') + super().tearDown() + def _get_data(self, course_id): """ Retrieve data for the specified course. @@ -923,6 +927,70 @@ def test_get(self, course_id): self.assertEqual(response.status_code, 200) self.assertListEqual([dict(d) for d in response.data], expected) + def test_get_uses_aurora_when_global_snowflake_flag_disabled(self): + course_id = CourseSamples.course_ids[0] + created = timezone.now() + G( + models.ProblemFirstLastResponseAnswerDistribution, + course_id=course_id, + module_id='i4x://test/problem/1', + correct=True, + last_response_count=100, + created=created, + ) + + with patch('analytics_data_api.v0.views.courses.is_insights_snowflake_enabled', return_value=False): + with patch('analytics_data_api.v0.views.courses.get_course_problems') as mock_get_problems: + response = self._get_data(course_id) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response['X-Insights-Data-Source'], 'aurora') + mock_get_problems.assert_not_called() + + def test_get_uses_snowflake_service_when_global_flag_enabled(self): + course_id = CourseSamples.course_ids[0] + created = timezone.now() + snowflake_data = [{ + 'module_id': 'i4x://test/problem/1', + 'total_submissions': 150, + 'correct_submissions': 50, + 'part_ids': ['part-1', 'part-2'], + 'created': created, + }] + expected = [{ + 'module_id': 'i4x://test/problem/1', + 'total_submissions': 150, + 'correct_submissions': 50, + 'part_ids': ['part-1', 'part-2'], + 'created': created.strftime(settings.DATETIME_FORMAT), + }] + + with patch('analytics_data_api.v0.views.courses.is_insights_snowflake_enabled', return_value=True): + with patch( + 'analytics_data_api.v0.views.courses.get_course_problems', + return_value=snowflake_data, + ) as mock_get_problems: + response = self.authenticated_get(f'/api/v1/courses/{course_id}/problems/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, expected) + self.assertEqual(response['X-Insights-Data-Source'], 'snowflake') + mock_get_problems.assert_called_once_with(course_id) + + def test_get_returns_404_when_snowflake_service_returns_no_data(self): + course_id = CourseSamples.course_ids[0] + + with patch('analytics_data_api.v0.views.courses.is_insights_snowflake_enabled', return_value=True): + with patch( + 'analytics_data_api.v0.views.courses.get_course_problems', + return_value=[], + ) as mock_get_problems: + response = self.authenticated_get(f'/api/v1/courses/{course_id}/problems/') + + self.assertEqual(response.status_code, 404) + self.assertEqual(response['X-Insights-Data-Source'], 'snowflake') + mock_get_problems.assert_called_once_with(course_id) + def test_get_404(self): """ The view should return 404 if no data exists for the course. diff --git a/analytics_data_api/v0/tests/views/test_problems.py b/analytics_data_api/v0/tests/views/test_problems.py index 84613833..a5015449 100644 --- a/analytics_data_api/v0/tests/views/test_problems.py +++ b/analytics_data_api/v0/tests/views/test_problems.py @@ -5,9 +5,12 @@ # pylint: disable=no-member,no-value-for-parameter import json +from unittest.mock import patch +from django.conf import settings from django_dynamic_fixture import G +from analytics_data_api.middleware import thread_data from analytics_data_api.tests.test_utils import set_databases from analytics_data_api.v0 import models from analytics_data_api.v0.serializers import ( @@ -23,6 +26,10 @@ class AnswerDistributionTests(TestCaseWithAuthentication): path = '/answer_distribution/' maxDiff = None + def tearDown(self): + thread_data.analyticsapi_database = getattr(settings, 'ANALYTICS_DATABASE', 'analytics') + super().tearDown() + @classmethod def setUpClass(cls): super().setUpClass() @@ -139,6 +146,78 @@ def test_get_404(self): response = self.authenticated_get('/api/v0/problems/%s%s' % ("DOES-NOT-EXIST", self.path)) self.assertEqual(response.status_code, 404) + def test_get_uses_aurora_when_global_snowflake_flag_disabled(self): + with patch('analytics_data_api.v0.views.problems.is_insights_snowflake_enabled', return_value=False): + with patch( + 'analytics_data_api.v0.views.problems.get_problem_answer_distribution', + ) as mock_get_answer_distribution: + response = self.authenticated_get('/api/v0/problems/%s%s' % (self.module_id2, self.path)) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response['X-Insights-Data-Source'], 'aurora') + mock_get_answer_distribution.assert_not_called() + + def test_get_uses_snowflake_service_when_global_flag_enabled(self): + created = self.ad1.created + snowflake_data = [ + models.ProblemFirstLastResponseAnswerDistribution( + course_id=self.course_id, + module_id=self.module_id1, + part_id=self.part_id, + correct=self.correct, + value_id=self.value_id1, + answer_value=self.answer_value, + problem_display_name=self.problem_display_name, + question_text=self.question_text, + variant=123, + first_response_count=1, + last_response_count=3, + created=created, + ), + models.ProblemFirstLastResponseAnswerDistribution( + course_id=self.course_id, + module_id=self.module_id1, + part_id=self.part_id, + correct=self.correct, + value_id=self.value_id1, + answer_value=self.answer_value, + problem_display_name=self.problem_display_name, + question_text=self.question_text, + variant=345, + first_response_count=0, + last_response_count=2, + created=created, + ), + ] + + with patch('analytics_data_api.v0.views.problems.is_insights_snowflake_enabled', return_value=True): + with patch( + 'analytics_data_api.v0.views.problems.get_problem_answer_distribution', + return_value=snowflake_data, + ) as mock_get_answer_distribution: + response = self.authenticated_get(f'/api/v1/problems/{self.module_id1}{self.path}') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response['X-Insights-Data-Source'], 'snowflake') + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['variant'], None) + self.assertTrue(response.data[0]['consolidated_variant']) + self.assertEqual(response.data[0]['first_response_count'], 1) + self.assertEqual(response.data[0]['last_response_count'], 5) + mock_get_answer_distribution.assert_called_once_with(self.module_id1) + + def test_get_returns_404_when_snowflake_service_returns_no_data(self): + with patch('analytics_data_api.v0.views.problems.is_insights_snowflake_enabled', return_value=True): + with patch( + 'analytics_data_api.v0.views.problems.get_problem_answer_distribution', + return_value=[], + ) as mock_get_answer_distribution: + response = self.authenticated_get(f'/api/v1/problems/{self.module_id1}{self.path}') + + self.assertEqual(response.status_code, 404) + self.assertEqual(response['X-Insights-Data-Source'], 'snowflake') + mock_get_answer_distribution.assert_called_once_with(self.module_id1) + @set_databases class GradeDistributionTests(TestCaseWithAuthentication): diff --git a/analytics_data_api/v0/views/courses.py b/analytics_data_api/v0/views/courses.py index 7c69aefc..38f8c1bb 100644 --- a/analytics_data_api/v0/views/courses.py +++ b/analytics_data_api/v0/views/courses.py @@ -7,6 +7,7 @@ from django.db import connections, router from django.db.models import Max from django.http import Http404 +from django.utils.dateparse import parse_datetime from django.utils.timezone import make_aware from opaque_keys.edx.keys import CourseKey from rest_framework import generics @@ -22,6 +23,7 @@ get_course_enrollment_gender, get_course_enrollment_location, get_course_enrollment_mode, + get_course_problems, get_course_videos, ) from analytics_data_api.insights_snowflake.toggles import ( @@ -683,7 +685,7 @@ def get_aurora_queryset(self): # pylint: disable=abstract-method -class ProblemsListView(BaseCourseView): +class ProblemsListView(InsightsDataSourceResponseMixin, BaseCourseView): """ Get the problems. @@ -705,6 +707,15 @@ class ProblemsListView(BaseCourseView): @raise_404_if_none def get_queryset(self): + if is_insights_snowflake_enabled(self.request): + self.set_insights_data_source_snowflake() + data = get_course_problems(self.course_id) + if data: + return data + raise Http404 + + self.set_insights_data_source_aurora() + # last_response_count is the number of submissions for the problem part and must # be divided by the number of problem parts to get the problem submission rather # than the problem *part* submissions @@ -755,7 +766,7 @@ def get_queryset(self): # Rather than write custom SQL for the SQLite backend, simply parse the timestamp. created = row['created'] if not isinstance(created, datetime.datetime): - row['created'] = datetime.datetime.strptime(created, '%Y-%m-%d %H:%M:%S') + row['created'] = parse_datetime(created) or datetime.datetime.strptime(created, '%Y-%m-%d %H:%M:%S') return rows diff --git a/analytics_data_api/v0/views/problems.py b/analytics_data_api/v0/views/problems.py index 9a029e86..e5e0da45 100644 --- a/analytics_data_api/v0/views/problems.py +++ b/analytics_data_api/v0/views/problems.py @@ -6,8 +6,12 @@ from itertools import groupby from django.db import OperationalError +from django.http import Http404 from rest_framework import generics +from analytics_data_api.insights_snowflake.response_headers import InsightsDataSourceResponseMixin +from analytics_data_api.insights_snowflake.service import get_problem_answer_distribution +from analytics_data_api.insights_snowflake.toggles import is_insights_snowflake_enabled from analytics_data_api.utils import matching_tuple from analytics_data_api.v0.models import ( GradeDistribution, @@ -24,7 +28,7 @@ from analytics_data_api.v0.views.utils import raise_404_if_none -class ProblemResponseAnswerDistributionView(generics.ListAPIView): +class ProblemResponseAnswerDistributionView(InsightsDataSourceResponseMixin, generics.ListAPIView): """ Get the distribution of student answers to a specific problem. @@ -104,12 +108,22 @@ def get_queryset(self): """Select all the answer distribution response having to do with this usage of the problem.""" problem_id = self.kwargs.get('problem_id') - try: - queryset = list(ProblemResponseAnswerDistribution.objects.filter(module_id=problem_id).order_by('part_id')) - except OperationalError: + if is_insights_snowflake_enabled(self.request): + self.set_insights_data_source_snowflake() self.serializer_class = ConsolidatedFirstLastAnswerDistributionSerializer - queryset = list(ProblemFirstLastResponseAnswerDistribution.objects.filter( - module_id=problem_id).order_by('part_id')) + queryset = get_problem_answer_distribution(problem_id) + if not queryset: + raise Http404 + else: + self.set_insights_data_source_aurora() + try: + queryset = list( + ProblemResponseAnswerDistribution.objects.filter(module_id=problem_id).order_by('part_id') + ) + except OperationalError: + self.serializer_class = ConsolidatedFirstLastAnswerDistributionSerializer + queryset = list(ProblemFirstLastResponseAnswerDistribution.objects.filter( + module_id=problem_id).order_by('part_id')) consolidated_rows = []