Skip to content

Commit

Permalink
feat: add new APIs for stats
Browse files Browse the repository at this point in the history
  • Loading branch information
Muhammad Faraz Maqsood authored and Muhammad Faraz Maqsood committed Jul 15, 2024
1 parent ad14dad commit 2c15cf6
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
"""

from django.urls import path # pylint: disable=unused-import
from .views import UserStatsAPIView
from .views import UserStatsAPIView, DashboardStatsAPIView


app_name = "nafath_api_v1"

urlpatterns = [
path(r"user_stats", UserStatsAPIView.as_view()),
path(r"dashboard_stats", DashboardStatsAPIView.as_view()),
]
99 changes: 96 additions & 3 deletions openedx/features/sdaia_features/course_progress/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,36 @@

from ccx_keys.locator import CCXLocator
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import F
from django.utils.decorators import method_decorator
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from rest_framework import permissions, status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated

from common.djangoapps.student.roles import (
CourseInstructorRole,
CourseStaffRole,
UserBasedRole,
)
from common.djangoapps.util.disable_rate_limit import can_disable_rate_limit
from edx_rest_framework_extensions.auth.session.authentication import (
SessionAuthenticationAllowInactiveUser,
)
from lms.djangoapps.badges.models import BadgeAssertion, LeaderboardEntry
from lms.djangoapps.certificates.models import GeneratedCertificate
from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain
from openedx.core.djangoapps.enrollments.errors import CourseEnrollmentError
from openedx.core.djangoapps.enrollments.data import get_course_enrollments
from openedx.core.djangoapps.enrollments.views import EnrollmentCrossDomainSessionAuth
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.core.lib.api.permissions import ApiKeyHeaderPermissionIsAuthenticated
from openedx.features.sdaia_features.course_progress.utils import (
get_user_certificates,
)


log = logging.getLogger(__name__)
Expand All @@ -34,16 +45,17 @@
@can_disable_rate_limit
class UserStatsAPIView(APIView):
"""
APIView to get the total watch hours for a user.
APIView to get the user stats.
**Example Requests**
GET /sdaia/api/v1/user_stats
It return watch_time in hours
Response: {
"watch_hours": 0.00043390860160191856,
"enrolled_courses": enrolled_courses,
"enrolled_programs": enrolled_programs,
"user_certificates": user_certificates,
"score": score,
}
"""

Expand All @@ -57,7 +69,7 @@ class UserStatsAPIView(APIView):
@method_decorator(ensure_csrf_cookie_cross_domain)
def get(self, request):
"""
Gets the total watch hours for a user.
Gets the stats for a user.
"""
user = request.user
user_id = user.id
Expand Down Expand Up @@ -94,12 +106,93 @@ def get(self, request):
},
)

############ USER CERTIFICATES ############
user_certificates = get_user_certificates(username)

############ USER BADGES ############
user_badges = BadgeAssertion.objects.values(
"image_url",
"assertion_url",
"created",
slug=F("badge_class__slug"),
issuing_component=F("badge_class__issuing_component"),
display_name=F("badge_class__display_name"),
course_id=F("badge_class__course_id"),
description=F("badge_class__description"),
criteria=F("badge_class__criteria"),
image=F("badge_class__image"),
)
for badge in user_badges:
badge["course_id"] = str(badge["course_id"])

############ USER SCORE ############
leaderboard = LeaderboardEntry.objects.filter(user=user)
score = leaderboard and leaderboard.first().score

############ Response ############
return Response(
status=status.HTTP_200_OK,
data={
"watch_hours": watch_time,
"enrolled_courses": enrolled_courses,
"enrolled_programs": no_of_programs,
"score": score,
"user_certificates": user_certificates,
"user_badges": user_badges,
},
)


@can_disable_rate_limit
class DashboardStatsAPIView(APIView):
"""
APIView to get the dashboard stats.
**Example Requests**
GET /sdaia/api/v1/dashboard_stats
Response: {
}
"""

authentication_classes = (
JwtAuthentication,
BearerAuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser,
)
permission_classes = (IsAuthenticated,)

@method_decorator(ensure_csrf_cookie_cross_domain)
def get(self, request):
"""
Gets the stats for dashboard.
"""
user = request.user
users_count = User.objects.all().count()
certificates_count = GeneratedCertificate.objects.all().count()
clickhouse_uri = (
f"{settings.CAIRN_CLICKHOUSE_HTTP_SCHEME}://{settings.CAIRN_CLICKHOUSE_USERNAME}:{settings.CAIRN_CLICKHOUSE_PASSWORD}@"
f"{settings.CAIRN_CLICKHOUSE_HOST}:{settings.CAIRN_CLICKHOUSE_HTTP_PORT}/?database={settings.CAIRN_CLICKHOUSE_DATABASE}"
)
query = f"SELECT SUM(duration) as `Watch time` FROM `openedx`.`video_view_segments`;"

############ TOTAL WATCH HOURS ############
try:
response = requests.get(clickhouse_uri, data=query.encode("utf8"))
watch_time = float(response.content.decode().strip()) / (60 * 60)
except Exception as e:
log.error(
f"Unable to fetch total watch hours due to this exception: {str(e)}"
)
raise HTTPException(status_code=500, detail=str(e))

############ Response ############
return Response(
status=status.HTTP_200_OK,
data={
"users_count": users_count,
"certificates_count": certificates_count,
"total_watch_time": watch_time,
},
)
76 changes: 72 additions & 4 deletions openedx/features/sdaia_features/course_progress/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
"""
Utility functions for the course progress emails
"""

from lms.djangoapps.certificates.api import (
certificates_viewable_for_course,
get_certificates_for_user,
)
from lms.djangoapps.courseware.courses import get_course_blocks_completion_summary
from openedx.core.djangoapps.content.course_overviews.api import (
get_course_overviews_from_ids,
get_pseudo_course_overview,
)


def get_user_course_progress(user, course_key):
Expand All @@ -13,10 +22,69 @@ def get_user_course_progress(user, course_key):
"""
completion_summary = get_course_blocks_completion_summary(course_key, user)

complete_count = completion_summary.get('complete_count', 0)
incomplete_count = completion_summary.get('incomplete_count', 0)
locked_count = completion_summary.get('locked_count', 0)
complete_count = completion_summary.get("complete_count", 0)
incomplete_count = completion_summary.get("incomplete_count", 0)
locked_count = completion_summary.get("locked_count", 0)
total_count = complete_count + incomplete_count + locked_count

completion_percentage = round((complete_count / total_count) * 100)
return completion_percentage
return completion_percentage


def get_user_certificates(username):
user_certs = []
for user_cert in _get_certificates_for_user(username):
user_certs.append(
{
"username": user_cert.get("username"),
"course_id": str(user_cert.get("course_key")),
"course_display_name": user_cert.get("course_display_name"),
"course_organization": user_cert.get("course_organization"),
"certificate_type": user_cert.get("type"),
"created_date": user_cert.get("created"),
"modified_date": user_cert.get("modified"),
"status": user_cert.get("status"),
"is_passing": user_cert.get("is_passing"),
"download_url": user_cert.get("download_url"),
"grade": user_cert.get("grade"),
}
)
return user_certs


def _get_certificates_for_user(username):
"""
Returns a user's viewable certificates sorted by course name.
"""
course_certificates = get_certificates_for_user(username)
passing_certificates = {}
for course_certificate in course_certificates:
if course_certificate.get("is_passing", False):
course_key = course_certificate["course_key"]
passing_certificates[course_key] = course_certificate

viewable_certificates = []
course_ids = list(passing_certificates.keys())
course_overviews = get_course_overviews_from_ids(course_ids)
for course_key, course_overview in course_overviews.items():
if not course_overview:
# For deleted XML courses in which learners have a valid certificate.
# i.e. MITx/7.00x/2013_Spring
course_overview = get_pseudo_course_overview(course_key)
if certificates_viewable_for_course(course_overview):
course_certificate = passing_certificates[course_key]
# add certificate into viewable certificate list only if it's a PDF certificate
# or there is an active certificate configuration.
if course_certificate["is_pdf_certificate"] or (
course_overview and course_overview.has_any_active_web_certificate
):
course_certificate["course_display_name"] = (
course_overview.display_name_with_default
)
course_certificate["course_organization"] = (
course_overview.display_org_with_default
)
viewable_certificates.append(course_certificate)

viewable_certificates.sort(key=lambda certificate: certificate["created"])
return viewable_certificates

0 comments on commit 2c15cf6

Please sign in to comment.