-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Added a new endpoint to get enterprise analytics aggregated data.
- Loading branch information
1 parent
b8637fd
commit 64048bf
Showing
35 changed files
with
747 additions
and
204 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
""" | ||
Constants for admin analytics. | ||
""" | ||
import mysql.connector | ||
from django.conf import settings | ||
|
||
DATABASE_CONNECTION_CONFIG = { | ||
'host': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['HOST'], | ||
'port': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['PORT'], | ||
'database': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['NAME'], | ||
'user': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['USER'], | ||
'password': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['PASSWORD'], | ||
} | ||
DATABASE_CONNECTOR = mysql.connector.connect |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
""" | ||
Utility functions for fetching data from the database. | ||
""" | ||
import numpy | ||
import pandas | ||
|
||
from django.http import Http404 | ||
|
||
from enterprise_data.admin_analytics.database import run_query | ||
|
||
|
||
def get_select_query(table: str, columns: list, enterprise_uuid: str) -> str: | ||
""" | ||
Generate a SELECT query for the given table and columns. | ||
Arguments: | ||
table (str): The table to query. | ||
columns (list): The columns to select. | ||
enterprise_uuid (str): The UUID of the enterprise customer. | ||
Returns: | ||
(str): The SELECT query. | ||
""" | ||
return f'SELECT {", ".join(columns)} FROM {table} WHERE enterprise_customer_uuid = "{enterprise_uuid}"' | ||
|
||
|
||
def fetch_enrollment_data(enterprise_uuid: str): | ||
""" | ||
Fetch enrollment data from the database for the given enterprise customer. | ||
Arguments: | ||
enterprise_uuid (str): The UUID of the enterprise customer. | ||
Returns: | ||
(pandas.DataFrame): The enrollment data. | ||
""" | ||
enterprise_uuid = enterprise_uuid.replace('-', '') | ||
|
||
columns = [ | ||
'enterprise_customer_name', | ||
'enterprise_customer_uuid', | ||
'lms_enrollment_id', | ||
'user_id', | ||
'email', | ||
'course_key', | ||
'courserun_key', | ||
'course_id', | ||
'course_subject', | ||
'course_title', | ||
'enterprise_enrollment_date', | ||
'lms_enrollment_mode', | ||
'enroll_type', | ||
'program_title', | ||
'date_certificate_awarded', | ||
'grade_percent', | ||
'cert_awarded', | ||
'date_certificate_created_raw', | ||
'passed_date_raw', | ||
'passed_date', | ||
'has_passed', | ||
] | ||
query = get_select_query( | ||
table='fact_enrollment_admin_dash', | ||
columns=columns, | ||
enterprise_uuid=enterprise_uuid, | ||
) | ||
|
||
results = run_query(query=query) | ||
if not results: | ||
raise Http404(f'No enrollment data found for enterprise {enterprise_uuid}') | ||
|
||
enrollments = pandas.DataFrame(numpy.array(results), columns=columns) | ||
|
||
# Convert date columns to datetime. | ||
enrollments['enterprise_enrollment_date'] = enrollments['enterprise_enrollment_date'].astype('datetime64[ns]') | ||
enrollments['date_certificate_awarded'] = enrollments['date_certificate_awarded'].astype('datetime64[ns]') | ||
enrollments['date_certificate_created_raw'] = enrollments['date_certificate_created_raw'].astype('datetime64[ns]') | ||
enrollments['passed_date_raw'] = enrollments['passed_date_raw'].astype('datetime64[ns]') | ||
enrollments['passed_date'] = enrollments['passed_date'].astype('datetime64[ns]') | ||
|
||
return enrollments | ||
|
||
|
||
def fetch_engagement_data(enterprise_uuid: str): | ||
""" | ||
Fetch engagement data from the database for the given enterprise customer. | ||
Arguments: | ||
enterprise_uuid (str): The UUID of the enterprise customer. | ||
Returns: | ||
(pandas.DataFrame): The engagement data. | ||
""" | ||
enterprise_uuid = enterprise_uuid.replace('-', '') | ||
|
||
columns = [ | ||
'user_id', | ||
'email', | ||
'enterprise_customer_uuid', | ||
'course_key', | ||
'enroll_type', | ||
'activity_date', | ||
'course_title', | ||
'course_subject', | ||
'is_engaged', | ||
'is_engaged_video', | ||
'is_engaged_forum', | ||
'is_engaged_problem', | ||
'is_active', | ||
'learning_time_seconds', | ||
] | ||
query = get_select_query( | ||
table='fact_enrollment_engagement_day_admin_dash', columns=columns, enterprise_uuid=enterprise_uuid | ||
) | ||
|
||
results = run_query(query=query) | ||
if not results: | ||
raise Http404(f'No engagement data found for enterprise {enterprise_uuid}') | ||
|
||
engagement = pandas.DataFrame(numpy.array(results), columns=columns) | ||
engagement['activity_date'] = engagement['activity_date'].astype('datetime64[ns]') | ||
|
||
return engagement | ||
|
||
|
||
def fetch_max_enrollment_datetime(): | ||
""" | ||
Fetch the latest created date from the enterprise_learner_enrollment table. | ||
""" | ||
query = "SELECT MAX(created) FROM enterprise_learner_enrollment" | ||
results = run_query(query) | ||
if not results: | ||
return None | ||
return pandas.to_datetime(results[0][0]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
""" | ||
Utility functions for interacting with the database. | ||
""" | ||
from contextlib import closing | ||
from logging import getLogger | ||
|
||
from enterprise_data.admin_analytics.constants import DATABASE_CONNECTION_CONFIG, DATABASE_CONNECTOR | ||
from enterprise_data.utils import timeit | ||
|
||
LOGGER = getLogger(__name__) | ||
|
||
|
||
@timeit | ||
def run_query(query): | ||
""" | ||
Run a query on the database and return the results. | ||
Arguments: | ||
query (str): The query to run. | ||
Returns: | ||
(list): The results of the query. | ||
""" | ||
try: | ||
with closing(DATABASE_CONNECTOR(**DATABASE_CONNECTION_CONFIG)) as connection: | ||
with closing(connection.cursor()) as cursor: | ||
cursor.execute(query) | ||
return cursor.fetchall() | ||
except Exception: | ||
LOGGER.exception(f'[run_query]: run_query failed for query "{query}".') | ||
raise |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
""" | ||
Utility functions for fetching data from the database. | ||
""" | ||
from datetime import datetime | ||
|
||
from edx_django_utils.cache import TieredCache, get_cache_key | ||
|
||
from enterprise_data.admin_analytics.data_loaders import fetch_engagement_data, fetch_enrollment_data | ||
|
||
|
||
def get_cache_timeout(cache_expiry): | ||
""" | ||
Helper method to calculate cache timeout in seconds. | ||
Arguments: | ||
cache_expiry (datetime): Datetime object denoting the cache expiry. | ||
Returns: | ||
(int): Cache timeout in seconds. | ||
""" | ||
now = datetime.now() | ||
cache_timeout = 0 | ||
if cache_expiry > now: | ||
# Calculate cache expiry in seconds from now. | ||
cache_timeout = (cache_expiry - now).seconds | ||
|
||
return cache_timeout | ||
|
||
|
||
def fetch_and_cache_enrollments_data(enterprise_id, cache_expiry): | ||
""" | ||
Helper method to fetch and cache enrollments data. | ||
Arguments: | ||
enterprise_id (str): UUID of the enterprise customer in string format. | ||
cache_expiry (datetime): Datetime object denoting the cache expiry. | ||
Returns: | ||
(pandas.DataFrame): The enrollments data. | ||
""" | ||
cache_key = get_cache_key( | ||
resource='enterprise-admin-analytics-aggregates-enrollments', | ||
enterprise_customer=enterprise_id, | ||
) | ||
cached_response = TieredCache.get_cached_response(cache_key) | ||
|
||
if cached_response.is_found: | ||
return cached_response.value | ||
else: | ||
enrollments = fetch_enrollment_data(enterprise_id) | ||
TieredCache.set_all_tiers( | ||
cache_key, enrollments, get_cache_timeout(cache_expiry) | ||
) | ||
return enrollments | ||
|
||
|
||
def fetch_and_cache_engagements_data(enterprise_id, cache_expiry): | ||
""" | ||
Helper method to fetch and cache engagements data. | ||
Arguments: | ||
enterprise_id (str): UUID of the enterprise customer in string format. | ||
cache_expiry (datetime): Datetime object denoting the cache expiry. | ||
Returns: | ||
(pandas.DataFrame): The engagements data. | ||
""" | ||
cache_key = get_cache_key( | ||
resource='enterprise-admin-analytics-aggregates-engagements', | ||
enterprise_customer=enterprise_id, | ||
) | ||
cached_response = TieredCache.get_cached_response(cache_key) | ||
|
||
if cached_response.is_found: | ||
return cached_response.value | ||
else: | ||
engagements = fetch_engagement_data(enterprise_id) | ||
TieredCache.set_all_tiers( | ||
cache_key, engagements, get_cache_timeout(cache_expiry) | ||
) | ||
return engagements |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
""" | ||
Base views for enterprise data api v1. | ||
""" | ||
from edx_rbac.mixins import PermissionRequiredMixin | ||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication | ||
from edx_rest_framework_extensions.paginators import DefaultPagination | ||
|
||
from enterprise_data.constants import ANALYTICS_API_VERSION_1 | ||
|
||
|
||
class EnterpriseViewSetMixin(PermissionRequiredMixin): | ||
""" | ||
Base class for all Enterprise view sets. | ||
""" | ||
authentication_classes = (JwtAuthentication,) | ||
pagination_class = DefaultPagination | ||
permission_required = 'can_access_enterprise' | ||
API_VERSION = ANALYTICS_API_VERSION_1 | ||
|
||
def paginate_queryset(self, queryset): | ||
""" | ||
Allows no_page query param to skip pagination | ||
""" | ||
if 'no_page' in self.request.query_params: | ||
return None | ||
return super().paginate_queryset(queryset) |
Oops, something went wrong.