Skip to content

Commit

Permalink
Merge branch 'master' into bilalqamar95/node-v20-upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
BilalQamar95 authored Sep 13, 2024
2 parents 6fc0d69 + 1745b40 commit 5b83379
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 71 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,34 @@ Unreleased
----------
* nothing unreleased

[4.25.12]
----------
* feat: add username query to enterprise customer user query

[4.25.11]
----------
* feat: add username query to enterprise customer user query

[4.25.10]
----------
* feat: update Enterprise Braze API key

[4.25.9]
----------
* fix: send LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED from the correct place.

[4.25.8]
----------
* feat: added migration for removing unencrypted client credentials

[4.25.7]
----------
* refactor: removed unencrypted credentials from sap config model

[4.25.6]
----------
* refactor: making unencrypted credentials nullable so after removing refs tests can run

[4.25.5]
----------
* feat: changing django enterprise customer summary columns
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.25.5"
__version__ = "4.25.12"
2 changes: 1 addition & 1 deletion enterprise/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1671,7 +1671,7 @@ class PendingEnterpriseCustomerAdminUserSerializer(serializers.ModelSerializer):
class Meta:
model = PendingEnterpriseCustomerAdminUser
fields = (
'enterprise_customer', 'user_email'
'id', 'enterprise_customer', 'user_email'
)

def validate(self, attrs):
Expand Down
6 changes: 4 additions & 2 deletions enterprise/api/v1/views/enterprise_customer_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ def filter_queryset_by_user_query(self, queryset, is_pending_user=False):

if user_query:
if not is_pending_user:
queryset = queryset.filter(
user_id__in=User.objects.filter(Q(email__icontains=user_query))
queryset = models.EnterpriseCustomerUser.objects.filter(
user_id__in=User.objects.filter(
Q(email__icontains=user_query) | Q(username__icontains=user_query)
)
)
else:
queryset = queryset.filter(user_email=user_query)
Expand Down
4 changes: 2 additions & 2 deletions enterprise/api_client/braze.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class BrazeAPIClient(BrazeClient):
API client for calls to Braze.
"""
def __init__(self):
braze_api_key = getattr(settings, 'EDX_BRAZE_API_KEY', None)
braze_api_key = getattr(settings, 'ENTERPRISE_BRAZE_API_KEY', None)
braze_api_url = getattr(settings, 'EDX_BRAZE_API_SERVER', None)
required_settings = ['EDX_BRAZE_API_KEY', 'EDX_BRAZE_API_SERVER']
required_settings = ['ENTERPRISE_BRAZE_API_KEY', 'EDX_BRAZE_API_SERVER']
for setting in required_settings:
if not getattr(settings, setting, None):
msg = f'Missing {setting} in settings required for Braze API Client.'
Expand Down
130 changes: 106 additions & 24 deletions enterprise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2097,14 +2097,37 @@ def is_audit_enrollment(self):
@property
def license(self):
"""
Returns the license associated with this enterprise course enrollment if one exists.
Returns the license fulfillment associated with this enterprise course enrollment if one exists.
"""
try:
associated_license = self.licensedenterprisecourseenrollment_enrollment_fulfillment # pylint: disable=no-member
except LicensedEnterpriseCourseEnrollment.DoesNotExist:
associated_license = None
return associated_license

@property
def learner_credit_fulfillment(self):
"""
Returns the Learner Credit fulfillment associated with this enterprise course enrollment if one exists.
"""
try:
associated_fulfillment = self.learnercreditenterprisecourseenrollment_enrollment_fulfillment # pylint: disable=no-member
except LearnerCreditEnterpriseCourseEnrollment.DoesNotExist:
associated_fulfillment = None
return associated_fulfillment

@property
def fulfillments(self):
"""
Find and return the related EnterpriseFulfillmentSource subclass, or empty list if there are none.
Returns:
list of EnterpriseFulfillmentSource subclass instances: all existing related fulfillments
"""
possible_fulfillments = [self.license, self.learner_credit_fulfillment]
existing_fulfillments = [f for f in possible_fulfillments if f]
return existing_fulfillments

@cached_property
def course_enrollment(self):
"""
Expand Down Expand Up @@ -2196,6 +2219,48 @@ def get_enterprise_uuids_with_user_and_course(cls, user_id, course_run_id, is_cu
)
return []

def set_unenrolled(self, desired_unenrolled):
"""
Idempotently set this object's fields to appear (un)enrolled and (un)saved-for-later.
Also, attempt to revoke any related fulfillment, which in turn is also idempotent.
This method and the fulfillment's revoke() call each other!!! If you edit either method, make sure to preserve
base cases that terminate infinite recursion.
TODO: revoke entitlements as well?
"""
changed = False
if desired_unenrolled:
if not self.unenrolled or not self.saved_for_later:
self.saved_for_later = True
self.unenrolled = True
self.unenrolled_at = localized_utcnow()
changed = True
else:
if self.unenrolled or self.saved_for_later:
self.saved_for_later = False
self.unenrolled = False
self.unenrolled_at = None
changed = True
if changed:
LOGGER.info(
f"Marking EnterpriseCourseEnrollment as unenrolled={desired_unenrolled} "
f"for LMS user {self.enterprise_customer_user.user_id} "
f"and course {self.course_id}"
)
self.save()
# Find and revoke/reactivate any related fulfillment if unenrolling the EnterpriseCourseEnrollment.
# By only updating the related object on updates to self, we prevent infinite recursion.
if desired_unenrolled:
for fulfillment in self.fulfillments:
if not fulfillment.is_revoked: # redundant base case to terminate loops.
fulfillment.revoke()
# Fulfillment reactivation on ECE reenrollment is unsupported. We'd need to collect a
# transaction UUID from the caller, but the caller at the time of writing is not aware of any
# transaction. Furthermore, we wouldn't know which fulfillment to reactivate, if there were multiple
# related fulfillment types.

def __str__(self):
"""
Create string representation of the enrollment.
Expand Down Expand Up @@ -2285,34 +2350,50 @@ def enterprise_customer_user(self):

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.
Idempotently unenroll/revoke this fulfillment and associated EnterpriseCourseEnrollment.
Subclasses may override this function to additionally emit revocation events.
This method and EnterpriseCourseEnrollment.set_unenrolled() call each other!!! If you edit either method, make
sure to preserve base cases that terminate infinite recursion.
TODO: revoke entitlements as well?
"""
if self.enterprise_course_enrollment:
self.enterprise_course_enrollment.saved_for_later = True
self.enterprise_course_enrollment.unenrolled = True
self.enterprise_course_enrollment.unenrolled_at = localized_utcnow()
self.enterprise_course_enrollment.save()
Notes:
* This object and the associated EnterpriseCourseEnrollment may both be saved.
* Subclasses may override this function to additionally emit revocation events.
self.is_revoked = True
self.save()
Returns:
bool: True if self.is_revoked was changed.
"""
changed = False
if not self.is_revoked:
LOGGER.info(f"Marking fulfillment {str(self)} as revoked.")
changed = True
self.is_revoked = True
self.save()
# Find and unenroll any related EnterpriseCourseEnrollment.
# By only updating the related object on updates to self, we prevent infinite recursion.
if ece := self.enterprise_course_enrollment:
if not ece.unenrolled: # redundant base case to terminate loops.
ece.set_unenrolled(True)
return changed

def reactivate(self, **kwargs):
"""
Idempotently reactivates this enterprise fulfillment source.
"""
if self.enterprise_course_enrollment:
self.enterprise_course_enrollment.saved_for_later = False
self.enterprise_course_enrollment.unenrolled = False
self.enterprise_course_enrollment.unenrolled_at = None
self.enterprise_course_enrollment.save()
self.is_revoked = False
self.save()
Returns:
bool: True if self.is_revoked was changed.
"""
changed = False
if self.is_revoked:
LOGGER.info(f"Marking fulfillment {str(self)} as reactivated.")
changed = True
self.is_revoked = False
self.save()
# Find and REenroll any related EnterpriseCourseEnrollment.
# By only updating the related object on updates to self, we prevent infinite recursion.
if ece := self.enterprise_course_enrollment:
if ece.unenrolled: # redundant base case to terminate loops.
ece.set_unenrolled(False)
return changed

def __str__(self):
"""
Expand All @@ -2337,8 +2418,9 @@ def revoke(self):
"""
Revoke this LearnerCreditEnterpriseCourseEnrollment, and emit a revoked event.
"""
super().revoke()
send_learner_credit_course_enrollment_revoked_event(self)
if changed := super().revoke():
send_learner_credit_course_enrollment_revoked_event(self)
return changed

def reactivate(self, transaction_id=None, **kwargs):
"""
Expand All @@ -2354,7 +2436,7 @@ def reactivate(self, transaction_id=None, **kwargs):
f"getting this enrollment for free."
)
self.transaction_id = transaction_id
super().reactivate()
return super().reactivate()

transaction_id = models.UUIDField(
primary_key=False,
Expand Down
4 changes: 2 additions & 2 deletions enterprise/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ def root(*args):
ENTERPRISE_SSO_ORCHESTRATOR_CONFIGURE_PATH = 'configure'
ENTERPRISE_SSO_ORCHESTRATOR_CONFIGURE_EDX_OAUTH_PATH = 'configure-edx-oauth'

EDX_BRAZE_API_KEY='test-api-key'
EDX_BRAZE_API_SERVER='test-api-server'
ENTERPRISE_BRAZE_API_KEY = 'test-api-key'
EDX_BRAZE_API_SERVER = 'test-api-server'
BRAZE_GROUPS_INVITATION_EMAIL_CAMPAIGN_ID = 'test-invitation-campaign-id'
BRAZE_GROUPS_REMOVAL_EMAIL_CAMPAIGN_ID = 'test-removal-campaign-id'
18 changes: 2 additions & 16 deletions enterprise/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from enterprise.utils import (
NotConnectedToOpenEdX,
get_default_catalog_content_filter,
localized_utcnow,
unset_enterprise_learner_language,
unset_language_of_all_enterprise_learners,
)
Expand Down Expand Up @@ -364,14 +363,7 @@ def course_enrollment_changed_receiver(sender, **kwargs): # pylint: disable=
enterprise_customer_user__user_id=enrollment.user.id,
).first()
if enterprise_enrollment and enrollment.is_active:
logger.info(
f"Marking EnterpriseCourseEnrollment as enrolled (unenrolled_at=NULL) for user {enrollment.user} and "
f"course {enrollment.course.course_key}"
)
enterprise_enrollment.unenrolled = False
enterprise_enrollment.unenrolled_at = None
enterprise_enrollment.saved_for_later = False
enterprise_enrollment.save()
enterprise_enrollment.set_unenrolled(False)
# Note: If the CourseEnrollment is being flipped to is_active=False, then this handler is a no-op.
# In that case, the `enterprise_unenrollment_receiver` signal handler below will run.

Expand All @@ -386,13 +378,7 @@ def enterprise_unenrollment_receiver(sender, **kwargs): # pylint: disable=un
enterprise_customer_user__user_id=enrollment.user.id,
).first()
if enterprise_enrollment:
logger.info(
f"Marking EnterpriseCourseEnrollment as unenrolled for user {enrollment.user} and "
f"course {enrollment.course.course_key}"
)
enterprise_enrollment.unenrolled = True
enterprise_enrollment.unenrolled_at = localized_utcnow()
enterprise_enrollment.save()
enterprise_enrollment.set_unenrolled(True)


def create_enterprise_enrollment_receiver(sender, instance, **kwargs): # pylint: disable=unused-argument
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.23 on 2024-09-09 15:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sap_success_factors', '0022_auto_20240906_1349'),
]

operations = [
migrations.AlterField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='key',
field=models.CharField(blank=True, default='', help_text='OAuth client identifier.', max_length=255, null=True, verbose_name='Client ID'),
),
migrations.AlterField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='secret',
field=models.CharField(blank=True, default='', help_text='OAuth client secret.', max_length=255, null=True, verbose_name='Client Secret'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.2.23 on 2024-09-09 16:14

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('sap_success_factors', '0023_auto_20240909_1556'),
]

operations = [
migrations.RemoveField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='key',
),
migrations.RemoveField(
model_name='sapsuccessfactorsenterprisecustomerconfiguration',
name='secret',
),
]
14 changes: 0 additions & 14 deletions integrated_channels/sap_success_factors/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,6 @@ class SAPSuccessFactorsEnterpriseCustomerConfiguration(EnterpriseCustomerPluginC
(USER_TYPE_USER, 'User'),
(USER_TYPE_ADMIN, 'Admin'),
)
key = models.CharField(
max_length=255,
blank=True,
default='',
verbose_name="Client ID",
help_text=_("OAuth client identifier.")
)

decrypted_key = EncryptedCharField(
max_length=255,
Expand Down Expand Up @@ -137,13 +130,6 @@ def encrypted_key(self, value):
verbose_name="SAP User ID",
help_text=_("Success factors user identifier.")
)
secret = models.CharField(
max_length=255,
blank=True,
default='',
verbose_name="Client Secret",
help_text=_("OAuth client secret.")
)

decrypted_secret = EncryptedCharField(
max_length=255,
Expand Down
Loading

0 comments on commit 5b83379

Please sign in to comment.