edx-enterprise-data 8.6.1__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.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/RECORD +23 -17
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/completions_utils.py +261 -0
- enterprise_data/admin_analytics/constants.py +9 -3
- enterprise_data/admin_analytics/utils.py +50 -11
- enterprise_data/api/v1/paginators.py +1 -1
- enterprise_data/api/v1/serializers.py +44 -30
- enterprise_data/api/v1/urls.py +21 -4
- 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_admin.py +9 -5
- enterprise_data/api/v1/views/enterprise_completions.py +177 -0
- 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 +23 -7
- 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 +202 -0
- enterprise_data/tests/api/v1/views/test_enterprise_admin.py +4 -0
- {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/top_level.txt +0 -0
@@ -1,19 +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
|
-
enterprise_data/admin_analytics/
|
13
|
+
enterprise_data/admin_analytics/completions_utils.py,sha256=kGmLy7x6aD0coNYgzLa5XzJypLkGTT5clDHLSH_QFDE,9442
|
14
|
+
enterprise_data/admin_analytics/constants.py,sha256=aHDgTHdsjbKNpgtNLDsl4giqhhrRkCGi72ysGIEk0Ao,817
|
14
15
|
enterprise_data/admin_analytics/data_loaders.py,sha256=x1XNYdtJV1G9cv0SeBZqYitRV8-GlJXtEZ2cc2OJU7M,5415
|
15
16
|
enterprise_data/admin_analytics/database.py,sha256=mNS_9xE5h6O7oMMzr6kr6LDTTSNvKzo8vaM-YG8tOd8,1312
|
16
|
-
enterprise_data/admin_analytics/utils.py,sha256=
|
17
|
+
enterprise_data/admin_analytics/utils.py,sha256=Bq5Vur5_DQbVoVPs7tUBPgW1xrPbhZYUffJaqe8zBmE,11948
|
17
18
|
enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
19
|
enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
|
19
20
|
enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
@@ -21,13 +22,15 @@ enterprise_data/api/v0/serializers.py,sha256=dngZTk6DhRxApchQKCMp1B_c8aVnQtH0NCq
|
|
21
22
|
enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum0,699
|
22
23
|
enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
|
23
24
|
enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
24
|
-
enterprise_data/api/v1/paginators.py,sha256=
|
25
|
-
enterprise_data/api/v1/serializers.py,sha256=
|
26
|
-
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
|
27
28
|
enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
|
-
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
|
29
31
|
enterprise_data/api/v1/views/base.py,sha256=FTAxlz5EzvAY657wzVgzhJPFSCHHzct7IDcvm71Smt8,866
|
30
|
-
enterprise_data/api/v1/views/enterprise_admin.py,sha256=
|
32
|
+
enterprise_data/api/v1/views/enterprise_admin.py,sha256=7f1RHlXxmH8oLr0WLxdGPNsxdhjubwyqNIefb7PMH68,9149
|
33
|
+
enterprise_data/api/v1/views/enterprise_completions.py,sha256=Tpj0Q3tdwFGtRPGNdmcr2_mKxxA90HvROEfPw79l_Gc,7433
|
31
34
|
enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
|
32
35
|
enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
|
33
36
|
enterprise_data/fixtures/enterprise_enrollment.json,sha256=6onPXXR29pMdTdbl_mn81sDi3Re5jkLUZz2TPMB_1IY,5786
|
@@ -101,9 +104,12 @@ enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bz
|
|
101
104
|
enterprise_data/tests/test_utils.py,sha256=vbmYM7DMN-lHS2p4yaa0Yd6uSGXd2qoZRDE9X3J4Sec,18385
|
102
105
|
enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
|
103
106
|
enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
104
|
-
enterprise_data/tests/admin_analytics/
|
105
|
-
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
|
106
111
|
enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=o3denJ4aUS1pI5Crksl4C6m-NtCBm8ynoHBnLkf-v2U,4641
|
112
|
+
enterprise_data/tests/admin_analytics/test_enterprise_completions.py,sha256=afkHQFy4bvqZ0pq5Drl1t2nv8zxbgca2jzOQbihlPG0,7359
|
107
113
|
enterprise_data/tests/admin_analytics/test_utils.py,sha256=y33HXy6BDOoftdcz3qYlOYhgx7JSXDki-OLzBdTpiwA,11449
|
108
114
|
enterprise_data/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
109
115
|
enterprise_data/tests/api/v0/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -112,7 +118,7 @@ enterprise_data/tests/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
|
|
112
118
|
enterprise_data/tests/api/v1/test_serializers.py,sha256=DwgEHcyOP3oqNUPB2O-NkJGeO_cYs9XJiq7791vJLZE,3682
|
113
119
|
enterprise_data/tests/api/v1/test_views.py,sha256=rLqUHfar0HdBNtz33hQxd_0qUUgr7Ku3KwQSQ1B4Ypg,15213
|
114
120
|
enterprise_data/tests/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
115
|
-
enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=
|
121
|
+
enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=At4g5Q4W4SNPk693HxuoEOHtMehvkscLvtXV4dbivvE,4969
|
116
122
|
enterprise_data_roles/__init__.py,sha256=toCpbypm2uDoWVw29_em9gPFNly8vNUS__C0b4TCqEg,112
|
117
123
|
enterprise_data_roles/admin.py,sha256=QNP0VeWE092vZzpyxOA5UJK1nNGl5e71B1J0RCwo_nU,998
|
118
124
|
enterprise_data_roles/apps.py,sha256=nKi8TyuQ5Q6WGtKs5QeXvUTc3N-YQjKhyBnm2EM3Bng,260
|
@@ -153,8 +159,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
|
|
153
159
|
enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
|
154
160
|
enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
|
155
161
|
enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
|
156
|
-
edx_enterprise_data-8.
|
157
|
-
edx_enterprise_data-8.
|
158
|
-
edx_enterprise_data-8.
|
159
|
-
edx_enterprise_data-8.
|
160
|
-
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
@@ -0,0 +1,261 @@
|
|
1
|
+
"""This module contains utility functions for completions analytics."""
|
2
|
+
from enterprise_data.utils import date_filter
|
3
|
+
|
4
|
+
|
5
|
+
def date_aggregation(level, group, date, df, type_="count"):
|
6
|
+
"""Perform date aggregation on a DataFrame.
|
7
|
+
|
8
|
+
This function aggregates data based on the specified level of aggregation (e.g., daily, weekly, monthly, quarterly)
|
9
|
+
and returns the aggregated data.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
level (str): The level of aggregation. Possible values are "Daily", "Weekly", "Monthly", and "Quarterly".
|
13
|
+
group (list): A list of column names to group the data by.
|
14
|
+
date (str): The name of the date column in the DataFrame.
|
15
|
+
df (pandas.DataFrame): The DataFrame containing the data to be aggregated.
|
16
|
+
type_ (str, optional): The type of aggregation to perform. Possible values
|
17
|
+
are "count" and "sum". Defaults to "count".
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
pandas.DataFrame: The aggregated data.
|
21
|
+
|
22
|
+
"""
|
23
|
+
if type_ == "count":
|
24
|
+
if level == "Daily":
|
25
|
+
df = df.groupby(group).size().reset_index()
|
26
|
+
group.append("count")
|
27
|
+
df.columns = group
|
28
|
+
elif level == "Weekly":
|
29
|
+
df[date] = df[date].dt.to_period("W").dt.start_time
|
30
|
+
df = df.groupby(group).size().reset_index()
|
31
|
+
group.append("count")
|
32
|
+
df.columns = group
|
33
|
+
elif level == "Monthly":
|
34
|
+
df[date] = df[date].dt.to_period("M").dt.start_time
|
35
|
+
df = df.groupby(group).size().reset_index()
|
36
|
+
group.append("count")
|
37
|
+
df.columns = group
|
38
|
+
elif level == "Quarterly":
|
39
|
+
df[date] = df[date].dt.to_period("Q").dt.start_time
|
40
|
+
df = df.groupby(group).size().reset_index()
|
41
|
+
group.append("count")
|
42
|
+
df.columns = group
|
43
|
+
elif type_ == "sum":
|
44
|
+
if level == "Daily":
|
45
|
+
df = df.groupby(group).sum().reset_index()
|
46
|
+
group.append("sum")
|
47
|
+
df.columns = group
|
48
|
+
elif level == "Weekly":
|
49
|
+
df[date] = df[date].dt.to_period("W").dt.start_time
|
50
|
+
df = df.groupby(group).sum().reset_index()
|
51
|
+
group.append("sum")
|
52
|
+
df.columns = group
|
53
|
+
elif level == "Monthly":
|
54
|
+
df[date] = df[date].dt.to_period("M").dt.start_time
|
55
|
+
df = df.groupby(group).sum().reset_index()
|
56
|
+
group.append("sum")
|
57
|
+
df.columns = group
|
58
|
+
elif level == "Quarterly":
|
59
|
+
df[date] = df[date].dt.to_period("Q").dt.start_time
|
60
|
+
df = df.groupby(group).sum().reset_index()
|
61
|
+
group.append("sum")
|
62
|
+
df.columns = group
|
63
|
+
|
64
|
+
return df
|
65
|
+
|
66
|
+
|
67
|
+
def calculation(calc, df, type_="count"):
|
68
|
+
"""Perform a calculation on the given DataFrame based on the specified calculation type.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
calc (str): The calculation type. Possible values are "Total", "Running Total",
|
72
|
+
"Moving Average (3 Period)", and "Moving Average (7 Period)".
|
73
|
+
df (pandas.DataFrame): The filtered enrollments data.
|
74
|
+
type_ (str, optional): The type of calculation to perform. Default is "count".
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
pandas.DataFrame: The aggregated data after performing the calculation.
|
78
|
+
"""
|
79
|
+
if type_ == "count":
|
80
|
+
if calc == "Total":
|
81
|
+
pass
|
82
|
+
elif calc == "Running Total":
|
83
|
+
df["count"] = df.groupby("enroll_type")["count"].cumsum()
|
84
|
+
elif calc == "Moving Average (3 Period)":
|
85
|
+
df["count"] = (
|
86
|
+
df.groupby("enroll_type")["count"]
|
87
|
+
.rolling(3)
|
88
|
+
.mean()
|
89
|
+
.droplevel(level=[0])
|
90
|
+
)
|
91
|
+
elif calc == "Moving Average (7 Period)":
|
92
|
+
df["count"] = (
|
93
|
+
df.groupby("enroll_type")["count"]
|
94
|
+
.rolling(7)
|
95
|
+
.mean()
|
96
|
+
.droplevel(level=[0])
|
97
|
+
)
|
98
|
+
elif type_ == "sum":
|
99
|
+
if calc == "Total":
|
100
|
+
pass
|
101
|
+
elif calc == "Running Total":
|
102
|
+
df["sum"] = df.groupby("enroll_type")["sum"].cumsum()
|
103
|
+
elif calc == "Moving Average (3 Period)":
|
104
|
+
df["sum"] = (
|
105
|
+
df.groupby("enroll_type")["sum"].rolling(3).mean().droplevel(level=[0])
|
106
|
+
)
|
107
|
+
elif calc == "Moving Average (7 Period)":
|
108
|
+
df["sum"] = (
|
109
|
+
df.groupby("enroll_type")["sum"].rolling(7).mean().droplevel(level=[0])
|
110
|
+
)
|
111
|
+
|
112
|
+
return df
|
113
|
+
|
114
|
+
|
115
|
+
def get_completions_over_time(start_date, end_date, dff, date_agg, calc):
|
116
|
+
"""Get agreggated data for completions over time graph.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
start_date (datetime): The start date for the date filter.
|
120
|
+
end_date (datetime): The end date for the date filter.
|
121
|
+
dff (pandas.DataFrame): enrollments data
|
122
|
+
date_agg (str): It denotes the granularity of the aggregated date which can be Daily, Weekly, Monthly, Quarterly
|
123
|
+
calc (str): Calculations denoiated the period for the running averages. It can be Total, Running Total, Moving
|
124
|
+
Average (3 Period), Moving Average (7 Period)
|
125
|
+
"""
|
126
|
+
|
127
|
+
dff = dff[dff["has_passed"] == 1]
|
128
|
+
|
129
|
+
# Date filtering.
|
130
|
+
dff = date_filter(start=start_date, end=end_date, data_frame=dff, date_column="passed_date")
|
131
|
+
|
132
|
+
# Date aggregation.
|
133
|
+
dff = date_aggregation(
|
134
|
+
level=date_agg, group=["passed_date", "enroll_type"], date="passed_date", df=dff
|
135
|
+
)
|
136
|
+
|
137
|
+
# Calculating metric.
|
138
|
+
dff = calculation(calc=calc, df=dff)
|
139
|
+
|
140
|
+
return dff
|
141
|
+
|
142
|
+
|
143
|
+
def get_top_courses_by_completions(start_date, end_date, dff):
|
144
|
+
"""Get top 10 courses by completions.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
start_date (datetime): The start date for the date filter.
|
148
|
+
end_date (datetime): The end date for the date filter.
|
149
|
+
dff (pandas.DataFrame): Enrollments data
|
150
|
+
"""
|
151
|
+
|
152
|
+
dff = dff[dff["has_passed"] == 1]
|
153
|
+
|
154
|
+
# Date filtering.
|
155
|
+
dff = date_filter(start=start_date, end=end_date, data_frame=dff, date_column="passed_date")
|
156
|
+
|
157
|
+
courses = list(
|
158
|
+
dff.groupby(["course_key"]).size().sort_values(ascending=False)[:10].index
|
159
|
+
)
|
160
|
+
|
161
|
+
dff = (
|
162
|
+
dff[dff.course_key.isin(courses)]
|
163
|
+
.groupby(["course_key", "course_title", "enroll_type"])
|
164
|
+
.size()
|
165
|
+
.reset_index()
|
166
|
+
)
|
167
|
+
dff.columns = ["course_key", "course_title", "enroll_type", "count"]
|
168
|
+
|
169
|
+
return dff
|
170
|
+
|
171
|
+
|
172
|
+
def get_top_subjects_by_completions(start_date, end_date, dff):
|
173
|
+
"""Get top 10 subjects by completions.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
start_date (datetime): The start date for the date filter.
|
177
|
+
end_date (datetime): The end date for the date filter.
|
178
|
+
dff (pandas.DataFrame): Enrollments data
|
179
|
+
"""
|
180
|
+
|
181
|
+
dff = dff[dff["has_passed"] == 1]
|
182
|
+
|
183
|
+
# Date filtering.
|
184
|
+
dff = date_filter(start=start_date, end=end_date, data_frame=dff, date_column="passed_date")
|
185
|
+
|
186
|
+
subjects = list(
|
187
|
+
dff.groupby(["course_subject"]).size().sort_values(ascending=False)[:10].index
|
188
|
+
)
|
189
|
+
|
190
|
+
dff = (
|
191
|
+
dff[dff.course_subject.isin(subjects)]
|
192
|
+
.groupby(["course_subject", "enroll_type"])
|
193
|
+
.size()
|
194
|
+
.reset_index()
|
195
|
+
)
|
196
|
+
dff.columns = ["course_subject", "enroll_type", "count"]
|
197
|
+
|
198
|
+
return dff
|
199
|
+
|
200
|
+
|
201
|
+
def get_csv_data_for_completions_over_time(
|
202
|
+
start_date, end_date, enrollments, date_agg, calc
|
203
|
+
):
|
204
|
+
"""Get csv data for completions over time graph.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
start_date (datetime): The start date for the date filter.
|
208
|
+
end_date (datetime): The end date for the date filter.
|
209
|
+
enrollments (pandas.DataFrame): Filtered enrollments data
|
210
|
+
date_agg (str): it denotes the granularity of the aggregated date which can be Daily, Weekly, Monthly, Quarterly
|
211
|
+
calc (str): calculations denoiated the period for the running averages. It can be Total, Running Total, Moving
|
212
|
+
Average (3 Period), Moving Average (7 Period)
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
dict: csv data
|
216
|
+
"""
|
217
|
+
|
218
|
+
dff = get_completions_over_time(start_date, end_date, enrollments, date_agg, calc)
|
219
|
+
dff = dff.pivot(index="passed_date", columns="enroll_type", values="count")
|
220
|
+
filename = (
|
221
|
+
f"Completions Timeseries, {start_date} - {end_date} ({date_agg} {calc}).csv"
|
222
|
+
)
|
223
|
+
return {"filename": filename, "data": dff}
|
224
|
+
|
225
|
+
|
226
|
+
def get_csv_data_for_top_courses_by_completions(start_date, end_date, enrollments):
|
227
|
+
"""Get csv data for top 10 courses by completions.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
start_date (datetime): The start date for the date filter.
|
231
|
+
end_date (datetime): The end date for the date filter.
|
232
|
+
enrollments (pandas.DataFrame): Filtered enrollments data
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
dict: csv data
|
236
|
+
"""
|
237
|
+
|
238
|
+
dff = get_top_courses_by_completions(start_date, end_date, enrollments)
|
239
|
+
dff = dff.pivot(
|
240
|
+
index=["course_key", "course_title"], columns="enroll_type", values="count"
|
241
|
+
)
|
242
|
+
filename = f"Top 10 Courses by Completions, {start_date} - {end_date}.csv"
|
243
|
+
return {"filename": filename, "data": dff}
|
244
|
+
|
245
|
+
|
246
|
+
def get_csv_data_for_top_subjects_by_completions(start_date, end_date, enrollments):
|
247
|
+
"""Get csv data for top 10 subjects by completions.
|
248
|
+
|
249
|
+
Args:
|
250
|
+
start_date (datetime): The start date for the date filter.
|
251
|
+
end_date (datetime): The end date for the date filter.
|
252
|
+
enrollments (pandas.DataFrame): Filtered enrollments data
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
dict: csv data
|
256
|
+
"""
|
257
|
+
|
258
|
+
dff = get_top_subjects_by_completions(start_date, end_date, enrollments)
|
259
|
+
dff = dff.pivot(index="course_subject", columns="enroll_type", values="count")
|
260
|
+
filename = f"Top 10 Subjects by Completions, {start_date} - {end_date}.csv"
|
261
|
+
return {"filename": filename, "data": dff}
|
@@ -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
|
|
@@ -18,6 +23,40 @@ class ChartType(Enum):
|
|
18
23
|
BUBBLE = 'bubble'
|
19
24
|
TOP_SKILLS_ENROLLMENT = 'top_skills_enrollment'
|
20
25
|
TOP_SKILLS_COMPLETION = 'top_skills_completion'
|
26
|
+
COMPLETIONS_OVER_TIME = 'completions_over_time'
|
27
|
+
TOP_COURSES_BY_COMPLETIONS = 'top_courses_by_completions'
|
28
|
+
TOP_SUBJECTS_BY_COMPLETIONS = 'top_subjects_by_completions'
|
29
|
+
|
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
|
21
60
|
|
22
61
|
|
23
62
|
def granularity_aggregation(level, group, date, data_frame, aggregation_type="count"):
|
@@ -25,9 +64,9 @@ def granularity_aggregation(level, group, date, data_frame, aggregation_type="co
|
|
25
64
|
df = data_frame
|
26
65
|
|
27
66
|
period_mapping = {
|
28
|
-
|
29
|
-
|
30
|
-
|
67
|
+
Granularity.WEEKLY.value: "W",
|
68
|
+
Granularity.MONTHLY.value: "M",
|
69
|
+
Granularity.QUARTERLY.value: "Q"
|
31
70
|
}
|
32
71
|
|
33
72
|
if level in period_mapping:
|
@@ -49,15 +88,15 @@ def calculation_aggregation(calc, data_frame, aggregation_type="count"):
|
|
49
88
|
df = data_frame
|
50
89
|
|
51
90
|
window_mapping = {
|
52
|
-
|
53
|
-
|
91
|
+
Calculation.MOVING_AVERAGE_3_PERIOD.value: 3,
|
92
|
+
Calculation.MOVING_AVERAGE_7_PERIOD.value: 7,
|
54
93
|
}
|
55
94
|
|
56
95
|
aggregation_column = "count" if aggregation_type == "count" else "sum"
|
57
96
|
|
58
|
-
if calc ==
|
97
|
+
if calc == Calculation.RUNNING_TOTAL.value:
|
59
98
|
df[aggregation_column] = df.groupby("enroll_type")[aggregation_column].cumsum()
|
60
|
-
elif calc in [
|
99
|
+
elif calc in [Calculation.MOVING_AVERAGE_3_PERIOD.value, Calculation.MOVING_AVERAGE_7_PERIOD.value]:
|
61
100
|
df[aggregation_column] = (
|
62
101
|
df.groupby("enroll_type")[aggregation_column]
|
63
102
|
.rolling(window_mapping[calc])
|
@@ -172,7 +211,7 @@ def get_skills_bubble_chart_df(skills_filtered):
|
|
172
211
|
""" Get the skills data for the bubble chart.
|
173
212
|
|
174
213
|
Args:
|
175
|
-
skills_filtered (
|
214
|
+
skills_filtered (pandas.DataFrame): The skills data.
|
176
215
|
|
177
216
|
Returns:
|
178
217
|
(pandas.DataFrame): The skills data for the bubble chart.
|
@@ -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,
|
@@ -206,6 +206,11 @@ class AdminAnalyticsAggregatesQueryParamsSerializer(serializers.Serializer): #
|
|
206
206
|
"""
|
207
207
|
start_date = serializers.DateField(required=False)
|
208
208
|
end_date = serializers.DateField(required=False)
|
209
|
+
granularity = serializers.CharField(required=False)
|
210
|
+
calculation = serializers.CharField(required=False)
|
211
|
+
response_type = serializers.CharField(required=False)
|
212
|
+
page = serializers.IntegerField(required=False)
|
213
|
+
chart_type = serializers.CharField(required=False)
|
209
214
|
|
210
215
|
def validate(self, attrs):
|
211
216
|
"""
|
@@ -232,23 +237,28 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
|
|
232
237
|
|
233
238
|
class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
234
239
|
"""Serializer for validating query params"""
|
240
|
+
RESPONSE_TYPES = [
|
241
|
+
ResponseType.JSON.value,
|
242
|
+
ResponseType.CSV.value
|
243
|
+
]
|
235
244
|
GRANULARITY_CHOICES = [
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
245
|
+
Granularity.DAILY.value,
|
246
|
+
Granularity.WEEKLY.value,
|
247
|
+
Granularity.MONTHLY.value,
|
248
|
+
Granularity.QUARTERLY.value
|
240
249
|
]
|
241
250
|
CALCULATION_CHOICES = [
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
251
|
+
Calculation.TOTAL.value,
|
252
|
+
Calculation.RUNNING_TOTAL.value,
|
253
|
+
Calculation.MOVING_AVERAGE_3_PERIOD.value,
|
254
|
+
Calculation.MOVING_AVERAGE_7_PERIOD.value
|
246
255
|
]
|
247
256
|
|
248
257
|
start_date = serializers.DateField(required=False)
|
249
258
|
end_date = serializers.DateField(required=False)
|
250
259
|
granularity = serializers.CharField(required=False)
|
251
260
|
calculation = serializers.CharField(required=False)
|
261
|
+
response_type = serializers.CharField(required=False)
|
252
262
|
|
253
263
|
def validate(self, attrs):
|
254
264
|
"""
|
@@ -265,6 +275,17 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
|
|
265
275
|
|
266
276
|
return attrs
|
267
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
|
+
|
268
289
|
def validate_granularity(self, value):
|
269
290
|
"""
|
270
291
|
Validate the granularity value.
|
@@ -288,32 +309,25 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
|
|
288
309
|
return value
|
289
310
|
|
290
311
|
|
291
|
-
class
|
292
|
-
|
293
|
-
|
294
|
-
|
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
|
295
320
|
]
|
296
321
|
|
297
|
-
|
322
|
+
chart_type = serializers.CharField(required=False)
|
298
323
|
|
299
|
-
def
|
324
|
+
def validate_chart_type(self, value):
|
300
325
|
"""
|
301
|
-
Validate the
|
326
|
+
Validate the chart_type value.
|
302
327
|
|
303
328
|
Raises:
|
304
|
-
serializers.ValidationError: If
|
329
|
+
serializers.ValidationError: If chart_type is not one of the valid choices
|
305
330
|
"""
|
306
|
-
if value not in self.
|
307
|
-
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}")
|
308
333
|
return value
|
309
|
-
|
310
|
-
|
311
|
-
class AdvanceAnalyticsEnrollmentStatsSerializer(
|
312
|
-
AdvanceAnalyticsEnrollmentSerializer
|
313
|
-
): # pylint: disable=abstract-method
|
314
|
-
"""Serializer for validating Advance Analytics Enrollments Stats API"""
|
315
|
-
CSV_TYPES = [
|
316
|
-
ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value,
|
317
|
-
ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value,
|
318
|
-
ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value
|
319
|
-
]
|