Skip to content

Commit

Permalink
feat: [AXM-33] create enrollments filtering by course completion stat…
Browse files Browse the repository at this point in the history
…uses
  • Loading branch information
NiedielnitsevIvan committed Apr 5, 2024
1 parent 41b1021 commit a87e8a3
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 15 deletions.
60 changes: 60 additions & 0 deletions common/djangoapps/student/models/course_enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,71 @@ class UnenrollmentNotAllowed(CourseEnrollmentException):
pass


class CourseEnrollmentQuerySet(models.QuerySet):
"""
Custom queryset for CourseEnrollment with Table-level filter methods.
"""

def active(self):
"""
Returns a queryset of CourseEnrollment objects for courses that are currently active.
"""
return self.filter(is_active=True)

def without_certificates(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that do not have a certificate.
"""
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
course_ids_with_certificates = GeneratedCertificate.objects.filter(
user__username=user_username
).values_list('course_id', flat=True)
return self.exclude(course_id__in=course_ids_with_certificates)

def with_certificates(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that have a certificate.
"""
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
course_ids_with_certificates = GeneratedCertificate.objects.filter(
user__username=user_username
).values_list('course_id', flat=True)
return self.filter(course_id__in=course_ids_with_certificates)

def in_progress(self, user_username, time_zone=UTC):
"""
Returns a queryset of CourseEnrollment objects for courses that are currently in progress.
"""
now = datetime.now(time_zone)
return self.active().without_certificates(user_username).filter(
Q(course__start__lte=now, course__end__gte=now)
| Q(course__start__isnull=True, course__end__isnull=True)
| Q(course__start__isnull=True, course__end__gte=now)
| Q(course__start__lte=now, course__end__isnull=True),
)

def completed(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that have been completed.
"""
return self.active().with_certificates(user_username)

def expired(self, user_username, time_zone=UTC):
"""
Returns a queryset of CourseEnrollment objects for courses that have expired.
"""
now = datetime.now(time_zone)
return self.active().without_certificates(user_username).filter(course__end__lt=now)


class CourseEnrollmentManager(models.Manager):
"""
Custom manager for CourseEnrollment with Table-level filter methods.
"""

def get_queryset(self):
return CourseEnrollmentQuerySet(self.model, using=self._db)

def is_small_course(self, course_id):
"""
Returns false if the number of enrollments are one greater than 'max_enrollments' else true
Expand Down
19 changes: 19 additions & 0 deletions lms/djangoapps/mobile_api/users/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from enum import Enum
from django.utils.functional import classproperty


class EnrollmentStatuses(Enum):
"""
"""

ALL = 'all'
IN_PROGRESS = 'in_progress'
COMPLETED = 'completed'
EXPIRED = 'expired'

# values = [ALL, IN_PROGRESS, COMPLETED, EXPIRED]

@classproperty
def values(cls):
return [e.value for e in cls]
2 changes: 1 addition & 1 deletion lms/djangoapps/mobile_api/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def get_audit_access_expires(self, model):
"""
Returns expiration date for a course audit expiration, if any or null
"""
return get_user_course_expiration_date(model.user, model.course)
return get_user_course_expiration_date(model.user, model.course, model)

def get_certificate(self, model):
"""Returns the information about the user's certificate in the course."""
Expand Down
52 changes: 40 additions & 12 deletions lms/djangoapps/mobile_api/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


import logging
from datetime import datetime
from functools import cached_property
from typing import List, Optional

Expand Down Expand Up @@ -42,6 +43,7 @@

from .. import errors
from ..decorators import mobile_course_access, mobile_view
from .enums import EnrollmentStatuses
from .serializers import (
CourseEnrollmentSerializer,
CourseEnrollmentSerializerModifiedForPrimary,
Expand Down Expand Up @@ -356,21 +358,36 @@ def get_serializer_class(self):

@cached_property
def queryset(self):
return CourseEnrollment.objects.all().select_related('course', 'user').filter(
user__username=self.kwargs['username'],
api_version = self.kwargs.get('api_version')
status = self.request.GET.get('status')
username = self.kwargs['username']

queryset = CourseEnrollment.objects.all().select_related('course', 'user').filter(
user__username=username,
is_active=True
).order_by('-created')

if api_version == API_V4 and status in EnrollmentStatuses.values:
if status == EnrollmentStatuses.IN_PROGRESS.value:
queryset = queryset.in_progress(user_username=username, time_zone=self.user_timezone)
elif status == EnrollmentStatuses.COMPLETED.value:
queryset = queryset.completed(user_username=username)
elif status == EnrollmentStatuses.EXPIRED.value:
queryset = queryset.expired(user_username=username, time_zone=self.user_timezone)

return queryset

def get_queryset(self):
api_version = self.kwargs.get('api_version')
status = self.request.GET.get('status')
mobile_available = self.get_mobile_available_enrollments()

not_duration_limited = (
enrollment for enrollment in mobile_available
if check_course_expired(self.request.user, enrollment.course) == ACCESS_GRANTED
)

if api_version == API_V4:
if api_version == API_V4 and status not in EnrollmentStatuses.values:
primary_enrollment_obj = self.get_primary_enrollment_by_latest_enrollment_or_progress()
if primary_enrollment_obj:
mobile_available.remove(primary_enrollment_obj)
Expand Down Expand Up @@ -401,26 +418,37 @@ def get_mobile_available_enrollments(self) -> List[Optional[CourseEnrollment]]:
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
api_version = self.kwargs.get('api_version')
status = self.request.GET.get('status')

if api_version in (API_V2, API_V3, API_V4):
enrollment_data = {
'configs': MobileConfig.get_structured_configs(),
'user_timezone': str(get_user_timezone_or_last_seen_timezone_or_utc(self.get_user())),
'user_timezone': str(self.user_timezone),
'enrollments': response.data
}
if api_version == API_V4:
primary_enrollment_obj = self.get_primary_enrollment_by_latest_enrollment_or_progress()
if primary_enrollment_obj:
serializer = CourseEnrollmentSerializerModifiedForPrimary(
primary_enrollment_obj,
context=self.get_serializer_context(),
)
enrollment_data.update({'primary': serializer.data})
if api_version == API_V4 and status not in EnrollmentStatuses.values:
if status in EnrollmentStatuses.values:
enrollment_data.update({'primary': None})
else:
primary_enrollment_obj = self.get_primary_enrollment_by_latest_enrollment_or_progress()
if primary_enrollment_obj:
serializer = CourseEnrollmentSerializerModifiedForPrimary(
primary_enrollment_obj,
context=self.get_serializer_context(),
)
enrollment_data.update({'primary': serializer.data})

return Response(enrollment_data)

return response

@cached_property
def user_timezone(self):
"""
Get the user's timezone.
"""
return get_user_timezone_or_last_seen_timezone_or_utc(self.get_user())

def get_user(self) -> User:
"""
Get user object by username.
Expand Down
4 changes: 2 additions & 2 deletions openedx/features/course_duration_limits/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def get_user_course_duration(user, course):
return get_expected_duration(course.id)


def get_user_course_expiration_date(user, course):
def get_user_course_expiration_date(user, course, enrollment=None):
"""
Return expiration date for given user course pair.
Return None if the course does not expire.
Expand All @@ -81,7 +81,7 @@ def get_user_course_expiration_date(user, course):
if access_duration is None:
return None

enrollment = CourseEnrollment.get_enrollment(user, course.id)
enrollment = CourseEnrollment.get_enrollment(user, course.id) if not enrollment else enrollment
if enrollment is None or enrollment.mode != CourseMode.AUDIT:
return None

Expand Down

0 comments on commit a87e8a3

Please sign in to comment.