edx-enterprise-data 8.7.0__py3-none-any.whl → 8.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.0.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.0.dist-info}/RECORD +20 -17
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/constants.py +9 -3
- enterprise_data/admin_analytics/utils.py +46 -10
- enterprise_data/api/v1/paginators.py +1 -1
- enterprise_data/api/v1/serializers.py +39 -30
- enterprise_data/api/v1/urls.py +12 -6
- enterprise_data/api/v1/views/analytics_enrollments.py +42 -53
- enterprise_data/api/v1/views/analytics_leaderboard.py +120 -0
- enterprise_data/api/v1/views/enterprise_completions.py +5 -5
- enterprise_data/renderers.py +14 -0
- enterprise_data/tests/admin_analytics/mock_analytics_data.py +501 -0
- enterprise_data/tests/admin_analytics/mock_enrollments.py +4 -4
- enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +23 -22
- enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +163 -0
- enterprise_data/tests/admin_analytics/test_enterprise_completions.py +1 -1
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.0.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.0.dist-info}/top_level.txt +0 -0
@@ -1,20 +1,20 @@
|
|
1
|
-
enterprise_data/__init__.py,sha256=
|
1
|
+
enterprise_data/__init__.py,sha256=RrNn-g6Sr1lxlOQ0yApeFQ3jHDqC66CWjYalcIXFhF0,123
|
2
2
|
enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
|
3
3
|
enterprise_data/clients.py,sha256=GvQupy5TVYfO_IKC3yzXSAgNP54r-PtIjidM5ws9Iks,3947
|
4
4
|
enterprise_data/constants.py,sha256=uCKjfpdlMYFZJsAj3n9RMw4Cmg5_6s3NuwocO-fch3s,238
|
5
5
|
enterprise_data/filters.py,sha256=D2EiK12MMpBoz6eOUmTpoJEhj_sH7bA93NRRAdvkDVo,6163
|
6
6
|
enterprise_data/models.py,sha256=khGcOh7NWP8KGu84t78Y2zAu3knREeXA_prApmU2NX8,24428
|
7
7
|
enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE,269
|
8
|
-
enterprise_data/renderers.py,sha256=
|
8
|
+
enterprise_data/renderers.py,sha256=9gIzavWspZTk4vDfVKXJtdn0tSZ2xNgkF-Akf7AWIDM,2389
|
9
9
|
enterprise_data/signals.py,sha256=8eqNPnlvmfsKf19lGWv5xTIuBgQIqR8EZSp9UYzC8Rc,1024
|
10
10
|
enterprise_data/urls.py,sha256=bqtKF5OEWEwrNmHG3os-pZNuNsmjlhxEqp7yM4TbPf4,243
|
11
11
|
enterprise_data/utils.py,sha256=kNO4nW_GBpBiIBlVUkCb4Xo0k1oVshT8nDOBP5eWoV8,2643
|
12
12
|
enterprise_data/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
enterprise_data/admin_analytics/completions_utils.py,sha256=kGmLy7x6aD0coNYgzLa5XzJypLkGTT5clDHLSH_QFDE,9442
|
14
|
-
enterprise_data/admin_analytics/constants.py,sha256=
|
14
|
+
enterprise_data/admin_analytics/constants.py,sha256=aHDgTHdsjbKNpgtNLDsl4giqhhrRkCGi72ysGIEk0Ao,817
|
15
15
|
enterprise_data/admin_analytics/data_loaders.py,sha256=x1XNYdtJV1G9cv0SeBZqYitRV8-GlJXtEZ2cc2OJU7M,5415
|
16
16
|
enterprise_data/admin_analytics/database.py,sha256=mNS_9xE5h6O7oMMzr6kr6LDTTSNvKzo8vaM-YG8tOd8,1312
|
17
|
-
enterprise_data/admin_analytics/utils.py,sha256=
|
17
|
+
enterprise_data/admin_analytics/utils.py,sha256=Bq5Vur5_DQbVoVPs7tUBPgW1xrPbhZYUffJaqe8zBmE,11948
|
18
18
|
enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
19
|
enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
|
20
20
|
enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
@@ -22,14 +22,15 @@ enterprise_data/api/v0/serializers.py,sha256=dngZTk6DhRxApchQKCMp1B_c8aVnQtH0NCq
|
|
22
22
|
enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum0,699
|
23
23
|
enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
|
24
24
|
enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
25
|
-
enterprise_data/api/v1/paginators.py,sha256=
|
26
|
-
enterprise_data/api/v1/serializers.py,sha256=
|
27
|
-
enterprise_data/api/v1/urls.py,sha256=
|
25
|
+
enterprise_data/api/v1/paginators.py,sha256=f0xsilLaU94jSBltJk46tR1rLEIt7YrqSzMAAVtPXjA,3592
|
26
|
+
enterprise_data/api/v1/serializers.py,sha256=9F2LGa8IKvglgeYNHw3Q0eEZUWknwHZMNZOdpDviEo4,12327
|
27
|
+
enterprise_data/api/v1/urls.py,sha256=xFsBf3TTsdblFAiHq1Bj3h82Ye1PS3cgqLC0pIso2js,3504
|
28
28
|
enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
|
-
enterprise_data/api/v1/views/analytics_enrollments.py,sha256=
|
29
|
+
enterprise_data/api/v1/views/analytics_enrollments.py,sha256=IRViGbPj8PjWRxnJ8ScfDVt8kfj03LG0veErDGLrKlY,15842
|
30
|
+
enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=oSww7866o1DdPIQU7J2sfXpbqqAgkqfC30cr7OI5C7w,5257
|
30
31
|
enterprise_data/api/v1/views/base.py,sha256=FTAxlz5EzvAY657wzVgzhJPFSCHHzct7IDcvm71Smt8,866
|
31
32
|
enterprise_data/api/v1/views/enterprise_admin.py,sha256=7f1RHlXxmH8oLr0WLxdGPNsxdhjubwyqNIefb7PMH68,9149
|
32
|
-
enterprise_data/api/v1/views/enterprise_completions.py,sha256=
|
33
|
+
enterprise_data/api/v1/views/enterprise_completions.py,sha256=Tpj0Q3tdwFGtRPGNdmcr2_mKxxA90HvROEfPw79l_Gc,7433
|
33
34
|
enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
|
34
35
|
enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
|
35
36
|
enterprise_data/fixtures/enterprise_enrollment.json,sha256=6onPXXR29pMdTdbl_mn81sDi3Re5jkLUZz2TPMB_1IY,5786
|
@@ -103,10 +104,12 @@ enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bz
|
|
103
104
|
enterprise_data/tests/test_utils.py,sha256=vbmYM7DMN-lHS2p4yaa0Yd6uSGXd2qoZRDE9X3J4Sec,18385
|
104
105
|
enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
|
105
106
|
enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
106
|
-
enterprise_data/tests/admin_analytics/
|
107
|
-
enterprise_data/tests/admin_analytics/
|
107
|
+
enterprise_data/tests/admin_analytics/mock_analytics_data.py,sha256=WO4JtnNaiMtnu-Bj59ejad-eoYUBeA90Kz26UqfS3yE,17695
|
108
|
+
enterprise_data/tests/admin_analytics/mock_enrollments.py,sha256=LfuMo9Kn-OQD4z42G3BRuM5MXUUXXlaAMhTqfJf46XE,7266
|
109
|
+
enterprise_data/tests/admin_analytics/test_analytics_enrollments.py,sha256=UdKRkP6BNbsSo-gm0YCoddT-ReUMI1x9E6HNLSHT7pY,15177
|
110
|
+
enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py,sha256=VSEyDAHfWBJvqmx9yzd4NnPAqK3TqaKrMBWswMAdzfU,6206
|
108
111
|
enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=o3denJ4aUS1pI5Crksl4C6m-NtCBm8ynoHBnLkf-v2U,4641
|
109
|
-
enterprise_data/tests/admin_analytics/test_enterprise_completions.py,sha256=
|
112
|
+
enterprise_data/tests/admin_analytics/test_enterprise_completions.py,sha256=afkHQFy4bvqZ0pq5Drl1t2nv8zxbgca2jzOQbihlPG0,7359
|
110
113
|
enterprise_data/tests/admin_analytics/test_utils.py,sha256=y33HXy6BDOoftdcz3qYlOYhgx7JSXDki-OLzBdTpiwA,11449
|
111
114
|
enterprise_data/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
112
115
|
enterprise_data/tests/api/v0/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -156,8 +159,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
|
|
156
159
|
enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
|
157
160
|
enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
|
158
161
|
enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
|
159
|
-
edx_enterprise_data-8.
|
160
|
-
edx_enterprise_data-8.
|
161
|
-
edx_enterprise_data-8.
|
162
|
-
edx_enterprise_data-8.
|
163
|
-
edx_enterprise_data-8.
|
162
|
+
edx_enterprise_data-8.8.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
163
|
+
edx_enterprise_data-8.8.0.dist-info/METADATA,sha256=8aDZ6UZXhRF0ZL2ZO0CULCGKUvQYW8dTR-bW1TYM5P0,1569
|
164
|
+
edx_enterprise_data-8.8.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
165
|
+
edx_enterprise_data-8.8.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
|
166
|
+
edx_enterprise_data-8.8.0.dist-info/RECORD,,
|
enterprise_data/__init__.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from enum import Enum
|
4
4
|
|
5
5
|
|
6
|
-
class
|
6
|
+
class Granularity(Enum):
|
7
7
|
"""Granularity choices"""
|
8
8
|
DAILY = 'Daily'
|
9
9
|
WEEKLY = 'Weekly'
|
@@ -11,7 +11,7 @@ class GRANULARITY(Enum):
|
|
11
11
|
QUARTERLY = 'Quarterly'
|
12
12
|
|
13
13
|
|
14
|
-
class
|
14
|
+
class Calculation(Enum):
|
15
15
|
"""Calculation choices"""
|
16
16
|
TOTAL = 'Total'
|
17
17
|
RUNNING_TOTAL = 'Running Total'
|
@@ -19,9 +19,15 @@ class CALCULATION(Enum):
|
|
19
19
|
MOVING_AVERAGE_7_PERIOD = 'Moving Average (7 Period)'
|
20
20
|
|
21
21
|
|
22
|
-
class
|
22
|
+
class EnrollmentChart(Enum):
|
23
23
|
"""CSV choices"""
|
24
24
|
ENROLLMENTS_OVER_TIME = 'enrollments_over_time'
|
25
25
|
TOP_COURSES_BY_ENROLLMENTS = 'top_courses_by_enrollments'
|
26
26
|
TOP_SUBJECTS_BY_ENROLLMENTS = 'top_subjects_by_enrollments'
|
27
27
|
INDIVIDUAL_ENROLLMENTS = 'individual_enrollments'
|
28
|
+
|
29
|
+
|
30
|
+
class ResponseType(Enum):
|
31
|
+
"""Response type choices"""
|
32
|
+
JSON = 'json'
|
33
|
+
CSV = 'csv'
|
@@ -1,13 +1,18 @@
|
|
1
1
|
"""
|
2
2
|
Utility functions for fetching data from the database.
|
3
3
|
"""
|
4
|
-
from datetime import datetime
|
4
|
+
from datetime import datetime, timedelta
|
5
5
|
from enum import Enum
|
6
6
|
|
7
7
|
from edx_django_utils.cache import TieredCache, get_cache_key
|
8
8
|
|
9
|
-
from enterprise_data.admin_analytics.constants import
|
10
|
-
from enterprise_data.admin_analytics.data_loaders import
|
9
|
+
from enterprise_data.admin_analytics.constants import Calculation, Granularity
|
10
|
+
from enterprise_data.admin_analytics.data_loaders import (
|
11
|
+
fetch_engagement_data,
|
12
|
+
fetch_enrollment_data,
|
13
|
+
fetch_max_enrollment_datetime,
|
14
|
+
fetch_skills_data,
|
15
|
+
)
|
11
16
|
from enterprise_data.utils import date_filter, primary_subject_truncate
|
12
17
|
|
13
18
|
|
@@ -23,14 +28,45 @@ class ChartType(Enum):
|
|
23
28
|
TOP_SUBJECTS_BY_COMPLETIONS = 'top_subjects_by_completions'
|
24
29
|
|
25
30
|
|
31
|
+
def fetch_enrollments_cache_expiry_timestamp():
|
32
|
+
"""Calculate cache expiry timestamp"""
|
33
|
+
# TODO: Implement correct cache expiry logic for `enrollments` data.
|
34
|
+
# Current cache expiry logic is based on `enterprise_learner_enrollment` table,
|
35
|
+
# Which has nothing to do with the `enrollments` data. Instead cache expiry should
|
36
|
+
# be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
|
37
|
+
# `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
|
38
|
+
# column in the table for this purpose and then use that column for cache expiry.
|
39
|
+
last_updated_at = fetch_max_enrollment_datetime()
|
40
|
+
cache_expiry = (
|
41
|
+
last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
42
|
+
)
|
43
|
+
return cache_expiry
|
44
|
+
|
45
|
+
|
46
|
+
def fetch_engagements_cache_expiry_timestamp():
|
47
|
+
"""Calculate cache expiry timestamp"""
|
48
|
+
# TODO: Implement correct cache expiry logic for `engagements` data.
|
49
|
+
# Current cache expiry logic is based on `enterprise_learner_enrollment` table,
|
50
|
+
# Which has nothing to do with the `engagements` data. Instead cache expiry should
|
51
|
+
# be based on `fact_enrollment_engagement_day_admin_dash` table. Currently we have
|
52
|
+
# no timestamp in `fact_enrollment_engagement_day_admin_dash` table that can be used
|
53
|
+
# for cache expiry. Add a new column in the table for this purpose and then use that
|
54
|
+
# column for cache expiry.
|
55
|
+
last_updated_at = fetch_max_enrollment_datetime()
|
56
|
+
cache_expiry = (
|
57
|
+
last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
58
|
+
)
|
59
|
+
return cache_expiry
|
60
|
+
|
61
|
+
|
26
62
|
def granularity_aggregation(level, group, date, data_frame, aggregation_type="count"):
|
27
63
|
"""Aggregate data based on granularity"""
|
28
64
|
df = data_frame
|
29
65
|
|
30
66
|
period_mapping = {
|
31
|
-
|
32
|
-
|
33
|
-
|
67
|
+
Granularity.WEEKLY.value: "W",
|
68
|
+
Granularity.MONTHLY.value: "M",
|
69
|
+
Granularity.QUARTERLY.value: "Q"
|
34
70
|
}
|
35
71
|
|
36
72
|
if level in period_mapping:
|
@@ -52,15 +88,15 @@ def calculation_aggregation(calc, data_frame, aggregation_type="count"):
|
|
52
88
|
df = data_frame
|
53
89
|
|
54
90
|
window_mapping = {
|
55
|
-
|
56
|
-
|
91
|
+
Calculation.MOVING_AVERAGE_3_PERIOD.value: 3,
|
92
|
+
Calculation.MOVING_AVERAGE_7_PERIOD.value: 7,
|
57
93
|
}
|
58
94
|
|
59
95
|
aggregation_column = "count" if aggregation_type == "count" else "sum"
|
60
96
|
|
61
|
-
if calc ==
|
97
|
+
if calc == Calculation.RUNNING_TOTAL.value:
|
62
98
|
df[aggregation_column] = df.groupby("enroll_type")[aggregation_column].cumsum()
|
63
|
-
elif calc in [
|
99
|
+
elif calc in [Calculation.MOVING_AVERAGE_3_PERIOD.value, Calculation.MOVING_AVERAGE_7_PERIOD.value]:
|
64
100
|
df[aggregation_column] = (
|
65
101
|
df.groupby("enroll_type")[aggregation_column]
|
66
102
|
.rolling(window_mapping[calc])
|
@@ -72,7 +72,7 @@ class AdvanceAnalyticsPagination(PageNumberPagination):
|
|
72
72
|
max_page_size (int): The maximum allowed page size.
|
73
73
|
"""
|
74
74
|
page_size_query_param = "page_size"
|
75
|
-
page_size =
|
75
|
+
page_size = 50
|
76
76
|
max_page_size = 100
|
77
77
|
|
78
78
|
def paginate_queryset(self, queryset, request, view=None):
|
@@ -5,7 +5,7 @@ from uuid import UUID
|
|
5
5
|
|
6
6
|
from rest_framework import serializers
|
7
7
|
|
8
|
-
from enterprise_data.admin_analytics.constants import
|
8
|
+
from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
|
9
9
|
from enterprise_data.models import (
|
10
10
|
EnterpriseAdminLearnerProgress,
|
11
11
|
EnterpriseAdminSummarizeInsights,
|
@@ -237,23 +237,28 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
|
|
237
237
|
|
238
238
|
class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
239
239
|
"""Serializer for validating query params"""
|
240
|
+
RESPONSE_TYPES = [
|
241
|
+
ResponseType.JSON.value,
|
242
|
+
ResponseType.CSV.value
|
243
|
+
]
|
240
244
|
GRANULARITY_CHOICES = [
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
+
Granularity.DAILY.value,
|
246
|
+
Granularity.WEEKLY.value,
|
247
|
+
Granularity.MONTHLY.value,
|
248
|
+
Granularity.QUARTERLY.value
|
245
249
|
]
|
246
250
|
CALCULATION_CHOICES = [
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
+
Calculation.TOTAL.value,
|
252
|
+
Calculation.RUNNING_TOTAL.value,
|
253
|
+
Calculation.MOVING_AVERAGE_3_PERIOD.value,
|
254
|
+
Calculation.MOVING_AVERAGE_7_PERIOD.value
|
251
255
|
]
|
252
256
|
|
253
257
|
start_date = serializers.DateField(required=False)
|
254
258
|
end_date = serializers.DateField(required=False)
|
255
259
|
granularity = serializers.CharField(required=False)
|
256
260
|
calculation = serializers.CharField(required=False)
|
261
|
+
response_type = serializers.CharField(required=False)
|
257
262
|
|
258
263
|
def validate(self, attrs):
|
259
264
|
"""
|
@@ -270,6 +275,17 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
|
|
270
275
|
|
271
276
|
return attrs
|
272
277
|
|
278
|
+
def validate_response_type(self, value):
|
279
|
+
"""
|
280
|
+
Validate the response_type value.
|
281
|
+
|
282
|
+
Raises:
|
283
|
+
serializers.ValidationError: If response_type is not one of the valid choices in `RESPONSE_TYPES`.
|
284
|
+
"""
|
285
|
+
if value not in self.RESPONSE_TYPES:
|
286
|
+
raise serializers.ValidationError(f"response_type must be one of {self.RESPONSE_TYPES}")
|
287
|
+
return value
|
288
|
+
|
273
289
|
def validate_granularity(self, value):
|
274
290
|
"""
|
275
291
|
Validate the granularity value.
|
@@ -293,32 +309,25 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
|
|
293
309
|
return value
|
294
310
|
|
295
311
|
|
296
|
-
class
|
297
|
-
|
298
|
-
|
299
|
-
|
312
|
+
class AdvanceAnalyticsEnrollmentStatsSerializer(
|
313
|
+
AdvanceAnalyticsQueryParamSerializer
|
314
|
+
): # pylint: disable=abstract-method
|
315
|
+
"""Serializer for validating Advance Analytics Enrollments Stats API"""
|
316
|
+
CHART_TYPES = [
|
317
|
+
EnrollmentChart.ENROLLMENTS_OVER_TIME.value,
|
318
|
+
EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value,
|
319
|
+
EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value
|
300
320
|
]
|
301
321
|
|
302
|
-
|
322
|
+
chart_type = serializers.CharField(required=False)
|
303
323
|
|
304
|
-
def
|
324
|
+
def validate_chart_type(self, value):
|
305
325
|
"""
|
306
|
-
Validate the
|
326
|
+
Validate the chart_type value.
|
307
327
|
|
308
328
|
Raises:
|
309
|
-
serializers.ValidationError: If
|
329
|
+
serializers.ValidationError: If chart_type is not one of the valid choices
|
310
330
|
"""
|
311
|
-
if value not in self.
|
312
|
-
raise serializers.ValidationError(f"
|
331
|
+
if value not in self.CHART_TYPES:
|
332
|
+
raise serializers.ValidationError(f"chart_type must be one of {self.CHART_TYPES}")
|
313
333
|
return value
|
314
|
-
|
315
|
-
|
316
|
-
class AdvanceAnalyticsEnrollmentStatsSerializer(
|
317
|
-
AdvanceAnalyticsEnrollmentSerializer
|
318
|
-
): # pylint: disable=abstract-method
|
319
|
-
"""Serializer for validating Advance Analytics Enrollments Stats API"""
|
320
|
-
CSV_TYPES = [
|
321
|
-
ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value,
|
322
|
-
ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value,
|
323
|
-
ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value
|
324
|
-
]
|
enterprise_data/api/v1/urls.py
CHANGED
@@ -15,6 +15,7 @@ from enterprise_data.api.v1.views.analytics_enrollments import (
|
|
15
15
|
AdvanceAnalyticsEnrollmentStatsView,
|
16
16
|
AdvanceAnalyticsIndividualEnrollmentsView,
|
17
17
|
)
|
18
|
+
from enterprise_data.api.v1.views.analytics_leaderboard import AdvanceAnalyticsLeaderboardView
|
18
19
|
from enterprise_data.constants import UUID4_REGEX
|
19
20
|
|
20
21
|
app_name = 'enterprise_data_api_v1'
|
@@ -53,32 +54,37 @@ urlpatterns = [
|
|
53
54
|
name='enterprise-admin-insights'
|
54
55
|
),
|
55
56
|
re_path(
|
56
|
-
fr'^admin/
|
57
|
+
fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})$',
|
57
58
|
enterprise_admin_views.EnterpriseAdminAnalyticsAggregatesView.as_view(),
|
58
59
|
name='enterprise-admin-analytics-aggregates'
|
59
60
|
),
|
60
61
|
re_path(
|
61
|
-
fr'^admin/
|
62
|
+
fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/leaderboard$',
|
63
|
+
AdvanceAnalyticsLeaderboardView.as_view(),
|
64
|
+
name='enterprise-admin-analytics-leaderboard'
|
65
|
+
),
|
66
|
+
re_path(
|
67
|
+
fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments/stats$',
|
62
68
|
AdvanceAnalyticsEnrollmentStatsView.as_view(),
|
63
69
|
name='enterprise-admin-analytics-enrollments-stats'
|
64
70
|
),
|
65
71
|
re_path(
|
66
|
-
fr'^admin/
|
72
|
+
fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments$',
|
67
73
|
AdvanceAnalyticsIndividualEnrollmentsView.as_view(),
|
68
74
|
name='enterprise-admin-analytics-enrollments'
|
69
75
|
),
|
70
76
|
re_path(
|
71
|
-
fr'^admin/
|
77
|
+
fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
|
72
78
|
enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),
|
73
79
|
name='enterprise-admin-analytics-skills'
|
74
80
|
),
|
75
81
|
re_path(
|
76
|
-
fr'^admin/
|
82
|
+
fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/completions/stats$',
|
77
83
|
enterprise_completions_views.EnterrpiseAdminCompletionsStatsView.as_view(),
|
78
84
|
name='enterprise-admin-analytics-completions-stats'
|
79
85
|
),
|
80
86
|
re_path(
|
81
|
-
fr'^admin/
|
87
|
+
fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/completions$',
|
82
88
|
enterprise_completions_views.EnterrpiseAdminCompletionsView.as_view(),
|
83
89
|
name='enterprise-admin-analytics-completions'
|
84
90
|
),
|
@@ -1,5 +1,5 @@
|
|
1
1
|
"""Advance Analytics for Enrollments"""
|
2
|
-
from datetime import datetime
|
2
|
+
from datetime import datetime
|
3
3
|
|
4
4
|
from edx_rbac.decorators import permission_required
|
5
5
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
@@ -8,37 +8,22 @@ from rest_framework.views import APIView
|
|
8
8
|
|
9
9
|
from django.http import HttpResponse, StreamingHttpResponse
|
10
10
|
|
11
|
-
from enterprise_data.admin_analytics.constants import
|
12
|
-
from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
|
11
|
+
from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
|
13
12
|
from enterprise_data.admin_analytics.utils import (
|
14
13
|
calculation_aggregation,
|
15
14
|
fetch_and_cache_enrollments_data,
|
15
|
+
fetch_enrollments_cache_expiry_timestamp,
|
16
16
|
granularity_aggregation,
|
17
17
|
)
|
18
18
|
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
19
19
|
from enterprise_data.api.v1.serializers import (
|
20
|
-
AdvanceAnalyticsEnrollmentSerializer,
|
21
20
|
AdvanceAnalyticsEnrollmentStatsSerializer,
|
21
|
+
AdvanceAnalyticsQueryParamSerializer,
|
22
22
|
)
|
23
23
|
from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
|
24
24
|
from enterprise_data.utils import date_filter
|
25
25
|
|
26
26
|
|
27
|
-
def fetch_enrollments_cache_expiry_timestamp():
|
28
|
-
"""Calculate cache expiry timestamp"""
|
29
|
-
# TODO: Implement correct cache expiry logic for `enrollments` data.
|
30
|
-
# Current cache expiry logic is based on `enterprise_learner_enrollment` table,
|
31
|
-
# Which has nothing to do with the `enrollments` data. Instead cache expiry should
|
32
|
-
# be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
|
33
|
-
# `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
|
34
|
-
# column in the table for this purpose and then use that column for cache expiry.
|
35
|
-
last_updated_at = fetch_max_enrollment_datetime()
|
36
|
-
cache_expiry = (
|
37
|
-
last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
38
|
-
)
|
39
|
-
return cache_expiry
|
40
|
-
|
41
|
-
|
42
27
|
class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
43
28
|
"""
|
44
29
|
API for getting the advance analytics individual enrollments data.
|
@@ -50,7 +35,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
50
35
|
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
51
36
|
def get(self, request, enterprise_uuid):
|
52
37
|
"""Get individual enrollments data"""
|
53
|
-
serializer =
|
38
|
+
serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
|
54
39
|
serializer.is_valid(raise_exception=True)
|
55
40
|
|
56
41
|
cache_expiry = fetch_enrollments_cache_expiry_timestamp()
|
@@ -59,7 +44,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
59
44
|
# get values from query params or use default values
|
60
45
|
start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
|
61
46
|
end_date = serializer.data.get('end_date', datetime.now())
|
62
|
-
|
47
|
+
response_type = request.query_params.get('response_type', ResponseType.JSON.value)
|
63
48
|
|
64
49
|
# filter enrollments by date
|
65
50
|
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
@@ -77,11 +62,12 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
77
62
|
enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
|
78
63
|
enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
|
79
64
|
|
80
|
-
if
|
65
|
+
if response_type == ResponseType.CSV.value:
|
66
|
+
filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
|
81
67
|
return StreamingHttpResponse(
|
82
68
|
IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(enrollments)),
|
83
69
|
content_type="text/csv",
|
84
|
-
headers={"Content-Disposition": 'attachment; filename="
|
70
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
85
71
|
)
|
86
72
|
|
87
73
|
paginator = self.pagination_class()
|
@@ -121,11 +107,12 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
121
107
|
# get values from query params or use default
|
122
108
|
start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
|
123
109
|
end_date = serializer.data.get('end_date', datetime.now())
|
124
|
-
granularity = serializer.data.get('granularity',
|
125
|
-
calculation = serializer.data.get('calculation',
|
126
|
-
|
110
|
+
granularity = serializer.data.get('granularity', Granularity.DAILY.value)
|
111
|
+
calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
|
112
|
+
response_type = serializer.data.get('response_type', ResponseType.JSON.value)
|
113
|
+
chart_type = serializer.data.get('chart_type')
|
127
114
|
|
128
|
-
if
|
115
|
+
if response_type == ResponseType.JSON.value:
|
129
116
|
data = {
|
130
117
|
"enrollments_over_time": self.construct_enrollments_over_time(
|
131
118
|
enrollments_df.copy(),
|
@@ -146,26 +133,28 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
146
133
|
),
|
147
134
|
}
|
148
135
|
return Response(data)
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
136
|
+
|
137
|
+
if response_type == ResponseType.CSV.value:
|
138
|
+
if chart_type == EnrollmentChart.ENROLLMENTS_OVER_TIME.value:
|
139
|
+
return self.construct_enrollments_over_time_csv(
|
140
|
+
enrollments_df.copy(),
|
141
|
+
start_date,
|
142
|
+
end_date,
|
143
|
+
granularity,
|
144
|
+
calculation,
|
145
|
+
)
|
146
|
+
elif chart_type == EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value:
|
147
|
+
return self.construct_top_courses_by_enrollments_csv(
|
148
|
+
enrollments_df.copy(),
|
149
|
+
start_date,
|
150
|
+
end_date,
|
151
|
+
)
|
152
|
+
elif chart_type == EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value:
|
153
|
+
return self.construct_top_subjects_by_enrollments_csv(
|
154
|
+
enrollments_df.copy(),
|
155
|
+
start_date,
|
156
|
+
end_date,
|
157
|
+
)
|
169
158
|
|
170
159
|
def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
|
171
160
|
"""
|
@@ -175,8 +164,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
175
164
|
enrollments_df {DataFrame} -- DataFrame of enrollments
|
176
165
|
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
177
166
|
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
178
|
-
granularity {str} -- Granularity of the data. One of
|
179
|
-
calculation {str} -- Calculation of the data. One of
|
167
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
168
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
180
169
|
"""
|
181
170
|
# filter enrollments by date
|
182
171
|
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
@@ -202,8 +191,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
202
191
|
enrollments_df {DataFrame} -- DataFrame of enrollments
|
203
192
|
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
204
193
|
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
205
|
-
granularity {str} -- Granularity of the data. One of
|
206
|
-
calculation {str} -- Calculation of the data. One of
|
194
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
195
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
207
196
|
"""
|
208
197
|
enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
|
209
198
|
|
@@ -218,8 +207,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
218
207
|
enrollments_df {DataFrame} -- DataFrame of enrollments
|
219
208
|
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
220
209
|
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
221
|
-
granularity {str} -- Granularity of the data. One of
|
222
|
-
calculation {str} -- Calculation of the data. One of
|
210
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
211
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
223
212
|
"""
|
224
213
|
enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
|
225
214
|
|