Files
edx-platform/lms/djangoapps/program_enrollments/api/reading.py

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()