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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 8.7.0
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,20 +1,20 @@
1
- enterprise_data/__init__.py,sha256=uaNc7nZk0jrnfiaQzk9MA9gKcNIQ30m99eUJ0R4k1X8,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
13
  enterprise_data/admin_analytics/completions_utils.py,sha256=kGmLy7x6aD0coNYgzLa5XzJypLkGTT5clDHLSH_QFDE,9442
14
- enterprise_data/admin_analytics/constants.py,sha256=6Gc9rP-J3nGgAk3fhzNlyR1HMq8Apjxs9ViWJiYrri4,722
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=w1GpdJCvt-ocwwLEaag4YVO5XH31NCvjwAGFYAiwUZE,10213
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=OHbuBP7hAFJ_ce0UAMfJ1pARMMzqvzVYiYeFMw3xZLU,3592
26
- enterprise_data/api/v1/serializers.py,sha256=SaMFlNRMPIqaKelFZa7nYn3su7DJFS2wJfYZ5sUl_gM,12000
27
- enterprise_data/api/v1/urls.py,sha256=JLjkMzTiJmHtDjN5_z4hOe0s1ug8Ec3A4Ll1E5pwVmY,3206
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=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
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=LGwwCiJSBVhsm52ROIDAJQkLAaj4Wbxh-A-1INfZbOE,7433
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/mock_enrollments.py,sha256=kINSGZx7M3CcsVZ04AZAy4AqNGVbRD47OfI2_3dEVSs,7262
107
- 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
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=BXptZVfnTReJfMSog-ZMDnoIe6Gh_sX2GDRzyugxSMI,7356
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.7.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
160
- edx_enterprise_data-8.7.0.dist-info/METADATA,sha256=6ZncVDwM9a49UHdde9Rk1fhjP1BEaradbGjKCHSX_WE,1569
161
- edx_enterprise_data-8.7.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
162
- edx_enterprise_data-8.7.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
163
- edx_enterprise_data-8.7.0.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.7.0"
5
+ __version__ = "8.8.0"
@@ -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
 
@@ -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
- GRANULARITY.WEEKLY.value: "W",
32
- GRANULARITY.MONTHLY.value: "M",
33
- GRANULARITY.QUARTERLY.value: "Q"
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
- CALCULATION.MOVING_AVERAGE_3_PERIOD.value: 3,
56
- CALCULATION.MOVING_AVERAGE_7_PERIOD.value: 7,
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 == CALCULATION.RUNNING_TOTAL.value:
97
+ if calc == Calculation.RUNNING_TOTAL.value:
62
98
  df[aggregation_column] = df.groupby("enroll_type")[aggregation_column].cumsum()
63
- 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]:
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 = 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,
@@ -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
- GRANULARITY.DAILY.value,
242
- GRANULARITY.WEEKLY.value,
243
- GRANULARITY.MONTHLY.value,
244
- GRANULARITY.QUARTERLY.value
245
+ Granularity.DAILY.value,
246
+ Granularity.WEEKLY.value,
247
+ Granularity.MONTHLY.value,
248
+ Granularity.QUARTERLY.value
245
249
  ]
246
250
  CALCULATION_CHOICES = [
247
- CALCULATION.TOTAL.value,
248
- CALCULATION.RUNNING_TOTAL.value,
249
- CALCULATION.MOVING_AVERAGE_3_PERIOD.value,
250
- 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
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 AdvanceAnalyticsEnrollmentSerializer(AdvanceAnalyticsQueryParamSerializer): # pylint: disable=abstract-method
297
- """Serializer for validating Advance Analytics Enrollments API"""
298
- CSV_TYPES = [
299
- 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
300
320
  ]
301
321
 
302
- csv_type = serializers.CharField(required=False)
322
+ chart_type = serializers.CharField(required=False)
303
323
 
304
- def validate_csv_type(self, value):
324
+ def validate_chart_type(self, value):
305
325
  """
306
- Validate the csv_type value.
326
+ Validate the chart_type value.
307
327
 
308
328
  Raises:
309
- 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
310
330
  """
311
- if value not in self.CSV_TYPES:
312
- 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}")
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
- ]
@@ -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/anlaytics/(?P<enterprise_id>{UUID4_REGEX})$',
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/anlaytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments/stats$',
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/anlaytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments$',
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/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
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/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/completions/stats$',
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/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/completions$',
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, timedelta
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 CALCULATION, ENROLLMENT_CSV, GRANULARITY
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 = AdvanceAnalyticsEnrollmentSerializer(data=request.GET)
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
- csv_type = request.query_params.get('csv_type')
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 csv_type == ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value:
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="individual_enrollments.csv"'},
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', GRANULARITY.DAILY.value)
125
- calculation = serializer.data.get('calculation', CALCULATION.TOTAL.value)
126
- csv_type = serializer.data.get('csv_type')
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 csv_type is None:
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
- elif csv_type == ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value:
150
- return self.construct_enrollments_over_time_csv(
151
- enrollments_df.copy(),
152
- start_date,
153
- end_date,
154
- granularity,
155
- calculation,
156
- )
157
- elif csv_type == ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value:
158
- return self.construct_top_courses_by_enrollments_csv(
159
- enrollments_df.copy(),
160
- start_date,
161
- end_date,
162
- )
163
- elif csv_type == ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value:
164
- return self.construct_top_subjects_by_enrollments_csv(
165
- enrollments_df.copy(),
166
- start_date,
167
- end_date,
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 GRANULARITY choices
179
- calculation {str} -- Calculation of the data. One of CALCULATION choices
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 GRANULARITY choices
206
- calculation {str} -- Calculation of the data. One of CALCULATION choices
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 GRANULARITY choices
222
- calculation {str} -- Calculation of the data. One of CALCULATION choices
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