Skip to content
3 changes: 3 additions & 0 deletions lms/envs/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,3 +1080,6 @@ def get_env_setting(setting):
"SECRET": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_SECRET', None),
"URL": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_URL', None),
}

############## Settings for survey report ##############
SURVEY_REPORT_EXTRA_DATA = ENV_TOKENS.get('SURVEY_REPORT_EXTRA_DATA', {})
3 changes: 3 additions & 0 deletions lms/envs/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,6 @@
"LOGO_URL": "https://courses.example.com/yourmfe-logo.png",
},
}

############## Settings for survey report ##############
SURVEY_REPORT_EXTRA_DATA = {}
35 changes: 35 additions & 0 deletions openedx/features/survey_report/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
Contains the logic to manage survey report model.
"""

from django.conf import settings

from openedx.features.survey_report.models import SurveyReport
from openedx.features.survey_report.queries import (
get_course_enrollments,
get_currently_learners,
get_generated_certificates,
get_learners_registered,
get_unique_courses_offered
)


def generate_report() -> None:
""" Generate a report with relevant data."""
courses_offered = get_unique_courses_offered()
learners = get_currently_learners()
registered = get_learners_registered()
certificates = get_generated_certificates()
enrollments = get_course_enrollments()
extra_data = settings.SURVEY_REPORT_EXTRA_DATA

survey_report = SurveyReport(
courses_offered=courses_offered,
learners=learners,
registered_learners=registered,
generated_certificates=certificates,
enrollments=enrollments,
extra_data=extra_data,
)

survey_report.save()
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
CLI command to generate survey report.
"""

from django.core.management.base import BaseCommand, CommandError

from openedx.features.survey_report.application import generate_report


class Command(BaseCommand):
"""
Management command to generate a new survey report with
non-sensitive data.
"""

help = """
This command will create a new survey report using some
models to get the total number oof courses offered, currently active learners,
learners ever registered, and generated certificates.
"""

def handle(self, *_args, **_options):
try:
generate_report()
except Exception as error:
raise CommandError(f'An error has occurred while report was generating. {error}') from error

self.stdout.write(self.style.SUCCESS('Survey report has been generated successfully.'))
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
Test for generate_report command.
"""

from io import StringIO
from unittest import mock

from django.core.management import call_command
from django.test import TestCase, override_settings

from openedx.features.survey_report.models import SurveyReport


class GenerateReportTest(TestCase):
"""
Test for generate_report command.
"""
@override_settings(SURVEY_REPORT_EXTRA_DATA={'extra_data': 'extra_data'})
@mock.patch('openedx.features.survey_report.queries.get_course_enrollments')
@mock.patch('openedx.features.survey_report.queries.get_generated_certificates')
@mock.patch('openedx.features.survey_report.queries.get_learners_registered')
@mock.patch('openedx.features.survey_report.queries.get_currently_learners')
@mock.patch('openedx.features.survey_report.queries.get_unique_courses_offered')
def test_generate_report(self, mock_get_unique_courses_offered, mock_get_currently_learners,
mock_get_learners_registered, mock_get_generated_certificates,
mock_get_course_enrollments):
"""
Test that generate_report command creates a survey report.
"""
mock_get_unique_courses_offered.return_value = 1
mock_get_currently_learners.return_value = 2
mock_get_learners_registered.return_value = 3
mock_get_generated_certificates.return_value = 4
mock_get_course_enrollments.return_value = 5
out = StringIO()
call_command('generate_report', stdout=out)

survey_report = SurveyReport.objects.last()
assert survey_report.courses_offered == 1
assert survey_report.learners == 2
assert survey_report.registered_learners == 3
assert survey_report.generated_certificates == 4
assert survey_report.enrollments == 5
assert survey_report.extra_data == {'extra_data': 'extra_data'}
3 changes: 1 addition & 2 deletions openedx/features/survey_report/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
Survey Report models.
"""

from jsonfield import JSONField

from django.db import models
from jsonfield import JSONField


class SurveyReport(models.Model):
Expand Down
58 changes: 58 additions & 0 deletions openedx/features/survey_report/queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Queries to get data from database.
"""

from datetime import datetime, timedelta

from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.db.models import Count, OuterRef, Q, Subquery

from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.grades.models import PersistentCourseGrade
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview


def get_unique_courses_offered() -> int:
"""
Get total number of unique courses offered.
"""
return CourseOverview.objects.annotate(
count=Subquery(
CourseEnrollment.objects
.filter(course_id=OuterRef('id'))
.values('course_id')
.annotate(count=Count('course_id'))
.values('count')
))\
.filter(count__gt=5)\
.filter(start__lt=datetime.now())\
.filter(Q(end__isnull=True) | Q(end__gt=datetime.now()))\
.count()


def get_currently_learners() -> int:
"""
Get total number of learners with last login in the last 3 weeks.
"""
return User.objects.filter(last_login__gte=datetime.now() - timedelta(weeks=3)).count()


def get_learners_registered() -> int:
"""
Get total number of active learners registered.
"""
return User.objects.filter(is_active=True).count()


def get_generated_certificates() -> int:
"""
Get total number of generated certificates.
"""
return PersistentCourseGrade.objects.filter(passed_timestamp__isnull=False).count()


def get_course_enrollments() -> int:
"""
Get total number of enrollments from users that aren't staff.
"""
return CourseEnrollment.objects.filter(user__is_superuser=False, user__is_staff=False).count()
114 changes: 114 additions & 0 deletions openedx/features/survey_report/tests/test_query_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
Test for survey report commands.
"""

from datetime import datetime, timedelta
from unittest.mock import patch

from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.grades.models import PersistentCourseGrade
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.features.survey_report.queries import (
get_course_enrollments,
get_currently_learners,
get_generated_certificates,
get_learners_registered,
get_unique_courses_offered
)
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order


class TestSurveyReportCommands(ModuleStoreTestCase):
"""
Test for survey report query methods.
"""

def setUp(self):
"""
Setup for users and courses.
"""
super().setUp()
self.store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # lint-amnesty, pylint: disable=protected-access
self.first_course = CourseFactory.create(
org="test", course="course1", display_name="run1", default_store=ModuleStoreEnum.Type.mongo
)
self.user = UserFactory.create(username='test_user', email='test@example.com', password='password')
self.user1 = UserFactory.create(username='test_user1', email='test1@example.com', password='password')
self.user2 = UserFactory.create(username='test_user2', email='test2@example.com', password='password')
self.user3 = UserFactory.create(username='test_user3', email='test3@example.com', password='password')
self.user4 = UserFactory.create(username='test_user4', email='test4@example.com', password='password')
self.user5 = UserFactory.create(username='test_user5', email='test5@example.com', password='password')

def test_get_unique_courses_offered(self):
"""
Test that get_unique_courses_offered returns the correct number of courses.
"""
course_overview = CourseOverviewFactory.create(id=self.first_course.id, start="2019-01-01", end="2024-01-01")
CourseEnrollmentFactory.create(user=self.user, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user1, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user2, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user3, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user4, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user5, course_id=course_overview.id)
with patch('openedx.features.survey_report.queries.datetime') as mock_datetime:
mock_datetime.now.return_value = datetime.now()
assert get_unique_courses_offered() == 1

def test_get_currently_learners(self):
"""
Test that get_currently_learners returns the correct number of learners.
"""
self.user.last_login = datetime.now() - timedelta(days=1)
self.user.save()
self.user1.last_login = datetime.now() - timedelta(weeks=2)
self.user1.save()
self.user2.last_login = datetime.now() - timedelta(weeks=4)
self.user2.save()
with patch('openedx.features.survey_report.queries.datetime') as mock_datetime:
mock_datetime.now.return_value = datetime.now()
assert get_currently_learners() == 2

def test_get_learners_registered(self):
"""
Test that get_learners_registered returns the correct number of learners.
"""
assert get_learners_registered() == 7

def test_get_generated_certificates(self):
"""
Test that get_generated_certificates returns the correct number of certificates.
"""
course_grade_params = {
"user_id": self.user.id,
"course_id": self.first_course.id,
"percent_grade": 77.7,
"letter_grade": "pass",
"passed": True,
"passed_timestamp": datetime.now(),
}
PersistentCourseGrade.update_or_create(**course_grade_params)

with patch('openedx.features.survey_report.queries.datetime') as mock_datetime:
mock_datetime.now.return_value = datetime.now()
assert get_generated_certificates() == 1

def test_get_course_enrollments(self):
"""
Test that get_course_enrollments returns the correct number of enrollments.
"""
self.user.is_superuser = True
self.user.save()
self.user1.is_staff = True
self.user1.save()
course_overview = CourseOverviewFactory.create(id=self.first_course.id, start="2019-01-01", end="2024-01-01")
CourseEnrollmentFactory.create(user=self.user, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user1, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user2, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user3, course_id=course_overview.id)
CourseEnrollmentFactory.create(user=self.user4, course_id=course_overview.id)
with patch('openedx.features.survey_report.queries.datetime') as mock_datetime:
mock_datetime.now.return_value = datetime.now()
assert get_course_enrollments() == 3