Skip to content

Commit

Permalink
Merge branch 'master' into kiram15/ENT-9670
Browse files Browse the repository at this point in the history
  • Loading branch information
kiram15 committed Oct 28, 2024
2 parents d4b6bcb + 3e127f4 commit 44d6a9a
Show file tree
Hide file tree
Showing 16 changed files with 542 additions and 43 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ Unreleased
----------
* nothing unreleased

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

[4.28.4]
--------
* feat: updating the character count for group name to 255

[4.28.3]
--------
* feat: removing all references of to-be-deleted field
Expand Down
17 changes: 17 additions & 0 deletions catalog-info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'edx-enterprise'
description: "The Open edx Enterprise Service app provides enterprise features to the Open edX platform"
tags:
- enterprise
- ent
- library
spec:
owner: group:openedx-unmaintained
type: 'library'
lifecycle: 'production'

9 changes: 5 additions & 4 deletions docs/decisions/0015-default-enrollments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ Core requirements
Decision
========
We will implement two new models:
* ``DefaultEnterpriseEnrollmentIntention`` to represent the course/runs that
learners should be automatically enrolled into, post-logistration, for a given enterprise.
* ``DefaultEnterpriseEnrollmentRealization``which represents the mapping between the intention
and actual, **realized** enrollment record(s) for the learner/customer.
* ``DefaultEnterpriseEnrollmentIntention`` to represent the course/runs that learners
should be automatically enrolled into, post-logistration, for a given enterprise.
* ``DefaultEnterpriseEnrollmentRealization`` which represents the mapping between the intention
and actual, **realized** enrollment record(s) for the learner/customer.

Qualities
---------
Expand All @@ -58,6 +58,7 @@ however, we will always discern the correct course run key to use for enrollment

Post-enrollment related models (e.g., ``EnterpriseCourseEnrollment`` and ``DefaultEnterpriseEnrollmentRealization``)
will always primarily be associated with the course run associated with the ``DefaultEnterpriseEnrollmentIntention``:

* If content_key is a top-level course, the course run key used when enrolling
(converting to ``EnterpriseCourseEnrollment`` and ``DefaultEnterpriseEnrollmentRealization``)
is the currently advertised course run.
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.3"
__version__ = "4.29.0"
74 changes: 69 additions & 5 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,34 @@ 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_key', 'course_run_key_for_enrollment',)
readonly_fields = ('course_key', 'course_run_key_for_enrollment',)
extra = 0
can_delete = True

@admin.display(description='Course key')
def course_key(self, obj):
"""
Returns the course run key.
"""
return obj.course_key

@admin.display(description='Course run key for enrollment')
def course_run_key_for_enrollment(self, obj):
"""
Returns the course run key.
"""
return obj.course_run_key


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

Expand Down Expand Up @@ -1294,7 +1324,6 @@ class LearnerCreditEnterpriseCourseEnrollmentAdmin(admin.ModelAdmin):
'uuid',
'fulfillment_type',
'enterprise_course_enrollment',
'is_revoked',
'modified',
)

Expand Down Expand Up @@ -1326,14 +1355,32 @@ class DefaultEnterpriseEnrollmentIntentionAdmin(admin.ModelAdmin):
list_display = (
'uuid',
'enterprise_customer',
'content_key',
'content_type',
'is_removed',
)

list_filter = ('is_removed',)

fields = (
'enterprise_customer',
'content_key',
'uuid',
'is_removed',
'content_type',
'course_key',
'course_run_key',
'created',
'modified',
)

readonly_fields = (
'current_course_run_key',
'current_course_run_enrollable',
'current_course_run_enroll_by_date',
'uuid',
'content_type',
'course_key',
'course_run_key',
'created',
'modified',
)

search_fields = (
Expand All @@ -1345,5 +1392,22 @@ class DefaultEnterpriseEnrollmentIntentionAdmin(admin.ModelAdmin):
ordering = ('-modified',)

class Meta:
fields = '__all__'
model = models.DefaultEnterpriseEnrollmentIntention

def get_queryset(self, request): # pylint: disable=unused-argument
"""
Return a QuerySet of all model instances.
"""
return self.model.all_objects.get_queryset()

def formfield_for_dbfield(self, db_field, request, **kwargs):
"""
Customize the form field for the `is_removed` field.
"""
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)

if db_field.name == 'is_removed':
formfield.help_text = 'Whether this record is soft-deleted. Soft-deleted records ' \
'are not used but may be re-enabled if needed.'

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

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)
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()
60 changes: 60 additions & 0 deletions enterprise/content_metadata/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Python API for interacting with content metadata.
"""
import logging

from edx_django_utils.cache import TieredCache
from requests.exceptions import HTTPError

from django.conf import settings

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_customer_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('get_content_metadata_content_identifier', enterprise_customer_uuid, content_key)
cached_response = TieredCache.get_cached_response(cache_key)
if cached_response.is_found:
logger.info(f'cache hit for enterprise customer {enterprise_customer_uuid} and content {content_key}')
return cached_response.value

try:
result = EnterpriseCatalogApiClient().get_content_metadata_content_identifier(
enterprise_uuid=enterprise_customer_uuid,
content_id=content_key,
)
except HTTPError as exc:
raise exc

if not result:
logger.warning(
'No content found for customer %s and content_key %s',
enterprise_customer_uuid,
content_key,
)
return {}

logger.info(
'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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 4.2.16 on 2024-10-23 22:38

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('enterprise', '0224_alter_enterprisegroup_applies_to_all_contexts_and_more'),
]

operations = [
migrations.RemoveField(
model_name='enterprisegroup',
name='applies_to_all_contexts',
),
migrations.RemoveField(
model_name='historicalenterprisegroup',
name='applies_to_all_contexts',
),
migrations.AlterField(
model_name='enterprisegroup',
name='name',
field=models.CharField(help_text='Specifies enterprise group name.', max_length=250),
),
migrations.AlterField(
model_name='historicalenterprisegroup',
name='name',
field=models.CharField(help_text='Specifies enterprise group name.', max_length=250),
),
]
23 changes: 23 additions & 0 deletions enterprise/migrations/0226_alter_enterprisegroup_name_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-10-23 22:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('enterprise', '0225_remove_enterprisegroup_applies_to_all_contexts_and_more'),
]

operations = [
migrations.AlterField(
model_name='enterprisegroup',
name='name',
field=models.CharField(help_text='Specifies enterprise group name.', max_length=255),
),
migrations.AlterField(
model_name='historicalenterprisegroup',
name='name',
field=models.CharField(help_text='Specifies enterprise group name.', max_length=255),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.16 on 2024-10-24 15:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('enterprise', '0226_alter_enterprisegroup_name_and_more'),
]

operations = [
migrations.AlterField(
model_name='defaultenterpriseenrollmentintention',
name='content_type',
field=models.CharField(blank=True, choices=[('course', 'Course'), ('course_run', 'Course Run')], help_text='The type of content (e.g. a course vs. a course run).', max_length=127, null=True),
),
migrations.AlterField(
model_name='historicaldefaultenterpriseenrollmentintention',
name='content_type',
field=models.CharField(blank=True, choices=[('course', 'Course'), ('course_run', 'Course Run')], help_text='The type of content (e.g. a course vs. a course run).', max_length=127, null=True),
),
migrations.AddConstraint(
model_name='defaultenterpriseenrollmentintention',
constraint=models.UniqueConstraint(fields=('enterprise_customer', 'content_key'), name='unique_default_enrollment_intention'),
),
]
Loading

0 comments on commit 44d6a9a

Please sign in to comment.