Commit 14e3a013 by Kyle McCormick Committed by GitHub

Merge pull request #178 from edx/edx/kdmccormick/course-listing-reimp-final

EDUCATOR-852: Add filtering, sorting, pagination to course_summaries/ and create course_totals/
parents 859d3768 7e5476c8
...@@ -5,4 +5,4 @@ PROFESSIONAL = u'professional' ...@@ -5,4 +5,4 @@ PROFESSIONAL = u'professional'
PROFESSIONAL_NO_ID = u'no-id-professional' PROFESSIONAL_NO_ID = u'no-id-professional'
VERIFIED = u'verified' VERIFIED = u'verified'
ALL = [AUDIT, CREDIT, HONOR, PROFESSIONAL, PROFESSIONAL_NO_ID, VERIFIED] ALL = frozenset([AUDIT, CREDIT, HONOR, PROFESSIONAL, PROFESSIONAL_NO_ID, VERIFIED])
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
"problem_display_name": "Earth Science Question", "problem_display_name": "Earth Science Question",
"question_text": "Enter your answer:" "question_text": "Enter your answer:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 1 "pk": 1
}, },
{ {
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
"problem_display_name": "Earth Science Question", "problem_display_name": "Earth Science Question",
"question_text": "Enter your answer:" "question_text": "Enter your answer:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 2 "pk": 2
}, },
{ {
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
"problem_display_name": "Earth Science Question", "problem_display_name": "Earth Science Question",
"question_text": "Enter your answer:" "question_text": "Enter your answer:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 3 "pk": 3
}, },
{ {
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
"problem_display_name": "Earth Science Question", "problem_display_name": "Earth Science Question",
"question_text": "Enter your answer:" "question_text": "Enter your answer:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 4 "pk": 4
}, },
{ {
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
"problem_display_name": null, "problem_display_name": null,
"question_text": null "question_text": null
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 5 "pk": 5
}, },
{ {
...@@ -104,7 +104,7 @@ ...@@ -104,7 +104,7 @@
"problem_display_name": null, "problem_display_name": null,
"question_text": null "question_text": null
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 6 "pk": 6
}, },
{ {
...@@ -122,7 +122,7 @@ ...@@ -122,7 +122,7 @@
"problem_display_name": null, "problem_display_name": null,
"question_text": null "question_text": null
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 7 "pk": 7
}, },
{ {
...@@ -140,7 +140,7 @@ ...@@ -140,7 +140,7 @@
"problem_display_name": "Example problem", "problem_display_name": "Example problem",
"question_text": "Enter an answer:" "question_text": "Enter an answer:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 8 "pk": 8
}, },
{ {
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
"problem_display_name": "Example problem", "problem_display_name": "Example problem",
"question_text": "Enter an answer:" "question_text": "Enter an answer:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 9 "pk": 9
}, },
{ {
...@@ -176,7 +176,7 @@ ...@@ -176,7 +176,7 @@
"problem_display_name": "Example problem", "problem_display_name": "Example problem",
"question_text": "Enter an answer:" "question_text": "Enter an answer:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 10 "pk": 10
}, },
{ {
...@@ -194,7 +194,7 @@ ...@@ -194,7 +194,7 @@
"problem_display_name": "Example problem", "problem_display_name": "Example problem",
"question_text": "Randomized answer" "question_text": "Randomized answer"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 11 "pk": 11
}, },
{ {
...@@ -212,7 +212,7 @@ ...@@ -212,7 +212,7 @@
"problem_display_name": "Example problem", "problem_display_name": "Example problem",
"question_text": "Randomized answer" "question_text": "Randomized answer"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 12 "pk": 12
}, },
...@@ -231,7 +231,7 @@ ...@@ -231,7 +231,7 @@
"problem_display_name": "Example problem", "problem_display_name": "Example problem",
"question_text": "Select from the choices below:" "question_text": "Select from the choices below:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 13 "pk": 13
}, },
{ {
...@@ -249,7 +249,7 @@ ...@@ -249,7 +249,7 @@
"problem_display_name": "Example problem", "problem_display_name": "Example problem",
"question_text": "Select from the choices below:" "question_text": "Select from the choices below:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 14 "pk": 14
}, },
{ {
...@@ -267,7 +267,7 @@ ...@@ -267,7 +267,7 @@
"problem_display_name": "Example problem", "problem_display_name": "Example problem",
"question_text": "Select from the choices below:" "question_text": "Select from the choices below:"
}, },
"model": "v0.problemfirstlastresponseanswerdistribution", "model": "v1.problemfirstlastresponseanswerdistribution",
"pk": 15 "pk": 15
} }
......
...@@ -12,7 +12,7 @@ from django.core.management.base import BaseCommand ...@@ -12,7 +12,7 @@ from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from analytics_data_api.constants import engagement_events from analytics_data_api.constants import engagement_events
from analytics_data_api.v0 import models from analytics_data_api.v1 import models
from analyticsdataserver.clients import CourseBlocksApiClient from analyticsdataserver.clients import CourseBlocksApiClient
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
......
...@@ -2,7 +2,7 @@ from django.conf.urls import url, include ...@@ -2,7 +2,7 @@ from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = [ urlpatterns = [
url(r'^v0/', include('analytics_data_api.v0.urls', 'v0')), url(r'^v1/', include('analytics_data_api.v1.urls', 'v1')),
] ]
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
...@@ -10,7 +10,7 @@ from rest_framework.authtoken.models import Token ...@@ -10,7 +10,7 @@ from rest_framework.authtoken.models import Token
from opaque_keys.edx.locator import CourseKey from opaque_keys.edx.locator import CourseKey
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from analytics_data_api.v0.exceptions import ( from analytics_data_api.v1.exceptions import (
ReportFileNotFoundError, ReportFileNotFoundError,
CannotCreateReportDownloadLinkError CannotCreateReportDownloadLinkError
) )
...@@ -230,3 +230,25 @@ def get_expiration_date(seconds): ...@@ -230,3 +230,25 @@ def get_expiration_date(seconds):
Determine when a given link will expire, based on a given lifetime Determine when a given link will expire, based on a given lifetime
""" """
return datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds) return datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
class classproperty(object):
"""
A decorator for declaring a class-level property.
Conceptually ike combining @classmethod and @property, however that
doesn't work in practice, so we have to define our own decorator here.
"""
def __init__(self, getter):
self.getter = getter
def __get__(self, instance, owner):
return self.getter(owner)
def join_dicts(*dicts):
joined = {}
for d in dicts:
joined.update(d)
return joined
default_app_config = 'analytics_data_api.v0.apps.ApiAppConfig'
import datetime
import ddt
from django_dynamic_fixture import G
import pytz
from django.conf import settings
from analytics_data_api.constants import enrollment_modes
from analytics_data_api.v0 import models, serializers
from analytics_data_api.v0.tests.views import CourseSamples, VerifyCourseIdMixin, APIListViewTestMixin
from analyticsdataserver.tests import TestCaseWithAuthentication
@ddt.ddt
class CourseSummariesViewTests(VerifyCourseIdMixin, TestCaseWithAuthentication, APIListViewTestMixin):
model = models.CourseMetaSummaryEnrollment
model_id = 'course_id'
ids_param = 'course_ids'
serializer = serializers.CourseMetaSummaryEnrollmentSerializer
expected_summaries = []
list_name = 'course_summaries'
default_ids = CourseSamples.course_ids
always_exclude = ['created', 'programs']
test_post_method = True
def setUp(self):
super(CourseSummariesViewTests, self).setUp()
self.now = datetime.datetime.utcnow()
self.maxDiff = None
def tearDown(self):
self.model.objects.all().delete()
def create_model(self, model_id, **kwargs):
for mode in kwargs['modes']:
G(self.model, course_id=model_id, catalog_course_title='Title', catalog_course='Catalog',
start_time=datetime.datetime(2016, 10, 11, tzinfo=pytz.utc),
end_time=datetime.datetime(2016, 12, 18, tzinfo=pytz.utc),
pacing_type='instructor', availability=kwargs['availability'], enrollment_mode=mode,
count=5, cumulative_count=10, count_change_7_days=1, passing_users=1, create=self.now,)
if 'programs' in kwargs and kwargs['programs']:
# Create a link from this course to a program
G(models.CourseProgramMetadata, course_id=model_id, program_id=CourseSamples.program_ids[0],
program_type='Demo', program_title='Test')
def generate_data(self, ids=None, modes=None, availability='Current', **kwargs):
"""Generate course summary data"""
if modes is None:
modes = enrollment_modes.ALL
super(CourseSummariesViewTests, self).generate_data(ids=ids, modes=modes, availability=availability, **kwargs)
def expected_result(self, item_id, modes=None, availability='Current', programs=False): # pylint: disable=arguments-differ
"""Expected summary information for a course and modes to populate with data."""
summary = super(CourseSummariesViewTests, self).expected_result(item_id)
if modes is None:
modes = enrollment_modes.ALL
num_modes = len(modes)
count_factor = 5
cumulative_count_factor = 10
count_change_factor = 1
summary.update([
('catalog_course_title', 'Title'),
('catalog_course', 'Catalog'),
('start_date', datetime.datetime(2016, 10, 11, tzinfo=pytz.utc).strftime(settings.DATETIME_FORMAT)),
('end_date', datetime.datetime(2016, 12, 18, tzinfo=pytz.utc).strftime(settings.DATETIME_FORMAT)),
('pacing_type', 'instructor'),
('availability', availability),
('count', count_factor * num_modes),
('cumulative_count', cumulative_count_factor * num_modes),
('count_change_7_days', count_change_factor * num_modes),
('passing_users', count_change_factor * num_modes),
('enrollment_modes', {}),
])
summary['enrollment_modes'].update({
mode: {
'count': count_factor,
'cumulative_count': cumulative_count_factor,
'count_change_7_days': count_change_factor,
'passing_users': count_change_factor,
} for mode in modes
})
summary['enrollment_modes'].update({
mode: {
'count': 0,
'cumulative_count': 0,
'count_change_7_days': 0,
'passing_users': 0,
} for mode in set(enrollment_modes.ALL) - set(modes)
})
no_prof = summary['enrollment_modes'].pop(enrollment_modes.PROFESSIONAL_NO_ID)
prof = summary['enrollment_modes'].get(enrollment_modes.PROFESSIONAL)
prof.update({
'count': prof['count'] + no_prof['count'],
'cumulative_count': prof['cumulative_count'] + no_prof['cumulative_count'],
'count_change_7_days': prof['count_change_7_days'] + no_prof['count_change_7_days'],
'passing_users': prof['passing_users'] + no_prof['passing_users'],
})
if programs:
summary['programs'] = [CourseSamples.program_ids[0]]
return summary
def all_expected_results(self, ids=None, modes=None, availability='Current', programs=False): # pylint: disable=arguments-differ
if modes is None:
modes = enrollment_modes.ALL
return super(CourseSummariesViewTests, self).all_expected_results(ids=ids, modes=modes,
availability=availability,
programs=programs)
@ddt.data(
None,
CourseSamples.course_ids,
['not/real/course'].extend(CourseSamples.course_ids),
)
def test_all_courses(self, course_ids):
self._test_all_items(course_ids)
@ddt.data(*CourseSamples.course_ids)
def test_one_course(self, course_id):
self._test_one_item(course_id)
@ddt.data(
['availability'],
['enrollment_mode', 'course_id'],
)
def test_fields(self, fields):
self._test_fields(fields)
@ddt.data(
[enrollment_modes.VERIFIED],
[enrollment_modes.HONOR, enrollment_modes.PROFESSIONAL],
)
def test_empty_modes(self, modes):
self.generate_data(modes=modes)
response = self.validated_request(exclude=self.always_exclude)
self.assertEquals(response.status_code, 200)
self.assertItemsEqual(response.data, self.all_expected_results(modes=modes))
@ddt.data(
['malformed-course-id'],
[CourseSamples.course_ids[0], 'malformed-course-id'],
)
def test_bad_course_id(self, course_ids):
response = self.validated_request(ids=course_ids)
self.verify_bad_course_id(response)
def test_collapse_upcoming(self):
self.generate_data(availability='Starting Soon')
self.generate_data(ids=['foo/bar/baz'], availability='Upcoming')
response = self.validated_request(exclude=self.always_exclude)
self.assertEquals(response.status_code, 200)
expected_summaries = self.all_expected_results(availability='Upcoming')
expected_summaries.extend(self.all_expected_results(ids=['foo/bar/baz'],
availability='Upcoming'))
self.assertItemsEqual(response.data, expected_summaries)
def test_programs(self):
self.generate_data(programs=True)
response = self.validated_request(exclude=self.always_exclude[:1], programs=['True'])
self.assertEquals(response.status_code, 200)
self.assertItemsEqual(response.data, self.all_expected_results(programs=True))
@ddt.data('passing_users', )
def test_exclude(self, field):
self.generate_data()
response = self.validated_request(exclude=[field])
self.assertEquals(response.status_code, 200)
self.assertEquals(str(response.data).count(field), 0)
from django.db.models import Q
from analytics_data_api.constants import enrollment_modes
from analytics_data_api.v0 import models, serializers
from analytics_data_api.v0.views import APIListView
from analytics_data_api.v0.views.utils import (
split_query_argument,
validate_course_id,
)
class CourseSummariesView(APIListView):
"""
Returns summary information for courses.
**Example Requests**
GET /api/v0/course_summaries/?course_ids={course_id_1},{course_id_2}
POST /api/v0/course_summaries/
{
"course_ids": [
"{course_id_1}",
"{course_id_2}",
...
"{course_id_200}"
]
}
**Response Values**
Returns enrollment counts and other metadata for each course:
* course_id: The ID of the course for which data is returned.
* catalog_course_title: The name of the course.
* catalog_course: Course identifier without run.
* start_date: The date and time that the course begins
* end_date: The date and time that the course ends
* pacing_type: The type of pacing for this course
* availability: Availability status of the course
* count: The total count of currently enrolled learners across modes.
* cumulative_count: The total cumulative total of all users ever enrolled across modes.
* count_change_7_days: Total difference in enrollment counts over the past 7 days across modes.
* enrollment_modes: For each enrollment mode, the count, cumulative_count, and count_change_7_days.
* created: The date the counts were computed.
* programs: List of program IDs that this course is a part of.
**Parameters**
Results can be filed to the course IDs specified or limited to the fields.
For GET requests, these parameters are passed in the query string.
For POST requests, these parameters are passed as a JSON dict in the request body.
course_ids -- The comma-separated course identifiers for which summaries are requested.
For example, 'edX/DemoX/Demo_Course,course-v1:edX+DemoX+Demo_2016'. Default is to
return all courses.
fields -- The comma-separated fields to return in the response.
For example, 'course_id,created'. Default is to return all fields.
exclude -- The comma-separated fields to exclude in the response.
For example, 'course_id,created'. Default is to exclude the programs array.
programs -- If included in the query parameters, will find each courses' program IDs
and include them in the response.
**Notes**
* GET is usable when the number of course IDs is relatively low
* POST is required when the number of course IDs would cause the URL to be too long.
* POST functions the same as GET for this endpoint. It does not modify any state.
"""
serializer_class = serializers.CourseMetaSummaryEnrollmentSerializer
programs_serializer_class = serializers.CourseProgramMetadataSerializer
model = models.CourseMetaSummaryEnrollment
model_id_field = 'course_id'
ids_param = 'course_ids'
programs_model = models.CourseProgramMetadata
count_fields = ('count', 'cumulative_count', 'count_change_7_days',
'passing_users') # are initialized to 0 by default
summary_meta_fields = ['catalog_course_title', 'catalog_course', 'start_time', 'end_time',
'pacing_type', 'availability'] # fields to extract from summary model
def get(self, request, *args, **kwargs):
query_params = self.request.query_params
programs = split_query_argument(query_params.get('programs'))
if not programs:
self.always_exclude = self.always_exclude + ['programs']
response = super(CourseSummariesView, self).get(request, *args, **kwargs)
return response
def post(self, request, *args, **kwargs):
# self.request.data is a QueryDict. For keys with singleton lists as values,
# QueryDicts return the singleton element of the list instead of the list itself,
# which is undesirable. So, we convert to a normal dict.
request_data_dict = dict(self.request.data)
programs = request_data_dict.get('programs')
if not programs:
self.always_exclude = self.always_exclude + ['programs']
response = super(CourseSummariesView, self).post(request, *args, **kwargs)
return response
def verify_ids(self):
"""
Raise an exception if any of the course IDs set as self.ids are invalid.
Overrides APIListView.verify_ids.
"""
if self.ids is not None:
for item_id in self.ids:
validate_course_id(item_id)
def base_field_dict(self, course_id):
"""Default summary with fields populated to default levels."""
summary = super(CourseSummariesView, self).base_field_dict(course_id)
summary.update({
'created': None,
'enrollment_modes': {},
})
summary.update({field: 0 for field in self.count_fields})
summary['enrollment_modes'].update({
mode: {
count_field: 0 for count_field in self.count_fields
} for mode in enrollment_modes.ALL
})
return summary
def update_field_dict_from_model(self, model, base_field_dict=None, field_list=None):
field_dict = super(CourseSummariesView, self).update_field_dict_from_model(model,
base_field_dict=base_field_dict,
field_list=self.summary_meta_fields)
field_dict['enrollment_modes'].update({
model.enrollment_mode: {field: getattr(model, field) for field in self.count_fields}
})
# treat the most recent as the authoritative created date -- should be all the same
field_dict['created'] = max(model.created, field_dict['created']) if field_dict['created'] else model.created
# update totals for all counts
field_dict.update({field: field_dict[field] + getattr(model, field) for field in self.count_fields})
return field_dict
def postprocess_field_dict(self, field_dict):
# Merge professional with non verified professional
modes = field_dict['enrollment_modes']
prof_no_id_mode = modes.pop(enrollment_modes.PROFESSIONAL_NO_ID, {})
prof_mode = modes[enrollment_modes.PROFESSIONAL]
for count_key in self.count_fields:
prof_mode[count_key] = prof_mode.get(count_key, 0) + prof_no_id_mode.pop(count_key, 0)
# AN-8236 replace "Starting Soon" to "Upcoming" availability to collapse the two into one value
if field_dict['availability'] == 'Starting Soon':
field_dict['availability'] = 'Upcoming'
if self.exclude == [] or (self.exclude and 'programs' not in self.exclude):
# don't do expensive looping for programs if we are just going to throw it away
field_dict = self.add_programs(field_dict)
for field in self.exclude:
for mode in field_dict['enrollment_modes']:
_ = field_dict['enrollment_modes'][mode].pop(field, None)
return field_dict
def add_programs(self, field_dict):
"""Query for programs attached to a course and include them (just the IDs) in the course summary dict"""
field_dict['programs'] = []
queryset = self.programs_model.objects.filter(course_id=field_dict['course_id'])
for program in queryset:
program = self.programs_serializer_class(program.__dict__)
field_dict['programs'].append(program.data['program_id'])
return field_dict
def get_query(self):
return reduce(lambda q, item_id: q | Q(course_id=item_id), self.ids, Q())
from django.db.models import Q
from analytics_data_api.v0 import models, serializers
from analytics_data_api.v0.views import APIListView
class ProgramsView(APIListView):
"""
Returns metadata information for programs.
**Example Request**
GET /api/v0/course_programs/?program_ids={program_id},{program_id}
**Response Values**
Returns metadata for every program:
* program_id: The ID of the program for which data is returned.
* program_type: The type of the program
* program_title: The title of the program
* created: The date the metadata was computed.
**Parameters**
Results can be filtered to the program IDs specified or limited to the fields.
program_ids -- The comma-separated program identifiers for which metadata is requested.
Default is to return all programs.
fields -- The comma-separated fields to return in the response.
For example, 'program_id,created'. Default is to return all fields.
exclude -- The comma-separated fields to exclude in the response.
For example, 'program_id,created'. Default is to not exclude any fields.
"""
serializer_class = serializers.CourseProgramMetadataSerializer
model = models.CourseProgramMetadata
model_id_field = 'program_id'
ids_param = 'program_ids'
program_meta_fields = ['program_type', 'program_title']
def base_field_dict(self, program_id):
"""Default program with id, empty metadata, and empty courses array."""
program = super(ProgramsView, self).base_field_dict(program_id)
program.update({
'program_type': '',
'program_title': '',
'created': None,
'course_ids': [],
})
return program
def update_field_dict_from_model(self, model, base_field_dict=None, field_list=None):
field_dict = super(ProgramsView, self).update_field_dict_from_model(model, base_field_dict=base_field_dict,
field_list=self.program_meta_fields)
field_dict['course_ids'].append(model.course_id)
# treat the most recent as the authoritative created date -- should be all the same
field_dict['created'] = max(model.created, field_dict['created']) if field_dict['created'] else model.created
return field_dict
def get_query(self):
return reduce(lambda q, item_id: q | Q(program_id=item_id), self.ids, Q())
default_app_config = 'analytics_data_api.v1.apps.ApiAppConfig'
...@@ -5,7 +5,7 @@ from elasticsearch_dsl import connections ...@@ -5,7 +5,7 @@ from elasticsearch_dsl import connections
class ApiAppConfig(AppConfig): class ApiAppConfig(AppConfig):
name = 'analytics_data_api.v0' name = 'analytics_data_api.v1'
def ready(self): def ready(self):
from analytics_data_api.utils import load_fully_qualified_definition from analytics_data_api.utils import load_fully_qualified_definition
......
...@@ -2,7 +2,7 @@ import abc ...@@ -2,7 +2,7 @@ import abc
from django.http.response import JsonResponse from django.http.response import JsonResponse
from rest_framework import status from rest_framework import status
from analytics_data_api.v0.exceptions import ( from analytics_data_api.v1.exceptions import (
CourseKeyMalformedError, CourseKeyMalformedError,
CourseNotSpecifiedError, CourseNotSpecifiedError,
LearnerEngagementTimelineNotFoundError, LearnerEngagementTimelineNotFoundError,
......
...@@ -68,12 +68,12 @@ class CourseEnrollmentModeDaily(BaseCourseEnrollment): ...@@ -68,12 +68,12 @@ class CourseEnrollmentModeDaily(BaseCourseEnrollment):
class CourseMetaSummaryEnrollment(BaseCourseModel): class CourseMetaSummaryEnrollment(BaseCourseModel):
catalog_course_title = models.CharField(db_index=True, max_length=255) catalog_course_title = models.CharField(db_index=True, null=True, max_length=255)
catalog_course = models.CharField(db_index=True, max_length=255) catalog_course = models.CharField(db_index=True, max_length=255)
start_time = models.DateTimeField() start_time = models.DateTimeField(null=True)
end_time = models.DateTimeField() end_time = models.DateTimeField(null=True)
pacing_type = models.CharField(db_index=True, max_length=255) pacing_type = models.CharField(db_index=True, max_length=255, null=True)
availability = models.CharField(db_index=True, max_length=255) availability = models.CharField(db_index=True, max_length=255, null=True)
enrollment_mode = models.CharField(max_length=255) enrollment_mode = models.CharField(max_length=255)
count = models.IntegerField(null=False) count = models.IntegerField(null=False)
cumulative_count = models.IntegerField(null=False) cumulative_count = models.IntegerField(null=False)
......
...@@ -8,7 +8,7 @@ from analytics_data_api.constants import ( ...@@ -8,7 +8,7 @@ from analytics_data_api.constants import (
engagement_events, engagement_events,
enrollment_modes, enrollment_modes,
) )
from analytics_data_api.v0 import models from analytics_data_api.v1 import models
# Below are the enrollment modes supported by this API. # Below are the enrollment modes supported by this API.
...@@ -561,6 +561,7 @@ class CourseMetaSummaryEnrollmentSerializer(ModelSerializerWithCreatedField, Dyn ...@@ -561,6 +561,7 @@ class CourseMetaSummaryEnrollmentSerializer(ModelSerializerWithCreatedField, Dyn
count = serializers.IntegerField(default=0) count = serializers.IntegerField(default=0)
cumulative_count = serializers.IntegerField(default=0) cumulative_count = serializers.IntegerField(default=0)
count_change_7_days = serializers.IntegerField(default=0) count_change_7_days = serializers.IntegerField(default=0)
verified_enrollment = serializers.IntegerField(default=0)
passing_users = serializers.IntegerField(default=0) passing_users = serializers.IntegerField(default=0)
enrollment_modes = serializers.SerializerMethodField() enrollment_modes = serializers.SerializerMethodField()
programs = serializers.SerializerMethodField() programs = serializers.SerializerMethodField()
...@@ -569,7 +570,7 @@ class CourseMetaSummaryEnrollmentSerializer(ModelSerializerWithCreatedField, Dyn ...@@ -569,7 +570,7 @@ class CourseMetaSummaryEnrollmentSerializer(ModelSerializerWithCreatedField, Dyn
return obj.get('enrollment_modes', None) return obj.get('enrollment_modes', None)
def get_programs(self, obj): def get_programs(self, obj):
return obj.get('programs', None) return list(obj.get('programs', set()))
class Meta(object): class Meta(object):
model = models.CourseMetaSummaryEnrollment model = models.CourseMetaSummaryEnrollment
...@@ -594,3 +595,13 @@ class CourseProgramMetadataSerializer(ModelSerializerWithCreatedField, DynamicFi ...@@ -594,3 +595,13 @@ class CourseProgramMetadataSerializer(ModelSerializerWithCreatedField, DynamicFi
# excluding course-related fields because the serialized output will be embedded in a course object # excluding course-related fields because the serialized output will be embedded in a course object
# with those fields already defined # with those fields already defined
exclude = ('id', 'created', 'course_id') exclude = ('id', 'created', 'course_id')
class CourseTotalsSerializer(serializers.Serializer):
"""
Serializer for course totals data.
"""
count = serializers.IntegerField()
cumulative_count = serializers.IntegerField()
count_change_7_days = serializers.IntegerField()
verified_enrollment = serializers.IntegerField()
...@@ -4,7 +4,7 @@ from django.test import TestCase ...@@ -4,7 +4,7 @@ from django.test import TestCase
from elasticsearch.exceptions import ElasticsearchException from elasticsearch.exceptions import ElasticsearchException
from mock import patch from mock import patch
from analytics_data_api.v0.connections import BotoHttpConnection, ESConnection from analytics_data_api.v1.connections import BotoHttpConnection, ESConnection
class ESConnectionTests(TestCase): class ESConnectionTests(TestCase):
...@@ -47,7 +47,7 @@ class ESConnectionTests(TestCase): ...@@ -47,7 +47,7 @@ class ESConnectionTests(TestCase):
class BotoHttpConnectionTests(TestCase): class BotoHttpConnectionTests(TestCase):
@patch('analytics_data_api.v0.connections.ESConnection.make_request') @patch('analytics_data_api.v1.connections.ESConnection.make_request')
def test_perform_request_success(self, mock_response): def test_perform_request_success(self, mock_response):
mock_response.return_value.status = 200 mock_response.return_value.status = 200
connection = BotoHttpConnection(aws_access_key_id='access_key', aws_secret_access_key='secret') connection = BotoHttpConnection(aws_access_key_id='access_key', aws_secret_access_key='secret')
...@@ -56,7 +56,7 @@ class BotoHttpConnectionTests(TestCase): ...@@ -56,7 +56,7 @@ class BotoHttpConnectionTests(TestCase):
self.assertEqual(status, 200) self.assertEqual(status, 200)
self.assertGreater(mock_logger.call_count, 0) self.assertGreater(mock_logger.call_count, 0)
@patch('analytics_data_api.v0.connections.ESConnection.make_request') @patch('analytics_data_api.v1.connections.ESConnection.make_request')
def test_perform_request_error(self, mock_response): def test_perform_request_error(self, mock_response):
mock_response.return_value.status = 500 mock_response.return_value.status = 500
connection = BotoHttpConnection(aws_access_key_id='access_key', aws_secret_access_key='secret') connection = BotoHttpConnection(aws_access_key_id='access_key', aws_secret_access_key='secret')
......
from django.test import TestCase from django.test import TestCase
from django_dynamic_fixture import G from django_dynamic_fixture import G
from analytics_data_api.v0 import models from analytics_data_api.v1 import models
from analytics_data_api.constants.country import UNKNOWN_COUNTRY, get_country from analytics_data_api.constants.country import UNKNOWN_COUNTRY, get_country
......
...@@ -2,7 +2,7 @@ from datetime import date ...@@ -2,7 +2,7 @@ from datetime import date
from django.test import TestCase from django.test import TestCase
from django_dynamic_fixture import G from django_dynamic_fixture import G
from analytics_data_api.v0 import models as api_models, serializers as api_serializers from analytics_data_api.v1 import models as api_models, serializers as api_serializers
class TestSerializer(api_serializers.CourseEnrollmentDailySerializer, api_serializers.DynamicFieldsModelSerializer): class TestSerializer(api_serializers.CourseEnrollmentDailySerializer, api_serializers.DynamicFieldsModelSerializer):
......
...@@ -3,7 +3,7 @@ from django.test import TestCase ...@@ -3,7 +3,7 @@ from django.test import TestCase
class UrlRedirectTests(TestCase): class UrlRedirectTests(TestCase):
api_root_path = '/api/v0/' api_root_path = '/api/v1/'
def assertRedirectsToRootPath(self, path, **kwargs): def assertRedirectsToRootPath(self, path, **kwargs):
assert_kwargs = {'status_code': 302} assert_kwargs = {'status_code': 302}
......
...@@ -7,7 +7,7 @@ from urllib import urlencode ...@@ -7,7 +7,7 @@ from urllib import urlencode
from django_dynamic_fixture import G from django_dynamic_fixture import G
from rest_framework import status from rest_framework import status
from analytics_data_api.v0.tests.utils import flatten from analytics_data_api.v1.tests.utils import flatten
class CourseSamples(object): class CourseSamples(object):
...@@ -18,6 +18,8 @@ class CourseSamples(object): ...@@ -18,6 +18,8 @@ class CourseSamples(object):
'ccx-v1:edx+1.005x-CCX+rerun+ccx@15' 'ccx-v1:edx+1.005x-CCX+rerun+ccx@15'
] ]
four_course_ids = course_ids[:3] + ['course-v1:A+B+C']
program_ids = [ program_ids = [
'482dee71-e4b9-4b42-a47b-3e16bb69e8f2', '482dee71-e4b9-4b42-a47b-3e16bb69e8f2',
'71c14f59-35d5-41f2-a017-e108d2d9f127', '71c14f59-35d5-41f2-a017-e108d2d9f127',
...@@ -100,28 +102,29 @@ class APIListViewTestMixin(object): ...@@ -100,28 +102,29 @@ class APIListViewTestMixin(object):
list_name = 'list' list_name = 'list'
default_ids = [] default_ids = []
always_exclude = ['created'] always_exclude = ['created']
test_post_method = False
def path(self, query_data=None): def path(self, query_data=None):
query_data = query_data or {} query_data = query_data or {}
concat_query_data = {param: ','.join(arg) for param, arg in query_data.items() if arg} concat_query_data = {param: ','.join(arg) for param, arg in query_data.items() if arg}
query_string = '?{}'.format(urlencode(concat_query_data)) if concat_query_data else '' query_string = '?{}'.format(urlencode(concat_query_data)) if concat_query_data else ''
return '/api/v0/{}/{}'.format(self.list_name, query_string) return '/api/v1/{}/{}'.format(self.list_name, query_string)
def validated_request(self, ids=None, fields=None, exclude=None, **extra_args): @classmethod
params = [self.ids_param, 'fields', 'exclude'] def build_request_data_dict(cls, ids=None, **kwargs):
args = [ids, fields, exclude] data = {cls.ids_param: ids} if ids else {}
data = {param: arg for param, arg in zip(params, args) if arg} data.update({
data.update(extra_args) key: value
for key, value in kwargs.iteritems()
get_response = self.authenticated_get(self.path(data)) if value not in [None, [None]]
if self.test_post_method: })
post_response = self.authenticated_post(self.path(), data=data) return data
self.assertEquals(get_response.status_code, post_response.status_code)
if 200 <= get_response.status_code < 300: def validated_request(self, expected_status_code, ids=None, **kwargs):
self.assertEquals(get_response.data, post_response.data) request_data = self.build_request_data_dict(ids, **kwargs)
response = self.authenticated_get(self.path(request_data))
return get_response print '**** GET **** ' + response.content
self.assertEqual(response.status_code, expected_status_code)
return response.data
def create_model(self, model_id, **kwargs): def create_model(self, model_id, **kwargs):
pass # implement in subclass pass # implement in subclass
...@@ -148,20 +151,18 @@ class APIListViewTestMixin(object): ...@@ -148,20 +151,18 @@ class APIListViewTestMixin(object):
def _test_all_items(self, ids): def _test_all_items(self, ids):
self.generate_data() self.generate_data()
response = self.validated_request(ids=ids, exclude=self.always_exclude) data = self.validated_request(200, ids=ids, exclude=self.always_exclude)
self.assertEquals(response.status_code, 200) self.assertItemsEqual(data, self.all_expected_results(ids=ids))
self.assertItemsEqual(response.data, self.all_expected_results(ids=ids))
def _test_one_item(self, item_id): def _test_one_item(self, item_id):
self.generate_data() self.generate_data()
response = self.validated_request(ids=[item_id], exclude=self.always_exclude) actual_results = self.validated_request(200, ids=[item_id], exclude=self.always_exclude)
self.assertEquals(response.status_code, 200) expected_results = [self.expected_result(item_id)]
self.assertItemsEqual(response.data, [self.expected_result(item_id)]) self.assertItemsEqual(actual_results, expected_results)
def _test_fields(self, fields): def _test_fields(self, fields):
self.generate_data() self.generate_data()
response = self.validated_request(fields=fields) data = self.validated_request(200, fields=fields)
self.assertEquals(response.status_code, 200)
# remove fields not requested from expected results # remove fields not requested from expected results
expected_results = self.all_expected_results() expected_results = self.all_expected_results()
...@@ -169,13 +170,52 @@ class APIListViewTestMixin(object): ...@@ -169,13 +170,52 @@ class APIListViewTestMixin(object):
for field_to_remove in set(expected_result.keys()) - set(fields): for field_to_remove in set(expected_result.keys()) - set(fields):
expected_result.pop(field_to_remove) expected_result.pop(field_to_remove)
self.assertItemsEqual(response.data, expected_results) self.assertItemsEqual(data, expected_results)
def test_no_items(self): def test_no_items(self):
response = self.validated_request() data = self.validated_request(200)
self.assertEquals(response.status_code, 404) self.assertEqual(data, [])
def test_no_matching_items(self): def test_no_matching_items(self):
self.generate_data() self.generate_data()
response = self.validated_request(ids=['no/items/found']) data = self.validated_request(200, ids=['no/items/found'])
self.assertEquals(response.status_code, 404) self.assertEqual(data, [])
class PostableAPIListViewTestMixin(APIListViewTestMixin):
max_ids_for_get = None
def validated_request(self, expected_status_code, ids=None, **kwargs):
request_data = self.build_request_data_dict(ids, **kwargs)
post_response = self.authenticated_post(self.path(), data=request_data)
print '**** POST **** ' + post_response.content
self.assertEqual(post_response.status_code, expected_status_code)
# If we can do a get, validate that the response is the same
if self.max_ids_for_get is None or (not ids) or len(ids) < self.max_ids_for_get:
get_data = super(PostableAPIListViewTestMixin, self).validated_request(
expected_status_code,
ids,
**kwargs
)
if expected_status_code >= 300:
return None
if {'next', 'prev'} & set(get_data.keys()):
for key in {'count', 'results', 'page'}:
self.assertEqual(get_data.get(key), post_response.data.get(key))
else:
self.assertDictEqual(get_data, post_response.data)
return post_response.data
class PaginatedAPIListViewTestMixin(APIListViewTestMixin):
def validated_request(self, expected_status_code, ids=None, extract_results=True, **kwargs):
data = super(PaginatedAPIListViewTestMixin, self).validated_request(
expected_status_code,
ids,
**kwargs
)
return data['results'] if extract_results and isinstance(data, dict) else data
import random
from urllib import quote_plus
import ddt
from django_dynamic_fixture import G
from analytics_data_api.v1 import models
from analytics_data_api.v1.tests.views import CourseSamples
from analyticsdataserver.tests import TestCaseWithAuthentication
@ddt.ddt
class CourseTotalsViewTests(TestCaseWithAuthentication):
SEED_DATA_BOUNDS = (10000, 100000)
OPTIONAL_COURSE_MODES = ['honor', 'credit', 'professional', 'professional-no-id']
@classmethod
def _get_counts(cls):
"""
Returns a triplet of viable (count, cumulative_count, count_change_7_days) numbers
"""
count = random.randint(CourseTotalsViewTests.SEED_DATA_BOUNDS[0], CourseTotalsViewTests.SEED_DATA_BOUNDS[1])
cumulative_count = random.randint(count, int(count * 1.5))
count_change_7_days = random.randint(int(count * .1), int(count * .3))
return (count, cumulative_count, count_change_7_days)
@classmethod
def setUpClass(cls):
super(CourseTotalsViewTests, cls).setUpClass()
cls.test_data = {
id: {
'count': 0,
'cumulative_count': 0,
'verified_enrollment': 0,
'count_change_7_days': 0
} for id in CourseSamples.course_ids
} # pylint: disable=attribute-defined-outside-init
for course in cls.test_data:
modes = ['verified'] # No choice here, everyone gets a verified mode
modes = modes + random.sample(CourseTotalsViewTests.OPTIONAL_COURSE_MODES, random.randint(1, 3))
for mode in modes:
counts = cls._get_counts()
cls.test_data[course]['count'] += counts[0]
if mode == 'verified':
cls.test_data[course]['verified_enrollment'] += counts[0]
cls.test_data[course]['cumulative_count'] += counts[1]
cls.test_data[course]['count_change_7_days'] += counts[2]
G(
models.CourseMetaSummaryEnrollment,
course_id=course,
enrollment_mode=mode,
count=counts[0],
cumulative_count=counts[1],
count_change_7_days=counts[2]
)
def _get_data(self, course_ids):
url = '/api/v1/course_totals/'
if course_ids:
url += '?course_ids={}'.format(",".join(map(quote_plus, course_ids)))
return self.authenticated_get(url)
@ddt.data(
None,
CourseSamples.course_ids,
[CourseSamples.course_ids[1]],
[CourseSamples.course_ids[0], CourseSamples.course_ids[2]]
)
def test_get(self, course_ids):
response = self._get_data(course_ids) # get response first so we can set expected if course_ids==[]
if not course_ids:
course_ids = CourseSamples.course_ids
expected = {
'count': sum(
[self.test_data[course]['count'] for course in course_ids]
),
'cumulative_count': sum(
[self.test_data[course]['cumulative_count'] for course in course_ids]
),
'verified_enrollment': sum(
[self.test_data[course]['verified_enrollment'] for course in course_ids]
),
'count_change_7_days': sum(
[self.test_data[course]['count_change_7_days'] for course in course_ids]
)
}
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, expected)
...@@ -17,8 +17,8 @@ from mock import patch, Mock ...@@ -17,8 +17,8 @@ from mock import patch, Mock
from analytics_data_api.constants import country, enrollment_modes, genders from analytics_data_api.constants import country, enrollment_modes, genders
from analytics_data_api.constants.country import get_country from analytics_data_api.constants.country import get_country
from analytics_data_api.v0 import models from analytics_data_api.v1 import models
from analytics_data_api.v0.tests.views import CourseSamples, VerifyCsvResponseMixin from analytics_data_api.v1.tests.views import CourseSamples, VerifyCsvResponseMixin
from analytics_data_api.utils import get_filename_safe_course_id from analytics_data_api.utils import get_filename_safe_course_id
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
...@@ -43,7 +43,7 @@ class DefaultFillTestMixin(object): ...@@ -43,7 +43,7 @@ class DefaultFillTestMixin(object):
@ddt.ddt @ddt.ddt
class CourseViewTestCaseMixin(VerifyCsvResponseMixin): class CourseViewTestCaseMixin(VerifyCsvResponseMixin):
model = None model = None
api_root_path = '/api/v0/' api_root_path = '/api/v1/'
path = None path = None
order_by = [] order_by = []
csv_filename_slug = None csv_filename_slug = None
...@@ -184,12 +184,12 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication): ...@@ -184,12 +184,12 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
def test_activity(self, course_id): def test_activity(self, course_id):
self.generate_data(course_id) self.generate_data(course_id)
response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity'.format(course_id)) response = self.authenticated_get(u'/api/v1/courses/{0}/recent_activity'.format(course_id))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record(course_id=course_id)) self.assertEquals(response.data, self.get_activity_record(course_id=course_id))
def assertValidActivityResponse(self, course_id, activity_type, count): def assertValidActivityResponse(self, course_id, activity_type, count):
response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity?activity_type={1}'.format( response = self.authenticated_get(u'/api/v1/courses/{0}/recent_activity?activity_type={1}'.format(
course_id, activity_type)) course_id, activity_type))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record(course_id=course_id, activity_type=activity_type, self.assertEquals(response.data, self.get_activity_record(course_id=course_id, activity_type=activity_type,
...@@ -212,14 +212,14 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication): ...@@ -212,14 +212,14 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
def test_activity_auth(self, course_id): def test_activity_auth(self, course_id):
self.generate_data(course_id) self.generate_data(course_id)
response = self.client.get(u'/api/v0/courses/{0}/recent_activity'.format(course_id), follow=True) response = self.client.get(u'/api/v1/courses/{0}/recent_activity'.format(course_id), follow=True)
self.assertEquals(response.status_code, 401) self.assertEquals(response.status_code, 401)
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
def test_url_encoded_course_id(self, course_id): def test_url_encoded_course_id(self, course_id):
self.generate_data(course_id) self.generate_data(course_id)
url_encoded_course_id = urllib.quote_plus(course_id) url_encoded_course_id = urllib.quote_plus(course_id)
response = self.authenticated_get(u'/api/v0/courses/{}/recent_activity'.format(url_encoded_course_id)) response = self.authenticated_get(u'/api/v1/courses/{}/recent_activity'.format(url_encoded_course_id))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record(course_id=course_id)) self.assertEquals(response.data, self.get_activity_record(course_id=course_id))
...@@ -238,23 +238,23 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication): ...@@ -238,23 +238,23 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
def test_unknown_activity(self, course_id): def test_unknown_activity(self, course_id):
self.generate_data(course_id) self.generate_data(course_id)
activity_type = 'missing_activity_type' activity_type = 'missing_activity_type'
response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity?activity_type={1}'.format( response = self.authenticated_get(u'/api/v1/courses/{0}/recent_activity?activity_type={1}'.format(
course_id, activity_type)) course_id, activity_type))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
def test_unknown_course_id(self): def test_unknown_course_id(self):
response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity'.format('foo')) response = self.authenticated_get(u'/api/v1/courses/{0}/recent_activity'.format('foo'))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
def test_missing_course_id(self): def test_missing_course_id(self):
response = self.authenticated_get(u'/api/v0/courses/recent_activity') response = self.authenticated_get(u'/api/v1/courses/recent_activity')
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
def test_label_parameter(self, course_id): def test_label_parameter(self, course_id):
self.generate_data(course_id) self.generate_data(course_id)
activity_type = 'played_video' activity_type = 'played_video'
response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity?label={1}'.format( response = self.authenticated_get(u'/api/v1/courses/{0}/recent_activity?label={1}'.format(
course_id, activity_type)) course_id, activity_type))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record(course_id=course_id, activity_type=activity_type, self.assertEquals(response.data, self.get_activity_record(course_id=course_id, activity_type=activity_type,
...@@ -282,7 +282,7 @@ class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, Te ...@@ -282,7 +282,7 @@ class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, Te
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
def test_get(self, course_id): def test_get(self, course_id):
self.generate_data(course_id) self.generate_data(course_id)
response = self.authenticated_get('/api/v0/courses/%s%s' % (course_id, self.path,)) response = self.authenticated_get('/api/v1/courses/%s%s' % (course_id, self.path,))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected = self.format_as_response(*self.model.objects.filter(date=self.date)) expected = self.format_as_response(*self.model.objects.filter(date=self.date))
...@@ -577,7 +577,7 @@ class CourseProblemsListViewTests(TestCaseWithAuthentication): ...@@ -577,7 +577,7 @@ class CourseProblemsListViewTests(TestCaseWithAuthentication):
""" """
Retrieve data for the specified course. Retrieve data for the specified course.
""" """
url = '/api/v0/courses/{}/problems/'.format(course_id) url = '/api/v1/courses/{}/problems/'.format(course_id)
return self.authenticated_get(url) return self.authenticated_get(url)
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
...@@ -642,7 +642,7 @@ class CourseProblemsAndTagsListViewTests(TestCaseWithAuthentication): ...@@ -642,7 +642,7 @@ class CourseProblemsAndTagsListViewTests(TestCaseWithAuthentication):
""" """
Retrieve data for the specified course. Retrieve data for the specified course.
""" """
url = '/api/v0/courses/{}/problems_and_tags/'.format(course_id) url = '/api/v1/courses/{}/problems_and_tags/'.format(course_id)
return self.authenticated_get(url) return self.authenticated_get(url)
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
...@@ -721,7 +721,7 @@ class CourseVideosListViewTests(TestCaseWithAuthentication): ...@@ -721,7 +721,7 @@ class CourseVideosListViewTests(TestCaseWithAuthentication):
""" """
Retrieve videos for a specified course. Retrieve videos for a specified course.
""" """
url = '/api/v0/courses/{}/videos/'.format(course_id) url = '/api/v1/courses/{}/videos/'.format(course_id)
return self.authenticated_get(url) return self.authenticated_get(url)
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
...@@ -776,7 +776,7 @@ class CourseVideosListViewTests(TestCaseWithAuthentication): ...@@ -776,7 +776,7 @@ class CourseVideosListViewTests(TestCaseWithAuthentication):
@ddt.ddt @ddt.ddt
class CourseReportDownloadViewTests(TestCaseWithAuthentication): class CourseReportDownloadViewTests(TestCaseWithAuthentication):
path = '/api/v0/courses/{course_id}/reports/{report_name}' path = '/api/v1/courses/{course_id}/reports/{report_name}'
@patch('django.core.files.storage.default_storage.exists', Mock(return_value=False)) @patch('django.core.files.storage.default_storage.exists', Mock(return_value=False))
@ddt.data(*CourseSamples.course_ids) @ddt.data(*CourseSamples.course_ids)
......
...@@ -11,14 +11,14 @@ from rest_framework import status ...@@ -11,14 +11,14 @@ from rest_framework import status
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
from analytics_data_api.constants.engagement_events import (ATTEMPTED, COMPLETED, CONTRIBUTED, DISCUSSION, from analytics_data_api.constants.engagement_events import (ATTEMPTED, COMPLETED, CONTRIBUTED, DISCUSSION,
PROBLEM, VIDEO, VIEWED) PROBLEM, VIDEO, VIEWED)
from analytics_data_api.v0 import models from analytics_data_api.v1 import models
from analytics_data_api.v0.tests.views import CourseSamples, VerifyCourseIdMixin from analytics_data_api.v1.tests.views import CourseSamples, VerifyCourseIdMixin
@ddt.ddt @ddt.ddt
class EngagementTimelineTests(VerifyCourseIdMixin, TestCaseWithAuthentication): class EngagementTimelineTests(VerifyCourseIdMixin, TestCaseWithAuthentication):
DEFAULT_USERNAME = 'ed_xavier' DEFAULT_USERNAME = 'ed_xavier'
path_template = '/api/v0/engagement_timelines/{}/?course_id={}' path_template = '/api/v1/engagement_timelines/{}/?course_id={}'
def create_engagement(self, course_id, entity_type, event_type, entity_id, count, date=None): def create_engagement(self, course_id, entity_type, event_type, entity_id, count, date=None):
"""Create a ModuleEngagement model""" """Create a ModuleEngagement model"""
...@@ -158,7 +158,7 @@ class EngagementTimelineTests(VerifyCourseIdMixin, TestCaseWithAuthentication): ...@@ -158,7 +158,7 @@ class EngagementTimelineTests(VerifyCourseIdMixin, TestCaseWithAuthentication):
self.assertDictEqual(json.loads(response.content), expected) self.assertDictEqual(json.loads(response.content), expected)
def test_no_course_id(self): def test_no_course_id(self):
base_path = '/api/v0/engagement_timelines/{}' base_path = '/api/v1/engagement_timelines/{}'
response = self.authenticated_get((base_path).format('ed_xavier')) response = self.authenticated_get((base_path).format('ed_xavier'))
self.verify_no_course_id(response) self.verify_no_course_id(response)
......
...@@ -18,9 +18,9 @@ from django.test import override_settings ...@@ -18,9 +18,9 @@ from django.test import override_settings
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
from analytics_data_api.constants import engagement_events from analytics_data_api.constants import engagement_events
from analytics_data_api.v0.models import ModuleEngagementMetricRanges from analytics_data_api.v1.models import ModuleEngagementMetricRanges
from analytics_data_api.v0.views import CsvViewMixin, PaginatedHeadersMixin from analytics_data_api.v1.views.base import CsvViewMixin, PaginatedHeadersMixin
from analytics_data_api.v0.tests.views import ( from analytics_data_api.v1.tests.views import (
CourseSamples, VerifyCourseIdMixin, VerifyCsvResponseMixin, CourseSamples, VerifyCourseIdMixin, VerifyCsvResponseMixin,
) )
...@@ -141,7 +141,7 @@ class LearnerAPITestMixin(CsvViewMixin): ...@@ -141,7 +141,7 @@ class LearnerAPITestMixin(CsvViewMixin):
course_q = urlencode({'course_id': course_id}) course_q = urlencode({'course_id': course_id})
page_q = '&page={}'.format(page) if page and page > 1 else '' page_q = '&page={}'.format(page) if page and page > 1 else ''
page_size_q = '&page_size={}'.format(page_size) if page_size > 0 else '' page_size_q = '&page_size={}'.format(page_size) if page_size > 0 else ''
return 'http://testserver/api/v0/learners/?{course_q}{page_q}{page_size_q}'.format( return 'http://testserver/api/v1/learners/?{course_q}{page_q}{page_size_q}'.format(
course_q=course_q, page_q=page_q, page_size_q=page_size_q, course_q=course_q, page_q=page_q, page_size_q=page_size_q,
) )
...@@ -149,7 +149,7 @@ class LearnerAPITestMixin(CsvViewMixin): ...@@ -149,7 +149,7 @@ class LearnerAPITestMixin(CsvViewMixin):
@ddt.ddt @ddt.ddt
class LearnerTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthentication): class LearnerTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthentication):
"""Tests for the single learner endpoint.""" """Tests for the single learner endpoint."""
path_template = '/api/v0/learners/{}/?course_id={}' path_template = '/api/v1/learners/{}/?course_id={}'
@ddt.data( @ddt.data(
('ed_xavier', 'Edward Xavier', 'edX/DemoX/Demo_Course', 'honor', ['has_potential'], 'Team edX', ('ed_xavier', 'Edward Xavier', 'edX/DemoX/Demo_Course', 'honor', ['has_potential'], 'Team edX',
...@@ -229,7 +229,7 @@ class LearnerTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthent ...@@ -229,7 +229,7 @@ class LearnerTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthent
} }
self.assertDictEqual(expected, response.data) self.assertDictEqual(expected, response.data)
@patch('analytics_data_api.v0.models.RosterEntry.get_course_user', Mock(return_value=[])) @patch('analytics_data_api.v1.models.RosterEntry.get_course_user', Mock(return_value=[]))
def test_not_found(self): def test_not_found(self):
user_name = 'a_user' user_name = 'a_user'
course_id = 'edX/DemoX/Demo_Course' course_id = 'edX/DemoX/Demo_Course'
...@@ -242,7 +242,7 @@ class LearnerTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthent ...@@ -242,7 +242,7 @@ class LearnerTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthent
self.assertDictEqual(json.loads(response.content), expected) self.assertDictEqual(json.loads(response.content), expected)
def test_no_course_id(self): def test_no_course_id(self):
base_path = '/api/v0/learners/{}' base_path = '/api/v1/learners/{}'
response = self.authenticated_get((base_path).format('ed_xavier')) response = self.authenticated_get((base_path).format('ed_xavier'))
self.verify_no_course_id(response) self.verify_no_course_id(response)
...@@ -263,7 +263,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut ...@@ -263,7 +263,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
def _get(self, course_id, **query_params): def _get(self, course_id, **query_params):
"""Helper to send a GET request to the API.""" """Helper to send a GET request to the API."""
query_params['course_id'] = course_id query_params['course_id'] = course_id
return self.authenticated_get('/api/v0/learners/', query_params) return self.authenticated_get('/api/v1/learners/', query_params)
def assert_learners_returned(self, response, expected_learners): def assert_learners_returned(self, response, expected_learners):
""" """
...@@ -494,7 +494,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut ...@@ -494,7 +494,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
) )
@ddt.unpack @ddt.unpack
def test_bad_request(self, parameters, expected_error_code, expected_status_code=400): def test_bad_request(self, parameters, expected_error_code, expected_status_code=400):
response = self.authenticated_get('/api/v0/learners/', parameters) response = self.authenticated_get('/api/v1/learners/', parameters)
self.assertEqual(response.status_code, expected_status_code) self.assertEqual(response.status_code, expected_status_code)
response_json = json.loads(response.content) response_json = json.loads(response.content)
self.assertEqual(response_json.get('error_code', response_json.get('detail')), expected_error_code) self.assertEqual(response_json.get('error_code', response_json.get('detail')), expected_error_code)
...@@ -508,7 +508,7 @@ class LearnerCsvListTests(LearnerAPITestMixin, VerifyCourseIdMixin, ...@@ -508,7 +508,7 @@ class LearnerCsvListTests(LearnerAPITestMixin, VerifyCourseIdMixin,
super(LearnerCsvListTests, self).setUp() super(LearnerCsvListTests, self).setUp()
self.course_id = 'edX/DemoX/Demo_Course' self.course_id = 'edX/DemoX/Demo_Course'
self.create_update_index('2015-09-28') self.create_update_index('2015-09-28')
self.path = '/api/v0/learners/' self.path = '/api/v1/learners/'
def test_empty_csv(self): def test_empty_csv(self):
""" Verify the endpoint returns data that has been properly converted to CSV. """ """ Verify the endpoint returns data that has been properly converted to CSV. """
...@@ -652,7 +652,7 @@ class CourseLearnerMetadataTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestC ...@@ -652,7 +652,7 @@ class CourseLearnerMetadataTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestC
def _get(self, course_id): def _get(self, course_id):
"""Helper to send a GET request to the API.""" """Helper to send a GET request to the API."""
return self.authenticated_get('/api/v0/course_learner_metadata/{}/'.format(course_id)) return self.authenticated_get('/api/v1/course_learner_metadata/{}/'.format(course_id))
def get_expected_json(self, course_id, segments, enrollment_modes, cohorts): def get_expected_json(self, course_id, segments, enrollment_modes, cohorts):
expected_json = self._get_full_engagement_ranges(course_id) expected_json = self._get_full_engagement_ranges(course_id)
...@@ -666,7 +666,7 @@ class CourseLearnerMetadataTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestC ...@@ -666,7 +666,7 @@ class CourseLearnerMetadataTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestC
self.assertDictEqual(json.loads(response.content), expected_data) self.assertDictEqual(json.loads(response.content), expected_data)
def test_no_course_id(self): def test_no_course_id(self):
response = self.authenticated_get('/api/v0/course_learner_metadata/') response = self.authenticated_get('/api/v1/course_learner_metadata/')
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@ddt.data( @ddt.data(
......
...@@ -9,8 +9,8 @@ import json ...@@ -9,8 +9,8 @@ import json
from django_dynamic_fixture import G from django_dynamic_fixture import G
from analytics_data_api.v0 import models from analytics_data_api.v1 import models
from analytics_data_api.v0.serializers import ProblemFirstLastResponseAnswerDistributionSerializer, \ from analytics_data_api.v1.serializers import ProblemFirstLastResponseAnswerDistributionSerializer, \
GradeDistributionSerializer, SequentialOpenDistributionSerializer GradeDistributionSerializer, SequentialOpenDistributionSerializer
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
...@@ -94,7 +94,7 @@ class AnswerDistributionTests(TestCaseWithAuthentication): ...@@ -94,7 +94,7 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
def test_nonconsolidated_get(self): def test_nonconsolidated_get(self):
""" Verify that answers which should not be consolidated are not. """ """ Verify that answers which should not be consolidated are not. """
response = self.authenticated_get('/api/v0/problems/%s%s' % (self.module_id2, self.path)) response = self.authenticated_get('/api/v1/problems/%s%s' % (self.module_id2, self.path))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected_data = models.ProblemFirstLastResponseAnswerDistribution.objects.filter(module_id=self.module_id2) expected_data = models.ProblemFirstLastResponseAnswerDistribution.objects.filter(module_id=self.module_id2)
...@@ -111,7 +111,7 @@ class AnswerDistributionTests(TestCaseWithAuthentication): ...@@ -111,7 +111,7 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
def test_consolidated_get(self): def test_consolidated_get(self):
""" Verify that valid consolidation does occur. """ """ Verify that valid consolidation does occur. """
response = self.authenticated_get( response = self.authenticated_get(
'/api/v0/problems/{0}{1}'.format(self.module_id1, self.path)) '/api/v1/problems/{0}{1}'.format(self.module_id1, self.path))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected_data = [self.ad1, self.ad3] expected_data = [self.ad1, self.ad3]
...@@ -132,7 +132,7 @@ class AnswerDistributionTests(TestCaseWithAuthentication): ...@@ -132,7 +132,7 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
self.assertEquals(set(response.data), set(expected_data)) self.assertEquals(set(response.data), set(expected_data))
def test_get_404(self): def test_get_404(self):
response = self.authenticated_get('/api/v0/problems/%s%s' % ("DOES-NOT-EXIST", self.path)) response = self.authenticated_get('/api/v1/problems/%s%s' % ("DOES-NOT-EXIST", self.path))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
...@@ -152,7 +152,7 @@ class GradeDistributionTests(TestCaseWithAuthentication): ...@@ -152,7 +152,7 @@ class GradeDistributionTests(TestCaseWithAuthentication):
) )
def test_get(self): def test_get(self):
response = self.authenticated_get('/api/v0/problems/%s%s' % (self.module_id, self.path)) response = self.authenticated_get('/api/v1/problems/%s%s' % (self.module_id, self.path))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected_dict = GradeDistributionSerializer(self.ad1).data expected_dict = GradeDistributionSerializer(self.ad1).data
...@@ -161,7 +161,7 @@ class GradeDistributionTests(TestCaseWithAuthentication): ...@@ -161,7 +161,7 @@ class GradeDistributionTests(TestCaseWithAuthentication):
self.assertDictEqual(actual_list[0], expected_dict) self.assertDictEqual(actual_list[0], expected_dict)
def test_get_404(self): def test_get_404(self):
response = self.authenticated_get('/api/v0/problems/%s%s' % ("DOES-NOT-EXIST", self.path)) response = self.authenticated_get('/api/v1/problems/%s%s' % ("DOES-NOT-EXIST", self.path))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
...@@ -181,7 +181,7 @@ class SequentialOpenDistributionTests(TestCaseWithAuthentication): ...@@ -181,7 +181,7 @@ class SequentialOpenDistributionTests(TestCaseWithAuthentication):
) )
def test_get(self): def test_get(self):
response = self.authenticated_get('/api/v0/problems/%s%s' % (self.module_id, self.path)) response = self.authenticated_get('/api/v1/problems/%s%s' % (self.module_id, self.path))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected_dict = SequentialOpenDistributionSerializer(self.ad1).data expected_dict = SequentialOpenDistributionSerializer(self.ad1).data
...@@ -190,5 +190,5 @@ class SequentialOpenDistributionTests(TestCaseWithAuthentication): ...@@ -190,5 +190,5 @@ class SequentialOpenDistributionTests(TestCaseWithAuthentication):
self.assertDictEqual(actual_list[0], expected_dict) self.assertDictEqual(actual_list[0], expected_dict)
def test_get_404(self): def test_get_404(self):
response = self.authenticated_get('/api/v0/problems/%s%s' % ("DOES-NOT-EXIST", self.path)) response = self.authenticated_get('/api/v1/problems/%s%s' % ("DOES-NOT-EXIST", self.path))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
...@@ -2,8 +2,8 @@ import datetime ...@@ -2,8 +2,8 @@ import datetime
import ddt import ddt
from django_dynamic_fixture import G from django_dynamic_fixture import G
from analytics_data_api.v0 import models, serializers from analytics_data_api.v1 import models, serializers
from analytics_data_api.v0.tests.views import CourseSamples, APIListViewTestMixin from analytics_data_api.v1.tests.views import CourseSamples, APIListViewTestMixin
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
...@@ -97,6 +97,6 @@ class ProgramsViewTests(TestCaseWithAuthentication, APIListViewTestMixin): ...@@ -97,6 +97,6 @@ class ProgramsViewTests(TestCaseWithAuthentication, APIListViewTestMixin):
@ddt.unpack @ddt.unpack
def test_all_programs_multi_courses(self, program_ids, course_ids): def test_all_programs_multi_courses(self, program_ids, course_ids):
self.generate_data(ids=program_ids, course_ids=course_ids) self.generate_data(ids=program_ids, course_ids=course_ids)
response = self.validated_request(ids=program_ids, exclude=self.always_exclude) actual_data = self.validated_request(200, ids=program_ids, exclude=self.always_exclude)
self.assertEquals(response.status_code, 200) expected_data = self.all_expected_results(ids=program_ids, course_ids=course_ids)
self.assertItemsEqual(response.data, self.all_expected_results(ids=program_ids, course_ids=course_ids)) self.assertItemsEqual(actual_data, expected_data)
...@@ -4,9 +4,9 @@ from mock import Mock ...@@ -4,9 +4,9 @@ from mock import Mock
from django.http import Http404 from django.http import Http404
from django.test import TestCase from django.test import TestCase
from analytics_data_api.v0.exceptions import CourseKeyMalformedError from analytics_data_api.v1.exceptions import CourseKeyMalformedError
from analytics_data_api.v0.tests.views import CourseSamples from analytics_data_api.v1.tests.views import CourseSamples
import analytics_data_api.v0.views.utils as utils import analytics_data_api.v1.views.utils as utils
@ddt.ddt @ddt.ddt
......
...@@ -4,14 +4,14 @@ from django.conf import settings ...@@ -4,14 +4,14 @@ from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django_dynamic_fixture import G from django_dynamic_fixture import G
from analytics_data_api.v0 import models from analytics_data_api.v1 import models
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
class VideoTimelineTests(TestCaseWithAuthentication): class VideoTimelineTests(TestCaseWithAuthentication):
def _get_data(self, video_id=None): def _get_data(self, video_id=None):
return self.authenticated_get('/api/v0/videos/{}/timeline'.format(video_id)) return self.authenticated_get('/api/v1/videos/{}/timeline'.format(video_id))
def test_get(self): def test_get(self):
# add a blank row, which shouldn't be included in results # add a blank row, which shouldn't be included in results
......
...@@ -2,15 +2,17 @@ from django.conf.urls import url, include ...@@ -2,15 +2,17 @@ from django.conf.urls import url, include
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.views.generic import RedirectView from django.views.generic import RedirectView
COURSE_ID_PATTERN = r'(?P<course_id>[^/+]+[/+][^/+]+[/+][^/]+)' COURSE_ID_PATTERN = r'(?P<course_id>[^/+]+[/+][^/+]+[/+][^/]+)'
urlpatterns = [ urlpatterns = [
url(r'^courses/', include('analytics_data_api.v0.urls.courses', 'courses')), url(r'^courses/', include('analytics_data_api.v1.urls.courses', 'courses')),
url(r'^problems/', include('analytics_data_api.v0.urls.problems', 'problems')), url(r'^problems/', include('analytics_data_api.v1.urls.problems', 'problems')),
url(r'^videos/', include('analytics_data_api.v0.urls.videos', 'videos')), url(r'^videos/', include('analytics_data_api.v1.urls.videos', 'videos')),
url('^', include('analytics_data_api.v0.urls.learners', 'learners')), url('^', include('analytics_data_api.v1.urls.learners', 'learners')),
url('^', include('analytics_data_api.v0.urls.course_summaries', 'course_summaries')), url('^', include('analytics_data_api.v1.urls.course_summaries', 'course_summaries')),
url('^', include('analytics_data_api.v0.urls.programs', 'programs')), url('^', include('analytics_data_api.v1.urls.course_totals', 'course_totals')),
url('^', include('analytics_data_api.v1.urls.programs', 'programs')),
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
url(r'^authenticated/$', RedirectView.as_view(url=reverse_lazy('authenticated')), name='authenticated'), url(r'^authenticated/$', RedirectView.as_view(url=reverse_lazy('authenticated')), name='authenticated'),
......
from django.conf.urls import url from django.conf.urls import url
from analytics_data_api.v0.views import course_summaries as views from analytics_data_api.v1.views import course_summaries as views
urlpatterns = [ urlpatterns = [
url(r'^course_summaries/$', views.CourseSummariesView.as_view(), name='course_summaries'), url(r'^course_summaries/$', views.CourseSummariesView.as_view(), name='course_summaries'),
......
from django.conf.urls import url
from analytics_data_api.v1.views import course_totals as views
urlpatterns = [
url(r'^course_totals/$', views.CourseTotalsView.as_view(), name='course_totals'),
]
from django.conf.urls import url from django.conf.urls import url
from analytics_data_api.v0.urls import COURSE_ID_PATTERN from analytics_data_api.v1.urls import COURSE_ID_PATTERN
from analytics_data_api.v0.views import courses as views from analytics_data_api.v1.views import courses as views
COURSE_URLS = [ COURSE_URLS = [
('activity', views.CourseActivityWeeklyView, 'activity'), ('activity', views.CourseActivityWeeklyView, 'activity'),
......
from django.conf.urls import url from django.conf.urls import url
from analytics_data_api.v0.urls import COURSE_ID_PATTERN from analytics_data_api.v1.urls import COURSE_ID_PATTERN
from analytics_data_api.v0.views import learners as views from analytics_data_api.v1.views import learners as views
USERNAME_PATTERN = r'(?P<username>[\w.+-]+)' USERNAME_PATTERN = r'(?P<username>[\w.+-]+)'
......
...@@ -2,7 +2,7 @@ import re ...@@ -2,7 +2,7 @@ import re
from django.conf.urls import url from django.conf.urls import url
from analytics_data_api.v0.views import problems as views from analytics_data_api.v1.views import problems as views
PROBLEM_URLS = [ PROBLEM_URLS = [
('answer_distribution', views.ProblemResponseAnswerDistributionView, 'answer_distribution'), ('answer_distribution', views.ProblemResponseAnswerDistributionView, 'answer_distribution'),
......
from django.conf.urls import url from django.conf.urls import url
from analytics_data_api.v0.views import programs as views from analytics_data_api.v1.views import programs as views
urlpatterns = [ urlpatterns = [
url(r'^programs/$', views.ProgramsView.as_view(), name='programs'), url(r'^programs/$', views.ProgramsView.as_view(), name='programs'),
......
...@@ -2,7 +2,7 @@ import re ...@@ -2,7 +2,7 @@ import re
from django.conf.urls import url from django.conf.urls import url
from analytics_data_api.v0.views import videos as views from analytics_data_api.v1.views import videos as views
VIDEO_URLS = [ VIDEO_URLS = [
('timeline', views.VideoTimelineView, 'timeline'), ('timeline', views.VideoTimelineView, 'timeline'),
......
from django.db.models import Sum
from rest_framework.generics import RetrieveAPIView
from analytics_data_api.v1.models import CourseMetaSummaryEnrollment
from analytics_data_api.v1.serializers import CourseTotalsSerializer
from analytics_data_api.v1.views.base import (
IDsAPIViewMixin,
PostAsGetAPIViewMixin,
)
class CourseTotalsView(PostAsGetAPIViewMixin, IDsAPIViewMixin, RetrieveAPIView):
"""
Returns totals of course enrollment statistics.
**Example Requests**
GET /api/v1/course_totals/?course_ids={course_id_1},{course_id_2}
POST /api/v1/course_totals/
{
"course_ids": [
"{course_id_1}",
"{course_id_2}",
...
"{course_id_200}"
]
}
```
**Parameters**
For GET requests:
* Arguments are passed in the query string.
* List values are passed in as comma-delimited strings.
For POST requests:
* Arguments are passed in as a JSON dict in the request body.
* List values are passed as JSON arrays of strings.
* course_ids -- List of course ID strings to derive totals from.
**Response Values**
Returns enrollment counts and other metadata for each course:
* count: Total number of learners currently enrolled in the specified courses.
* cumulative_count: Total number of learners ever enrolled in the specified courses.
* count_change_7_days: Total change in enrollment across specified courses.
* verified_enrollment: Total number of leaners currently enrolled as verified in specified courses.
"""
serializer_class = CourseTotalsSerializer
# From IDsAPIViewMixin
ids_param = 'course_ids'
def get_object(self):
queryset = CourseMetaSummaryEnrollment.objects.all()
if self.ids:
queryset = queryset.filter(course_id__in=self.ids)
data = queryset.aggregate(
count=Sum('count'),
cumulative_count=Sum('cumulative_count'),
count_change_7_days=Sum('count_change_7_days')
)
data.update(
queryset.filter(enrollment_mode='verified').aggregate(
verified_enrollment=Sum('count')
)
)
return data
...@@ -15,10 +15,10 @@ from opaque_keys.edx.keys import CourseKey ...@@ -15,10 +15,10 @@ from opaque_keys.edx.keys import CourseKey
from analytics_data_api.constants import enrollment_modes from analytics_data_api.constants import enrollment_modes
from analytics_data_api.utils import dictfetchall, get_course_report_download_details from analytics_data_api.utils import dictfetchall, get_course_report_download_details
from analytics_data_api.v0 import models, serializers from analytics_data_api.v1 import models, serializers
from analytics_data_api.v0.exceptions import ReportFileNotFoundError from analytics_data_api.v1.exceptions import ReportFileNotFoundError
from analytics_data_api.v0.views.utils import raise_404_if_none from analytics_data_api.v1.views.utils import raise_404_if_none
class BaseCourseView(generics.ListAPIView): class BaseCourseView(generics.ListAPIView):
...@@ -75,7 +75,7 @@ class CourseActivityWeeklyView(BaseCourseView): ...@@ -75,7 +75,7 @@ class CourseActivityWeeklyView(BaseCourseView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/activity/ GET /api/v1/courses/{course_id}/activity/
**Response Values** **Response Values**
...@@ -183,7 +183,7 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView): ...@@ -183,7 +183,7 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/recent_activity/ GET /api/v1/courses/{course_id}/recent_activity/
**Response Values** **Response Values**
...@@ -283,7 +283,7 @@ class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView): ...@@ -283,7 +283,7 @@ class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/enrollment/birth_year/ GET /api/v1/courses/{course_id}/enrollment/birth_year/
**Response Values** **Response Values**
...@@ -323,7 +323,7 @@ class CourseEnrollmentByEducationView(BaseCourseEnrollmentView): ...@@ -323,7 +323,7 @@ class CourseEnrollmentByEducationView(BaseCourseEnrollmentView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/enrollment/education/ GET /api/v1/courses/{course_id}/enrollment/education/
**Response Values** **Response Values**
...@@ -364,7 +364,7 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView): ...@@ -364,7 +364,7 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/enrollment/gender/ GET /api/v1/courses/{course_id}/enrollment/gender/
**Response Values** **Response Values**
...@@ -431,7 +431,7 @@ class CourseEnrollmentView(BaseCourseEnrollmentView): ...@@ -431,7 +431,7 @@ class CourseEnrollmentView(BaseCourseEnrollmentView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/enrollment/ GET /api/v1/courses/{course_id}/enrollment/
**Response Values** **Response Values**
...@@ -468,7 +468,7 @@ class CourseEnrollmentModeView(BaseCourseEnrollmentView): ...@@ -468,7 +468,7 @@ class CourseEnrollmentModeView(BaseCourseEnrollmentView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/enrollment/mode/ GET /api/v1/courses/{course_id}/enrollment/mode/
**Response Values** **Response Values**
...@@ -548,7 +548,7 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView): ...@@ -548,7 +548,7 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/enrollment/location/ GET /api/v1/courses/{course_id}/enrollment/location/
**Response Values** **Response Values**
...@@ -629,7 +629,7 @@ class ProblemsListView(BaseCourseView): ...@@ -629,7 +629,7 @@ class ProblemsListView(BaseCourseView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/problems/ GET /api/v1/courses/{course_id}/problems/
**Response Values** **Response Values**
...@@ -705,7 +705,7 @@ class ProblemsAndTagsListView(BaseCourseView): ...@@ -705,7 +705,7 @@ class ProblemsAndTagsListView(BaseCourseView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/problems_and_tags/ GET /api/v1/courses/{course_id}/problems_and_tags/
**Response Values** **Response Values**
...@@ -755,7 +755,7 @@ class VideosListView(BaseCourseView): ...@@ -755,7 +755,7 @@ class VideosListView(BaseCourseView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/videos/ GET /api/v1/courses/{course_id}/videos/
**Response Values** **Response Values**
...@@ -786,7 +786,7 @@ class ReportDownloadView(APIView): ...@@ -786,7 +786,7 @@ class ReportDownloadView(APIView):
**Example request** **Example request**
GET /api/v0/courses/{course_id}/reports/{report_name}/ GET /api/v1/courses/{course_id}/reports/{report_name}/
**Response Values** **Response Values**
......
...@@ -5,26 +5,30 @@ import logging ...@@ -5,26 +5,30 @@ import logging
from rest_framework import generics, status from rest_framework import generics, status
from analytics_data_api.v0.exceptions import ( from analytics_data_api.v1.exceptions import (
LearnerEngagementTimelineNotFoundError, LearnerEngagementTimelineNotFoundError,
LearnerNotFoundError, LearnerNotFoundError,
ParameterValueError, ParameterValueError,
) )
from analytics_data_api.v0.models import ( from analytics_data_api.v1.models import (
ModuleEngagement, ModuleEngagement,
ModuleEngagementMetricRanges, ModuleEngagementMetricRanges,
RosterEntry, RosterEntry,
RosterUpdate, RosterUpdate,
) )
from analytics_data_api.v0.serializers import ( from analytics_data_api.v1.serializers import (
CourseLearnerMetadataSerializer, CourseLearnerMetadataSerializer,
EdxPaginationSerializer, EdxPaginationSerializer,
EngagementDaySerializer, EngagementDaySerializer,
LastUpdatedSerializer, LastUpdatedSerializer,
LearnerSerializer, LearnerSerializer,
) )
from analytics_data_api.v0.views import CourseViewMixin, PaginatedHeadersMixin, CsvViewMixin from analytics_data_api.v1.views.base import (
from analytics_data_api.v0.views.utils import split_query_argument CourseViewMixin,
PaginatedHeadersMixin,
CsvViewMixin,
)
from analytics_data_api.v1.views.utils import split_query_argument
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -50,7 +54,7 @@ class LearnerView(LastUpdateMixin, CourseViewMixin, generics.RetrieveAPIView): ...@@ -50,7 +54,7 @@ class LearnerView(LastUpdateMixin, CourseViewMixin, generics.RetrieveAPIView):
**Example Request** **Example Request**
GET /api/v0/learners/{username}/?course_id={course_id} GET /api/v1/learners/{username}/?course_id={course_id}
**Response Values** **Response Values**
...@@ -126,7 +130,7 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, PaginatedHeadersMixin, C ...@@ -126,7 +130,7 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, PaginatedHeadersMixin, C
**Example Request** **Example Request**
GET /api/v0/learners/?course_id={course_id} GET /api/v1/learners/?course_id={course_id}
**Response Values** **Response Values**
...@@ -305,7 +309,7 @@ class EngagementTimelineView(CourseViewMixin, generics.ListAPIView): ...@@ -305,7 +309,7 @@ class EngagementTimelineView(CourseViewMixin, generics.ListAPIView):
**Example Request** **Example Request**
GET /api/v0/engagement_timeline/{username}/?course_id={course_id} GET /api/v1/engagement_timeline/{username}/?course_id={course_id}
**Response Values** **Response Values**
...@@ -362,7 +366,7 @@ class CourseLearnerMetadata(CourseViewMixin, generics.RetrieveAPIView): ...@@ -362,7 +366,7 @@ class CourseLearnerMetadata(CourseViewMixin, generics.RetrieveAPIView):
**Example Request** **Example Request**
GET /api/v0/course_learner_metadata/{course_id}/ GET /api/v1/course_learner_metadata/{course_id}/
**Response Values** **Response Values**
......
from django.core.paginator import InvalidPage
from rest_framework.exceptions import NotFound
from rest_framework.pagination import PageNumberPagination
def _positive_int(integer_string, strict=False, cutoff=None):
"""
Cast a string to a strictly positive integer.
"""
ret = int(integer_string)
if ret < 0 or (ret == 0 and strict):
raise ValueError()
if cutoff:
return min(ret, cutoff)
return ret
class PostAsGetPaginationBase(PageNumberPagination):
page_size_query_param = 'page_size'
# Override in subclass
page_size = None
max_page_size = None
# pylint: disable=attribute-defined-outside-init
def paginate_queryset(self, queryset, request, view=None):
"""
Paginate a queryset if required, either returning a
page object, or `None` if pagination is not configured for this view.
"""
if request.method == 'GET':
return super(PostAsGetPaginationBase, self).paginate_queryset(
queryset,
request,
view=view
)
page_size = self.get_page_size(request)
if not page_size:
return None
paginator = self.django_paginator_class(queryset, page_size)
page_number = request.data.get(self.page_query_param, 1)
if page_number in self.last_page_strings:
page_number = paginator.num_pages
try:
self.page = paginator.page(page_number)
except InvalidPage as exc:
msg = self.invalid_page_message.format(
page_number=page_number, message=exc.message
)
raise NotFound(msg)
if paginator.num_pages > 1 and self.template is not None:
# The browsable API should display pagination controls.
self.display_page_controls = True
self.request = request
return list(self.page)
def get_page_size(self, request):
if request.method == 'GET':
if self._is_all_in_params(request.query_params):
return None
return super(PostAsGetPaginationBase, self).get_page_size(request)
if self._is_all_in_params(request.data):
return None
if self.page_size_query_param and self.page_size_query_param in request.data:
try:
return _positive_int(
request.data.get(self.page_size_query_param),
strict=True,
cutoff=self.max_page_size
)
except (KeyError, ValueError):
pass
return self.page_size
@staticmethod
def _is_all_in_params(params):
param = params.get('all')
return param and param.lower() == 'true'
...@@ -8,13 +8,13 @@ from itertools import groupby ...@@ -8,13 +8,13 @@ from itertools import groupby
from django.db import OperationalError from django.db import OperationalError
from rest_framework import generics from rest_framework import generics
from analytics_data_api.v0.models import ( from analytics_data_api.v1.models import (
GradeDistribution, GradeDistribution,
ProblemResponseAnswerDistribution, ProblemResponseAnswerDistribution,
ProblemFirstLastResponseAnswerDistribution, ProblemFirstLastResponseAnswerDistribution,
SequentialOpenDistribution, SequentialOpenDistribution,
) )
from analytics_data_api.v0.serializers import ( from analytics_data_api.v1.serializers import (
ConsolidatedAnswerDistributionSerializer, ConsolidatedAnswerDistributionSerializer,
ConsolidatedFirstLastAnswerDistributionSerializer, ConsolidatedFirstLastAnswerDistributionSerializer,
GradeDistributionSerializer, GradeDistributionSerializer,
...@@ -22,7 +22,7 @@ from analytics_data_api.v0.serializers import ( ...@@ -22,7 +22,7 @@ from analytics_data_api.v0.serializers import (
) )
from analytics_data_api.utils import matching_tuple from analytics_data_api.utils import matching_tuple
from analytics_data_api.v0.views.utils import raise_404_if_none from analytics_data_api.v1.views.utils import raise_404_if_none
class ProblemResponseAnswerDistributionView(generics.ListAPIView): class ProblemResponseAnswerDistributionView(generics.ListAPIView):
...@@ -31,7 +31,7 @@ class ProblemResponseAnswerDistributionView(generics.ListAPIView): ...@@ -31,7 +31,7 @@ class ProblemResponseAnswerDistributionView(generics.ListAPIView):
**Example request** **Example request**
GET /api/v0/problems/{problem_id}/answer_distribution GET /api/v1/problems/{problem_id}/answer_distribution
**Response Values** **Response Values**
...@@ -126,7 +126,7 @@ class GradeDistributionView(generics.ListAPIView): ...@@ -126,7 +126,7 @@ class GradeDistributionView(generics.ListAPIView):
**Example request** **Example request**
GET /api/v0/problems/{problem_id}/grade_distribution GET /api/v1/problems/{problem_id}/grade_distribution
**Response Values** **Response Values**
...@@ -158,7 +158,7 @@ class SequentialOpenDistributionView(generics.ListAPIView): ...@@ -158,7 +158,7 @@ class SequentialOpenDistributionView(generics.ListAPIView):
**Example request** **Example request**
GET /api/v0/problems/{module_id}/sequential_open_distribution GET /api/v1/problems/{module_id}/sequential_open_distribution
**Response Values** **Response Values**
......
from rest_framework.generics import ListAPIView
from analytics_data_api.v1 import models, serializers
from analytics_data_api.v1.views.base import (
AggregatedListAPIViewMixin,
DynamicFieldsAPIViewMixin,
ModelListAPIViewMixin,
)
class ProgramsView(
AggregatedListAPIViewMixin,
ModelListAPIViewMixin,
DynamicFieldsAPIViewMixin,
ListAPIView,
):
"""
Returns metadata information for programs.
**Example Request**
GET /api/v1/course_programs/?program_ids={program_id},{program_id}
**Response Values**
Returns metadata for every program:
* program_id: The ID of the program for which data is returned.
* program_type: The type of the program
* program_title: The title of the program
* created: The date the metadata was computed.
**Parameters**
Results can be filtered to the program IDs specified or limited to the fields.
program_ids -- The comma-separated program identifiers for which metadata is requested.
Default is to return all programs.
fields -- The comma-separated fields to return in the response.
For example, 'program_id,created'. Default is to return all fields.
exclude -- The comma-separated fields to exclude in the response.
For example, 'program_id,created'. Default is to not exclude any fields.
"""
id_field = 'program_id'
# From ListAPIView
serializer_class = serializers.CourseProgramMetadataSerializer
# From ListAPIViewMixinBase
ids_param = id_field + 's'
# From ModelListAPIViewMixin
model_class = models.CourseProgramMetadata
model_id_field = id_field
# From AggregatedListAPIViewMixin
raw_item_id_field = id_field
aggregate_item_id_field = id_field
basic_aggregate_fields = frozenset(['program_title', 'program_type'])
calculated_aggregate_fields = {
'course_ids': (list, 'course_id', []),
'created': (max, 'created', None),
}
...@@ -4,7 +4,7 @@ from django.http import Http404 ...@@ -4,7 +4,7 @@ from django.http import Http404
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from analytics_data_api.v0.exceptions import CourseKeyMalformedError from analytics_data_api.v1.exceptions import CourseKeyMalformedError
def split_query_argument(argument): def split_query_argument(argument):
......
...@@ -4,10 +4,10 @@ API methods for module level data. ...@@ -4,10 +4,10 @@ API methods for module level data.
from rest_framework import generics from rest_framework import generics
from analytics_data_api.v0.models import VideoTimeline from analytics_data_api.v1.models import VideoTimeline
from analytics_data_api.v0.serializers import VideoTimelineSerializer from analytics_data_api.v1.serializers import VideoTimelineSerializer
from analytics_data_api.v0.views.utils import raise_404_if_none from analytics_data_api.v1.views.utils import raise_404_if_none
class VideoTimelineView(generics.ListAPIView): class VideoTimelineView(generics.ListAPIView):
...@@ -16,7 +16,7 @@ class VideoTimelineView(generics.ListAPIView): ...@@ -16,7 +16,7 @@ class VideoTimelineView(generics.ListAPIView):
**Example Request** **Example Request**
GET /api/v0/videos/{video_id}/timeline/ GET /api/v1/videos/{video_id}/timeline/
**Response Values** **Response Values**
......
...@@ -7,7 +7,7 @@ class AnalyticsApiRouter(object): ...@@ -7,7 +7,7 @@ class AnalyticsApiRouter(object):
return self._get_database(model._meta.app_label) return self._get_database(model._meta.app_label)
def _get_database(self, app_label): def _get_database(self, app_label):
if app_label == 'v0': if app_label == 'v1':
return getattr(settings, 'ANALYTICS_DATABASE', 'default') return getattr(settings, 'ANALYTICS_DATABASE', 'default')
return None return None
......
...@@ -58,7 +58,7 @@ ELASTICSEARCH_LEARNERS_UPDATE_INDEX = environ.get('ELASTICSEARCH_LEARNERS_UPDATE ...@@ -58,7 +58,7 @@ ELASTICSEARCH_LEARNERS_UPDATE_INDEX = environ.get('ELASTICSEARCH_LEARNERS_UPDATE
ELASTICSEARCH_AWS_ACCESS_KEY_ID = None ELASTICSEARCH_AWS_ACCESS_KEY_ID = None
ELASTICSEARCH_AWS_SECRET_ACCESS_KEY = None ELASTICSEARCH_AWS_SECRET_ACCESS_KEY = None
# override the default elasticsearch connection class and useful for signing certificates # override the default elasticsearch connection class and useful for signing certificates
# e.g. 'analytics_data_api.v0.connections.BotoHttpConnection' # e.g. 'analytics_data_api.v1.connections.BotoHttpConnection'
ELASTICSEARCH_CONNECTION_CLASS = None ELASTICSEARCH_CONNECTION_CLASS = None
# only needed with BotoHttpConnection, e.g. 'us-east-1' # only needed with BotoHttpConnection, e.g. 'us-east-1'
ELASTICSEARCH_CONNECTION_DEFAULT_REGION = None ELASTICSEARCH_CONNECTION_DEFAULT_REGION = None
...@@ -163,13 +163,13 @@ MIDDLEWARE_CLASSES = ( ...@@ -163,13 +163,13 @@ MIDDLEWARE_CLASSES = (
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'analytics_data_api.v0.middleware.LearnerEngagementTimelineNotFoundErrorMiddleware', 'analytics_data_api.v1.middleware.LearnerEngagementTimelineNotFoundErrorMiddleware',
'analytics_data_api.v0.middleware.LearnerNotFoundErrorMiddleware', 'analytics_data_api.v1.middleware.LearnerNotFoundErrorMiddleware',
'analytics_data_api.v0.middleware.CourseNotSpecifiedErrorMiddleware', 'analytics_data_api.v1.middleware.CourseNotSpecifiedErrorMiddleware',
'analytics_data_api.v0.middleware.CourseKeyMalformedErrorMiddleware', 'analytics_data_api.v1.middleware.CourseKeyMalformedErrorMiddleware',
'analytics_data_api.v0.middleware.ParameterValueErrorMiddleware', 'analytics_data_api.v1.middleware.ParameterValueErrorMiddleware',
'analytics_data_api.v0.middleware.ReportFileNotFoundErrorMiddleware', 'analytics_data_api.v1.middleware.ReportFileNotFoundErrorMiddleware',
'analytics_data_api.v0.middleware.CannotCreateDownloadLinkErrorMiddleware', 'analytics_data_api.v1.middleware.CannotCreateDownloadLinkErrorMiddleware',
) )
########## END MIDDLEWARE CONFIGURATION ########## END MIDDLEWARE CONFIGURATION
...@@ -202,7 +202,7 @@ THIRD_PARTY_APPS = ( ...@@ -202,7 +202,7 @@ THIRD_PARTY_APPS = (
LOCAL_APPS = ( LOCAL_APPS = (
'analytics_data_api', 'analytics_data_api',
'analytics_data_api.v0', 'analytics_data_api.v1',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
...@@ -324,3 +324,15 @@ DATA_UPLOAD_MAX_NUMBER_FIELDS = None ...@@ -324,3 +324,15 @@ DATA_UPLOAD_MAX_NUMBER_FIELDS = None
DATE_FORMAT = '%Y-%m-%d' DATE_FORMAT = '%Y-%m-%d'
DATETIME_FORMAT = '%Y-%m-%dT%H%M%S' DATETIME_FORMAT = '%Y-%m-%dT%H%M%S'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'summaries': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'OPTIONS': {
'MAX_ENTRIES': 100000
},
},
}
...@@ -40,17 +40,6 @@ DATABASES = { ...@@ -40,17 +40,6 @@ DATABASES = {
} }
########## END DATABASE CONFIGURATION ########## END DATABASE CONFIGURATION
########## CACHE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
########## END CACHE CONFIGURATION
########## ANALYTICS DATA API CONFIGURATION ########## ANALYTICS DATA API CONFIGURATION
ANALYTICS_DATABASE = 'analytics' ANALYTICS_DATABASE = 'analytics'
......
...@@ -40,3 +40,7 @@ FTP_STORAGE_LOCATION = 'ftp://localhost:80/path' ...@@ -40,3 +40,7 @@ FTP_STORAGE_LOCATION = 'ftp://localhost:80/path'
# Default settings for report download endpoint # Default settings for report download endpoint
COURSE_REPORT_FILE_LOCATION_TEMPLATE = '/{course_id}_{report_name}.csv' COURSE_REPORT_FILE_LOCATION_TEMPLATE = '/{course_id}_{report_name}.csv'
COURSE_REPORT_DOWNLOAD_EXPIRY_TIME = 120 COURSE_REPORT_DOWNLOAD_EXPIRY_TIME = 120
CACHES['summaries'] = {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
...@@ -13,7 +13,7 @@ from django.test.utils import override_settings ...@@ -13,7 +13,7 @@ from django.test.utils import override_settings
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
from analytics_data_api.v0.models import CourseEnrollmentDaily, CourseEnrollmentByBirthYear from analytics_data_api.v1.models import CourseEnrollmentDaily, CourseEnrollmentByBirthYear
from analyticsdataserver.clients import CourseBlocksApiClient from analyticsdataserver.clients import CourseBlocksApiClient
from analyticsdataserver.router import AnalyticsApiRouter from analyticsdataserver.router import AnalyticsApiRouter
from analyticsdataserver.utils import temp_log_level from analyticsdataserver.utils import temp_log_level
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment