Commit dbde3921 by Clinton Blackburn

Added enrollment-location resource

Change-Id: I14c30ad846ae873296ebb6b27bea0e540e1f1e96
parent 3a917a41
...@@ -50,7 +50,7 @@ syncdb: ...@@ -50,7 +50,7 @@ syncdb:
$(foreach db_name,$(DATABASES),./manage.py syncdb --migrate --noinput --database=$(db_name);) $(foreach db_name,$(DATABASES),./manage.py syncdb --migrate --noinput --database=$(db_name);)
loaddata: syncdb loaddata: syncdb
python manage.py loaddata courses education_levels single_course_activity course_enrollment_birth_year course_enrollment_education course_enrollment_gender problem_response_answer_distribution course_enrollment_daily --database=analytics python manage.py loaddata courses education_levels single_course_activity course_enrollment_birth_year course_enrollment_education course_enrollment_gender problem_response_answer_distribution course_enrollment_daily countries course_enrollment_country --database=analytics
demo: clean requirements loaddata demo: clean requirements loaddata
python manage.py set_api_key analytics analytics python manage.py set_api_key analytics analytics
[
{
"model": "v0.CourseEnrollmentByCountry",
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"country": "US",
"date": "2014-06-01",
"count": 100
}
},
{
"model": "v0.CourseEnrollmentByCountry",
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"country": "IN",
"date": "2014-06-01",
"count": 240
}
},
{
"model": "v0.CourseEnrollmentByCountry",
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"country": "US",
"date": "2014-06-02",
"count": 106
}
},
{
"model": "v0.CourseEnrollmentByCountry",
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"country": "IN",
"date": "2014-06-02",
"count": 199
}
},
{
"model": "v0.CourseEnrollmentByCountry",
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"country": "US",
"date": "2014-06-03",
"count": 200
}
},
{
"model": "v0.CourseEnrollmentByCountry",
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"country": "IN",
"date": "2014-06-03",
"count": 300
}
},
{
"model": "v0.CourseEnrollmentByCountry",
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"country": "IS",
"date": "2014-06-03",
"count": 6
}
}
]
...@@ -4,3 +4,8 @@ from django.db import models ...@@ -4,3 +4,8 @@ from django.db import models
class CourseManager(models.Manager): class CourseManager(models.Manager):
def get_by_natural_key(self, course_id): def get_by_natural_key(self, course_id):
return self.get(course_id=course_id) return self.get(course_id=course_id)
class CountryManager(models.Manager):
def get_by_natural_key(self, code):
return self.get(code=code)
from django.db import models from django.db import models
from analytics_data_api.v0.managers import CourseManager from analytics_data_api.v0.managers import CourseManager, CountryManager
class Course(models.Model): class Course(models.Model):
...@@ -36,23 +36,22 @@ class BaseCourseEnrollment(models.Model): ...@@ -36,23 +36,22 @@ class BaseCourseEnrollment(models.Model):
class Meta(object): class Meta(object):
abstract = True abstract = True
unique_together = [('course', 'date',)] get_latest_by = 'date'
class CourseEnrollmentDaily(BaseCourseEnrollment): class CourseEnrollmentDaily(BaseCourseEnrollment):
class Meta(object): class Meta(BaseCourseEnrollment.Meta):
db_table = 'course_enrollment_daily' db_table = 'course_enrollment_daily'
ordering = ('course', '-date') ordering = ('-date', 'course')
unique_together = [('course', 'date',)] unique_together = [('course', 'date',)]
get_latest_by = 'date'
class CourseEnrollmentByBirthYear(BaseCourseEnrollment): class CourseEnrollmentByBirthYear(BaseCourseEnrollment):
birth_year = models.IntegerField(null=False) birth_year = models.IntegerField(null=False)
class Meta(object): class Meta(BaseCourseEnrollment.Meta):
db_table = 'course_enrollment_birth_year' db_table = 'course_enrollment_birth_year'
ordering = ('course', 'birth_year') ordering = ('-date', 'birth_year', 'course')
unique_together = [('course', 'date', 'birth_year')] unique_together = [('course', 'date', 'birth_year')]
...@@ -63,22 +62,25 @@ class EducationLevel(models.Model): ...@@ -63,22 +62,25 @@ class EducationLevel(models.Model):
class Meta(object): class Meta(object):
db_table = 'education_levels' db_table = 'education_levels'
def __unicode__(self):
return "{0} - {1}".format(self.short_name, self.name)
class CourseEnrollmentByEducation(BaseCourseEnrollment): class CourseEnrollmentByEducation(BaseCourseEnrollment):
education_level = models.ForeignKey(EducationLevel) education_level = models.ForeignKey(EducationLevel)
class Meta(object): class Meta(BaseCourseEnrollment.Meta):
db_table = 'course_enrollment_education_level' db_table = 'course_enrollment_education_level'
ordering = ('course', 'education_level') ordering = ('-date', 'education_level', 'course')
unique_together = [('course', 'date', 'education_level')] unique_together = [('course', 'date', 'education_level')]
class CourseEnrollmentByGender(BaseCourseEnrollment): class CourseEnrollmentByGender(BaseCourseEnrollment):
gender = models.CharField(max_length=255, null=False) gender = models.CharField(max_length=255, null=False)
class Meta(object): class Meta(BaseCourseEnrollment.Meta):
db_table = 'course_enrollment_gender' db_table = 'course_enrollment_gender'
ordering = ('course', 'gender') ordering = ('-date', 'gender', 'course')
unique_together = [('course', 'date', 'gender')] unique_together = [('course', 'date', 'gender')]
...@@ -98,3 +100,25 @@ class ProblemResponseAnswerDistribution(models.Model): ...@@ -98,3 +100,25 @@ class ProblemResponseAnswerDistribution(models.Model):
answer_value_numeric = models.FloatField(db_column='answer_value_numeric', null=True) answer_value_numeric = models.FloatField(db_column='answer_value_numeric', null=True)
variant = models.IntegerField(db_column='variant', null=True) variant = models.IntegerField(db_column='variant', null=True)
created = models.DateTimeField(auto_now_add=True, db_column='created') created = models.DateTimeField(auto_now_add=True, db_column='created')
class Country(models.Model):
code = models.CharField(max_length=2, primary_key=True)
name = models.CharField(max_length=255, unique=True, null=False)
objects = CountryManager() # pylint: disable=no-value-for-parameter
class Meta(object):
db_table = 'countries'
def __unicode__(self):
return "{0} - {1}".format(self.code, self.name)
class CourseEnrollmentByCountry(BaseCourseEnrollment):
country = models.ForeignKey(Country, null=False, db_column='country_code')
class Meta(BaseCourseEnrollment.Meta):
db_table = 'course_enrollment_location'
ordering = ('-date', 'country', 'course')
unique_together = [('course', 'date', 'country')]
from django.conf import settings
from rest_framework import serializers from rest_framework import serializers
from analytics_data_api.v0.models import CourseActivityByWeek, ProblemResponseAnswerDistribution, CourseEnrollmentDaily from analytics_data_api.v0.models import CourseActivityByWeek, ProblemResponseAnswerDistribution, \
CourseEnrollmentDaily, CourseEnrollmentByCountry, Country
class CourseIdMixin(object): class CourseIdMixin(object):
...@@ -7,6 +9,10 @@ class CourseIdMixin(object): ...@@ -7,6 +9,10 @@ class CourseIdMixin(object):
return obj.course.course_id return obj.course.course_id
class RequiredSerializerMethodField(serializers.SerializerMethodField):
required = True
class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin): class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin):
""" """
Representation of CourseActivityByWeek that excludes the id field. Representation of CourseActivityByWeek that excludes the id field.
...@@ -14,7 +20,8 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin) ...@@ -14,7 +20,8 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin)
This table is managed by the data pipeline, and records can be removed and added at any time. The id for a This table is managed by the data pipeline, and records can be removed and added at any time. The id for a
particular record is likely to change unexpectedly so we avoid exposing it. particular record is likely to change unexpectedly so we avoid exposing it.
""" """
course_id = serializers.SerializerMethodField('get_course_id')
course_id = RequiredSerializerMethodField('get_course_id')
class Meta(object): class Meta(object):
model = CourseActivityByWeek model = CourseActivityByWeek
...@@ -50,8 +57,25 @@ class CourseEnrollmentDailySerializer(serializers.ModelSerializer, CourseIdMixin ...@@ -50,8 +57,25 @@ class CourseEnrollmentDailySerializer(serializers.ModelSerializer, CourseIdMixin
Representation of course enrollment for a single day and course. Representation of course enrollment for a single day and course.
""" """
course_id = serializers.SerializerMethodField('get_course_id') course_id = RequiredSerializerMethodField('get_course_id')
class Meta(object): class Meta(object):
model = CourseEnrollmentDaily model = CourseEnrollmentDaily
fields = ('course_id', 'date', 'count') fields = ('course_id', 'date', 'count')
# pylint: disable=no-value-for-parameter
class CountrySerializer(serializers.ModelSerializer):
class Meta(object):
model = Country
fields = ('code', 'name')
class CourseEnrollmentByCountrySerializer(serializers.ModelSerializer, CourseIdMixin):
course_id = RequiredSerializerMethodField('get_course_id')
country = CountrySerializer()
date = serializers.DateField(format=settings.DATE_FORMAT)
class Meta(object):
model = CourseEnrollmentByCountry
fields = ('date', 'course_id', 'country', 'count')
from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase
from django_dynamic_fixture import G
from analytics_data_api.v0.models import Course, Country
class CourseManagerTests(TestCase):
def test_get_by_natural_key(self):
course_id = 'edX/DemoX/Demo_Course'
self.assertRaises(ObjectDoesNotExist, Course.objects.get_by_natural_key, course_id)
course = G(Course, course_id=course_id)
self.assertEqual(course, Course.objects.get_by_natural_key(course_id))
class CountryManagerTests(TestCase):
def test_get_by_natural_key(self):
code = 'US'
self.assertRaises(ObjectDoesNotExist, Country.objects.get_by_natural_key, code)
country = G(Country, code=code)
self.assertEqual(country, Country.objects.get_by_natural_key(code))
from django.test import TestCase
from django_dynamic_fixture import G
from analytics_data_api.v0.models import EducationLevel, Country
class EducationLevelTests(TestCase):
def test_unicode(self):
short_name = 'high_school'
name = 'High School'
education_level = G(EducationLevel, short_name=short_name, name=name)
self.assertEqual(unicode(education_level), "{0} - {1}".format(short_name, name))
class CountryTests(TestCase):
def test_unicode(self):
code = 'US'
name = 'United States of America'
country = G(Country, code=code, name=name)
self.assertEqual(unicode(country), "{0} - {1}".format(code, name))
...@@ -2,16 +2,17 @@ ...@@ -2,16 +2,17 @@
# change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added # change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added
# for subsequent versions if there are breaking changes introduced in those versions. # for subsequent versions if there are breaking changes introduced in those versions.
from datetime import datetime, date import datetime
import random import random
from django.core.exceptions import ObjectDoesNotExist from django.conf import settings
from django.test import TestCase
from django_dynamic_fixture import G from django_dynamic_fixture import G
import pytz import pytz
from analytics_data_api.v0.models import CourseEnrollmentByBirthYear, CourseEnrollmentByEducation, EducationLevel, \ from analytics_data_api.v0.models import CourseEnrollmentByBirthYear, CourseEnrollmentByEducation, EducationLevel, \
CourseEnrollmentByGender, CourseActivityByWeek, Course, ProblemResponseAnswerDistribution, CourseEnrollmentDaily CourseEnrollmentByGender, CourseActivityByWeek, Course, ProblemResponseAnswerDistribution, CourseEnrollmentDaily, \
Country, \
CourseEnrollmentByCountry
from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
...@@ -41,8 +42,8 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication): ...@@ -41,8 +42,8 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
def get_activity_record(**kwargs): def get_activity_record(**kwargs):
default = { default = {
'course_id': 'edX/DemoX/Demo_Course', 'course_id': 'edX/DemoX/Demo_Course',
'interval_start': datetime(2014, 5, 24, 0, 0, tzinfo=pytz.utc), 'interval_start': datetime.datetime(2014, 5, 24, 0, 0, tzinfo=pytz.utc),
'interval_end': datetime(2014, 6, 1, 0, 0, tzinfo=pytz.utc), 'interval_end': datetime.datetime(2014, 6, 1, 0, 0, tzinfo=pytz.utc),
'activity_type': 'any', 'activity_type': 'any',
'count': 300, 'count': 300,
} }
...@@ -171,15 +172,6 @@ class CourseEnrollmentByGenderViewTests(TestCaseWithAuthentication, CourseEnroll ...@@ -171,15 +172,6 @@ class CourseEnrollmentByGenderViewTests(TestCaseWithAuthentication, CourseEnroll
self.assertEquals(actual, expected) self.assertEquals(actual, expected)
class CourseManagerTests(TestCase):
def test_get_by_natural_key(self):
course_id = 'edX/DemoX/Demo_Course'
self.assertRaises(ObjectDoesNotExist, Course.objects.get_by_natural_key, course_id)
course = G(Course, course_id=course_id)
self.assertEqual(course, Course.objects.get_by_natural_key(course_id))
# pylint: disable=no-member,no-value-for-parameter # pylint: disable=no-member,no-value-for-parameter
class AnswerDistributionTests(TestCaseWithAuthentication): class AnswerDistributionTests(TestCaseWithAuthentication):
path = '/answer_distribution' path = '/answer_distribution'
...@@ -218,10 +210,60 @@ class CourseEnrollmentLatestViewTests(TestCaseWithAuthentication, CourseEnrollme ...@@ -218,10 +210,60 @@ class CourseEnrollmentLatestViewTests(TestCaseWithAuthentication, CourseEnrollme
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.course = G(Course) cls.course = G(Course)
cls.ce = G(CourseEnrollmentDaily, course=cls.course, date=date(2014, 1, 1), count=203) cls.ce = G(CourseEnrollmentDaily, course=cls.course, date=datetime.date(2014, 1, 1), count=203)
def test_get(self): def test_get(self):
response = self.authenticated_get('/api/v0/courses/%s%s' % (self.course.course_id, self.path,)) response = self.authenticated_get('/api/v0/courses/%s%s' % (self.course.course_id, self.path,))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected = {'course_id': self.ce.course.course_id, 'count': self.ce.count, 'date': self.ce.date} expected = {'course_id': self.ce.course.course_id, 'count': self.ce.count, 'date': self.ce.date}
self.assertDictEqual(response.data, expected) self.assertDictEqual(response.data, expected)
class CourseEnrollmentByLocationViewTests(TestCaseWithAuthentication, CourseEnrollmentViewTestCase):
path = '/enrollment/location/'
model = CourseEnrollmentByCountry
def get_expected_response(self, *args):
return [{'course_id': ce.course.course_id, 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT),
'country': {'code': ce.country.code, 'name': ce.country.name}} for ce in args]
def test_get(self):
course = G(Course)
date1 = datetime.date(2014, 1, 1)
date2 = datetime.date(2013, 1, 1)
ce1 = G(CourseEnrollmentByCountry, course=course, country=G(Country), count=455, date=date1)
ce2 = G(CourseEnrollmentByCountry, course=course, country=G(Country), count=356, date=date1)
# This should not be returned as the view should return only the latest data when no interval is supplied.
G(CourseEnrollmentByCountry, course=course, country=G(Country), count=12, date=date2)
response = self.authenticated_get('/api/v0/courses/%s%s' % (course.course_id, self.path,))
self.assertEquals(response.status_code, 200)
expected = self.get_expected_response(ce1, ce2)
self.assertListEqual(response.data, expected)
def test_get_with_intervals(self):
course = G(Course)
country1 = G(Country)
country2 = G(Country)
date = datetime.date(2014, 1, 1)
ce1 = G(CourseEnrollmentByCountry, course=course, country=country1, date=date)
ce2 = G(CourseEnrollmentByCountry, course=course, country=country2, date=date)
# If start date is after date of existing data, no data should be returned
response = self.authenticated_get('/api/v0/courses/%s%s?start_date=2014-02-01' % (course.course_id, self.path,))
self.assertEquals(response.status_code, 200)
self.assertListEqual([], response.data)
# If end date is before date of existing data, no data should be returned
response = self.authenticated_get('/api/v0/courses/%s%s?end_date=2013-02-01' % (course.course_id, self.path,))
self.assertEquals(response.status_code, 200)
self.assertListEqual([], response.data)
# If data falls in date range, data should be returned
response = self.authenticated_get(
'/api/v0/courses/%s%s?start_date=2013-02-01&end_date=2014-02-01' % (course.course_id, self.path,))
self.assertEquals(response.status_code, 200)
expected = self.get_expected_response(ce1, ce2)
self.assertListEqual(response.data, expected)
...@@ -2,16 +2,16 @@ import re ...@@ -2,16 +2,16 @@ import re
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from analytics_data_api.v0.views.courses import CourseActivityMostRecentWeekView, CourseEnrollmentByEducationView, \ from analytics_data_api.v0.views import courses as views
CourseEnrollmentByBirthYearView, CourseEnrollmentByGenderView, CourseEnrollmentLatestView
COURSE_URLS = [ COURSE_URLS = [
('recent_activity', CourseActivityMostRecentWeekView, 'recent_activity'), ('recent_activity', views.CourseActivityMostRecentWeekView, 'recent_activity'),
('enrollment', CourseEnrollmentLatestView, 'enrollment_latest'), ('enrollment', views.CourseEnrollmentLatestView, 'enrollment_latest'),
('enrollment/birth_year', CourseEnrollmentByBirthYearView, 'enrollment_by_birth_year'), ('enrollment/birth_year', views.CourseEnrollmentByBirthYearView, 'enrollment_by_birth_year'),
('enrollment/education', CourseEnrollmentByEducationView, 'enrollment_by_education'), ('enrollment/education', views.CourseEnrollmentByEducationView, 'enrollment_by_education'),
('enrollment/gender', CourseEnrollmentByGenderView, 'enrollment_by_gender'), ('enrollment/gender', views.CourseEnrollmentByGenderView, 'enrollment_by_gender'),
('enrollment/location', views.CourseEnrollmentByLocationView, 'enrollment_by_location'),
] ]
urlpatterns = [] urlpatterns = []
......
import datetime
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Max
from django.http import Http404 from django.http import Http404
from rest_framework import generics from rest_framework import generics
from rest_framework.generics import RetrieveAPIView from rest_framework.generics import RetrieveAPIView, get_object_or_404
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from analytics_data_api.v0.models import CourseActivityByWeek, CourseEnrollmentByBirthYear, \ from analytics_data_api.v0.models import CourseActivityByWeek, CourseEnrollmentByBirthYear, \
CourseEnrollmentByEducation, CourseEnrollmentByGender, CourseEnrollmentDaily CourseEnrollmentByEducation, CourseEnrollmentByGender, CourseEnrollmentByCountry, CourseEnrollmentDaily, Course
from analytics_data_api.v0.serializers import CourseActivityByWeekSerializer, CourseEnrollmentDailySerializer from analytics_data_api.v0.serializers import CourseActivityByWeekSerializer, CourseEnrollmentByCountrySerializer, \
CourseEnrollmentDailySerializer
class CourseActivityMostRecentWeekView(generics.RetrieveAPIView): class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
...@@ -133,3 +137,46 @@ class CourseEnrollmentLatestView(RetrieveAPIView): ...@@ -133,3 +137,46 @@ class CourseEnrollmentLatestView(RetrieveAPIView):
return CourseEnrollmentDaily.objects.filter(course__course_id=course_id).order_by('-date')[0] return CourseEnrollmentDaily.objects.filter(course__course_id=course_id).order_by('-date')[0]
except IndexError: except IndexError:
raise Http404 raise Http404
# pylint: disable=line-too-long
class CourseEnrollmentByLocationView(generics.ListAPIView):
"""
Course enrollment broken down by user location
Returns the enrollment of a course with users binned by their location. Location is calculated based on the user's
IP address. If no start or end dates are passed, the data for the latest date is returned.
Countries are denoted by their <a href="http://www.iso.org/iso/country_codes/country_codes" target="_blank">ISO 3166 country code</a>.
Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
serializer_class = CourseEnrollmentByCountrySerializer
def get_queryset(self):
course = get_object_or_404(Course, course_id=self.kwargs.get('course_id'))
queryset = CourseEnrollmentByCountry.objects.filter(course=course)
if 'start_date' in self.request.QUERY_PARAMS or 'end_date' in self.request.QUERY_PARAMS:
# Filter by start/end date
start_date = self.request.QUERY_PARAMS.get('start_date')
if start_date:
start_date = datetime.datetime.strptime(start_date, settings.DATE_FORMAT)
queryset = queryset.filter(date__gte=start_date)
end_date = self.request.QUERY_PARAMS.get('end_date')
if end_date:
end_date = datetime.datetime.strptime(end_date, settings.DATE_FORMAT)
queryset = queryset.filter(date__lt=end_date)
else:
# No date filter supplied, so only return data for the latest date
latest_date = queryset.aggregate(Max('date'))
if latest_date:
latest_date = latest_date['date__max']
queryset = queryset.filter(date=latest_date)
return queryset
from django.conf import settings from django.conf import settings
class DatabaseFromSettingRouter(object): class AnalyticsApiRouter(object):
def db_for_read(self, model, **hints): # pylint: disable=unused-argument def db_for_read(self, model, **hints): # pylint: disable=unused-argument
return self._get_database(model) return self._get_database(model)
...@@ -9,9 +9,6 @@ class DatabaseFromSettingRouter(object): ...@@ -9,9 +9,6 @@ class DatabaseFromSettingRouter(object):
if model._meta.app_label == 'v0': # pylint: disable=protected-access if model._meta.app_label == 'v0': # pylint: disable=protected-access
return getattr(settings, 'ANALYTICS_DATABASE', 'default') return getattr(settings, 'ANALYTICS_DATABASE', 'default')
if getattr(model, 'db_from_setting', None):
return getattr(settings, model.db_from_setting, 'default')
return None return None
def db_for_write(self, model, **hints): # pylint: disable=unused-argument def db_for_write(self, model, **hints): # pylint: disable=unused-argument
......
...@@ -257,8 +257,10 @@ REST_FRAMEWORK = { ...@@ -257,8 +257,10 @@ REST_FRAMEWORK = {
########## ANALYTICS DATA API CONFIGURATION ########## ANALYTICS DATA API CONFIGURATION
ANALYTICS_DATABASE = 'default' ANALYTICS_DATABASE = 'default'
DATABASE_ROUTERS = ['analyticsdataserver.router.DatabaseFromSettingRouter'] DATABASE_ROUTERS = ['analyticsdataserver.router.AnalyticsApiRouter']
ENABLE_ADMIN_SITE = False ENABLE_ADMIN_SITE = False
########## END ANALYTICS DATA API CONFIGURATION ########## END ANALYTICS DATA API CONFIGURATION
DATE_FORMAT = '%Y-%m-%d'
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