diff --git a/lms/urls.py b/lms/urls.py index 461b3ca062..ce5631347e 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -665,6 +665,12 @@ urlpatterns += [ 'u/', include('openedx.features.learner_profile.urls'), ), + + # Survey Report + re_path( + fr'^survey_report/', + include('openedx.features.survey_report.urls'), + ), ] if settings.FEATURES.get('ENABLE_TEAMS'): diff --git a/openedx/features/survey_report/admin.py b/openedx/features/survey_report/admin.py index cac84681ab..368159a08d 100644 --- a/openedx/features/survey_report/admin.py +++ b/openedx/features/survey_report/admin.py @@ -11,14 +11,16 @@ class SurveyReportAdmin(admin.ModelAdmin): """ Admin to manage survey reports. """ + change_list_template = "survey_report/change_list.html" + readonly_fields = ( 'courses_offered', 'learners', 'registered_learners', 'enrollments', 'generated_certificates', 'extra_data', - 'created_at' + 'created_at', 'state', ) list_display = ( - 'id', 'summary', 'created_at' + 'id', 'summary', 'created_at', 'state' ) def summary(self, obj) -> str: diff --git a/openedx/features/survey_report/api.py b/openedx/features/survey_report/api.py index dc1f621a6a..7cd8633b2f 100644 --- a/openedx/features/survey_report/api.py +++ b/openedx/features/survey_report/api.py @@ -12,12 +12,13 @@ from openedx.features.survey_report.queries import ( get_registered_learners, get_unique_courses_offered ) +from .models import SURVEY_REPORT_ERROR, SURVEY_REPORT_GENERATED MAX_WEEKS_SINCE_LAST_LOGIN: int = 4 -def generate_report() -> None: - """ Generate a report with relevant data.""" +def get_report_data() -> dict: + """ Get data from database to generate a new report.""" courses_offered = get_unique_courses_offered() learners = get_recently_active_users(weeks=MAX_WEEKS_SINCE_LAST_LOGIN) registered_learners = get_registered_learners() @@ -25,13 +26,30 @@ def generate_report() -> None: enrollments = get_course_enrollments() extra_data = settings.SURVEY_REPORT_EXTRA_DATA - survey_report = SurveyReport( - courses_offered=courses_offered, - learners=learners, - registered_learners=registered_learners, - generated_certificates=certificates, - enrollments=enrollments, - extra_data=extra_data, - ) + return { + "courses_offered": courses_offered, + "learners": learners, + "registered_learners": registered_learners, + "generated_certificates": certificates, + "enrollments": enrollments, + "extra_data": extra_data, + } + +def generate_report() -> None: + """ Generate a report with relevant data.""" + data = {} + survey_report = SurveyReport(**data) survey_report.save() + + try: + data = get_report_data() + data["state"] = SURVEY_REPORT_GENERATED + update_report(survey_report.id, data) + except (Exception, ) as update_report_error: + update_report(survey_report.id, {"state": SURVEY_REPORT_ERROR}) + raise Exception(update_report_error) from update_report_error + + +def update_report(survey_report_id: int, data: dict) -> None: + SurveyReport.objects.filter(id=survey_report_id).update(**data) diff --git a/openedx/features/survey_report/management/commands/generate_report.py b/openedx/features/survey_report/management/commands/generate_report.py index cb94f9ad2e..5c8978e4c2 100644 --- a/openedx/features/survey_report/management/commands/generate_report.py +++ b/openedx/features/survey_report/management/commands/generate_report.py @@ -28,4 +28,6 @@ class Command(BaseCommand): except Exception as error: raise CommandError(f'An error has occurred while survey report was generating. {error}') from error - self.stdout.write(self.style.SUCCESS('Survey report has been generated successfully.')) + self.stdout.write( + self.style.SUCCESS('Survey report has been generated successfully.') + ) diff --git a/openedx/features/survey_report/management/commands/tests/test_generate_report.py b/openedx/features/survey_report/management/commands/tests/test_generate_report.py index 84e7a38325..a2156fb019 100644 --- a/openedx/features/survey_report/management/commands/tests/test_generate_report.py +++ b/openedx/features/survey_report/management/commands/tests/test_generate_report.py @@ -6,7 +6,7 @@ from io import StringIO from unittest import mock from django.core.management import call_command -from django.test import TestCase, override_settings +from django.test import TestCase from openedx.features.survey_report.models import SurveyReport @@ -15,30 +15,29 @@ 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_registered_learners') - @mock.patch('openedx.features.survey_report.queries.get_recently_active_users') - @mock.patch('openedx.features.survey_report.queries.get_unique_courses_offered') - def test_generate_report(self, mock_get_unique_courses_offered, mock_get_recently_active_users, - mock_get_registered_learners, mock_get_generated_certificates, - mock_get_course_enrollments): + + @mock.patch('openedx.features.survey_report.api.get_report_data') + def test_generate_report(self, mock_get_report_data): """ Test that generate_report command creates a survey report. """ - mock_get_unique_courses_offered.return_value = 1 - mock_get_recently_active_users.return_value = 2 - mock_get_registered_learners.return_value = 3 - mock_get_generated_certificates.return_value = 4 - mock_get_course_enrollments.return_value = 5 + report_test_data = { + 'courses_offered': 1, + 'learners': 2, + 'registered_learners': 3, + 'generated_certificates': 4, + 'enrollments': 5, + 'extra_data': {'extra': 'data'}, + } + mock_get_report_data.return_value = report_test_data 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'} + + assert survey_report.courses_offered == report_test_data['courses_offered'] + assert survey_report.learners == report_test_data['learners'] + assert survey_report.registered_learners == report_test_data['registered_learners'] + assert survey_report.generated_certificates == report_test_data['generated_certificates'] + assert survey_report.enrollments == report_test_data['enrollments'] + assert survey_report.extra_data == report_test_data['extra_data'] diff --git a/openedx/features/survey_report/migrations/0003_add_state_field_and_add_default_values_to_fields.py b/openedx/features/survey_report/migrations/0003_add_state_field_and_add_default_values_to_fields.py new file mode 100644 index 0000000000..e019de9f45 --- /dev/null +++ b/openedx/features/survey_report/migrations/0003_add_state_field_and_add_default_values_to_fields.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.16 on 2022-12-11 15:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('survey_report', '0002_auto_20221130_1533'), + ] + + operations = [ + migrations.AddField( + model_name='surveyreport', + name='state', + field=models.CharField(choices=[('processing', 'Processing'), ('generated', 'Generated'), ('error', 'Error')], default='processing', help_text='State of the async generating process.', max_length=24), + ), + migrations.AlterField( + model_name='surveyreport', + name='courses_offered', + field=models.BigIntegerField(default=0, help_text='Total number of active unique courses.'), + ), + migrations.AlterField( + model_name='surveyreport', + name='enrollments', + field=models.BigIntegerField(default=0, help_text='Total number of active enrollments in the platform.'), + ), + migrations.AlterField( + model_name='surveyreport', + name='generated_certificates', + field=models.BigIntegerField(default=0, help_text='Total number of generated certificates.'), + ), + migrations.AlterField( + model_name='surveyreport', + name='learners', + field=models.BigIntegerField(default=0, help_text='Total number of recently active users with login in some weeks.'), + ), + migrations.AlterField( + model_name='surveyreport', + name='registered_learners', + field=models.BigIntegerField(default=0, help_text='Total number of users ever registered in the platform.'), + ), + ] diff --git a/openedx/features/survey_report/models.py b/openedx/features/survey_report/models.py index 3e34b75a80..ebcf4195c8 100644 --- a/openedx/features/survey_report/models.py +++ b/openedx/features/survey_report/models.py @@ -5,6 +5,16 @@ Survey Report models. from django.db import models from jsonfield import JSONField +SURVEY_REPORT_PROCESSING = 'processing' +SURVEY_REPORT_GENERATED = 'generated' +SURVEY_REPORT_ERROR = 'error' + +SURVEY_REPORT_STATES = [ + (SURVEY_REPORT_PROCESSING, 'Processing'), + (SURVEY_REPORT_GENERATED, 'Generated'), + (SURVEY_REPORT_ERROR, 'Error'), +] + class SurveyReport(models.Model): """ @@ -19,18 +29,31 @@ class SurveyReport(models.Model): - enrollments: Total number of active enrollments in the platform. - generated_certificates: Total number of generated certificates. - extra_data: Extra information that will be saved in the report, E.g: site_name, openedx-release. + - state: State of the async generating process. """ - courses_offered = models.BigIntegerField(help_text="Total number of active unique courses.") - learners = models.BigIntegerField(help_text="Total number of recently active users with login in some weeks.") - registered_learners = models.BigIntegerField(help_text="Total number of users ever registered in the platform.") - enrollments = models.BigIntegerField(help_text="Total number of active enrollments in the platform.") - generated_certificates = models.BigIntegerField(help_text="Total number of generated certificates.") + courses_offered = models.BigIntegerField(default=0, help_text="Total number of active unique courses.") + learners = models.BigIntegerField( + default=0, + help_text="Total number of recently active users with login in some weeks." + ) + registered_learners = models.BigIntegerField( + default=0, + help_text="Total number of users ever registered in the platform." + ) + enrollments = models.BigIntegerField(default=0, help_text="Total number of active enrollments in the platform.") + generated_certificates = models.BigIntegerField(default=0, help_text="Total number of generated certificates.") extra_data = JSONField( blank=True, default=dict, help_text="Extra information that will be saved in the report, E.g: site_name, openedx-release.", ) created_at = models.DateTimeField(auto_now=True) + state = models.CharField( + max_length=24, + choices=SURVEY_REPORT_STATES, + default=SURVEY_REPORT_PROCESSING, + help_text="State of the async generating process." + ) class Meta: ordering = ["-created_at"] diff --git a/openedx/features/survey_report/tasks.py b/openedx/features/survey_report/tasks.py new file mode 100644 index 0000000000..9f02b9795c --- /dev/null +++ b/openedx/features/survey_report/tasks.py @@ -0,0 +1,27 @@ +""" +Tasks for Survey Report. +""" + + +import logging + +from celery import shared_task +from .api import generate_report + +log = logging.getLogger('edx.celery.task') + + +@shared_task(name='openedx.features.survey_report.tasks.generate_survey_report') +def generate_survey_report(): + """ + Tasks to generate a new survey report with non-sensitive data. + """ + log.info( + 'Started - generate survey report' + ) + + try: + generate_report() + log.info('Done - generate survey report') + except (Exception, ): # pylint: disable=broad-except + log.error('Error - generate survey report') diff --git a/openedx/features/survey_report/templates/survey_report/change_list.html b/openedx/features/survey_report/templates/survey_report/change_list.html new file mode 100644 index 0000000000..c4c0ebcb67 --- /dev/null +++ b/openedx/features/survey_report/templates/survey_report/change_list.html @@ -0,0 +1,12 @@ +{% extends 'admin/change_list.html' %} + +{% block object-tools %} + +{% endblock %} diff --git a/openedx/features/survey_report/urls.py b/openedx/features/survey_report/urls.py new file mode 100644 index 0000000000..dc7d2e2907 --- /dev/null +++ b/openedx/features/survey_report/urls.py @@ -0,0 +1,14 @@ +""" +Defines URLs for Survey Report. +""" + +from django.urls import path +from .views import SurveyReportView + +urlpatterns = [ + path( + 'generate_report', + SurveyReportView.as_view(), + name='openedx.generate_survey_report', + ), +] diff --git a/openedx/features/survey_report/views.py b/openedx/features/survey_report/views.py new file mode 100644 index 0000000000..d8487e8cd4 --- /dev/null +++ b/openedx/features/survey_report/views.py @@ -0,0 +1,27 @@ +""" +Views to manage the Survey Reports. +""" + + +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.generic import View +from .tasks import generate_survey_report + + +class SurveyReportView(View): + """ + View for Survey Reports. + """ + @method_decorator(login_required) + @method_decorator(ensure_csrf_cookie) + def post(self, _request): + """ + Generate a new survey report using the generate_report method in api.py + Arguments: + _request: HTTP request + """ + generate_survey_report.delay() + return redirect("admin:survey_report_surveyreport_changelist")