601 lines
19 KiB
Python
601 lines
19 KiB
Python
"""
|
|
Python API functions related to reading program enrollments.
|
|
|
|
Outside of this subpackage, import these functions
|
|
from `lms.djangoapps.program_enrollments.api`.
|
|
"""
|
|
|
|
import re
|
|
from functools import reduce
|
|
from operator import or_
|
|
|
|
from django.db.models import Q
|
|
from organizations.models import Organization
|
|
from social_django.models import UserSocialAuth
|
|
|
|
from common.djangoapps.student.roles import CourseStaffRole
|
|
from openedx.core.djangoapps.catalog.utils import get_programs
|
|
|
|
from ..constants import ProgramCourseEnrollmentRoles
|
|
from ..exceptions import (
|
|
BadOrganizationShortNameException,
|
|
ProgramDoesNotExistException,
|
|
ProgramHasNoAuthoringOrganizationException,
|
|
ProviderDoesNotExistException
|
|
)
|
|
from ..models import ProgramCourseEnrollment, ProgramEnrollment
|
|
|
|
_STUDENT_ARG_ERROR_MESSAGE = (
|
|
"user and external_user_key are both None; at least one must be provided."
|
|
)
|
|
_REALIZED_FILTER_ERROR_TEMPLATE = (
|
|
"{} and {} are mutually exclusive; at most one of them may be passed in as True."
|
|
)
|
|
_STUDENT_LIST_ARG_ERROR_MESSAGE = (
|
|
'user list and external_user_key_list are both empty or None;'
|
|
' At least one of the lists must be provided.'
|
|
)
|
|
|
|
|
|
def get_program_enrollment(
|
|
program_uuid,
|
|
user=None,
|
|
external_user_key=None,
|
|
curriculum_uuid=None,
|
|
):
|
|
"""
|
|
Get a single program enrollment.
|
|
|
|
Required arguments:
|
|
* program_uuid (UUID|str)
|
|
* At least one of:
|
|
* user (User)
|
|
* external_user_key (str)
|
|
|
|
Optional arguments:
|
|
* curriculum_uuid (UUID|str) [optional]
|
|
|
|
Returns: ProgramEnrollment
|
|
|
|
Raises: ProgramEnrollment.DoesNotExist, ProgramEnrollment.MultipleObjectsReturned
|
|
"""
|
|
if not (user or external_user_key):
|
|
raise ValueError(_STUDENT_ARG_ERROR_MESSAGE)
|
|
filters = {
|
|
"user": user,
|
|
"external_user_key__iexact": external_user_key,
|
|
"curriculum_uuid": curriculum_uuid,
|
|
}
|
|
return ProgramEnrollment.objects.get(
|
|
program_uuid=program_uuid, **_remove_none_values(filters)
|
|
)
|
|
|
|
|
|
def get_program_course_enrollment(
|
|
program_uuid,
|
|
course_key,
|
|
user=None,
|
|
external_user_key=None,
|
|
curriculum_uuid=None,
|
|
):
|
|
"""
|
|
Get a single program-course enrollment.
|
|
|
|
Required arguments:
|
|
* program_uuid (UUID|str)
|
|
* course_key (CourseKey|str)
|
|
* At least one of:
|
|
* user (User)
|
|
* external_user_key (str)
|
|
|
|
Optional arguments:
|
|
* curriculum_uuid (UUID|str) [optional]
|
|
|
|
Returns: ProgramCourseEnrollment
|
|
|
|
Raises:
|
|
* ProgramCourseEnrollment.DoesNotExist
|
|
* ProgramCourseEnrollment.MultipleObjectsReturned
|
|
"""
|
|
if not (user or external_user_key):
|
|
raise ValueError(_STUDENT_ARG_ERROR_MESSAGE)
|
|
filters = {
|
|
"program_enrollment__user": user,
|
|
"program_enrollment__external_user_key__iexact": external_user_key,
|
|
"program_enrollment__curriculum_uuid": curriculum_uuid,
|
|
}
|
|
return ProgramCourseEnrollment.objects.get(
|
|
program_enrollment__program_uuid=program_uuid,
|
|
course_key=course_key,
|
|
**_remove_none_values(filters)
|
|
)
|
|
|
|
|
|
def fetch_program_enrollments(
|
|
program_uuid,
|
|
curriculum_uuids=None,
|
|
users=None,
|
|
external_user_keys=None,
|
|
program_enrollment_statuses=None,
|
|
realized_only=False,
|
|
waiting_only=False,
|
|
):
|
|
"""
|
|
Fetch program enrollments for a specific program.
|
|
|
|
Required argument:
|
|
* program_uuid (UUID|str)
|
|
|
|
Optional arguments:
|
|
* curriculum_uuids (iterable[UUID|str])
|
|
* users (iterable[User])
|
|
* external_user_keys (iterable[str])
|
|
* program_enrollment_statuses (iterable[str])
|
|
* realized_only (bool)
|
|
* waiting_only (bool)
|
|
|
|
Optional arguments are used as filtersets if they are not None.
|
|
At most one of (realized_only, waiting_only) may be provided.
|
|
|
|
Returns: queryset[ProgramEnrollment]
|
|
"""
|
|
if realized_only and waiting_only:
|
|
raise ValueError(
|
|
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
|
)
|
|
external_user_key_regex = None
|
|
if external_user_keys:
|
|
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
|
|
filters = {
|
|
"curriculum_uuid__in": curriculum_uuids,
|
|
"user__in": users,
|
|
"external_user_key__iregex": external_user_key_regex,
|
|
"status__in": program_enrollment_statuses,
|
|
}
|
|
if realized_only:
|
|
filters["user__isnull"] = False
|
|
if waiting_only:
|
|
filters["user__isnull"] = True
|
|
return ProgramEnrollment.objects.filter(
|
|
program_uuid=program_uuid, **_remove_none_values(filters)
|
|
)
|
|
|
|
|
|
def fetch_program_course_enrollments(
|
|
program_uuid,
|
|
course_key,
|
|
curriculum_uuids=None,
|
|
users=None,
|
|
external_user_keys=None,
|
|
program_enrollment_statuses=None,
|
|
program_enrollments=None,
|
|
active_only=False,
|
|
inactive_only=False,
|
|
realized_only=False,
|
|
waiting_only=False,
|
|
):
|
|
"""
|
|
Fetch program-course enrollments for a specific program and course run.
|
|
|
|
Required argument:
|
|
* program_uuid (UUID|str)
|
|
* course_key (CourseKey|str)
|
|
|
|
Optional arguments:
|
|
* curriculum_uuids (iterable[UUID|str])
|
|
* users (iterable[User])
|
|
* external_user_keys (iterable[str])
|
|
* program_enrollment_statuses (iterable[str])
|
|
* program_enrollments (iterable[ProgramEnrollment])
|
|
* active_only (bool)
|
|
* inactive_only (bool)
|
|
* realized_only (bool)
|
|
* waiting_only (bool)
|
|
|
|
Optional arguments are used as filtersets if they are not None.
|
|
At most one of (realized_only, waiting_only) may be provided.
|
|
At most one of (active_only, inactive_only) may be provided.
|
|
|
|
Returns: queryset[ProgramCourseEnrollment]
|
|
"""
|
|
if active_only and inactive_only:
|
|
raise ValueError(
|
|
_REALIZED_FILTER_ERROR_TEMPLATE.format("active_only", "inactive_only")
|
|
)
|
|
if realized_only and waiting_only:
|
|
raise ValueError(
|
|
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
|
)
|
|
external_user_key_regex = None
|
|
if external_user_keys:
|
|
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
|
|
filters = {
|
|
"program_enrollment__curriculum_uuid__in": curriculum_uuids,
|
|
"program_enrollment__user__in": users,
|
|
"program_enrollment__external_user_key__iregex": external_user_key_regex,
|
|
"program_enrollment__status__in": program_enrollment_statuses,
|
|
"program_enrollment__in": program_enrollments,
|
|
}
|
|
if active_only:
|
|
filters["status"] = "active"
|
|
if inactive_only:
|
|
filters["status"] = "inactive"
|
|
if realized_only:
|
|
filters["program_enrollment__user__isnull"] = False
|
|
if waiting_only:
|
|
filters["program_enrollment__user__isnull"] = True
|
|
return ProgramCourseEnrollment.objects.filter(
|
|
program_enrollment__program_uuid=program_uuid,
|
|
course_key=course_key,
|
|
**_remove_none_values(filters)
|
|
)
|
|
|
|
|
|
def fetch_program_enrollments_by_student(
|
|
user=None,
|
|
external_user_key=None,
|
|
program_uuids=None,
|
|
curriculum_uuids=None,
|
|
program_enrollment_statuses=None,
|
|
realized_only=False,
|
|
waiting_only=False,
|
|
):
|
|
"""
|
|
Fetch program enrollments for a specific student.
|
|
|
|
Required arguments (at least one must be provided):
|
|
* user (User)
|
|
* external_user_key (str)
|
|
|
|
Optional arguments:
|
|
* provided_uuids (iterable[UUID|str])
|
|
* curriculum_uuids (iterable[UUID|str])
|
|
* program_enrollment_statuses (iterable[str])
|
|
* realized_only (bool)
|
|
* waiting_only (bool)
|
|
|
|
Optional arguments are used as filtersets if they are not None.
|
|
At most one of (realized_only, waiting_only) may be provided.
|
|
|
|
Returns: queryset[ProgramEnrollment]
|
|
"""
|
|
if not (user or external_user_key):
|
|
raise ValueError(_STUDENT_ARG_ERROR_MESSAGE)
|
|
if realized_only and waiting_only:
|
|
raise ValueError(
|
|
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
|
)
|
|
filters = {
|
|
"user": user,
|
|
"external_user_key__iexact": external_user_key,
|
|
"program_uuid__in": program_uuids,
|
|
"curriculum_uuid__in": curriculum_uuids,
|
|
"status__in": program_enrollment_statuses,
|
|
}
|
|
if realized_only:
|
|
filters["user__isnull"] = False
|
|
if waiting_only:
|
|
filters["user__isnull"] = True
|
|
return ProgramEnrollment.objects.filter(**_remove_none_values(filters))
|
|
|
|
|
|
def fetch_program_enrollments_by_students(
|
|
users=None,
|
|
external_user_keys=None,
|
|
program_enrollment_statuses=None,
|
|
realized_only=False,
|
|
waiting_only=False,
|
|
):
|
|
"""
|
|
Fetch program enrollments for a specific list of students.
|
|
|
|
Required arguments (at least one must be provided):
|
|
* users (iterable[User])
|
|
* external_user_keys (iterable[str])
|
|
|
|
Optional arguments:
|
|
* program_enrollment_statuses (iterable[str])
|
|
* realized_only (bool)
|
|
* waiting_only (bool)
|
|
|
|
Optional arguments are used as filtersets if they are not None.
|
|
|
|
Returns: queryset[ProgramEnrollment]
|
|
"""
|
|
if not (users or external_user_keys):
|
|
raise ValueError(_STUDENT_LIST_ARG_ERROR_MESSAGE)
|
|
if realized_only and waiting_only:
|
|
raise ValueError(
|
|
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
|
)
|
|
external_user_key_regex = None
|
|
if external_user_keys:
|
|
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
|
|
filters = {
|
|
"user__in": users,
|
|
"external_user_key__iregex": external_user_key_regex,
|
|
"status__in": program_enrollment_statuses,
|
|
}
|
|
if realized_only:
|
|
filters["user__isnull"] = False
|
|
if waiting_only:
|
|
filters["user__isnull"] = True
|
|
return ProgramEnrollment.objects.filter(**_remove_none_values(filters))
|
|
|
|
|
|
def fetch_program_course_enrollments_by_students(
|
|
users=None,
|
|
external_user_keys=None,
|
|
program_uuids=None,
|
|
curriculum_uuids=None,
|
|
course_keys=None,
|
|
program_enrollment_statuses=None,
|
|
active_only=False,
|
|
inactive_only=False,
|
|
realized_only=False,
|
|
waiting_only=False,
|
|
):
|
|
"""
|
|
Fetch program-course enrollments for a specific list of students.
|
|
|
|
Required arguments (at least one must be provided):
|
|
* users (iterable[User])
|
|
* external_user_keys (iterable[str])
|
|
|
|
Optional arguments:
|
|
* provided_uuids (iterable[UUID|str])
|
|
* curriculum_uuids (iterable[UUID|str])
|
|
* course_keys (iterable[CourseKey|str])
|
|
* program_enrollment_statuses (iterable[str])
|
|
* realized_only (bool)
|
|
* waiting_only (bool)
|
|
|
|
Optional arguments are used as filtersets if they are not None.
|
|
At most one of (realized_only, waiting_only) may be provided.
|
|
At most one of (active_only, inactive_only) may be provided.
|
|
|
|
Returns: queryset[ProgramCourseEnrollment]
|
|
"""
|
|
if not (users or external_user_keys):
|
|
raise ValueError(_STUDENT_LIST_ARG_ERROR_MESSAGE)
|
|
|
|
if active_only and inactive_only:
|
|
raise ValueError(
|
|
_REALIZED_FILTER_ERROR_TEMPLATE.format("active_only", "inactive_only")
|
|
)
|
|
if realized_only and waiting_only:
|
|
raise ValueError(
|
|
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
|
|
)
|
|
external_user_key_regex = None
|
|
if external_user_keys:
|
|
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
|
|
filters = {
|
|
"program_enrollment__user__in": users,
|
|
"program_enrollment__external_user_key__iregex": external_user_key_regex,
|
|
"program_enrollment__program_uuid__in": program_uuids,
|
|
"program_enrollment__curriculum_uuid__in": curriculum_uuids,
|
|
"course_key__in": course_keys,
|
|
"program_enrollment__status__in": program_enrollment_statuses,
|
|
}
|
|
if active_only:
|
|
filters["status"] = "active"
|
|
if inactive_only:
|
|
filters["status"] = "inactive"
|
|
if realized_only:
|
|
filters["program_enrollment__user__isnull"] = False
|
|
if waiting_only:
|
|
filters["program_enrollment__user__isnull"] = True
|
|
return ProgramCourseEnrollment.objects.filter(**_remove_none_values(filters))
|
|
|
|
|
|
def _remove_none_values(dictionary):
|
|
"""
|
|
Return a dictionary where key-value pairs with `None` as the value
|
|
are removed.
|
|
"""
|
|
return {
|
|
key: value for key, value in dictionary.items() if value is not None
|
|
}
|
|
|
|
|
|
def get_users_by_external_keys_and_org_key(external_user_keys, org_key):
|
|
"""
|
|
Given an organization_key and a set of external keys,
|
|
return a dict from external user keys to Users.
|
|
|
|
Args:
|
|
external_user_keys (sequence[str]):
|
|
external user keys used by the program creator's IdP.
|
|
org_key (str):
|
|
The organization short name of which the external_user_key belongs to
|
|
|
|
Returns: dict[str: User|None]
|
|
A dict mapping external user keys to Users.
|
|
If an external user key is not registered, then None is returned instead
|
|
of a User for that key.
|
|
|
|
Raises:
|
|
BadOrganizationShortNameException
|
|
ProviderDoesNotExistsException
|
|
"""
|
|
saml_providers = get_saml_providers_by_org_key(org_key)
|
|
found_users_by_external_keys = {}
|
|
# if the same external id exists in multiple providers (for this organization)
|
|
# it is expected both providers return the same user
|
|
for saml_provider in saml_providers:
|
|
social_auth_uids = {
|
|
saml_provider.get_social_auth_uid(external_user_key)
|
|
for external_user_key in external_user_keys
|
|
}
|
|
if social_auth_uids:
|
|
# Filter should be case insensitive
|
|
query_filter = reduce(or_, [Q(uid__iexact=uid) for uid in social_auth_uids])
|
|
social_auths = UserSocialAuth.objects.filter(query_filter)
|
|
found_users_by_external_keys.update({
|
|
saml_provider.get_remote_id_from_social_auth(social_auth): social_auth.user
|
|
for social_auth in social_auths
|
|
})
|
|
|
|
# Default all external keys to None, because external keys
|
|
# without a User will not appear in `found_users_by_external_keys`.
|
|
users_by_external_keys = {key: None for key in external_user_keys}
|
|
users_by_external_keys.update(found_users_by_external_keys)
|
|
return users_by_external_keys
|
|
|
|
|
|
def get_users_by_external_keys(program_uuid, external_user_keys):
|
|
"""
|
|
Given a program and a set of external keys,
|
|
return a dict from external user keys to Users.
|
|
|
|
Args:
|
|
program_uuid (UUID|str):
|
|
uuid for program these users is/will be enrolled in
|
|
external_user_keys (sequence[str]):
|
|
external user keys used by the program creator's IdP.
|
|
|
|
Returns: dict[str: User|None]
|
|
A dict mapping external user keys to Users.
|
|
If an external user key is not registered, then None is returned instead
|
|
of a User for that key.
|
|
|
|
Raises:
|
|
ProgramDoesNotExistException
|
|
ProgramHasNoAuthoringOrganizationException
|
|
BadOrganizationShortNameException
|
|
ProviderDoesNotExistsException
|
|
"""
|
|
org_key = get_org_key_for_program(program_uuid)
|
|
return get_users_by_external_keys_and_org_key(external_user_keys, org_key)
|
|
|
|
|
|
def get_external_key_by_user_and_course(user, course_key):
|
|
"""
|
|
Returns the external_user_key of the edX account/user
|
|
enrolled into the course
|
|
|
|
Arguments:
|
|
user (User):
|
|
The edX account representing the user in auth_user table
|
|
course_key (CourseKey|str):
|
|
The course key of the course user is enrolled in
|
|
|
|
Returns: external_user_key (str|None)
|
|
The external user key provided by Masters degree provider
|
|
Or None if cannot find edX user to Masters learner mapping
|
|
"""
|
|
program_course_enrollments = ProgramCourseEnrollment.objects.filter(
|
|
course_enrollment__user=user,
|
|
course_key=course_key
|
|
).order_by('status', '-modified')
|
|
|
|
if not program_course_enrollments:
|
|
return None
|
|
|
|
relevant_pce = program_course_enrollments.first()
|
|
return relevant_pce.program_enrollment.external_user_key
|
|
|
|
|
|
def get_saml_providers_by_org_key(org_key):
|
|
"""
|
|
Returns a list of SAML providers associated with the provided org_key.
|
|
In most cases an organization will only have one configured provider.
|
|
However, multiple may be returned during a migration between two active
|
|
providers.
|
|
|
|
Arguments:
|
|
org_key (str)
|
|
|
|
Returns: list[SAMLProvider]
|
|
|
|
Raises:
|
|
BadOrganizationShortNameException
|
|
"""
|
|
try:
|
|
organization = Organization.objects.get(short_name=org_key)
|
|
except Organization.DoesNotExist:
|
|
raise BadOrganizationShortNameException(org_key) # lint-amnesty, pylint: disable=raise-missing-from
|
|
return get_saml_providers_for_organization(organization)
|
|
|
|
|
|
def get_org_key_for_program(program_uuid):
|
|
"""
|
|
Return the key of the first Organization
|
|
administering the given program.
|
|
|
|
Arguments:
|
|
program_uuid (UUID|str)
|
|
|
|
Returns: org_key (str)
|
|
|
|
Raises:
|
|
ProgramDoesNotExistException
|
|
ProgramHasNoAuthoringOrganizationException
|
|
"""
|
|
program = get_programs(uuid=program_uuid)
|
|
if program is None:
|
|
raise ProgramDoesNotExistException(program_uuid)
|
|
authoring_orgs = program.get('authoring_organizations')
|
|
org_key = authoring_orgs[0].get('key') if authoring_orgs else None
|
|
if not org_key:
|
|
raise ProgramHasNoAuthoringOrganizationException(program_uuid)
|
|
return org_key
|
|
|
|
|
|
def get_saml_providers_for_organization(organization):
|
|
"""
|
|
Return currently configured SAML provider(s) for the given Organization.
|
|
In most cases an organization will only have one configured provider.
|
|
However, multiple may be returned during a migration between two active
|
|
providers.
|
|
|
|
Arguments:
|
|
organization: Organization
|
|
|
|
Returns: list[SAMLProvider]
|
|
|
|
Raises:
|
|
ProviderDoesNotExistsException
|
|
"""
|
|
provider_configs = organization.samlproviderconfig_set.current_set().filter(enabled=True)
|
|
if not provider_configs:
|
|
raise ProviderDoesNotExistException(organization) # lint-amnesty, pylint: disable=raise-missing-from
|
|
return list(provider_configs)
|
|
|
|
|
|
def remove_prefix(text, prefix):
|
|
if text.startswith(prefix):
|
|
return text[len(prefix):]
|
|
return text
|
|
|
|
|
|
def get_provider_slug(provider_config):
|
|
"""
|
|
Returns slug identifying a SAML provider.
|
|
|
|
Arguments:
|
|
provider_config: SAMLProvider
|
|
|
|
Returns: str
|
|
"""
|
|
return remove_prefix(provider_config.provider_id, 'saml-')
|
|
|
|
|
|
def is_course_staff_enrollment(program_course_enrollment):
|
|
"""
|
|
Returns whether the provided program_course_enrollment have the
|
|
course staff role on the course.
|
|
|
|
Arguments:
|
|
program_course_enrollment: ProgramCourseEnrollment
|
|
|
|
returns: bool
|
|
"""
|
|
associated_user = program_course_enrollment.program_enrollment.user
|
|
if associated_user:
|
|
return CourseStaffRole(program_course_enrollment.course_key).has_user(associated_user)
|
|
return program_course_enrollment.courseaccessroleassignment_set.filter(
|
|
role=ProgramCourseEnrollmentRoles.COURSE_STAFF
|
|
).exists()
|