Skip to content

Commit

Permalink
feat: course details API
Browse files Browse the repository at this point in the history
  • Loading branch information
shadinaif committed Apr 18, 2024
1 parent 6add8d9 commit 3c60279
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 3 deletions.
64 changes: 64 additions & 0 deletions futurex_openedx_extensions/dashboard/details/courses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Courses details collectors"""
from __future__ import annotations

from typing import List

from common.djangoapps.student.models import CourseAccessRole, CourseEnrollment, UserSignupSource
from django.db.models import Count, Exists, OuterRef, Q, Subquery
from django.db.models.query import QuerySet
from lms.djangoapps.certificates.models import GeneratedCertificate
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview

from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list, get_tenant_site


def get_courses_queryset(tenant_ids: List, search_text: str = None) -> QuerySet:
"""
Get the courses queryset for the given tenant IDs and search text.
:param tenant_ids: List of tenant IDs to get the courses for
:type tenant_ids: List
:param search_text: Search text to filter the courses by
:type search_text: str
"""
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 = CourseOverview.objects.filter(
org__in=course_org_filter_list
)
search_text = (search_text or '').strip()
if search_text:
queryset = queryset.filter(
Q(display_name__icontains=search_text) |
Q(id__icontains=search_text)
)
queryset = queryset.annotate(
rating=1
).annotate(
enrolled_count=Count(
CourseEnrollment.objects.filter(
course_id=OuterRef('id'),
is_active=True,
)
)
).annotate(
active_count=Count(
CourseEnrollment.objects.filter(
course_id=OuterRef('id'),
is_active=True,
)
)
).annotate(
certificates_count=Count(
GeneratedCertificate.objects.filter(
course_id=OuterRef('id'),
status='downloadable'
)
)
)

return queryset
78 changes: 77 additions & 1 deletion futurex_openedx_extensions/dashboard/serializers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""Serializers for the dashboard details API."""

from django.contrib.auth import get_user_model
from django.utils.timezone import now
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from rest_framework import serializers

from helpers.tenants import get_tenants_by_org


class LearnerDetailsSerializer(serializers.ModelSerializer):
"""Serializer for learner details."""
Expand Down Expand Up @@ -66,3 +69,76 @@ def get_certificates_count(self, obj):
def get_enrolled_courses_count(self, obj):
"""Return enrolled courses count."""
return obj.courses_count


class CourseDetailsSerializer(serializers.ModelSerializer):
"""Serializer for course details."""
STATUS_ACTIVE = 'active'
STATUS_ARCHIVED = 'archived'
STATUS_SOON = 'soon'
STATUS_SELF_PREFIX = 'self_'

status = serializers.SerializerMethodField()
rating = serializers.SerializerMethodField()
# enrolled_count = serializers.SerializerMethodField()
# active_count = serializers.SerializerMethodField()
# certificates_count = serializers.SerializerMethodField()
# start_date = serializers.SerializerMethodField()
# end_date = serializers.SerializerMethodField()
start_enrollment_date = serializers.SerializerMethodField()
end_enrollment_date = serializers.SerializerMethodField()
# display_name = serializers.SerializerMethodField()
image_url = serializers.SerializerMethodField()
# org = serializers.SerializerMethodField()
tenant_ids = serializers.SerializerMethodField()

class Meta:
model = CourseOverview
fields = [
'id',
'status',
'self_paced',
'rating',
'enrolled_count',
'active_count',
'certificates_count',
'start_date',
'end_date',
'start_enrollment_date',
'end_enrollment_date',
'display_name',
'image_url',
'org',
'tenant_ids',
]

def get_status(self, obj):
"""Return the course status."""
if obj.end and obj.end < now():
status = self.STATUS_ARCHIVED
elif obj.start and obj.start > now():
status = self.STATUS_SOON
else:
status = self.STATUS_ACTIVE

return f'{self.STATUS_SELF_PREFIX if obj.self_paced else ""}{status}'

def get_rating(self, obj):
"""Return the course rating."""
return 3.5

def get_start_enrollment_date(self, obj):
"""Return the start enrollment date."""
return obj.enrollment_start

def get_end_enrollment_date(self, obj):
"""Return the end enrollment date."""
return obj.enrollment_end

def get_image_url(self, obj):
"""Return the course image URL."""
return obj.course_image_url

def get_tenant_ids(self, obj):
"""Return the tenant IDs."""
return get_tenants_by_org(obj.org)
1 change: 1 addition & 0 deletions futurex_openedx_extensions/dashboard/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
urlpatterns = [
re_path(r'^api/fx/statistics/v1/total_counts', TotalCountsView.as_view(), name='total-counts'),
re_path(r'^api/fx/learners/v1/learners', LearnersView.as_view(), name='learners'),
re_path(r'^api/fx/courses/v1/courses', LearnersView.as_view(), name='courses'),
]
33 changes: 31 additions & 2 deletions futurex_openedx_extensions/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from futurex_openedx_extensions.dashboard.details.courses import get_courses_queryset
from futurex_openedx_extensions.dashboard.details.learners import get_learners_queryset
from futurex_openedx_extensions.dashboard.serializers import LearnerDetailsSerializer
from futurex_openedx_extensions.dashboard.serializers import CourseDetailsSerializer, LearnerDetailsSerializer
from futurex_openedx_extensions.dashboard.statistics.certificates import get_certificates_count
from futurex_openedx_extensions.dashboard.statistics.courses import get_courses_count
from futurex_openedx_extensions.dashboard.statistics.learners import get_learners_count
from futurex_openedx_extensions.helpers.converters import error_details_to_dictionary, ids_string_to_list
from futurex_openedx_extensions.helpers.pagination import DefaultPagination
from futurex_openedx_extensions.helpers.pagination import DefaultOrderingFilter, DefaultPagination
from futurex_openedx_extensions.helpers.permissions import HasTenantAccess
from futurex_openedx_extensions.helpers.tenants import get_accessible_tenant_ids

Expand Down Expand Up @@ -109,3 +110,31 @@ def get_queryset(self):
tenant_ids=ids_string_to_list(tenant_ids) if tenant_ids else get_accessible_tenant_ids(self.request.user),
search_text=search_text,
)


class CoursesView(ListAPIView):
"""View to get the list of courses"""
serializer_class = CourseDetailsSerializer
permission_classes = [HasTenantAccess]
pagination_class = DefaultPagination
filter_backends = [DefaultOrderingFilter]
ordering_fields = [
'id', 'status', 'self_paced', 'rating', 'enrolled_count', 'active_count',
'certificates_count', 'start_date', 'end_date', 'start_enrollment_date',
'end_enrollment_date', 'display_name', 'image_url', 'org', 'tenant_ids',
]
ordering = ['display_name']

def get_queryset(self):
"""Get the list of learners"""
tenant_ids = self.request.query_params.get('tenant_ids')
search_text = self.request.query_params.get('search_text')
return get_courses_queryset(
tenant_ids=ids_string_to_list(tenant_ids) if tenant_ids else get_accessible_tenant_ids(self.request.user),
search_text=search_text,
)

# def filter_queryset(self, queryset):
# """Filter the queryset"""
# queryset = super().filter_queryset(queryset)
# return super().filter_queryset(queryset)
5 changes: 5 additions & 0 deletions futurex_openedx_extensions/helpers/pagination.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""Pagination helpers and classes for the API views."""
from rest_framework.filters import OrderingFilter
from rest_framework.pagination import PageNumberPagination


class DefaultPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100


class DefaultOrderingFilter(OrderingFilter):
ordering_param = 'sort'
13 changes: 13 additions & 0 deletions futurex_openedx_extensions/helpers/tenants.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,16 @@ def check_tenant_access(user: get_user_model(), tenant_ids_string: str) -> tuple
)

return True, {}


def get_tenants_by_org(org: str) -> List[int]:
"""
Get the tenants that have <org> in their course org filter
:param org: The org to check
:type org: str
:return: List of tenant IDs
:rtype: List[int]
"""
tenant_configs = get_all_course_org_filter_list()
return [t_id for t_id, course_org_filter in tenant_configs.items() if org in course_org_filter]

0 comments on commit 3c60279

Please sign in to comment.