Skip to content

Commit

Permalink
Merge branch 'master' into dependabot/github_actions/codecov/codecov-…
Browse files Browse the repository at this point in the history
…action-5
  • Loading branch information
macdiesel authored Jan 16, 2025
2 parents 15d6a4d + 9eba450 commit 2a9faa1
Show file tree
Hide file tree
Showing 26 changed files with 429 additions and 191 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/mysql8-migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache pip dependencies
id: cache-dependencies
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pip-${{ hashFiles('requirements/pip_tools.txt') }}
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ Unreleased

=========================

[10.7.1] - 2025-01-07
---------------------
* feat: add group_membership table
* feat: add APIs to support LPR filtering for enterprise groups

[10.7.0] - 2024-12-24
---------------------
* feat: Added user's first and last name in the enterprise enrollments API and related DB table.

[10.6.1] - 2024-12-10
---------------------
* feat: add course_title in top courses in enrollments csv

[10.6.0] - 2024-12-09
---------------------
* chore: upgrade python requirements

[10.5.1] - 2024-11-14
---------------------
* chore: upgrade python requirements
Expand Down
2 changes: 1 addition & 1 deletion enterprise_data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Enterprise data api application. This Django app exposes API endpoints used by enterprises.
"""

__version__ = "10.5.1"
__version__ = "10.7.1"
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def get_top_courses_by_enrollments_query(record_count=10):
SELECT
d.course_key,
MAX(d.course_title) AS course_title,
d.enroll_type,
COUNT(*) AS enrollment_count
FROM filtered_data d
Expand Down
20 changes: 17 additions & 3 deletions enterprise_data/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
EnterpriseAdminLearnerProgress,
EnterpriseAdminSummarizeInsights,
EnterpriseExecEdLCModulePerformance,
EnterpriseGroupMembership,
EnterpriseLearner,
EnterpriseLearnerEnrollment,
EnterpriseOffer,
Expand Down Expand Up @@ -41,9 +42,9 @@ class Meta:
'course_primary_program', 'primary_program_type', 'course_primary_subject', 'has_passed',
'last_activity_date', 'progress_status', 'passed_date', 'current_grade',
'letter_grade', 'enterprise_user_id', 'user_email', 'user_account_creation_date',
'user_country_code', 'user_username', 'enterprise_name', 'enterprise_customer_uuid',
'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours', 'is_subsidy',
'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
'user_country_code', 'user_username', 'user_first_name', 'user_last_name', 'enterprise_name',
'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours',
'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
)

def get_course_api_url(self, obj):
Expand Down Expand Up @@ -253,6 +254,19 @@ class Meta:
)


class EnterpriseGroupMembershipSerializer(serializers.ModelSerializer):
"""
Serializer for EnterpriseGroupMembership model.
"""

class Meta:
model = EnterpriseGroupMembership
fields = (
'enterprise_group_uuid',
'enterprise_group_name',
)


class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""Serializer for validating query params"""
RESPONSE_TYPES = [
Expand Down
5 changes: 5 additions & 0 deletions enterprise_data/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
enterprise_admin_views.EnterpriseBudgetView.as_view(),
name='enterprise-budgets'
),
re_path(
fr'^enterprise/(?P<enterprise_uuid>{UUID4_REGEX})/groups',
enterprise_admin_views.EnterpriseGroupMembershipView.as_view(),
name='enterprise-groups'
),
]

urlpatterns += router.urls
22 changes: 22 additions & 0 deletions enterprise_data/api/v1/views/enterprise_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
EnterpriseAdminLearnerProgress,
EnterpriseAdminSummarizeInsights,
EnterpriseExecEdLCModulePerformance,
EnterpriseGroupMembership,
EnterpriseSubsidyBudget,
)
from enterprise_data.utils import timer
Expand Down Expand Up @@ -230,3 +231,24 @@ def get(self, request, enterprise_uuid):

serializer = serializers.EnterpriseBudgetSerializer(budgets, many=True)
return Response(serializer.data)


class EnterpriseGroupMembershipView(APIView):
"""
View for getting Group Memberships information for an enterprise.
"""
authentication_classes = (JwtAuthentication,)
http_method_names = ["get"]

@permission_required("can_access_enterprise", fn=lambda request, enterprise_uuid: enterprise_uuid)
def get(self, request, enterprise_uuid):
"""
Returns the groups and budgets for an enterprise.
"""
groups = EnterpriseGroupMembership.objects.filter(
enterprise_customer_id=enterprise_uuid,
group_type='flex',
)

serializer = serializers.EnterpriseGroupMembershipSerializer(groups, many=True)
return Response(serializer.data)
19 changes: 14 additions & 5 deletions enterprise_data/api/v1/views/enterprise_learner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@

from django.conf import settings
from django.core.paginator import Paginator
from django.db.models import Count, Max, OuterRef, Prefetch, Q, Subquery, Value
from django.db.models import Count, Exists, Max, OuterRef, Prefetch, Q, Subquery, Value
from django.db.models.fields import IntegerField
from django.db.models.functions import Coalesce
from django.http import StreamingHttpResponse
from django.utils import timezone

from enterprise_data.admin_analytics.database.utils import LOGGER
from enterprise_data.api.v1 import serializers
from enterprise_data.models import EnterpriseLearner, EnterpriseLearnerEnrollment
from enterprise_data.models import EnterpriseGroupMembership, EnterpriseLearner, EnterpriseLearnerEnrollment
from enterprise_data.paginators import EnterpriseEnrollmentsPagination
from enterprise_data.renderers import EnrollmentsCSVRenderer
from enterprise_data.utils import subtract_one_month
Expand Down Expand Up @@ -59,9 +59,9 @@ class EnterpriseLearnerEnrollmentViewSet(EnterpriseViewSetMixin, viewsets.ReadOn
'course_primary_program', 'primary_program_type', 'course_primary_subject', 'has_passed',
'last_activity_date', 'progress_status', 'passed_date', 'current_grade',
'letter_grade', 'enterprise_user_id', 'user_email', 'user_account_creation_date',
'user_country_code', 'user_username', 'enterprise_name', 'enterprise_customer_uuid',
'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours', 'is_subsidy',
'course_product_line', 'budget_id'
'user_country_code', 'user_username', 'user_first_name', 'user_last_name', 'enterprise_name',
'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours',
'is_subsidy', 'course_product_line', 'budget_id',
]

# TODO: Remove after we release the streaming csv changes
Expand Down Expand Up @@ -174,6 +174,15 @@ def apply_filters(self, queryset):
if is_subsidy:
queryset = queryset.filter(is_subsidy=is_subsidy)

group_uuid = query_filters.get('group_uuid')
if group_uuid:
flex_group_exists = EnterpriseGroupMembership.objects.filter(
enterprise_customer_user_id=OuterRef('enterprise_user_id'),
enterprise_group_uuid=group_uuid,
group_type='flex'
)
queryset = queryset.filter(Exists(flex_group_exists))

return queryset

def filter_by_offer_id(self, queryset, offer_id):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def test_create_enterprise_learner_enrollment_lpr_v1_with_dsc_disabled(self):
assert enterprise_learner_enrollment[0].letter_grade is None
assert enterprise_learner_enrollment[0].enterprise_user_id is None
assert enterprise_learner_enrollment[0].user_username is None
assert enterprise_learner_enrollment[0].user_first_name is None
assert enterprise_learner_enrollment[0].user_last_name is None
assert enterprise_learner_enrollment[0].user_email is None
assert enterprise_learner_enrollment[0].enterprise_user is None

Expand All @@ -62,6 +64,8 @@ def test_create_enterprise_learner_enrollment_lpr_v1_with_dsc_enabled(self):
assert enterprise_learner_enrollment[0].progress_status is not None
assert enterprise_learner_enrollment[0].enterprise_user_id is not None
assert enterprise_learner_enrollment[0].user_username is not None
assert enterprise_learner_enrollment[0].user_first_name is not None
assert enterprise_learner_enrollment[0].user_last_name is not None
assert enterprise_learner_enrollment[0].enterprise_user is not None
assert enterprise_learner_enrollment[0].user_email is not None
assert EnterpriseLearner.objects.count() == 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.15 on 2024-12-23 12:55

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('enterprise_data', '0044_enterpriseexecedlcmoduleperformance'),
]

operations = [
migrations.AlterModelOptions(
name='enterpriseexecedlcmoduleperformance',
options={'verbose_name': 'Exec Ed LC Module Performance', 'verbose_name_plural': 'Exec Ed LC Module Performance'},
),
migrations.AddField(
model_name='enterpriselearnerenrollment',
name='user_first_name',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='enterpriselearnerenrollment',
name='user_last_name',
field=models.CharField(max_length=255, null=True),
),
]
36 changes: 36 additions & 0 deletions enterprise_data/migrations/0046_enterprisegroupmembership.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.16 on 2025-01-08 07:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('enterprise_data', '0045_alter_enterpriseexecedlcmoduleperformance_options_and_more'),
]

operations = [
migrations.CreateModel(
name='EnterpriseGroupMembership',
fields=[
('group_membership_unique_id', models.CharField(max_length=64, primary_key=True, serialize=False)),
('is_applies_to_all_contexts', models.BooleanField(default=False)),
('enterprise_customer_id', models.UUIDField(db_index=True, null=True)),
('enterprise_group_name', models.CharField(max_length=255, null=True)),
('enterprise_group_uuid', models.UUIDField(null=True)),
('group_is_removed', models.BooleanField(default=False)),
('group_type', models.CharField(max_length=128, null=True)),
('activated_at', models.DateTimeField(null=True)),
('enterprise_customer_user_id', models.PositiveIntegerField(null=True)),
('membership_is_removed', models.BooleanField(default=False)),
('membership_status', models.CharField(max_length=128, null=True)),
('enterprise_group_membership_uuid', models.UUIDField(null=True)),
],
options={
'verbose_name': 'Group Membership',
'verbose_name_plural': 'Group Memberships',
'db_table': 'group_membership',
'indexes': [models.Index(fields=['enterprise_group_uuid', 'group_type', 'enterprise_customer_user_id'], name='group_membe_enterpr_796f48_idx')],
},
),
]
46 changes: 46 additions & 0 deletions enterprise_data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class Meta:
user_account_creation_date = models.DateTimeField(null=True)
user_country_code = models.CharField(max_length=2, null=True)
user_username = models.CharField(max_length=255, null=True)
user_first_name = models.CharField(max_length=255, null=True)
user_last_name = models.CharField(max_length=255, null=True)
enterprise_name = models.CharField(max_length=255, db_index=True, null=False)
enterprise_customer_uuid = models.UUIDField(db_index=True, null=False)
enterprise_sso_uid = models.CharField(max_length=255, null=True)
Expand Down Expand Up @@ -396,6 +398,50 @@ def __repr__(self):
return self.__str__()


class EnterpriseGroupMembership(models.Model):
"""
Details of group memberships in enterprise reports.
"""

objects = EnterpriseReportingModelManager()

class Meta:
app_label = 'enterprise_data'
db_table = 'group_membership'
verbose_name = _("Group Membership")
verbose_name_plural = _("Group Memberships")
indexes = [
models.Index(fields=['enterprise_group_uuid', 'group_type', 'enterprise_customer_user_id']),
]

group_membership_unique_id = models.CharField(max_length=64, primary_key=True)
is_applies_to_all_contexts = models.BooleanField(default=False)
enterprise_customer_id = models.UUIDField(db_index=True, null=True)
enterprise_group_name = models.CharField(max_length=255, null=True)
enterprise_group_uuid = models.UUIDField(null=True)
group_is_removed = models.BooleanField(default=False)
group_type = models.CharField(max_length=128, null=True)
activated_at = models.DateTimeField(null=True)
enterprise_customer_user_id = models.PositiveIntegerField(null=True)
membership_is_removed = models.BooleanField(default=False)
membership_status = models.CharField(max_length=128, null=True)
enterprise_group_membership_uuid = models.UUIDField(null=True)

def __str__(self):
"""
Return a human-readable string representation of the object.
"""
return (f'<Enterprise Group Membership: {self.enterprise_group_name} '
f'(Group UUID: {self.enterprise_group_uuid}) '
f'for Customer ID {self.enterprise_customer_id}>')

def __repr__(self):
"""
Return uniquely identifying string representation.
"""
return self.__str__()


class EnterpriseUser(models.Model):
"""Information includes a mix of the user's meta data.
Expand Down
6 changes: 3 additions & 3 deletions enterprise_data/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ class EnrollmentsCSVRenderer(CSVStreamingRenderer):
'course_primary_program', 'primary_program_type', 'course_primary_subject', 'has_passed',
'last_activity_date', 'progress_status', 'passed_date', 'current_grade',
'letter_grade', 'enterprise_user_id', 'user_email', 'user_account_creation_date',
'user_country_code', 'user_username', 'enterprise_name', 'enterprise_customer_uuid',
'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours', 'is_subsidy',
'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
'user_country_code', 'user_username', 'user_first_name', 'user_last_name', 'enterprise_name',
'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours',
'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
]


Expand Down
27 changes: 27 additions & 0 deletions enterprise_data/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from enterprise_data.tests.test_utils import (
EnterpriseEnrollmentFactory,
EnterpriseGroupMembershipFactory,
EnterpriseOfferFactory,
EnterpriseSubsidyBudgetFactory,
EnterpriseUserFactory,
Expand Down Expand Up @@ -91,6 +92,32 @@ def test_string_conversion(self, method):
assert expected_str == method(self.enterprise_subsidy_budget)


@mark.django_db
@ddt.ddt
class TestEnterpriseGroupMembership(unittest.TestCase):
"""
Tests for Enterprise Group Membership model
"""

def setUp(self):
self.enterprise_group_membership = EnterpriseGroupMembershipFactory(
enterprise_customer_id='ee5e6b3a-069a-4947-bb8d-d2dbc323396c',
enterprise_group_name='Test Group',
enterprise_group_uuid='ee5e6b3a-069a-4947-bb8d-d2dbc323396d',
)
super().setUp()

@ddt.data(str, repr)
def test_string_conversion(self, method):
"""
Test conversion to string.
"""
expected_str = ('<Enterprise Group Membership: Test Group '
'(Group UUID: ee5e6b3a-069a-4947-bb8d-d2dbc323396d) '
'for Customer ID ee5e6b3a-069a-4947-bb8d-d2dbc323396c>')
assert expected_str == method(self.enterprise_group_membership)


@mark.django_db
@ddt.ddt
class TestEnterpriseUser(unittest.TestCase):
Expand Down
Loading

0 comments on commit 2a9faa1

Please sign in to comment.