Skip to content
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: add new APIs for stats #566

Merged
merged 1 commit into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading