edx-enterprise-data 8.5.0__py3-none-any.whl → 8.6.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.5.0
3
+ Version: 8.6.0
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -1,18 +1,19 @@
1
- enterprise_data/__init__.py,sha256=93nSn1NykQTFtU7czMTFJab_r7CaAtL_iv-q3peH5ME,123
1
+ enterprise_data/__init__.py,sha256=EPPAk93ePtVZFtG1D6BcUQ1SPs07JiPQCLJhZ41gW-Y,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=WVt0qy9Ippdnl404Zsq-MruB9oQfY6h87ZzpScYBeaw,1770
8
+ enterprise_data/renderers.py,sha256=eh-FZFbP_yWcfDemavUGB7vYIJA2PjW_dvM79qxYZz8,2085
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
14
  enterprise_data/admin_analytics/data_loaders.py,sha256=x1XNYdtJV1G9cv0SeBZqYitRV8-GlJXtEZ2cc2OJU7M,5415
14
15
  enterprise_data/admin_analytics/database.py,sha256=mNS_9xE5h6O7oMMzr6kr6LDTTSNvKzo8vaM-YG8tOd8,1312
15
- enterprise_data/admin_analytics/utils.py,sha256=J6E9JhZedWkJQ9TJJF_z95sL5Oka1_x7gs_st4Z35Yo,8420
16
+ enterprise_data/admin_analytics/utils.py,sha256=65HjDdNJkQTHqS6KatHE4UpEzKDMj2_v21O-YBf130Q,10023
16
17
  enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
18
  enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
18
19
  enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
@@ -20,9 +21,11 @@ enterprise_data/api/v0/serializers.py,sha256=dngZTk6DhRxApchQKCMp1B_c8aVnQtH0NCq
20
21
  enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum0,699
21
22
  enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
22
23
  enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
23
- enterprise_data/api/v1/serializers.py,sha256=5C7IYNUuW7HvFVqtbsLIBE6A2Ywzmy6SFzPc4FjtclY,8562
24
- enterprise_data/api/v1/urls.py,sha256=OlchBMQmyvhM2HpVbbSZCMpUD_y8tP9-vsorGtBvxVo,2048
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
27
  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
26
29
  enterprise_data/api/v1/views/base.py,sha256=FTAxlz5EzvAY657wzVgzhJPFSCHHzct7IDcvm71Smt8,866
27
30
  enterprise_data/api/v1/views/enterprise_admin.py,sha256=RTSRyPfHvbzV_ihSbGjYi0VuE6AjaYZIyqpAKTMYa5Q,8980
28
31
  enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
@@ -98,6 +101,8 @@ enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bz
98
101
  enterprise_data/tests/test_utils.py,sha256=vbmYM7DMN-lHS2p4yaa0Yd6uSGXd2qoZRDE9X3J4Sec,18385
99
102
  enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
100
103
  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
101
106
  enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=o3denJ4aUS1pI5Crksl4C6m-NtCBm8ynoHBnLkf-v2U,4641
102
107
  enterprise_data/tests/admin_analytics/test_utils.py,sha256=y33HXy6BDOoftdcz3qYlOYhgx7JSXDki-OLzBdTpiwA,11449
103
108
  enterprise_data/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -148,8 +153,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
148
153
  enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
149
154
  enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
150
155
  enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
151
- edx_enterprise_data-8.5.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
152
- edx_enterprise_data-8.5.0.dist-info/METADATA,sha256=oabxLQt9xaTLpOgQX1KdSryfXehxEHBMEroSMHcU6-I,1569
153
- edx_enterprise_data-8.5.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
154
- edx_enterprise_data-8.5.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
155
- edx_enterprise_data-8.5.0.dist-info/RECORD,,
156
+ edx_enterprise_data-8.6.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
157
+ edx_enterprise_data-8.6.0.dist-info/METADATA,sha256=UT5OiAzT8dXg3nZ9psbT8eqAbGjEnDD1GjRb8YEitv8,1569
158
+ edx_enterprise_data-8.6.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
159
+ edx_enterprise_data-8.6.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
160
+ edx_enterprise_data-8.6.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.5.0"
5
+ __version__ = "8.6.0"
@@ -0,0 +1,27 @@
1
+ """Advanced Analytics Constants"""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class GRANULARITY(Enum):
7
+ """Granularity choices"""
8
+ DAILY = 'Daily'
9
+ WEEKLY = 'Weekly'
10
+ MONTHLY = 'Monthly'
11
+ QUARTERLY = 'Quarterly'
12
+
13
+
14
+ class CALCULATION(Enum):
15
+ """Calculation choices"""
16
+ TOTAL = 'Total'
17
+ RUNNING_TOTAL = 'Running Total'
18
+ MOVING_AVERAGE_3_PERIOD = 'Moving Average (3 Period)'
19
+ MOVING_AVERAGE_7_PERIOD = 'Moving Average (7 Period)'
20
+
21
+
22
+ class ENROLLMENT_CSV(Enum):
23
+ """CSV choices"""
24
+ ENROLLMENTS_OVER_TIME = 'enrollments_over_time'
25
+ TOP_COURSES_BY_ENROLLMENTS = 'top_courses_by_enrollments'
26
+ TOP_SUBJECTS_BY_ENROLLMENTS = 'top_subjects_by_enrollments'
27
+ INDIVIDUAL_ENROLLMENTS = 'individual_enrollments'
@@ -6,6 +6,7 @@ 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
9
10
  from enterprise_data.admin_analytics.data_loaders import fetch_engagement_data, fetch_enrollment_data, fetch_skills_data
10
11
  from enterprise_data.utils import date_filter, primary_subject_truncate
11
12
 
@@ -19,6 +20,54 @@ class ChartType(Enum):
19
20
  TOP_SKILLS_COMPLETION = 'top_skills_completion'
20
21
 
21
22
 
23
+ def granularity_aggregation(level, group, date, data_frame, aggregation_type="count"):
24
+ """Aggregate data based on granularity"""
25
+ df = data_frame
26
+
27
+ period_mapping = {
28
+ GRANULARITY.WEEKLY.value: "W",
29
+ GRANULARITY.MONTHLY.value: "M",
30
+ GRANULARITY.QUARTERLY.value: "Q"
31
+ }
32
+
33
+ if level in period_mapping:
34
+ df[date] = df[date].dt.to_period(period_mapping[level]).dt.start_time
35
+
36
+ agg_column_name = "count"
37
+ if aggregation_type == "count":
38
+ df = df.groupby(group).size().reset_index()
39
+ elif aggregation_type == "sum":
40
+ df = df.groupby(group).sum().reset_index()
41
+ agg_column_name = "sum"
42
+
43
+ df.columns = group + [agg_column_name]
44
+ return df
45
+
46
+
47
+ def calculation_aggregation(calc, data_frame, aggregation_type="count"):
48
+ """Aggregate data based on calculation"""
49
+ df = data_frame
50
+
51
+ window_mapping = {
52
+ CALCULATION.MOVING_AVERAGE_3_PERIOD.value: 3,
53
+ CALCULATION.MOVING_AVERAGE_7_PERIOD.value: 7,
54
+ }
55
+
56
+ aggregation_column = "count" if aggregation_type == "count" else "sum"
57
+
58
+ if calc == CALCULATION.RUNNING_TOTAL.value:
59
+ 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]:
61
+ df[aggregation_column] = (
62
+ df.groupby("enroll_type")[aggregation_column]
63
+ .rolling(window_mapping[calc])
64
+ .mean()
65
+ .droplevel(level=[0])
66
+ )
67
+
68
+ return df
69
+
70
+
22
71
  def get_cache_timeout(cache_expiry):
23
72
  """
24
73
  Helper method to calculate cache timeout in seconds.
@@ -0,0 +1,121 @@
1
+ """Custom paginator for the Advance Analytics API."""
2
+
3
+ import math
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from rest_framework.exceptions import NotFound
8
+ from rest_framework.pagination import PageNumberPagination
9
+ from rest_framework.response import Response
10
+
11
+
12
+ @dataclass
13
+ class Page:
14
+ """
15
+ A class representing a single page of paginated data.
16
+
17
+ Attributes:
18
+ data (Any): The data contained in the current page.
19
+ count (int): The total number of items across all pages.
20
+ num_pages (int): The total number of pages.
21
+ current_page (int): The current page number.
22
+ """
23
+ data: Any
24
+ count: int
25
+ num_pages: int
26
+ current_page: int
27
+
28
+ def has_next(self):
29
+ """
30
+ Check if there is a next page.
31
+
32
+ Returns:
33
+ bool: True if there is a next page, False otherwise.
34
+ """
35
+ return self.current_page < self.num_pages
36
+
37
+ def has_previous(self):
38
+ """
39
+ Check if there is a previous page.
40
+
41
+ Returns:
42
+ bool: True if there is a previous page, False otherwise.
43
+ """
44
+ return self.current_page > 1
45
+
46
+ def next_page_number(self):
47
+ """
48
+ Get the next page number.
49
+
50
+ Returns:
51
+ int: The next page number.
52
+ """
53
+ return self.current_page + 1
54
+
55
+ def previous_page_number(self):
56
+ """
57
+ Get the previous page number.
58
+
59
+ Returns:
60
+ int: The previous page number.
61
+ """
62
+ return self.current_page - 1
63
+
64
+
65
+ class AdvanceAnalyticsPagination(PageNumberPagination):
66
+ """
67
+ Custom pagination class for advanced analytics.
68
+
69
+ Attributes:
70
+ page_size_query_param (str): The query parameter for the page size.
71
+ page_size (int): The default page size.
72
+ max_page_size (int): The maximum allowed page size.
73
+ """
74
+ page_size_query_param = "page_size"
75
+ page_size = 10
76
+ max_page_size = 100
77
+
78
+ def paginate_queryset(self, queryset, request, view=None):
79
+ """
80
+ Paginate a given dataframe based on the request parameters.
81
+
82
+ Args:
83
+ queryset (pd.DataFrame): The dataframe to paginate.
84
+ request (Request): The request object containing query parameters.
85
+ view (View, optional): The view that is calling the paginator.
86
+
87
+ Returns:
88
+ Page: A Page object. `data` attribute of the object will contain the paginated data.
89
+ """
90
+ dataframe = queryset
91
+
92
+ self.request = request # pylint: disable=attribute-defined-outside-init
93
+ page_size = self.get_page_size(request)
94
+ if not page_size:
95
+ return None
96
+
97
+ total_rows = dataframe.shape[0]
98
+ num_pages = math.ceil(total_rows / page_size)
99
+
100
+ page_number = int(request.query_params.get(self.page_query_param) or 1)
101
+ if page_number <= 0 or page_number > num_pages:
102
+ raise NotFound('Invalid page.')
103
+
104
+ start_index = (page_number - 1) * page_size
105
+ end_index = min(start_index + page_size, total_rows)
106
+ data_frame_page = dataframe.iloc[start_index:end_index]
107
+
108
+ # pylint: disable=attribute-defined-outside-init
109
+ self.page = Page(data_frame_page, total_rows, num_pages, page_number)
110
+
111
+ return self.page
112
+
113
+ def get_paginated_response(self, data):
114
+ return Response({
115
+ 'next': self.get_next_link(),
116
+ 'previous': self.get_previous_link(),
117
+ 'count': self.page.count,
118
+ 'num_pages': self.page.num_pages,
119
+ 'current_page': self.page.current_page,
120
+ 'results': data
121
+ })
@@ -5,6 +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
9
  from enterprise_data.models import (
9
10
  EnterpriseAdminLearnerProgress,
10
11
  EnterpriseAdminSummarizeInsights,
@@ -227,3 +228,92 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
227
228
  class Meta:
228
229
  model = EnterpriseExecEdLCModulePerformance
229
230
  fields = '__all__'
231
+
232
+
233
+ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: disable=abstract-method
234
+ """Serializer for validating query params"""
235
+ GRANULARITY_CHOICES = [
236
+ GRANULARITY.DAILY.value,
237
+ GRANULARITY.WEEKLY.value,
238
+ GRANULARITY.MONTHLY.value,
239
+ GRANULARITY.QUARTERLY.value
240
+ ]
241
+ 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
246
+ ]
247
+
248
+ start_date = serializers.DateField(required=False)
249
+ end_date = serializers.DateField(required=False)
250
+ granularity = serializers.CharField(required=False)
251
+ calculation = serializers.CharField(required=False)
252
+
253
+ def validate(self, attrs):
254
+ """
255
+ Validate the query params.
256
+
257
+ Raises:
258
+ serializers.ValidationError: If start_date is greater than end_date.
259
+ """
260
+ start_date = attrs.get('start_date')
261
+ end_date = attrs.get('end_date')
262
+
263
+ if start_date and end_date and start_date > end_date:
264
+ raise serializers.ValidationError("start_date should be less than or equal to end_date.")
265
+
266
+ return attrs
267
+
268
+ def validate_granularity(self, value):
269
+ """
270
+ Validate the granularity value.
271
+
272
+ Raises:
273
+ serializers.ValidationError: If granularity is not one of the valid choices.
274
+ """
275
+ if value not in self.GRANULARITY_CHOICES:
276
+ raise serializers.ValidationError(f"Granularity must be one of {self.GRANULARITY_CHOICES}")
277
+ return value
278
+
279
+ def validate_calculation(self, value):
280
+ """
281
+ Validate the calculation value.
282
+
283
+ Raises:
284
+ serializers.ValidationError: If calculation is not one of the valid choices
285
+ """
286
+ if value not in self.CALCULATION_CHOICES:
287
+ raise serializers.ValidationError(f"Calculation must be one of {self.CALCULATION_CHOICES}")
288
+ return value
289
+
290
+
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
295
+ ]
296
+
297
+ csv_type = serializers.CharField(required=False)
298
+
299
+ def validate_csv_type(self, value):
300
+ """
301
+ Validate the csv_type value.
302
+
303
+ Raises:
304
+ serializers.ValidationError: If csv_type is not one of the valid choices
305
+ """
306
+ if value not in self.CSV_TYPES:
307
+ raise serializers.ValidationError(f"csv_type must be one of {self.CSV_TYPES}")
308
+ 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
+ ]
@@ -10,6 +10,10 @@ from django.urls import re_path
10
10
  from enterprise_data.api.v1.views import enterprise_admin as enterprise_admin_views
11
11
  from enterprise_data.api.v1.views import enterprise_learner as enterprise_learner_views
12
12
  from enterprise_data.api.v1.views import enterprise_offers as enterprise_offers_views
13
+ from enterprise_data.api.v1.views.analytics_enrollments import (
14
+ AdvanceAnalyticsEnrollmentStatsView,
15
+ AdvanceAnalyticsIndividualEnrollmentsView,
16
+ )
13
17
  from enterprise_data.constants import UUID4_REGEX
14
18
 
15
19
  app_name = 'enterprise_data_api_v1'
@@ -52,6 +56,16 @@ urlpatterns = [
52
56
  enterprise_admin_views.EnterpriseAdminAnalyticsAggregatesView.as_view(),
53
57
  name='enterprise-admin-analytics-aggregates'
54
58
  ),
59
+ re_path(
60
+ fr'^admin/anlaytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments/stats$',
61
+ AdvanceAnalyticsEnrollmentStatsView.as_view(),
62
+ name='enterprise-admin-analytics-enrollments-stats'
63
+ ),
64
+ re_path(
65
+ fr'^admin/anlaytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments$',
66
+ AdvanceAnalyticsIndividualEnrollmentsView.as_view(),
67
+ name='enterprise-admin-analytics-enrollments'
68
+ ),
55
69
  re_path(
56
70
  fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
57
71
  enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),