-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from nelc/shadinaif/dashbord-stats-learners-count
feat: statistics API
- Loading branch information
Showing
65 changed files
with
2,245 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.1.0' | ||
__version__ = '0.1.1' |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
"""Learners details collectors""" | ||
from __future__ import annotations | ||
|
||
from typing import List | ||
|
||
from common.djangoapps.student.models import CourseAccessRole, UserSignupSource | ||
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 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_learners_queryset(tenant_ids: List, search_text: str = None) -> QuerySet: | ||
""" | ||
Get the learners queryset for the given tenant IDs and search text. | ||
:param tenant_ids: List of tenant IDs to get the learners for | ||
:type tenant_ids: List | ||
:param search_text: Search text to filter the learners 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 = get_user_model().objects.filter( | ||
is_superuser=False, | ||
is_staff=False, | ||
is_active=True, | ||
) | ||
search_text = (search_text or '').strip() | ||
if search_text: | ||
queryset = queryset.filter( | ||
Q(username__icontains=search_text) | | ||
Q(email__icontains=search_text) | | ||
Q(profile__name__icontains=search_text) | ||
) | ||
|
||
queryset = queryset.annotate( | ||
courses_count=Count( | ||
'courseenrollment', | ||
filter=( | ||
Q(courseenrollment__course__org__in=course_org_filter_list) & | ||
~Exists( | ||
CourseAccessRole.objects.filter( | ||
user_id=OuterRef('id'), | ||
org=OuterRef('courseenrollment__course__org') | ||
) | ||
) | ||
), | ||
distinct=True | ||
) | ||
).annotate( | ||
certificates_count=Count( | ||
'generatedcertificate', | ||
filter=( | ||
Q(generatedcertificate__course_id__in=Subquery( | ||
CourseOverview.objects.filter( | ||
org__in=course_org_filter_list | ||
).values_list('id', flat=True) | ||
)) & | ||
Q(generatedcertificate__status='downloadable') | ||
), | ||
distinct=True | ||
) | ||
).annotate( | ||
has_site_login=Exists( | ||
UserSignupSource.objects.filter( | ||
user_id=OuterRef('id'), | ||
site__in=tenant_sites | ||
) | ||
) | ||
).filter( | ||
Q(courses_count__gt=0) | Q(has_site_login=True) | ||
).select_related('profile').order_by('id') | ||
|
||
return queryset |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
"""Serializers for the dashboard details API.""" | ||
|
||
from django.contrib.auth import get_user_model | ||
from rest_framework import serializers | ||
|
||
|
||
class LearnerDetailsSerializer(serializers.ModelSerializer): | ||
"""Serializer for learner details.""" | ||
user_id = serializers.SerializerMethodField() | ||
full_name = serializers.SerializerMethodField() | ||
username = serializers.CharField() | ||
email = serializers.EmailField() | ||
mobile_no = serializers.SerializerMethodField() | ||
date_of_birth = serializers.SerializerMethodField() | ||
gender = serializers.SerializerMethodField() | ||
date_joined = serializers.DateTimeField() | ||
last_login = serializers.DateTimeField() | ||
enrolled_courses_count = serializers.SerializerMethodField() | ||
certificates_count = serializers.SerializerMethodField() | ||
|
||
class Meta: | ||
model = get_user_model() | ||
fields = [ | ||
'user_id', | ||
'full_name', | ||
'username', | ||
'email', | ||
'mobile_no', | ||
'date_of_birth', | ||
'gender', | ||
'date_joined', | ||
'last_login', | ||
'enrolled_courses_count', | ||
'certificates_count', | ||
] | ||
|
||
@staticmethod | ||
def _get_profile_field(obj, field_name): | ||
"""Get the profile field value.""" | ||
return getattr(obj.profile, field_name) if hasattr(obj, 'profile') and obj.profile else None | ||
|
||
def get_user_id(self, obj): | ||
"""Return user ID.""" | ||
return obj.id | ||
|
||
def get_full_name(self, obj): | ||
"""Return full name.""" | ||
return self._get_profile_field(obj, 'name') | ||
|
||
def get_mobile_no(self, obj): | ||
"""Return mobile number.""" | ||
return self._get_profile_field(obj, 'phone_number') | ||
|
||
def get_date_of_birth(self, obj): # pylint: disable=unused-argument | ||
"""Return date of birth.""" | ||
return None | ||
|
||
def get_gender(self, obj): | ||
"""Return gender.""" | ||
return self._get_profile_field(obj, 'gender') | ||
|
||
def get_certificates_count(self, obj): | ||
"""Return certificates count.""" | ||
return obj.certificates_count | ||
|
||
def get_enrolled_courses_count(self, obj): | ||
"""Return enrolled courses count.""" | ||
return obj.courses_count |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
36 changes: 36 additions & 0 deletions
36
futurex_openedx_extensions/dashboard/statistics/certificates.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
"""functions for getting statistics about certificates""" | ||
from __future__ import annotations | ||
|
||
from typing import Dict, List | ||
|
||
from django.db.models import Count, OuterRef, Subquery | ||
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 | ||
|
||
|
||
def get_certificates_count(tenant_ids: List[int]) -> Dict[str, int]: | ||
""" | ||
Get the count of issued certificates in the given tenants. The count is grouped by organization. Certificates | ||
for admins, staff, and superusers are also included. | ||
:param tenant_ids: List of tenant IDs to get the count for | ||
:type tenant_ids: List[int] | ||
:return: Count of certificates per organization | ||
:rtype: Dict[str, int] | ||
""" | ||
course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list'] | ||
|
||
result = list(GeneratedCertificate.objects.filter( | ||
status='downloadable', | ||
course_id__in=CourseOverview.objects.filter( | ||
org__in=course_org_filter_list | ||
), | ||
).annotate(course_org=Subquery( | ||
CourseOverview.objects.filter( | ||
id=OuterRef('course_id') | ||
).values('org') | ||
)).values('course_org').annotate(certificates_count=Count('id')).values_list('course_org', 'certificates_count')) | ||
|
||
return dict(result) |
41 changes: 41 additions & 0 deletions
41
futurex_openedx_extensions/dashboard/statistics/courses.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
"""functions for getting statistics about courses""" | ||
from __future__ import annotations | ||
|
||
from typing import List | ||
|
||
from django.db.models import Count, Q | ||
from django.db.models.query import QuerySet | ||
from django.utils.timezone import now | ||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview | ||
|
||
from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list | ||
|
||
|
||
def get_courses_count(tenant_ids: List[int], only_active=False, only_visible=False) -> QuerySet: | ||
""" | ||
Get the count of courses in the given tenants | ||
:param tenant_ids: List of tenant IDs to get the count for | ||
:type tenant_ids: List[int] | ||
:param only_active: Whether to only count active courses (according to dates) | ||
:type only_active: bool | ||
:param only_visible: Whether to only count visible courses (according to staff-only visibility) | ||
:type only_visible: bool | ||
:return: QuerySet of courses count per organization | ||
:rtype: QuerySet | ||
""" | ||
course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list'] | ||
|
||
q_set = CourseOverview.objects.filter(org__in=course_org_filter_list) | ||
if only_active: | ||
q_set = q_set.filter( | ||
Q(start__isnull=True) | Q(start__lte=now()), | ||
).filter( | ||
Q(end__isnull=True) | Q(end__gte=now()), | ||
) | ||
if only_visible: | ||
q_set = q_set.filter(visible_to_staff_only=False) | ||
|
||
return q_set.values('org').annotate( | ||
courses_count=Count('id') | ||
).order_by('org') |
Oops, something went wrong.