Skip to content

Commit db7b8ec

Browse files
NiedielnitsevIvanmonteri
authored andcommitted
feat: [AXM-33] create enrollments filtering by course completion statuses (#2532)
* feat: [AXM-33] create enrollments filtering by course completion statuses * test: [AXM-33] add tests for filtrations * style: [AXM-33] fix pylint issues
1 parent 3868840 commit db7b8ec

File tree

6 files changed

+325
-15
lines changed

6 files changed

+325
-15
lines changed

common/djangoapps/student/models/course_enrollment.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,71 @@ class UnenrollmentNotAllowed(CourseEnrollmentException):
129129
pass
130130

131131

132+
class CourseEnrollmentQuerySet(models.QuerySet):
133+
"""
134+
Custom queryset for CourseEnrollment with Table-level filter methods.
135+
"""
136+
137+
def active(self):
138+
"""
139+
Returns a queryset of CourseEnrollment objects for courses that are currently active.
140+
"""
141+
return self.filter(is_active=True)
142+
143+
def without_certificates(self, user_username):
144+
"""
145+
Returns a queryset of CourseEnrollment objects for courses that do not have a certificate.
146+
"""
147+
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
148+
course_ids_with_certificates = GeneratedCertificate.objects.filter(
149+
user__username=user_username
150+
).values_list('course_id', flat=True)
151+
return self.exclude(course_id__in=course_ids_with_certificates)
152+
153+
def with_certificates(self, user_username):
154+
"""
155+
Returns a queryset of CourseEnrollment objects for courses that have a certificate.
156+
"""
157+
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
158+
course_ids_with_certificates = GeneratedCertificate.objects.filter(
159+
user__username=user_username
160+
).values_list('course_id', flat=True)
161+
return self.filter(course_id__in=course_ids_with_certificates)
162+
163+
def in_progress(self, user_username, time_zone=UTC):
164+
"""
165+
Returns a queryset of CourseEnrollment objects for courses that are currently in progress.
166+
"""
167+
now = datetime.now(time_zone)
168+
return self.active().without_certificates(user_username).filter(
169+
Q(course__start__lte=now, course__end__gte=now)
170+
| Q(course__start__isnull=True, course__end__isnull=True)
171+
| Q(course__start__isnull=True, course__end__gte=now)
172+
| Q(course__start__lte=now, course__end__isnull=True),
173+
)
174+
175+
def completed(self, user_username):
176+
"""
177+
Returns a queryset of CourseEnrollment objects for courses that have been completed.
178+
"""
179+
return self.active().with_certificates(user_username)
180+
181+
def expired(self, user_username, time_zone=UTC):
182+
"""
183+
Returns a queryset of CourseEnrollment objects for courses that have expired.
184+
"""
185+
now = datetime.now(time_zone)
186+
return self.active().without_certificates(user_username).filter(course__end__lt=now)
187+
188+
132189
class CourseEnrollmentManager(models.Manager):
133190
"""
134191
Custom manager for CourseEnrollment with Table-level filter methods.
135192
"""
136193

194+
def get_queryset(self):
195+
return CourseEnrollmentQuerySet(self.model, using=self._db)
196+
137197
def is_small_course(self, course_id):
138198
"""
139199
Returns false if the number of enrollments are one greater than 'max_enrollments' else true
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""
2+
Enums for mobile_api users app.
3+
"""
4+
from enum import Enum
5+
6+
7+
class EnrollmentStatuses(Enum):
8+
"""
9+
Enum for enrollment statuses.
10+
"""
11+
12+
ALL = 'all'
13+
IN_PROGRESS = 'in_progress'
14+
COMPLETED = 'completed'
15+
EXPIRED = 'expired'
16+
17+
@classmethod
18+
def values(cls):
19+
"""
20+
Returns string representation of all enum values.
21+
"""
22+
return [e.value for e in cls]

lms/djangoapps/mobile_api/users/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def get_audit_access_expires(self, model):
110110
"""
111111
Returns expiration date for a course audit expiration, if any or null
112112
"""
113-
return get_user_course_expiration_date(model.user, model.course)
113+
return get_user_course_expiration_date(model.user, model.course, model)
114114

115115
def get_certificate(self, model):
116116
"""Returns the information about the user's certificate in the course."""

lms/djangoapps/mobile_api/users/tests.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
MobileAuthUserTestMixin,
3838
MobileCourseAccessTestMixin
3939
)
40+
from lms.djangoapps.mobile_api.users.enums import EnrollmentStatuses
4041
from lms.djangoapps.mobile_api.utils import API_V1, API_V05, API_V2, API_V3, API_V4
4142
from openedx.core.lib.courses import course_image_url
4243
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
@@ -707,6 +708,201 @@ def test_course_status_in_primary_obj_when_student_have_progress(
707708
self.assertEqual(response.data['primary']['course_status'], expected_course_status)
708709
get_last_completed_block_mock.assert_called_once_with(self.user, course.id)
709710

711+
@patch('lms.djangoapps.mobile_api.users.serializers.cache.set', return_value=None)
712+
def test_user_enrollment_api_v4_in_progress_status(self, cache_mock: MagicMock):
713+
"""
714+
Testing
715+
"""
716+
self.login()
717+
old_course = CourseFactory.create(
718+
org="edx",
719+
mobile_available=True,
720+
start=self.THREE_YEARS_AGO,
721+
end=self.LAST_WEEK
722+
)
723+
actual_course = CourseFactory.create(
724+
org="edx",
725+
mobile_available=True,
726+
start=self.LAST_WEEK,
727+
end=self.NEXT_WEEK
728+
)
729+
infinite_course = CourseFactory.create(
730+
org="edx",
731+
mobile_available=True,
732+
start=self.LAST_WEEK,
733+
end=None
734+
)
735+
736+
self.enroll(old_course.id)
737+
self.enroll(actual_course.id)
738+
self.enroll(infinite_course.id)
739+
740+
response = self.api_response(api_version=API_V4, data={'status': EnrollmentStatuses.IN_PROGRESS.value})
741+
enrollments = response.data['enrollments']
742+
743+
self.assertEqual(response.status_code, status.HTTP_200_OK)
744+
self.assertEqual(enrollments['count'], 2)
745+
self.assertEqual(enrollments['results'][1]['course']['id'], str(actual_course.id))
746+
self.assertEqual(enrollments['results'][0]['course']['id'], str(infinite_course.id))
747+
self.assertNotIn('primary', response.data)
748+
749+
def test_user_enrollment_api_v4_completed_status(self):
750+
"""
751+
Testing
752+
"""
753+
self.login()
754+
old_course = CourseFactory.create(
755+
org="edx",
756+
mobile_available=True,
757+
start=self.THREE_YEARS_AGO,
758+
end=self.LAST_WEEK
759+
)
760+
actual_course = CourseFactory.create(
761+
org="edx",
762+
mobile_available=True,
763+
start=self.LAST_WEEK,
764+
end=self.NEXT_WEEK
765+
)
766+
infinite_course = CourseFactory.create(
767+
org="edx",
768+
mobile_available=True,
769+
start=self.LAST_WEEK,
770+
end=None
771+
)
772+
GeneratedCertificateFactory.create(
773+
user=self.user,
774+
course_id=infinite_course.id,
775+
status=CertificateStatuses.downloadable,
776+
mode='verified',
777+
)
778+
779+
self.enroll(old_course.id)
780+
self.enroll(actual_course.id)
781+
self.enroll(infinite_course.id)
782+
783+
response = self.api_response(api_version=API_V4, data={'status': EnrollmentStatuses.COMPLETED.value})
784+
enrollments = response.data['enrollments']
785+
786+
self.assertEqual(response.status_code, status.HTTP_200_OK)
787+
self.assertEqual(enrollments['count'], 1)
788+
self.assertEqual(enrollments['results'][0]['course']['id'], str(infinite_course.id))
789+
self.assertNotIn('primary', response.data)
790+
791+
def test_user_enrollment_api_v4_expired_status(self):
792+
"""
793+
Testing
794+
"""
795+
self.login()
796+
old_course = CourseFactory.create(
797+
org="edx",
798+
mobile_available=True,
799+
start=self.THREE_YEARS_AGO,
800+
end=self.LAST_WEEK
801+
)
802+
actual_course = CourseFactory.create(
803+
org="edx",
804+
mobile_available=True,
805+
start=self.LAST_WEEK,
806+
end=self.NEXT_WEEK
807+
)
808+
infinite_course = CourseFactory.create(
809+
org="edx",
810+
mobile_available=True,
811+
start=self.LAST_WEEK,
812+
end=None
813+
)
814+
self.enroll(old_course.id)
815+
self.enroll(actual_course.id)
816+
self.enroll(infinite_course.id)
817+
818+
response = self.api_response(api_version=API_V4, data={'status': EnrollmentStatuses.EXPIRED.value})
819+
enrollments = response.data['enrollments']
820+
821+
self.assertEqual(response.status_code, status.HTTP_200_OK)
822+
self.assertEqual(enrollments['count'], 1)
823+
self.assertEqual(enrollments['results'][0]['course']['id'], str(old_course.id))
824+
self.assertNotIn('primary', response.data)
825+
826+
def test_user_enrollment_api_v4_expired_course_with_certificate(self):
827+
"""
828+
Testing that the API returns a course with
829+
an expiration date in the past if the user has a certificate for this course.
830+
"""
831+
self.login()
832+
expired_course = CourseFactory.create(
833+
org="edx",
834+
mobile_available=True,
835+
start=self.THREE_YEARS_AGO,
836+
end=self.LAST_WEEK
837+
)
838+
expired_course_with_cert = CourseFactory.create(
839+
org="edx",
840+
mobile_available=True,
841+
start=self.THREE_YEARS_AGO,
842+
end=self.LAST_WEEK
843+
)
844+
GeneratedCertificateFactory.create(
845+
user=self.user,
846+
course_id=expired_course_with_cert.id,
847+
status=CertificateStatuses.downloadable,
848+
mode='verified',
849+
)
850+
851+
self.enroll(expired_course_with_cert.id)
852+
self.enroll(expired_course.id)
853+
854+
response = self.api_response(api_version=API_V4, data={'status': EnrollmentStatuses.COMPLETED.value})
855+
enrollments = response.data['enrollments']
856+
857+
self.assertEqual(response.status_code, status.HTTP_200_OK)
858+
self.assertEqual(enrollments['count'], 1)
859+
self.assertEqual(enrollments['results'][0]['course']['id'], str(expired_course_with_cert.id))
860+
self.assertNotIn('primary', response.data)
861+
862+
def test_user_enrollment_api_v4_status_all(self):
863+
"""
864+
Testing
865+
"""
866+
self.login()
867+
old_course = CourseFactory.create(
868+
org="edx",
869+
mobile_available=True,
870+
start=self.THREE_YEARS_AGO,
871+
end=self.LAST_WEEK
872+
)
873+
actual_course = CourseFactory.create(
874+
org="edx",
875+
mobile_available=True,
876+
start=self.LAST_WEEK,
877+
end=self.NEXT_WEEK
878+
)
879+
infinite_course = CourseFactory.create(
880+
org="edx",
881+
mobile_available=True,
882+
start=self.LAST_WEEK,
883+
end=None
884+
)
885+
GeneratedCertificateFactory.create(
886+
user=self.user,
887+
course_id=infinite_course.id,
888+
status=CertificateStatuses.downloadable,
889+
mode='verified',
890+
)
891+
892+
self.enroll(old_course.id)
893+
self.enroll(actual_course.id)
894+
self.enroll(infinite_course.id)
895+
896+
response = self.api_response(api_version=API_V4, data={'status': EnrollmentStatuses.ALL.value})
897+
enrollments = response.data['enrollments']
898+
899+
self.assertEqual(response.status_code, status.HTTP_200_OK)
900+
self.assertEqual(enrollments['count'], 3)
901+
self.assertEqual(enrollments['results'][0]['course']['id'], str(infinite_course.id))
902+
self.assertEqual(enrollments['results'][1]['course']['id'], str(actual_course.id))
903+
self.assertEqual(enrollments['results'][2]['course']['id'], str(old_course.id))
904+
self.assertNotIn('primary', response.data)
905+
710906

711907
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
712908
class TestUserEnrollmentCertificates(UrlResetMixin, MobileAPITestCase, MilestonesTestCaseMixin):

0 commit comments

Comments
 (0)