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.
Files changed (23) hide show
  1. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/METADATA +1 -1
  2. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/RECORD +23 -17
  3. enterprise_data/__init__.py +1 -1
  4. enterprise_data/admin_analytics/completions_utils.py +261 -0
  5. enterprise_data/admin_analytics/constants.py +9 -3
  6. enterprise_data/admin_analytics/utils.py +50 -11
  7. enterprise_data/api/v1/paginators.py +1 -1
  8. enterprise_data/api/v1/serializers.py +44 -30
  9. enterprise_data/api/v1/urls.py +21 -4
  10. enterprise_data/api/v1/views/analytics_enrollments.py +42 -53
  11. enterprise_data/api/v1/views/analytics_leaderboard.py +120 -0
  12. enterprise_data/api/v1/views/enterprise_admin.py +9 -5
  13. enterprise_data/api/v1/views/enterprise_completions.py +177 -0
  14. enterprise_data/renderers.py +14 -0
  15. enterprise_data/tests/admin_analytics/mock_analytics_data.py +501 -0
  16. enterprise_data/tests/admin_analytics/mock_enrollments.py +23 -7
  17. enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +23 -22
  18. enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +163 -0
  19. enterprise_data/tests/admin_analytics/test_enterprise_completions.py +202 -0
  20. enterprise_data/tests/api/v1/views/test_enterprise_admin.py +4 -0
  21. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/LICENSE +0 -0
  22. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/WHEEL +0 -0
  23. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 8.6.1
3
+ Version: 8.8.0
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -1,19 +1,20 @@
1
- enterprise_data/__init__.py,sha256=yTU6goEK5b0JnABkDcmfrzKV2uFXdfMs4toWJ3zozNI,123
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=eh-FZFbP_yWcfDemavUGB7vYIJA2PjW_dvM79qxYZz8,2085
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/constants.py,sha256=6Gc9rP-J3nGgAk3fhzNlyR1HMq8Apjxs9ViWJiYrri4,722
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=65HjDdNJkQTHqS6KatHE4UpEzKDMj2_v21O-YBf130Q,10023
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=OHbuBP7hAFJ_ce0UAMfJ1pARMMzqvzVYiYeFMw3xZLU,3592
25
- enterprise_data/api/v1/serializers.py,sha256=oNLxBbHa6CJ7d7mdA0hpmLFjKMe-S6nOIguQkEu_D4Y,11723
26
- enterprise_data/api/v1/urls.py,sha256=wMw-h0NlkOgS1HYUm2FqGOLA-BHBdFZYaL-QNZn0T60,2635
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=SvuK53i_4S3etqktoJ0h5ky4dntD176u6DXZuqHTsEg,16352
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=RTSRyPfHvbzV_ihSbGjYi0VuE6AjaYZIyqpAKTMYa5Q,8980
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/mock_enrollments.py,sha256=ApRzq6LxdAqGNNZqGoPSdLbGCf6R-z8qL8FoRf4wvvs,6712
105
- enterprise_data/tests/admin_analytics/test_analytics_enrollments.py,sha256=WNdJCM52zUJikBTl3VakPIvNiVNvUed-8vk275njSdY,14847
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=7FCPpfFrv-pisJ9cxt3B-KIf-KM3a7XzQ8MweLp23wI,4783
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.6.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
157
- edx_enterprise_data-8.6.1.dist-info/METADATA,sha256=vRh1poHTKIiOVQOUYVWW8on4KR7YEtWuMRXrf5BFxbk,1569
158
- edx_enterprise_data-8.6.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
159
- edx_enterprise_data-8.6.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
160
- edx_enterprise_data-8.6.1.dist-info/RECORD,,
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,,
@@ -2,4 +2,4 @@
2
2
  Enterprise data api application. This Django app exposes API endpoints used by enterprises.
3
3
  """
4
4
 
5
- __version__ = "8.6.1"
5
+ __version__ = "8.8.0"
@@ -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 GRANULARITY(Enum):
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 CALCULATION(Enum):
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 ENROLLMENT_CSV(Enum):
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 CALCULATION, GRANULARITY
10
- from enterprise_data.admin_analytics.data_loaders import fetch_engagement_data, fetch_enrollment_data, fetch_skills_data
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
- GRANULARITY.WEEKLY.value: "W",
29
- GRANULARITY.MONTHLY.value: "M",
30
- GRANULARITY.QUARTERLY.value: "Q"
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
- CALCULATION.MOVING_AVERAGE_3_PERIOD.value: 3,
53
- CALCULATION.MOVING_AVERAGE_7_PERIOD.value: 7,
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 == CALCULATION.RUNNING_TOTAL.value:
97
+ if calc == Calculation.RUNNING_TOTAL.value:
59
98
  df[aggregation_column] = df.groupby("enroll_type")[aggregation_column].cumsum()
60
- elif calc in [CALCULATION.MOVING_AVERAGE_3_PERIOD.value, CALCULATION.MOVING_AVERAGE_7_PERIOD.value]:
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 (list): The skills data.
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 = 10
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 CALCULATION, ENROLLMENT_CSV, GRANULARITY
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
- GRANULARITY.DAILY.value,
237
- GRANULARITY.WEEKLY.value,
238
- GRANULARITY.MONTHLY.value,
239
- GRANULARITY.QUARTERLY.value
245
+ Granularity.DAILY.value,
246
+ Granularity.WEEKLY.value,
247
+ Granularity.MONTHLY.value,
248
+ Granularity.QUARTERLY.value
240
249
  ]
241
250
  CALCULATION_CHOICES = [
242
- CALCULATION.TOTAL.value,
243
- CALCULATION.RUNNING_TOTAL.value,
244
- CALCULATION.MOVING_AVERAGE_3_PERIOD.value,
245
- CALCULATION.MOVING_AVERAGE_7_PERIOD.value
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 AdvanceAnalyticsEnrollmentSerializer(AdvanceAnalyticsQueryParamSerializer): # pylint: disable=abstract-method
292
- """Serializer for validating Advance Analytics Enrollments API"""
293
- CSV_TYPES = [
294
- ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value
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
- csv_type = serializers.CharField(required=False)
322
+ chart_type = serializers.CharField(required=False)
298
323
 
299
- def validate_csv_type(self, value):
324
+ def validate_chart_type(self, value):
300
325
  """
301
- Validate the csv_type value.
326
+ Validate the chart_type value.
302
327
 
303
328
  Raises:
304
- serializers.ValidationError: If csv_type is not one of the valid choices
329
+ serializers.ValidationError: If chart_type is not one of the valid choices
305
330
  """
306
- if value not in self.CSV_TYPES:
307
- raise serializers.ValidationError(f"csv_type must be one of {self.CSV_TYPES}")
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
- ]