diff --git a/.gitignore b/.gitignore index 572da7dc8..00e70cf9f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Packaging files: *.egg* +.python # Sphinx docs: build diff --git a/analytics_dashboard/courses/presenters/__init__.py b/analytics_dashboard/courses/presenters/__init__.py index a42ad9acc..aeb391af9 100644 --- a/analytics_dashboard/courses/presenters/__init__.py +++ b/analytics_dashboard/courses/presenters/__init__.py @@ -16,8 +16,9 @@ class BasePresenter: - def __init__(self, timeout=settings.ANALYTICS_API_DEFAULT_TIMEOUT): - self.client = Client(base_url=settings.DATA_API_URL, + def __init__(self, timeout=settings.ANALYTICS_API_DEFAULT_TIMEOUT, use_v1_api=False): + base_url = settings.DATA_API_URL_V1 if use_v1_api else settings.DATA_API_URL + self.client = Client(base_url=base_url, auth_token=settings.DATA_API_AUTH_TOKEN, timeout=timeout) @@ -48,8 +49,8 @@ class CoursePresenter(BasePresenter): This is the base class for the course pages and sets up the analytics client for the presenters to use to access the data API. """ - def __init__(self, course_id, timeout=settings.ANALYTICS_API_DEFAULT_TIMEOUT): - super().__init__(timeout) + def __init__(self, course_id, timeout=settings.ANALYTICS_API_DEFAULT_TIMEOUT, use_v1_api=False): + super().__init__(timeout, use_v1_api) self.course_id = course_id self.course = self.client.courses(self.course_id) @@ -62,8 +63,8 @@ class CourseAPIPresenterMixin(metaclass=abc.ABCMeta): _last_updated = None - def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT): - super().__init__(course_id, timeout) + def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT, use_v1_api=False): + super().__init__(course_id, timeout, use_v1_api) self.course_api_client = CourseStructureApiClient(settings.COURSE_API_URL, access_token) def _get_structure(self): diff --git a/analytics_dashboard/courses/presenters/performance.py b/analytics_dashboard/courses/presenters/performance.py index 69708c2e8..b45161008 100644 --- a/analytics_dashboard/courses/presenters/performance.py +++ b/analytics_dashboard/courses/presenters/performance.py @@ -41,9 +41,10 @@ class CoursePerformancePresenter(CourseAPIPresenterMixin, CoursePresenter): # minimum screen space a grading policy bar will take up (even if a policy is 0%, display some bar) MIN_POLICY_DISPLAY_PERCENT = 5 - def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT): - super().__init__(access_token, course_id, timeout) + def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT, use_v1_api=False): + super().__init__(access_token, course_id, timeout, use_v1_api) self.grading_policy_client = CourseStructureApiClient(settings.GRADING_POLICY_API_URL, access_token) + self.use_v1_api = use_v1_api def course_module_data(self): try: diff --git a/analytics_dashboard/courses/tests/test_presenters/test_course_summaries.py b/analytics_dashboard/courses/tests/test_presenters/test_course_summaries.py index cc48a78f3..fd59a1eb1 100644 --- a/analytics_dashboard/courses/tests/test_presenters/test_course_summaries.py +++ b/analytics_dashboard/courses/tests/test_presenters/test_course_summaries.py @@ -256,3 +256,7 @@ def test_get_course_summary_metrics(self): 'masters_enrollment': 1111, } self.assertEqual(metrics, expected) + + def test_use_v1_api(self): + presenter = CourseSummariesPresenter(use_v1_api=True) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) diff --git a/analytics_dashboard/courses/tests/test_presenters/test_presenters.py b/analytics_dashboard/courses/tests/test_presenters/test_presenters.py index 9770ca063..10b9eaa5c 100644 --- a/analytics_dashboard/courses/tests/test_presenters/test_presenters.py +++ b/analytics_dashboard/courses/tests/test_presenters/test_presenters.py @@ -49,6 +49,7 @@ def setUp(self): def test_init(self): presenter = CoursePresenter('edX/DemoX/Demo_Course') self.assertEqual(presenter.client.timeout, settings.ANALYTICS_API_DEFAULT_TIMEOUT) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL) presenter = CoursePresenter('edX/DemoX/Demo_Course', timeout=15) self.assertEqual(presenter.client.timeout, 15) @@ -67,6 +68,10 @@ def test_get_current_date(self): dt_format = '%Y-%m-%d' self.assertEqual(self.presenter.get_current_date(), datetime.datetime.utcnow().strftime(dt_format)) + def test_use_v1_api(self): + presenter = CoursePresenter('edX/DemoX/Demo_Course', use_v1_api=True) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) + class CourseEngagementActivityPresenterTests(TestCase): @@ -75,6 +80,10 @@ def setUp(self): self.course_id = 'this/course/id' self.presenter = CourseEngagementActivityPresenter(self.course_id) + def test_use_v1_api(self): + presenter = CourseEngagementActivityPresenter(self.course_id, use_v1_api=True) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) + def get_expected_trends(self, include_forum_data): trends = [ { @@ -221,6 +230,12 @@ def setUp(self): self.course_id = 'this/course/id' self.presenter = CourseEngagementVideoPresenter(settings.COURSE_API_KEY, self.course_id) + def test_use_v1_api(self): + presenter = CourseEngagementVideoPresenter( + settings.COURSE_API_KEY, self.course_id, use_v1_api=True + ) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) + def test_default_block_data(self): self.assertDictEqual(self.presenter.default_block_data, { 'users_at_start': 0, @@ -650,6 +665,10 @@ def setUp(self): self.course_id = 'edX/DemoX/Demo_Course' self.presenter = CourseEnrollmentPresenter(self.course_id) + def test_use_v1_api(self): + presenter = CourseEnrollmentPresenter(self.course_id, use_v1_api=True) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) + @mock.patch('analyticsclient.course.Course.enrollment', mock.Mock(return_value=[])) def test_get_trend_summary_no_data(self): actual_summary, actual_trend = self.presenter.get_summary_and_trend_data() @@ -758,6 +777,10 @@ def setUp(self): self.course_id = 'edX/DemoX/Demo_Course' self.presenter = CourseEnrollmentDemographicsPresenter(self.course_id) + def test_use_v1_api(self): + presenter = CourseEnrollmentDemographicsPresenter(self.course_id, use_v1_api=True) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) + @mock.patch('analyticsclient.course.Course.enrollment') def test_get_gender(self, mock_gender): mock_data = utils.get_mock_api_enrollment_gender_data(self.course_id) @@ -817,6 +840,12 @@ def setUp(self): self.presenter = CoursePerformancePresenter(settings.COURSE_API_KEY, self.course_id) self.factory = CoursePerformanceDataFactory() + def test_use_v1_api(self): + presenter = CoursePerformancePresenter( + settings.COURSE_API_KEY, self.course_id, use_v1_api=True + ) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) + # First and last response counts were added, insights can handle both types of API responses at the moment. @data( annotated( @@ -1046,6 +1075,12 @@ def setUp(self): self.course_id = PERFORMER_PRESENTER_COURSE_ID self.presenter = TagsDistributionPresenter(settings.COURSE_API_KEY, self.course_id) + def test_use_v1_api(self): + presenter = TagsDistributionPresenter( + settings.COURSE_API_KEY, self.course_id, use_v1_api=True + ) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) + @data(annotated([{"total_submissions": 21, "correct_submissions": 5, "tags": {"difficulty": ["Hard"]}}, {"total_submissions": 11, "correct_submissions": 10, @@ -1205,6 +1240,10 @@ def setUp(self): self.course_id = PERFORMER_PRESENTER_COURSE_ID self.presenter = CourseReportDownloadPresenter(self.course_id) + def test_use_v1_api(self): + presenter = CourseReportDownloadPresenter(self.course_id, use_v1_api=True) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) + @mock.patch('analyticsclient.course.Course.reports') def test_report_presenter(self, mock_reports): api_data = { diff --git a/analytics_dashboard/courses/tests/test_presenters/test_programs.py b/analytics_dashboard/courses/tests/test_presenters/test_programs.py index 81dbd5f5e..f39a4df5f 100644 --- a/analytics_dashboard/courses/tests/test_presenters/test_programs.py +++ b/analytics_dashboard/courses/tests/test_presenters/test_programs.py @@ -1,5 +1,6 @@ import unittest.mock as mock from ddt import data, ddt, unpack +from django.conf import settings from django.test import TestCase, override_settings from analytics_dashboard.courses.presenters.programs import ProgramsPresenter @@ -78,3 +79,7 @@ def test_get_programs(self, program_ids, course_ids): actual_programs = presenter.get_programs(program_ids=program_ids, course_ids=course_ids) self.assertListEqual(actual_programs, self.get_expected_programs(program_ids=program_ids, course_ids=course_ids)) + + def test_use_v1_api(self): + presenter = ProgramsPresenter(use_v1_api=True) + self.assertEqual(presenter.client.base_url, settings.DATA_API_URL_V1) diff --git a/analytics_dashboard/courses/views/__init__.py b/analytics_dashboard/courses/views/__init__.py index c1490ce25..169360b46 100644 --- a/analytics_dashboard/courses/views/__init__.py +++ b/analytics_dashboard/courses/views/__init__.py @@ -435,12 +435,18 @@ class CourseView(LoginRequiredMixin, CourseValidMixin, CoursePermissionMixin, Te course_id = None course_key = None user = None + api_version = None def dispatch(self, request, *args, **kwargs): self.user = request.user self.course_id = request.course_id self.course_key = request.course_key + try: + self.api_version = int(request.GET.get('v', 0)) + except ValueError: + self.api_version = 0 + # some views will catch the NotFoundError to set data to a state that # the template can rendering a loading error message for the section try: @@ -454,7 +460,8 @@ def dispatch(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - self.client = Client(base_url=settings.DATA_API_URL, + base_url = settings.DATA_API_URL_V1 if self.api_version == 1 else settings.DATA_API_URL + self.client = Client(base_url=base_url, auth_token=settings.DATA_API_AUTH_TOKEN, timeout=settings.LMS_DEFAULT_TIMEOUT) self.course = self.client.courses(self.course_id) return context diff --git a/analytics_dashboard/courses/views/course_summaries.py b/analytics_dashboard/courses/views/course_summaries.py index cb204d7de..1ee508f9c 100644 --- a/analytics_dashboard/courses/views/course_summaries.py +++ b/analytics_dashboard/courses/views/course_summaries.py @@ -46,7 +46,14 @@ def get_context_data(self, **kwargs): # The user is probably not a course administrator and should not be using this application. raise PermissionDenied - summaries_presenter = CourseSummariesPresenter() + try: + api_version = int(self.request.GET.get('v', 0)) + except ValueError: + api_version = 0 + + use_v1_api = api_version == 1 + + summaries_presenter = CourseSummariesPresenter(use_v1_api=use_v1_api) summaries, last_updated = summaries_presenter.get_course_summaries(courses) context.update({ @@ -62,7 +69,7 @@ def get_context_data(self, **kwargs): } if enable_course_filters: - programs_presenter = ProgramsPresenter() + programs_presenter = ProgramsPresenter(use_v1_api=use_v1_api) programs = programs_presenter.get_programs(course_ids=courses) data['programs_json'] = programs @@ -100,7 +107,14 @@ def get_data(self): enable_course_filters = switch_is_active('enable_course_filters') - presenter = CourseSummariesPresenter() + try: + api_version = int(self.request.GET.get('v', 0)) + except ValueError: + api_version = 0 + + use_v1_api = api_version == 1 + + presenter = CourseSummariesPresenter(use_v1_api=use_v1_api) summaries, _ = presenter.get_course_summaries(courses) if not summaries: @@ -112,7 +126,7 @@ def get_data(self): if enable_course_filters: # Add list of associated program IDs to each summary entry - programs_presenter = ProgramsPresenter() + programs_presenter = ProgramsPresenter(use_v1_api=use_v1_api) programs = programs_presenter.get_programs(course_ids=courses) for summary in summaries: summary_programs = [program for program in programs if summary['course_id'] in program['course_ids']] diff --git a/analytics_dashboard/settings/base.py b/analytics_dashboard/settings/base.py index ed5c85ac5..9a4979b48 100644 --- a/analytics_dashboard/settings/base.py +++ b/analytics_dashboard/settings/base.py @@ -316,6 +316,7 @@ ########## DATA API CONFIGURATION DATA_API_URL = 'http://127.0.0.1:9001/api/v0' +DATA_API_URL_V1 = 'http://127.0.0.1:9001/api/v1' DATA_API_AUTH_TOKEN = 'changeme' ########## END DATA API CONFIGURATION diff --git a/analytics_dashboard/settings/devstack.py b/analytics_dashboard/settings/devstack.py index c6e1da5fe..06506bc44 100644 --- a/analytics_dashboard/settings/devstack.py +++ b/analytics_dashboard/settings/devstack.py @@ -14,6 +14,7 @@ DATABASES['default'][override] = value DATA_API_URL = os.environ.get("API_SERVER_URL", 'http://edx.devstack.analyticsapi:19001/api/v0') +DATA_API_URL_V1 = os.environ.get("API_SERVER_URL", 'http://edx.devstack.analyticsapi:19001/api/v1') # Set these to the correct values for your OAuth2/OpenID Connect provider (e.g., devstack) SOCIAL_AUTH_EDX_OAUTH2_KEY = os.environ.get('SOCIAL_AUTH_EDX_OAUTH2_KEY', 'insights-sso-key') diff --git a/analytics_dashboard/settings/local.py b/analytics_dashboard/settings/local.py index fb992bc9d..6d1ea570a 100644 --- a/analytics_dashboard/settings/local.py +++ b/analytics_dashboard/settings/local.py @@ -22,6 +22,7 @@ ########## DATA API CONFIGURATION DATA_API_URL = os.getenv("API_SERVER_URL", DATA_API_URL) +DATA_API_URL_V1 = os.getenv("API_SERVER_URL_V1", DATA_API_URL_V1) ########## END DATA API CONFIGURATION ENABLE_AUTO_AUTH = True diff --git a/analytics_dashboard/settings/test.py b/analytics_dashboard/settings/test.py index 7f5afd76c..ef42e6e82 100644 --- a/analytics_dashboard/settings/test.py +++ b/analytics_dashboard/settings/test.py @@ -26,5 +26,6 @@ COURSE_API_KEY = 'test_course_api_key' DATA_API_URL = 'http://data-api-host/api/v0' +DATA_API_URL_V1 = 'http://data-api-host/api/v1' LOGGING = get_logger_config(debug=DEBUG, dev_env=True, local_loglevel='DEBUG')