Skip to content

Commit

Permalink
Merge branch 'master' into bilalqamar95/node20-upgrade-3
Browse files Browse the repository at this point in the history
  • Loading branch information
BilalQamar95 authored Oct 31, 2024
2 parents 5a502c5 + f3092a7 commit 582159c
Show file tree
Hide file tree
Showing 50 changed files with 2,457 additions and 257 deletions.
52 changes: 50 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,60 @@ Unreleased
----------
* nothing unreleased

[4.30.1]
--------
* fix: serialize best_mode_for_course_run field in DefaultEnterpriseEnrollmentIntentionSerializer.

[4.30.0]
--------
* feat: REST APIs for default-enterprise-enrollment-intentions

[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

[4.28.2]
--------
* fix: added content_title, progress_status in get_learner_data_records for derived classed of learner data exporters.

[4.28.1]
--------
* feat: making to-be-deleted model field nullable

[4.28.0]
--------
* feat: add default enrollment models

[4.27.3]
--------
* fix: Updating the EnterpriseGroup serializer with created variable

[4.27.2]
--------
* fix: updates `get_all_learners` to remove `_get_implicit_group_members`

[4.27.1]
--------
* chore: remove `replaces` sections from squashing migrations.

[4.27.0]
--------
* chore: Add index to the username field in the `Consent` model

[4.26.1]
---------
--------
* feat: proxy login now redirects to LMS register page instead of login page

[4.26.0]
---------
--------
* feat: add new field to EnterpriseGroup model and EnterpriseGroupSerializer

[4.25.19]
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'

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.15 on 2024-10-07 14:33

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('consent', '0006_alter_historicaldatasharingconsent_options'),
]

operations = [
migrations.AddIndex(
model_name='datasharingconsent',
index=models.Index(fields=['username'], name='consent_dat_usernam_fae23a_idx'),
),
]
3 changes: 3 additions & 0 deletions consent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ class Meta:

abstract = True
app_label = 'consent'
indexes = [
models.Index(fields=['username'])
]

enterprise_customer = models.ForeignKey(
EnterpriseCustomer,
Expand Down
88 changes: 88 additions & 0 deletions docs/decisions/0015-default-enrollments.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
==============================
Default Enterprise Enrollments
==============================

Status
======
Proposed - October 2024

Context
=======
Enterprise needs a solution for managing automated enrollments into "default" courses or specific course runs
for enterprise learners. Importantly, we need this solution to work for learners where we have no information
about the identity of learner who will eventually be associated with an ``EnterpriseCustomer``. For that reason,
the solution described below does not involve ``PendingEnrollments`` or anything similar -
that model/domain depends on knowing the email address of learners prior to their becoming associated with the customer.
The solution also needs to support customer-specific enrollment configurations
without tightly coupling these configurations to a specific subsidy type, i.e. we should be able in the future
to manage default enrollments via both learner credit and subscription subsidy types.

Core requirements
-----------------
1. Internal staff should be able to configure one or more default enrollments with either a course
or a specific course run for automatic enrollment. In the case of specifying a course,
the default enrollment flow should cause the "realization" of default enrollments for learners
into the currently-advertised, enrollable run for a course.
2. Default Enrollments should be loosely coupled to subsidy type.
3. Unenrollment: If a learner chooses to unenroll from a default course, they should not be automatically re-enrolled.
4. Graceful handling of license revocation: Upon license revocation, we currently downgrade the learner’s
enrollment mode to ``audit``. This fact should be visible from any new APIs exposed
in the domain of default enrollments.
5. Non-enrollable courses: If a course becomes unenrollable, our intent is that default enrollments for such
a course no longer are processed. Ideally this happens in a way that is observable to operators of the system.

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.

Qualities
---------
1. Flexibility: The ``DefaultEnterpriseEnrollmentIntention`` model will allow specification of either a course
or course run.
2. Business logic: The API for this domain (future ADR) will implement the business logic around choosing
the appropriate course run, for answering which if any catalogs are applicable to the course,
and the enrollability of the course (the last of which takes into account the state of existing enrollment records).
3. Non-Tightly Coupled to subsidy type: Nothing in the domain of default enrollments will persist data
related to a subsidy (although a license or transaction identifier will ultimately become associated with
an ``EnterpriseCourseEnrollment`` record during realization).

Flexible content keys on the intention model
--------------------------------------------
The ``content_key`` on ``DefaultEnterpriseEnrollmentIntention`` is either a top-level course key
or a course run key during configuration to remain flexible for internal operators;
however, we will always discern the correct course run key to use for enrollment based on the provided ``content_key``.

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.
* If the content_key is a specific course run, we'll always try to enroll in the explicitly
configured course run key (not the advertised course run).

This content_key will be passed to the ``EnterpriseCatalogApiClient().get_content_metadata_content_identifier``
method. When passing a course run key to this endpoint, it'll actually return the top-level *course* metadata
associated with the course run, not just the specified course run's metadata
(this is primarily due to catalogs containing courses, not course runs, and we generally say that
if the top-level course is in a customer's catalog, all of its course runs are, too).

If the ``content_key`` configured for a ``DefaultEnterpriseEnrollmentIntention`` is a top-level course,
there is a chance the currently advertised course run used for future enrollment intentions might
change over time from previous redeemed/enrolled ``DefaultEnterpriseEnrollmentIntentions``.
However, this is mitigated in that the ``DefaultEnterpriseEnrollmentRealization``
ensures the resulting, converted enrollment is still related to the original ``DefaultEnterpriseEnrollmentIntention``,
despite a potential content_key vs. enrollment course run key mismatch.

Consequences
============
1. It's a flexible design.
2. It relies on a network call(s) to enterprise-catalog to fetch content metadata and understand which if any customer
catalog are applicable to the indended course (we can use caching to make this efficient).
3. We're introducing more complexity in terms of how subsidized enterprise enrollments
can come into existence.
4. The realization model makes the provenance of default enrollments explicit and easy to examine.
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.26.1"
__version__ = "4.30.1"
101 changes: 98 additions & 3 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 @@ -1221,8 +1251,8 @@ class EnterpriseGroupAdmin(admin.ModelAdmin):
Django admin for EnterpriseGroup model.
"""
model = models.EnterpriseGroup
list_display = ('uuid', 'enterprise_customer', 'applies_to_all_contexts', )
list_filter = ('applies_to_all_contexts',)
list_display = ('uuid', 'enterprise_customer', )
list_filter = ('group_type',)
search_fields = (
'uuid',
'name',
Expand Down Expand Up @@ -1294,7 +1324,6 @@ class LearnerCreditEnterpriseCourseEnrollmentAdmin(admin.ModelAdmin):
'uuid',
'fulfillment_type',
'enterprise_course_enrollment',
'is_revoked',
'modified',
)

Expand All @@ -1316,3 +1345,69 @@ class LearnerCreditEnterpriseCourseEnrollmentAdmin(admin.ModelAdmin):
class Meta:
fields = '__all__'
model = models.LearnerCreditEnterpriseCourseEnrollment


@admin.register(models.DefaultEnterpriseEnrollmentIntention)
class DefaultEnterpriseEnrollmentIntentionAdmin(admin.ModelAdmin):
"""
Django admin model for DefaultEnterpriseEnrollmentIntentions.
"""
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 = (
'uuid',
'content_type',
'course_key',
'course_run_key',
'created',
'modified',
)

search_fields = (
'uuid',
'enterprise_customer__uuid',
'content_key',
)

ordering = ('-modified',)

class Meta:
model = models.DefaultEnterpriseEnrollmentIntention

def get_queryset(self, request):
"""
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
Loading

0 comments on commit 582159c

Please sign in to comment.