-
Notifications
You must be signed in to change notification settings - Fork 0
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
feat: statistics API #4
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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' |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why this is the case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need the list of users related to the tenant. So, the user must be enrolled in one of the tenant's courses, or have a site login in the tenant's site |
||
).select_related('profile').order_by('id') | ||
|
||
return queryset |
This file was deleted.
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 |
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) |
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') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This query is a bit slow on production. Any idea how can we make it faster?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the issue is not this particular query. It's the other helper queries, you'll find a few methods with
TODO
to add cache. These will increase the performance significantly as far as I can seeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if caching didn't fix it, we might need to do something with
UserSignupSource
. We need it to record logins, not just signups. So we can use it to directly filter the user by site.Currently, there are too many records for users with enrollments in a site, but without a record in
UserSignupSource