Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cherry-pick edx-enterprise customizations to Palm [BB-7878] #10

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [master]
pull_request:
branches: [master]

concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/mysql8-migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('requirements/pip_tools.txt') }}
restore-keys: ${{ runner.os }}-pip-

- name: Downgrade pip to work around https://github.com/mitsuhiko/rye/issues/368
run: |
pip install --upgrade pip==23.1

- name: Ubuntu and sql Versions
run: |
lsb_release -a
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Unreleased
----------
* Nothing

[3.61.12]
---------
This version is based on v3.61.11 and contains some backports needed to OpenCraft's clients. There is no "official" v3.61.12.
See this PR for more details: https://github.com/open-craft/edx-enterprise/pull/10

[3.61.11]
---------
feat: include owners and longer descriptions for degreed2 content metadata transmissions
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
Your project description goes here.
"""

__version__ = "3.61.11"
__version__ = "3.61.12"

default_app_config = "enterprise.apps.EnterpriseConfig"
8 changes: 8 additions & 0 deletions enterprise/admin/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ class ManageLearnersForm(forms.Form):
label=_("Enroll these learners in this course"), required=False,
help_text=_("To enroll learners in a course, enter a course ID."),
)
force_enrollment = forms.BooleanField(
label=_("Force Enrollment"),
help_text=_("The selected course is 'Invite Only'. Only staff can enroll learners to this course."),
required=False,
)
course_mode = forms.ChoiceField(
label=_("Course enrollment track"), required=False,
choices=BLANK_CHOICE_DASH + [
Expand Down Expand Up @@ -130,6 +135,7 @@ class Fields:
REASON = "reason"
SALES_FORCE_ID = "sales_force_id"
DISCOUNT = "discount"
FORCE_ENROLLMENT = "force_enrollment"

class CsvColumns:
"""
Expand Down Expand Up @@ -394,6 +400,8 @@ class Meta:
"enable_audit_data_reporting",
"replace_sensitive_sso_username",
"hide_course_original_price",
"hide_course_price_when_zero",
"allow_enrollment_in_invite_only_courses",
"enable_portal_code_management_screen",
"enable_portal_subscription_management_screen",
"enable_learner_portal",
Expand Down
12 changes: 9 additions & 3 deletions enterprise/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,8 @@ def _enroll_users(
notify=True,
enrollment_reason=None,
sales_force_id=None,
discount=0.0
discount=0.0,
force_enrollment=False,
):
"""
Enroll the users with the given email addresses to the course.
Expand All @@ -689,6 +690,7 @@ def _enroll_users(
mode: The enrollment mode the users will be enrolled in the course with
course_id: The ID of the course in which we want to enroll
notify: Whether to notify (by email) the users that have been enrolled
force_enrollment: Force enrollment into "Invite Only" courses
"""
pending_messages = []
paid_modes = ['verified', 'professional']
Expand All @@ -702,6 +704,7 @@ def _enroll_users(
enrollment_reason=enrollment_reason,
discount=discount,
sales_force_id=sales_force_id,
force_enrollment=force_enrollment,
)
all_successes = succeeded + pending
if notify:
Expand Down Expand Up @@ -818,6 +821,7 @@ def post(self, request, customer_uuid):
sales_force_id = manage_learners_form.cleaned_data.get(ManageLearnersForm.Fields.SALES_FORCE_ID)
course_mode = manage_learners_form.cleaned_data.get(ManageLearnersForm.Fields.COURSE_MODE)
course_id = None
force_enrollment = manage_learners_form.cleaned_data.get(ManageLearnersForm.Fields.FORCE_ENROLLMENT)

if not course_id_with_emails:
course_details = manage_learners_form.cleaned_data.get(ManageLearnersForm.Fields.COURSE) or {}
Expand All @@ -832,7 +836,8 @@ def post(self, request, customer_uuid):
notify=notify,
enrollment_reason=manual_enrollment_reason,
sales_force_id=sales_force_id,
discount=discount
discount=discount,
force_enrollment=force_enrollment,
)
else:
for course_id, emails in course_id_with_emails.items():
Expand All @@ -847,7 +852,8 @@ def post(self, request, customer_uuid):
notify=notify,
enrollment_reason=manual_enrollment_reason,
sales_force_id=sales_force_id,
discount=discount
discount=discount,
force_enrollment=force_enrollment,
)

# Redirect to GET if everything went smooth.
Expand Down
32 changes: 31 additions & 1 deletion enterprise/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from django.contrib import auth

from enterprise.models import EnterpriseCustomerUser, SystemWideEnterpriseUserRoleAssignment
from enterprise.models import EnterpriseCustomer, EnterpriseCustomerUser, SystemWideEnterpriseUserRoleAssignment

User = auth.get_user_model()

Expand All @@ -33,6 +33,36 @@ def filter_queryset(self, request, queryset, view):
return queryset


class EnterpriseCourseEnrollmentFilterBackend(filters.BaseFilterBackend):
"""
Filter backend to return enrollments under the user's enterprise(s) only.

* Staff users will bypass this filter.
* Non-staff users will receive enrollments under their linked enterprises,
only if they have the `enterprise.can_enroll_learners` permission.
* Non-staff users without the `enterprise.can_enroll_learners` permission
will receive only their own enrollments.
"""

def filter_queryset(self, request, queryset, view):
"""
Filter out enrollments if learner is not linked
"""

if request.user.is_staff:
return queryset

if request.user.has_perm('enterprise.can_enroll_learners'):
enterprise_customers = EnterpriseCustomer.objects.filter(enterprise_customer_users__user_id=request.user.id)
return queryset.filter(enterprise_customer_user__enterprise_customer__in=enterprise_customers)

filter_kwargs = {
view.USER_ID_FILTER: request.user.id,
}

return queryset.filter(**filter_kwargs)


class EnterpriseCustomerUserFilterBackend(filters.BaseFilterBackend):
"""
Allow filtering on the enterprise customer user api endpoint.
Expand Down
27 changes: 26 additions & 1 deletion enterprise/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,35 @@ class EnterpriseCourseEnrollmentReadOnlySerializer(serializers.ModelSerializer):
class Meta:
model = models.EnterpriseCourseEnrollment
fields = (
'enterprise_customer_user', 'course_id'
'enterprise_customer_user', 'course_id', 'created'
)


class EnterpriseCourseEnrollmentWithAdditionalFieldsReadOnlySerializer(EnterpriseCourseEnrollmentReadOnlySerializer):
"""
Serializer for EnterpriseCourseEnrollment model with additional fields.
"""

class Meta:
model = models.EnterpriseCourseEnrollment
fields = (
'enterprise_customer_user',
'course_id',
'created',
'enrollment_date',
'enrollment_track',
'user_email',
'course_start',
'course_end',
)

enrollment_track = serializers.CharField()
enrollment_date = serializers.DateTimeField()
user_email = serializers.EmailField()
course_start = serializers.DateTimeField()
course_end = serializers.DateTimeField()


class EnterpriseCourseEnrollmentWriteSerializer(serializers.ModelSerializer):
"""
Serializer for writing to the EnterpriseCourseEnrollment model.
Expand Down
6 changes: 4 additions & 2 deletions enterprise/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

from enterprise import models
from enterprise.api.filters import (
EnterpriseCourseEnrollmentFilterBackend,
EnterpriseCustomerInviteKeyFilterBackend,
EnterpriseCustomerUserFilterBackend,
EnterpriseLinkedUserFilterBackend,
Expand Down Expand Up @@ -530,7 +531,8 @@ class EnterpriseCourseEnrollmentViewSet(EnterpriseReadWriteModelViewSet):
API views for the ``enterprise-course-enrollment`` API endpoint.
"""

queryset = models.EnterpriseCourseEnrollment.objects.all()
queryset = models.EnterpriseCourseEnrollment.with_additional_fields.all()
filter_backends = (filters.OrderingFilter, DjangoFilterBackend, EnterpriseCourseEnrollmentFilterBackend)

USER_ID_FILTER = 'enterprise_customer_user__user_id'
FIELDS = (
Expand All @@ -544,7 +546,7 @@ def get_serializer_class(self):
Use a special serializer for any requests that aren't read-only.
"""
if self.request.method in ('GET',):
return serializers.EnterpriseCourseEnrollmentReadOnlySerializer
return serializers.EnterpriseCourseEnrollmentWithAdditionalFieldsReadOnlySerializer
return serializers.EnterpriseCourseEnrollmentWriteSerializer


Expand Down
14 changes: 12 additions & 2 deletions enterprise/api_client/lms.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,15 @@ def has_course_mode(self, course_run_id, mode):
course_modes = self.get_course_modes(course_run_id)
return any(course_mode for course_mode in course_modes if course_mode['slug'] == mode)

def enroll_user_in_course(self, username, course_id, mode, cohort=None, enterprise_uuid=None):
def enroll_user_in_course(
self,
username,
course_id,
mode,
cohort=None,
enterprise_uuid=None,
force_enrollment=False,
):
"""
Call the enrollment API to enroll the user in the course specified by course_id.

Expand All @@ -138,6 +146,7 @@ def enroll_user_in_course(self, username, course_id, mode, cohort=None, enterpri
mode (str): The enrollment mode which should be used for the enrollment
cohort (str): Add the user to this named cohort
enterprise_uuid (str): Add course enterprise uuid
force_enrollment (bool): Force the enrollment even if course is Invite Only

Returns:
dict: A dictionary containing details of the enrollment, including course details, mode, username, etc.
Expand All @@ -152,7 +161,8 @@ def enroll_user_in_course(self, username, course_id, mode, cohort=None, enterpri
'is_active': True,
'mode': mode,
'cohort': cohort,
'enterprise_uuid': str(enterprise_uuid)
'enterprise_uuid': str(enterprise_uuid),
'force_enrollment': force_enrollment,
}
)
response.raise_for_status()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('enterprise', '0170_auto_20230301_1627'),
]

operations = [
migrations.AddField(
model_name='enterprisecustomer',
name='hide_course_price_when_zero',
field=models.BooleanField(default=False, help_text='Specify whether course cost should be hidden in the landing page when the final price is zero.'),
),
migrations.AddField(
model_name='historicalenterprisecustomer',
name='hide_course_price_when_zero',
field=models.BooleanField(default=False, help_text='Specify whether course cost should be hidden in the landing page when the final price is zero.'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('enterprise', '0171_adds_hide_course_price_when_zero_to_enterprisecustomer'),
]

operations = [
migrations.AddField(
model_name='enterprisecustomer',
name='allow_enrollment_in_invite_only_courses',
field=models.BooleanField(default=False, help_text="Specifies if learners are allowed to enroll into courses marked as 'invitation-only', when they attempt to enroll from the landing page."),
),
migrations.AddField(
model_name='historicalenterprisecustomer',
name='allow_enrollment_in_invite_only_courses',
field=models.BooleanField(default=False, help_text="Specifies if learners are allowed to enroll into courses marked as 'invitation-only', when they attempt to enroll from the landing page."),
),
]
Loading
Loading