Skip to content

Commit

Permalink
feat: add stats API view
Browse files Browse the repository at this point in the history
  • Loading branch information
andrey-canon committed Jun 29, 2023
1 parent b56fcaf commit 02c763f
Show file tree
Hide file tree
Showing 24 changed files with 1,087 additions and 1 deletion.
13 changes: 13 additions & 0 deletions eox_nelp/edxapp_wrapper/backends/branding_m_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Backend for branding django app module.
This file contains all the necessary branding dependencies from
https://github.com/eduNEXT/edunext-platform/blob/ednx-release/mango.master/lms/djangoapps/branding/__init__.py"""
from lms.djangoapps.branding import get_visible_courses


def get_visible_courses_method():
"""Allow to get the get_visible_courses function from
https://github.com/eduNEXT/edunext-platform/blob/ednx-release/mango.master/lms/djangoapps/branding/__init__.py#L17
Returns:
get_visible_courses function.
"""
return get_visible_courses
11 changes: 10 additions & 1 deletion eox_nelp/edxapp_wrapper/backends/student_m_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This file contains all the necessary student dependencies from
https://github.com/eduNEXT/edunext-platform/tree/ednx-release/mango.master/common/djangoapps/student
"""
from common.djangoapps.student.models import CourseEnrollment # pylint: disable=import-error
from common.djangoapps.student.models import CourseAccessRole, CourseEnrollment # pylint: disable=import-error


def get_course_enrollment_model():
Expand All @@ -12,3 +12,12 @@ def get_course_enrollment_model():
CourseEnrollment Model.
"""
return CourseEnrollment


def get_course_access_role_model():
"""Allow to get the CourseAccessRole Model from
https://github.com/eduNEXT/edunext-platform/blob/ednx-release/mango.master/common/djangoapps/student/models.py#L2554
Returns:
CourseAccessRole Model.
"""
return CourseAccessRole
12 changes: 12 additions & 0 deletions eox_nelp/edxapp_wrapper/branding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Wrapper for module in branding app.
This contains all the required dependencies from branding.
Attributes:
get_visible_courses: Wrapper get_visible_courses function.
"""
from importlib import import_module

from django.conf import settings

backend = import_module(settings.EOX_NELP_BRANDING_BACKEND)

get_visible_courses = backend.get_visible_courses_method()
1 change: 1 addition & 0 deletions eox_nelp/edxapp_wrapper/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
backend = import_module(settings.EOX_NELP_STUDENT_BACKEND)

CourseEnrollment = backend.get_course_enrollment_model()
CourseAccessRole = backend.get_course_access_role_model()
10 changes: 10 additions & 0 deletions eox_nelp/edxapp_wrapper/test_backends/branding_m_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Test backend for bulk-email module."""
from mock import Mock


def get_visible_courses_method():
"""Return test function.
Returns:
Mock class.
"""
return Mock()
8 changes: 8 additions & 0 deletions eox_nelp/edxapp_wrapper/test_backends/student_m_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ def get_course_enrollment_model():
Mock class.
"""
return Mock()


def get_course_access_role_model():
"""Return test Model.
Returns:
Mock class.
"""
return Mock()
1 change: 1 addition & 0 deletions eox_nelp/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def plugin_settings(settings):
settings.EOX_NELP_BULK_EMAIL_BACKEND = 'eox_nelp.edxapp_wrapper.backends.bulk_email_m_v1'
settings.EOX_NELP_STUDENT_BACKEND = 'eox_nelp.edxapp_wrapper.backends.student_m_v1'
settings.EOX_NELP_EDXMAKO_BACKEND = 'eox_nelp.edxapp_wrapper.backends.edxmako_m_v1'
settings.EOX_NELP_BRANDING_BACKEND = 'eox_nelp.edxapp_wrapper.backends.branding_m_v1'

settings.FUTUREX_API_URL = 'https://testing-site.com'
settings.FUTUREX_API_CLIENT_ID = 'my-test-client-id'
Expand Down
5 changes: 5 additions & 0 deletions eox_nelp/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def plugin_settings(settings): # pylint: disable=function-redefined
settings.EOX_NELP_BULK_EMAIL_BACKEND = 'eox_nelp.edxapp_wrapper.test_backends.bulk_email_m_v1'
settings.EOX_NELP_STUDENT_BACKEND = 'eox_nelp.edxapp_wrapper.test_backends.student_m_v1'
settings.EOX_NELP_EDXMAKO_BACKEND = 'eox_nelp.edxapp_wrapper.test_backends.edxmako_m_v1'
settings.EOX_NELP_BRANDING_BACKEND = 'eox_nelp.edxapp_wrapper.test_backends.branding_m_v1'

settings.FUTUREX_API_URL = 'https://testing.com'
settings.FUTUREX_API_CLIENT_ID = 'my-test-client-id'
Expand Down Expand Up @@ -94,6 +95,10 @@ def plugin_settings(settings): # pylint: disable=function-redefined
EOX_CORE_COURSEWARE_BACKEND = "eox_nelp.edxapp_wrapper.test_backends.courseware_m_v1"
EOX_CORE_GRADES_BACKEND = "eox_nelp.edxapp_wrapper.test_backends.grades_m_v1"

GET_SITE_CONFIGURATION_MODULE = 'eox_tenant.edxapp_wrapper.backends.site_configuration_module_test_v1'
GET_THEMING_HELPERS = 'eox_tenant.edxapp_wrapper.backends.theming_helpers_test_v1'
EOX_TENANT_USERS_BACKEND = 'eox_tenant.edxapp_wrapper.backends.users_test_v1'

# ------------eox-audit external config for tests------------------------------
if find_spec('eox_audit_model') and EOX_AUDIT_MODEL_APP not in INSTALLED_APPS: # noqa: F405
INSTALLED_APPS.append(EOX_AUDIT_MODEL_APP) # noqa: F405
Expand Down
Empty file added eox_nelp/stats/__init__.py
Empty file.
Empty file added eox_nelp/stats/api/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions eox_nelp/stats/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""eox_nelp course_experience api urls
"""
from django.urls import include, path

app_name = "eox_nelp" # pylint: disable=invalid-name

urlpatterns = [ # pylint: disable=invalid-name
path("v1/", include("eox_nelp.stats.api.v1.urls", namespace="v1")),
]
Empty file.
15 changes: 15 additions & 0 deletions eox_nelp/stats/api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Course API URLs
"""
from django.conf import settings
from django.urls import path, re_path

from eox_nelp.stats.api.v1.views import GeneralCourseStatsView, GeneralTenantStatsView

app_name = "eox_nelp" # pylint: disable=invalid-name

urlpatterns = [
path('tenant/', GeneralTenantStatsView.as_view(), name="general-stats"),
path('courses/', GeneralCourseStatsView.as_view(), name="courses-stats"),
re_path(rf'^courses/{settings.COURSE_ID_PATTERN}', GeneralCourseStatsView.as_view(), name="course-stats"),
]
145 changes: 145 additions & 0 deletions eox_nelp/stats/api/v1/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""Stats API v1 view file.
views:
GeneralTenantStatsView: View that handles the general tenant stats.
GeneralTenantCoursesView: View that handles the general courses stats.
"""
from django.contrib.auth import get_user_model
from django.http import Http404
from rest_framework.response import Response
from rest_framework.views import APIView

from eox_nelp.stats import metrics

User = get_user_model()


class GeneralTenantStatsView(APIView):
"""Class view. Handle general tenant stats.
## Usage
The components key depends on the setting API_XBLOCK_TYPES, this should be
a list of strings like the following
``` json
[" problem", "video", "discussion"]
```
### **GET** /eox-nelp/api/stats/v1/tenant/
**GET Response Values**
``` json
{
"learners": 1,
"courses": 3,
"instructors": 2,
"components": {
"discussion": 0,
"drag-and-drop-v2": 0,
"html": 133,
"openassessment": 0,
"problem": 49,
"video": 0
}
}
```
"""

def get(self, request):
"""Return general tenant stats."""
tenant = request.site.domain
courses = metrics.get_courses_metrics(tenant)
components = {}

for metric in courses.get("metrics", []):
course_components = metric.get("components", {})

for key, value in course_components.items():
components[key] = components.get(key, 0) + value

return Response({
"learners": metrics.get_learners_metric(tenant),
"courses": courses.get("total_courses", 0),
"instructors": metrics.get_instructors_metric(tenant),
"components": components
})


class GeneralCourseStatsView(APIView):
"""Class view that returns a list of course stats or a specific course stats.
## Usage
The components key depends on the setting ALLOWED_VERTICAL_BLOCK_TYPES, this should be
a list of strings like the following
``` json
[" problem", "video", "discussion"]
```
### **GET** /eox-nelp/api/stats/v1/courses/
**GET Response Values**
``` json
{
"total_courses": 4,
"metrics": [
{
"id": "course-v1:patata+CS102+2023",
"name": "PROCEDURAL SEDATION AND ANALGESIA COURSE",
"learners": 0,
"instructors": 1,
"sections": 18,
"sub_sections": 144,
"units": 184,
"components": {
"discussion": 0,
"drag-and-drop-v2": 0,
"html": 133,
"openassessment": 0,
"problem": 49,
"video": 0
}
},
...
]
}
```
### **GET** /eox-nelp/api/stats/v1/courses/course-v1:potato+CS102+2023/
**GET Response Values**
``` json
{
"id": "course-v1:potato+CS102+2023",
"name": "PROCEDURAL SEDATION AND ANALGESIA COURSE",
"learners": 0,
"instructors": 1,
"sections": 18,
"sub_sections": 144,
"units": 184,
"components": {
"discussion": 0,
"drag-and-drop-v2": 0,
"html": 133,
"openassessment": 0,
"problem": 49,
"video": 0
}
}
```
"""

def get(self, request, course_id=None):
"""Return general course stats."""
tenant = request.site.domain

if course_id:
courses = metrics.get_cached_courses(tenant)
course = courses.filter(id=course_id).first()

if not course:
raise Http404

return Response(metrics.get_course_metrics(course.id))

return Response(metrics.get_courses_metrics(tenant))
37 changes: 37 additions & 0 deletions eox_nelp/stats/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""eox-nelp Metrics file.
decorators:
cache_method: Cache the result of the inner method.
"""
from django.conf import settings
from django.core.cache import cache


def cache_method(func):
"""
Cache the function result to improve the response time.
Args:
func<function>: Target function to be cached.
Return:
<funtion>: Wrapper function.
"""
def wrapper(*args, **kwargs):
key = f"{func.__name__}.{'-'.join(map(str, args))}.STATS_CACHE_KEY"
result = cache.get(key)

if result:
return result

result = func(*args, **kwargs)

cache.set(
key,
result,
timeout=getattr(settings, "STATS_SETTINGS", {}).get("STATS_TIMEOUT", 3600),
)

return result

return wrapper
Loading

0 comments on commit 02c763f

Please sign in to comment.