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

More APIs for Student Information #18

Merged
merged 10 commits into from
Jun 6, 2024
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
matrix:
os: [ubuntu-20.04]
python-version: ['3.8']
toxenv: [quality, django32]
toxenv: [quality, django32, django40]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,5 @@ requirements/private.txt

# temporary tests migration files
/test_utils/edx_platform_mocks/fake_models/migrations/

default.db
2 changes: 1 addition & 1 deletion futurex_openedx_extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""One-line description for README and other doc files."""

__version__ = '0.2.1'
__version__ = '0.3.7'
99 changes: 87 additions & 12 deletions futurex_openedx_extensions/dashboard/details/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@
from typing import List

from common.djangoapps.student.models import CourseAccessRole
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery, Sum
from completion.models import BlockCompletion
from django.db.models import (
Case,
Count,
DateTimeField,
Exists,
F,
IntegerField,
Max,
OuterRef,
Q,
Subquery,
Sum,
Value,
When,
)
from django.db.models.functions import Coalesce
from django.db.models.query import QuerySet
from eox_nelp.course_experience.models import FeedbackCourse
from lms.djangoapps.certificates.models import GeneratedCertificate

from futurex_openedx_extensions.helpers.querysets import get_base_queryset_courses
from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list, get_tenant_site
from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list


def get_courses_queryset(
tenant_ids: List, search_text: str = None, only_visible: bool = True, only_active: bool = False
tenant_ids: List, search_text: str = None, visible_filter: bool = True, active_filter: bool = None
) -> QuerySet:
"""
Get the courses queryset for the given tenant IDs and search text.
Expand All @@ -24,20 +39,18 @@ def get_courses_queryset(
:type tenant_ids: List
:param search_text: Search text to filter the courses by
:type search_text: str
:param only_visible: Whether to only include courses that are visible in the catalog
:type only_visible: bool
:param only_active: Whether to only include active courses
:type only_active: bool
:param visible_filter: Whether to only include courses that are visible in the catalog
:type visible_filter: bool
:param active_filter: Whether to only include active courses
:type active_filter: bool
:return: QuerySet of courses
:rtype: QuerySet
"""
course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list']
tenant_sites = []
for tenant_id in tenant_ids:
if site := get_tenant_site(tenant_id):
tenant_sites.append(site)

queryset = get_base_queryset_courses(course_org_filter_list, only_visible=only_visible, only_active=only_active)
queryset = get_base_queryset_courses(
course_org_filter_list, visible_filter=visible_filter, active_filter=active_filter,
)

search_text = (search_text or '').strip()
if search_text:
Expand Down Expand Up @@ -105,3 +118,65 @@ def get_courses_queryset(
)

return queryset


def get_learner_courses_info_queryset(
tenant_ids: List, user_id: int, visible_filter: bool = True, active_filter: bool = None
) -> QuerySet:
"""
Get the learner's courses queryset for the given user ID. This method assumes a valid user ID.

:param tenant_ids: List of tenant IDs to get the learner for
:type tenant_ids: List
:param user_id: The user ID to get the learner for
:type user_id: int
:param visible_filter: Whether to only count courses that are visible in the catalog
:type visible_filter: bool
:param active_filter: Whether to only count active courses
:type active_filter: bool
:return: QuerySet of learners
:rtype: QuerySet
"""
course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list']

queryset = get_base_queryset_courses(
course_org_filter_list, visible_filter=visible_filter, active_filter=active_filter,
).filter(
courseenrollment__user_id=user_id,
courseenrollment__is_active=True,
).annotate(
related_user_id=Value(user_id, output_field=IntegerField()),
).annotate(
enrollment_date=Case(
When(
courseenrollment__user_id=user_id,
then=F('courseenrollment__created'),
),
default=None,
output_field=DateTimeField(),
)
).annotate(
last_activity=Case(
When(
Exists(
BlockCompletion.objects.filter(
user_id=user_id,
context_key=OuterRef('id'),
),
),
then=Subquery(
BlockCompletion.objects.filter(
user_id=user_id,
context_key=OuterRef('id'),
).values('context_key').annotate(
last_activity=Max('modified'),
).values('last_activity'),
output_field=DateTimeField(),
),
),
default=F('enrollment_date'),
output_field=DateTimeField(),
)
)

return queryset
170 changes: 123 additions & 47 deletions futurex_openedx_extensions/dashboard/details/learners.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,86 @@

from typing import List

from common.djangoapps.student.models import CourseAccessRole, UserSignupSource
from common.djangoapps.student.models import CourseAccessRole
from django.contrib.auth import get_user_model
from django.db.models import Count, Exists, OuterRef, Q, Subquery
from django.db.models.query import QuerySet

from futurex_openedx_extensions.helpers.querysets import get_base_queryset_courses
from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list, get_tenant_site
from futurex_openedx_extensions.helpers.querysets import get_base_queryset_courses, get_has_site_login_queryset
from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list, get_tenants_sites


def get_courses_count_for_learner_queryset(
course_org_filter_list: List[str],
visible_courses_filter: bool = True,
active_courses_filter: bool = None,
) -> QuerySet:
"""
Get the courses count for the given learner.

:param course_org_filter_list: List of course organizations to filter by
:type course_org_filter_list: List[str]
:param visible_courses_filter: Value to filter courses on catalog visibility. None means no filter.
:type visible_courses_filter: bool
:param active_courses_filter: Value to filter courses on active status. None means no filter.
:type active_courses_filter: bool
:return: QuerySet of learners
:rtype: QuerySet
"""
return Count(
'courseenrollment',
filter=(
Q(courseenrollment__course_id__in=get_base_queryset_courses(
course_org_filter_list,
visible_filter=visible_courses_filter,
active_filter=active_courses_filter,
)) &
~Exists(
CourseAccessRole.objects.filter(
user_id=OuterRef('id'),
org=OuterRef('courseenrollment__course__org')
)
)
),
distinct=True
)


def get_certificates_count_for_learner_queryset(
course_org_filter_list: List[str],
visible_courses_filter: bool = True,
active_courses_filter: bool = None,
) -> QuerySet:
"""
Annotate the given queryset with the certificate counts.

:param course_org_filter_list: List of course organizations to filter by
:type course_org_filter_list: List[str]
:param visible_courses_filter: Value to filter courses on catalog visibility. None means no filter.
:type visible_courses_filter: bool
:param active_courses_filter: Value to filter courses on active status. None means no filter.
:type active_courses_filter: bool
:return: QuerySet of learners
:rtype: QuerySet
"""
return Count(
'generatedcertificate',
filter=(
Q(generatedcertificate__course_id__in=Subquery(
get_base_queryset_courses(
course_org_filter_list,
visible_filter=visible_courses_filter,
active_filter=active_courses_filter
).values_list('id', flat=True)
)) &
Q(generatedcertificate__status='downloadable')
),
distinct=True
)


def get_learners_queryset(
tenant_ids: List, search_text: str = None, only_visible_courses: bool = True, only_active_courses: bool = False
tenant_ids: List, search_text: str = None, visible_courses_filter: bool = True, active_courses_filter: bool = None
) -> QuerySet:
"""
Get the learners queryset for the given tenant IDs and search text.
Expand All @@ -22,18 +91,15 @@ def get_learners_queryset(
:type tenant_ids: List
:param search_text: Search text to filter the learners by
:type search_text: str
:param only_visible_courses: Whether to only count courses that are visible in the catalog
:type only_visible_courses: bool
:param only_active_courses: Whether to only count active courses
:type only_active_courses: bool
:param visible_courses_filter: Whether to only count courses that are visible in the catalog
:type visible_courses_filter: bool
:param active_courses_filter: Whether to only count active courses
:type active_courses_filter: bool
:return: QuerySet of learners
:rtype: QuerySet
"""
course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list']
tenant_sites = []
for tenant_id in tenant_ids:
if site := get_tenant_site(tenant_id):
tenant_sites.append(site)
tenant_sites = get_tenants_sites(tenant_ids)

queryset = get_user_model().objects.filter(
is_superuser=False,
Expand All @@ -49,47 +115,57 @@ def get_learners_queryset(
)

queryset = queryset.annotate(
courses_count=Count(
'courseenrollment',
filter=(
Q(courseenrollment__course_id__in=get_base_queryset_courses(
course_org_filter_list,
only_visible=only_visible_courses,
only_active=only_active_courses,
)) &
~Exists(
CourseAccessRole.objects.filter(
user_id=OuterRef('id'),
org=OuterRef('courseenrollment__course__org')
)
)
),
distinct=True
courses_count=get_courses_count_for_learner_queryset(
course_org_filter_list,
visible_courses_filter=visible_courses_filter,
active_courses_filter=active_courses_filter,
)
).annotate(
certificates_count=Count(
'generatedcertificate',
filter=(
Q(generatedcertificate__course_id__in=Subquery(
get_base_queryset_courses(
course_org_filter_list,
only_visible=only_visible_courses,
only_active=only_active_courses
).values_list('id', flat=True)
)) &
Q(generatedcertificate__status='downloadable')
),
distinct=True
certificates_count=get_certificates_count_for_learner_queryset(
course_org_filter_list,
visible_courses_filter=visible_courses_filter,
active_courses_filter=active_courses_filter,
)
).annotate(
has_site_login=Exists(
UserSignupSource.objects.filter(
user_id=OuterRef('id'),
site__in=tenant_sites
)
)
has_site_login=get_has_site_login_queryset(tenant_sites)
).filter(
Q(courses_count__gt=0) | Q(has_site_login=True)
).select_related('profile').order_by('id')

return queryset


def get_learner_info_queryset(
tenant_ids: List, user_id: int, visible_courses_filter: bool = True, active_courses_filter: bool = None
) -> QuerySet:
"""
Get the learner queryset for the given user ID. This method assumes a valid user ID.

:param tenant_ids: List of tenant IDs to get the learner for
:type tenant_ids: List
:param user_id: The user ID to get the learner for
:type user_id: int
:param visible_courses_filter: Whether to only count courses that are visible in the catalog
:type visible_courses_filter: bool
:param active_courses_filter: Whether to only count active courses
:type active_courses_filter: bool
:return: QuerySet of learners
:rtype: QuerySet
"""
course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list']

queryset = get_user_model().objects.filter(id=user_id).annotate(
courses_count=get_courses_count_for_learner_queryset(
course_org_filter_list,
visible_courses_filter=visible_courses_filter,
active_courses_filter=active_courses_filter,
)
).annotate(
certificates_count=get_certificates_count_for_learner_queryset(
course_org_filter_list,
visible_courses_filter=visible_courses_filter,
active_courses_filter=active_courses_filter,
)
).select_related('profile')

return queryset
Loading