Skip to content

Commit

Permalink
feat: Create django admin for default enrollments
Browse files Browse the repository at this point in the history
  • Loading branch information
brobro10000 committed Oct 18, 2024
1 parent a274850 commit fa7ec13
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Unreleased
----------
* nothing unreleased

[4.28.1]
--------
* feat: Create django admin for default enrollments

[4.28.0]
--------
* feat: add default enrollment models
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.28.0"
__version__ = "4.28.1"
27 changes: 27 additions & 0 deletions enterprise/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
)
from enterprise.api_client.lms import CourseApiClient, EnrollmentApiClient
from enterprise.config.models import UpdateRoleAssignmentsWithCustomersConfig
from enterprise.models import DefaultEnterpriseEnrollmentIntention
from enterprise.utils import (
discovery_query_url,
get_all_field_names,
Expand Down Expand Up @@ -109,6 +110,31 @@ def get_formset(self, request, obj=None, **kwargs):
return formset


class EnterpriseCustomerDefaultEnterpriseEnrollmentIntentionInline(admin.TabularInline):
"""
Django admin model for EnterpriseCustomerCatalog.
The admin interface has the ability to edit models on the same page as a parent model. These are called inlines.
https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.StackedInline
"""

model = models.DefaultEnterpriseEnrollmentIntention
fields = ('content_key', 'course_run_key_for_enrollment',)
readonly_fields = ('course_run_key_for_enrollment',)
extra = 0
can_delete = True

@admin.display(description='Course run key for enrollment')
def course_run_key_for_enrollment(self, obj):
"""
Returns the course run key based on the content type.
If the content type is a course, we retrieve the advertised_course_run key.
"""
content_type = obj.content_type
if content_type == 'course_run':
return obj.content_key
return obj.current_course_run_key


class PendingEnterpriseCustomerAdminUserInline(admin.TabularInline):
"""
Django admin inline model for PendingEnterpriseCustomerAdminUser.
Expand Down Expand Up @@ -227,6 +253,7 @@ class EnterpriseCustomerAdmin(DjangoObjectActions, SimpleHistoryAdmin):
EnterpriseCustomerBrandingConfigurationInline,
EnterpriseCustomerIdentityProviderInline,
EnterpriseCustomerCatalogInline,
EnterpriseCustomerDefaultEnterpriseEnrollmentIntentionInline,
PendingEnterpriseCustomerAdminUserInline,
]

Expand Down
34 changes: 34 additions & 0 deletions enterprise/cache_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Utils for interacting with cache interfaces.
"""
import hashlib

from edx_django_utils.cache import TieredCache

from django.conf import settings

from enterprise import __version__ as code_version

CACHE_KEY_SEP = ':'
DEFAULT_NAMESPACE = 'edx-enterprise-default'


def versioned_cache_key(*args):
"""
Utility to produce a versioned cache key, which includes
an optional settings variable and the current code version,
so that we can perform key-based cache invalidation.
"""
components = [str(arg) for arg in args]
components.append(code_version)

Check warning on line 23 in enterprise/cache_utils.py

View check run for this annotation

Codecov / codecov/patch

enterprise/cache_utils.py#L23

Added line #L23 was not covered by tests
if stamp_from_settings := getattr(settings, 'CACHE_KEY_VERSION_STAMP', None):
components.append(stamp_from_settings)
decoded_cache_key = CACHE_KEY_SEP.join(components)
return hashlib.sha512(decoded_cache_key.encode()).hexdigest()

Check warning on line 27 in enterprise/cache_utils.py

View check run for this annotation

Codecov / codecov/patch

enterprise/cache_utils.py#L25-L27

Added lines #L25 - L27 were not covered by tests


def request_cache(namespace=DEFAULT_NAMESPACE):
"""
Helper that returns a namespaced RequestCache instance.
"""
return TieredCache(namespace=namespace)

Check warning on line 34 in enterprise/cache_utils.py

View check run for this annotation

Codecov / codecov/patch

enterprise/cache_utils.py#L34

Added line #L34 was not covered by tests
56 changes: 56 additions & 0 deletions enterprise/content_metadata/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Python API for interacting with content metadata.
TODO: refactor subsidy_access_policy/content_metadata_api.py
into this module.
"""
import logging

from requests.exceptions import HTTPError

from django.conf import settings
from django.core.cache import cache

from edx_django_utils.cache import TieredCache
from enterprise.api_client.enterprise_catalog import EnterpriseCatalogApiClient
from enterprise.cache_utils import versioned_cache_key

logger = logging.getLogger(__name__)

DEFAULT_CACHE_TIMEOUT = getattr(settings, 'CONTENT_METADATA_CACHE_TIMEOUT', 60 * 5)


def get_and_cache_catalog_content_metadata(enterprise_customer_uuid, content_key, timeout=None):
"""
Returns the metadata corresponding to the requested
``content_key`` within catalogs associated to the provided ``enterprise_customer``.
The response is cached in a ``TieredCache`` (meaning in both the RequestCache,
_and_ the django cache for the configured expiration period).
Returns: A dict with content metadata for the given key.
Raises: An HTTPError if there's a problem getting the content metadata
via the enterprise-catalog service.
"""
cache_key = versioned_cache_key('contains_content_key', enterprise_customer_uuid, content_key)
cached_response = TieredCache.get_cached_response(cache_key)

Check warning on line 35 in enterprise/content_metadata/api.py

View check run for this annotation

Codecov / codecov/patch

enterprise/content_metadata/api.py#L34-L35

Added lines #L34 - L35 were not covered by tests
if cached_response.is_found:
logger.info(f'cache hit for enterprise customer {enterprise_customer_uuid} and content {content_key}')
return cached_response.value

Check warning on line 38 in enterprise/content_metadata/api.py

View check run for this annotation

Codecov / codecov/patch

enterprise/content_metadata/api.py#L37-L38

Added lines #L37 - L38 were not covered by tests

try:
result = EnterpriseCatalogApiClient().get_content_metadata_content_identifier(

Check warning on line 41 in enterprise/content_metadata/api.py

View check run for this annotation

Codecov / codecov/patch

enterprise/content_metadata/api.py#L40-L41

Added lines #L40 - L41 were not covered by tests
enterprise_uuid=enterprise_customer_uuid,
content_id=content_key,
)
except HTTPError as exc:
raise exc

Check warning on line 46 in enterprise/content_metadata/api.py

View check run for this annotation

Codecov / codecov/patch

enterprise/content_metadata/api.py#L45-L46

Added lines #L45 - L46 were not covered by tests

logger.info(

Check warning on line 48 in enterprise/content_metadata/api.py

View check run for this annotation

Codecov / codecov/patch

enterprise/content_metadata/api.py#L48

Added line #L48 was not covered by tests
'Fetched catalog for customer %s and content_key %s. Result = %s',
enterprise_customer_uuid,
content_key,
result,
)
TieredCache.set_all_tiers(cache_key, result, timeout or DEFAULT_CACHE_TIMEOUT)
return result

Check warning on line 55 in enterprise/content_metadata/api.py

View check run for this annotation

Codecov / codecov/patch

enterprise/content_metadata/api.py#L54-L55

Added lines #L54 - L55 were not covered by tests

28 changes: 27 additions & 1 deletion enterprise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
FulfillmentTypes,
json_serialized_course_modes,
)
from enterprise.content_metadata.api import get_and_cache_catalog_content_metadata
from enterprise.errors import LinkUserToEnterpriseError
from enterprise.event_bus import send_learner_credit_course_enrollment_revoked_event
from enterprise.logging import getEnterpriseLogger
Expand All @@ -71,6 +72,7 @@
CourseEnrollmentDowngradeError,
CourseEnrollmentPermissionError,
NotConnectedToOpenEdX,
get_advertised_course_run,
get_configuration_value,
get_default_invite_key_expiration_date,
get_ecommerce_worker_user,
Expand Down Expand Up @@ -2511,12 +2513,31 @@ class DefaultEnterpriseEnrollmentIntention(TimeStampedModel, SoftDeletableModel)
)
history = HistoricalRecords()

def get_cached_content_metadata(self):
"""
Retrieves the cached content metadata for the instance content_key
Determines catalog from the catalogs associated to the enterprise customer
"""
return get_and_cache_catalog_content_metadata(
enterprise_customer_uuid=self.enterprise_customer.uuid,
content_key=self.content_key,
)


@cached_property
def current_course_run(self): # pragma: no cover
"""
Metadata describing the current course run for this default enrollment intention.
"""
return {}
content_metadata_items = self.get_cached_content_metadata()
if not content_metadata_items:
return {}

content_metadata_item = content_metadata_items[0]
if self.content_type == 'course':
return get_advertised_course_run(content_metadata_item)
course_runs = content_metadata_item.get('course_runs', {})
return course_runs.get(self.content_key, {})

@property
def current_course_run_key(self): # pragma: no cover
Expand All @@ -2539,6 +2560,11 @@ def current_course_run_enroll_by_date(self): # pragma: no cover
"""
return datetime.datetime.min

def save(self, *args, **kwargs):
if not self.content_type:
self.content_type = 'course_run' if self.content_key.startswith('course-v1') else 'course'
super().save(*args, **kwargs)


class DefaultEnterpriseEnrollmentRealization(TimeStampedModel):
"""
Expand Down

0 comments on commit fa7ec13

Please sign in to comment.