Skip to content

Commit

Permalink
Merge branch 'master' of github.com:openedx/edx-enterprise into Mueez…
Browse files Browse the repository at this point in the history
…Khan/Added-Encrypted-Client-Secret-In-SAP-Configuration
  • Loading branch information
MueezKhan246 committed Sep 6, 2024
2 parents b5cc335 + 336dee5 commit 0ddfe45
Show file tree
Hide file tree
Showing 16 changed files with 349 additions and 84 deletions.
14 changes: 11 additions & 3 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,26 @@ Unreleased
----------
* nothing unreleased

[4.23.23]
[4.25.3]
----------
* feat: added encrypted client secret for SAP config

[4.23.22]
[4.25.2]
----------
* feat: added migration file for removing char field decrypted_secret

[4.23.21]
[4.25.1]
----------
* feat: removed char field decrypted_secret references

[4.25.0]
----------
* feat: emit learner credit unenrollment event

[4.24.0]
----------
* fix: customer sorting error in customer support tool endpoint and added user query param

[4.23.20]
----------
* feat: added migrations to remove client_id and client_secret from CanvasEnterpriseCustomerConfiguration
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.23.23"
__version__ = "4.25.3"
128 changes: 67 additions & 61 deletions enterprise/api/v1/views/enterprise_customer_support.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

"""
Views for the ``enterprise-user`` API endpoint.
"""
Expand All @@ -25,18 +24,21 @@

class EnterpriseCustomerSupportPaginator(PageNumberPagination):
"""Custom paginator for the enterprise customer support."""

page_size = 8

def get_paginated_response(self, data):
"""Return a paginated style `Response` object for the given output data."""
return response.Response(
OrderedDict([
('count', self.page.paginator.count),
('num_pages', self.page.paginator.num_pages),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('results', data),
])
OrderedDict(
[
("count", self.page.paginator.count),
("num_pages", self.page.paginator.num_pages),
("next", self.get_next_link()),
("previous", self.get_previous_link()),
("results", data),
]
)
)

def paginate_queryset(self, queryset, request, view=None):
Expand All @@ -55,100 +57,100 @@ class EnterpriseCustomerSupportViewSet(EnterpriseReadOnlyModelViewSet):
"""
API views for the ``enterprise-customer-support`` API endpoint.
"""

queryset = models.PendingEnterpriseCustomerUser.objects.all()
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
permission_classes = (permissions.IsAuthenticated,)
paginator = EnterpriseCustomerSupportPaginator()

ordering_fields = ['id']
filterset_fields = ['user_email']
ordering_fields = ["is_admin", "username", "pending_enterprise_customer"]

def filter_queryset_by_email(self, queryset, is_pending_user=False):
def filter_queryset_by_user_query(self, queryset, is_pending_user=False):
"""
Filter queryset based on user provided email address
Filter queryset based on user provided query
"""
filter_email = self.request.query_params.get('user_email', None)
user_query = self.request.query_params.get("user_query", None)

if filter_email:
if user_query:
if not is_pending_user:
queryset = queryset.filter(
user_id__in=User.objects.filter(Q(email__icontains=filter_email))
user_id__in=User.objects.filter(Q(email__icontains=user_query))
)
else:
queryset = queryset.filter(user_email=filter_email)
queryset = queryset.filter(user_email=user_query)

return queryset

def retrieve(self, request, *args, **kwargs):
"""
Filter down the queryset of groups available to the requesting uuid.
"""
enterprise_uuid = kwargs.get('enterprise_uuid', None)
enterprise_uuid = kwargs.get("enterprise_uuid", None)
users = []

try:
enterprise_customer_queryset = models.EnterpriseCustomerUser.objects.filter(
enterprise_customer__uuid=enterprise_uuid,
)
enterprise_customer_queryset = self.filter_queryset_by_email(enterprise_customer_queryset)
enterprise_customer_queryset = self.filter_queryset_by_user_query(
enterprise_customer_queryset
)
users.extend(enterprise_customer_queryset)

pending_enterprise_customer_queryset = models.PendingEnterpriseCustomerUser.objects.filter(
enterprise_customer__uuid=enterprise_uuid
).order_by('user_email')
pending_enterprise_customer_queryset = self.filter_queryset_by_email(
pending_enterprise_customer_queryset,
is_pending_user=True
pending_enterprise_customer_queryset = (
models.PendingEnterpriseCustomerUser.objects.filter(
enterprise_customer__uuid=enterprise_uuid
).order_by("user_email")
)
pending_enterprise_customer_queryset = self.filter_queryset_by_user_query(
pending_enterprise_customer_queryset, is_pending_user=True
)
users.extend(pending_enterprise_customer_queryset)

except ValidationError:
# did not find UUID match in either EnterpriseCustomerUser or PendingEnterpriseCustomerUser
return response.Response(
{'detail': 'Could not find enterprise uuid {}'.format(enterprise_uuid)},
status=status.HTTP_404_NOT_FOUND
{"detail": "Could not find enterprise uuid {}".format(enterprise_uuid)},
status=status.HTTP_404_NOT_FOUND,
)

# default sort criteria
is_reversed = False
sort_field = 'first_name'

ordering_criteria = self.request.query_params.get('ordering', None)

# apply pre-serialization ordering by user criteria before the users
# get divvied up by pagination
ordering_criteria = self.request.query_params.get("ordering", None)
if ordering_criteria:
is_reversed = '-' in ordering_criteria
sort_field = 'user_id'

# sort the users by default or specified criteria since the queryset will get
# split up during pagination and the post-serialization sort operations
# will be only applied to a single page of results
users = sorted(
users,
key=(
lambda k:
getattr(k, sort_field)
if hasattr(k, sort_field)
else k.id
),
reverse=is_reversed
)

# paginate the queryset
users_page = self.paginator.paginate_queryset(
users,
request,
view=self
)
users_page = self.paginator.paginate_queryset(users, request, view=self)

# serialize the paged dataset
serializer = serializers.EnterpriseUserSerializer(
users_page,
many=True
)
serializer = serializers.EnterpriseUserSerializer(users_page, many=True)
serializer_data = serializer.data

# apply pre-serialization ordering by user criteria before the users
# get divvied up by pagination
if ordering_criteria in ("administrator", "learner"):
serializer_data = sorted(
serializer_data,
key=lambda k: (
(k["is_admin"])
),
reverse=is_reversed
)
if ordering_criteria == "details":
serializer_data = sorted(
serializer_data,
key=lambda k: (
(
(
k["enterprise_customer_user"]["username"]
if k["enterprise_customer_user"] is not None
else k["pending_enterprise_customer_user"]["user_email"]
),
)
),
reverse=is_reversed,
)
# Apply post-serialization default ordering criteria (first by is_admin,
# then first name) only if user does not specify ordering criteria;
# Process this after the data has been serialized since the is_admin
Expand All @@ -159,10 +161,14 @@ def retrieve(self, request, *args, **kwargs):
key=lambda k: (
# sort by is_admin = True first (i.e. -1),
# then sort by first_name lexicographically
(-1 * k['is_admin'], k['enterprise_customer_user']['first_name'])
if k['enterprise_customer_user'] is not None
else -1 * k['is_admin']
)
(
-k["is_admin"],
(
k["enterprise_customer_user"]["username"]
if k["enterprise_customer_user"] is not None
else k["pending_enterprise_customer_user"]["user_email"]
),
)
),
)

return self.paginator.get_paginated_response(serializer_data)
3 changes: 3 additions & 0 deletions enterprise/api/v1/views/enterprise_subsidy_fulfillment.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ def unenrolled(self, request, *args, **kwargs):
retrieve_licensed_enrollments (bool): If true, return data related to licensed enrollments instead of
learner credit
"""
LOGGER.warning(
"[DEPRECATION] This view is deprecated for lack of purpose. Logging to confirm utilization drop-off.",
)
queryset = self._get_unenrolled_fulfillments()
serializer_class = self.get_unenrolled_fulfillment_serializer_class()
serializer = serializer_class(queryset, many=True)
Expand Down
70 changes: 70 additions & 0 deletions enterprise/event_bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Functions for serializing and emiting Open edX event bus signals.
"""
from openedx_events.enterprise.data import (
EnterpriseCourseEnrollment,
EnterpriseCustomerUser,
LearnerCreditEnterpriseCourseEnrollment,
)
from openedx_events.enterprise.signals import LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED


def serialize_learner_credit_course_enrollment(learner_credit_course_enrollment):
"""
Serializes the ``LearnerCreditEnterpriseCourseEnrollment`` into a defined set of attributes
for use in the event-bus signal.
"""
enterprise_course_enrollment = learner_credit_course_enrollment.enterprise_course_enrollment
enterprise_customer_user = enterprise_course_enrollment.enterprise_customer_user

enterprise_customer_user_data = EnterpriseCustomerUser(
id=enterprise_customer_user.id,
created=enterprise_customer_user.created,
modified=enterprise_customer_user.modified,
enterprise_customer_uuid=enterprise_customer_user.enterprise_customer.uuid,
user_id=enterprise_customer_user.user_id,
active=enterprise_customer_user.active,
linked=enterprise_customer_user.linked,
is_relinkable=enterprise_customer_user.is_relinkable,
invite_key=enterprise_customer_user.invite_key.uuid if enterprise_customer_user.invite_key else None,
should_inactivate_other_customers=enterprise_customer_user.should_inactivate_other_customers,
)
enterprise_course_enrollment_data = EnterpriseCourseEnrollment(
id=enterprise_course_enrollment.id,
created=enterprise_course_enrollment.created,
modified=enterprise_course_enrollment.modified,
enterprise_customer_user=enterprise_customer_user_data,
course_id=enterprise_course_enrollment.course_id,
saved_for_later=enterprise_course_enrollment.saved_for_later,
source_slug=enterprise_course_enrollment.source.slug if enterprise_course_enrollment.source else None,
unenrolled=enterprise_course_enrollment.unenrolled,
unenrolled_at=enterprise_course_enrollment.unenrolled_at,
)
data = LearnerCreditEnterpriseCourseEnrollment(
uuid=learner_credit_course_enrollment.uuid,
created=learner_credit_course_enrollment.created,
modified=learner_credit_course_enrollment.modified,
fulfillment_type=learner_credit_course_enrollment.fulfillment_type,
enterprise_course_entitlement_uuid=(
learner_credit_course_enrollment.enterprise_course_entitlement.uuid
if learner_credit_course_enrollment.enterprise_course_entitlement
else None
),
enterprise_course_enrollment=enterprise_course_enrollment_data,
is_revoked=learner_credit_course_enrollment.is_revoked,
transaction_id=learner_credit_course_enrollment.transaction_id,
)
return data


def send_learner_credit_course_enrollment_revoked_event(learner_credit_course_enrollment):
"""
Sends the LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED openedx event.
Args:
learner_credit_course_enrollment (enterprise.models.LearnerCreditEnterpriseCourseEnrollment):
An enterprise learner credit fulfillment record that was revoked.
"""
LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED.send_event(
learner_credit_course_enrollment=serialize_learner_credit_course_enrollment(learner_credit_course_enrollment),
)
10 changes: 10 additions & 0 deletions enterprise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
json_serialized_course_modes,
)
from enterprise.errors import LinkUserToEnterpriseError
from enterprise.event_bus import send_learner_credit_course_enrollment_revoked_event
from enterprise.logging import getEnterpriseLogger
from enterprise.tasks import send_enterprise_email_notification
from enterprise.utils import (
Expand Down Expand Up @@ -2287,6 +2288,8 @@ def revoke(self):
Marks this object as revoked and marks the associated EnterpriseCourseEnrollment
as "saved for later". This object and the associated EnterpriseCourseEnrollment are both saved.
Subclasses may override this function to additionally emit revocation events.
TODO: revoke entitlements as well?
"""
if self.enterprise_course_enrollment:
Expand Down Expand Up @@ -2330,6 +2333,13 @@ class LearnerCreditEnterpriseCourseEnrollment(EnterpriseFulfillmentSource):
.. no_pii:
"""

def revoke(self):
"""
Revoke this LearnerCreditEnterpriseCourseEnrollment, and emit a revoked event.
"""
super().revoke()
send_learner_credit_course_enrollment_revoked_event(self)

def reactivate(self, transaction_id=None, **kwargs):
"""
Idmpotently reactivates this LearnerCreditEnterpriseCourseEnrollment.
Expand Down
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ edx-tincan-py35
edx-toggles
jsondiff
jsonfield
openedx-events
paramiko
path.py
pillow
Expand Down
3 changes: 3 additions & 0 deletions requirements/common_constraints.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@




# A central location for most common version constraints
# (across edx repos) for pip-installation.
#
Expand Down
Loading

0 comments on commit 0ddfe45

Please sign in to comment.