-
Notifications
You must be signed in to change notification settings - Fork 4.3k
[FC-005] feat: add necessary models for Openedx survey report #31183
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
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
0c90861
feat: add survey_report djangoapp
Henrrypg e143333
Merge branch 'master' into survey_report
Alec4r 1a85171
feat: add survey report cli command and query methods
Alec4r c06ad16
fix: add init file
Henrrypg 484ee03
Merge branch 'master' into survey_report
Alec4r fc85c63
Merge branch 'master' into survey_report
Alec4r 2a11601
refactor: change model fields
Alec4r 4ff6352
refactor: rename application file and rename methods
Alec4r 5387bce
refactor: add is_active to get course enrollments
Alec4r 2930cbe
refactor: rename method to get active users
Alec4r 9fc6cac
refactor: remove fields useless
Alec4r ececb95
test: rename mocks in command tests
Alec4r e57b10d
test: update test name
Alec4r 614bf1c
docs: add README file
Alec4r c633c43
docs: add selection criteria to get unique courses
Alec4r d7d6934
docs: update README
Alec4r 5db5ef9
test: remove useless mocks and use default modulestore
Alec4r 4a2117d
docs: change command error message
Alec4r f1a611a
docs: add docs decisions
Alec4r 2c7de0c
docs: Update openedx/features/survey_report/management/commands/gener…
Alec4r de046f3
docs: add fields descriptions
Alec4r eacaeba
docs: add logs for each query
Alec4r a603a83
style: add blank lines
Alec4r 8383568
refactor: rename variables and add a constant for weeks
Alec4r a26f453
refactor: add constant MIN_ENROLLS_ACTIVE_COURSE
Alec4r File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| Survey Report | ||
| -------------------- | ||
| This Django app was created for the purpose of gathering aggregated, anonymized data | ||
| about Open edX courses at scale, so that we can begin to track the growth | ||
| and trends in Open edX usage over time, namely in the annual Open edX | ||
| Impact Report. | ||
|
|
||
| You could find in this directory some methods to manage survey | ||
| reports, one command to generate the report, some queries to get the | ||
| information from database and one method to send the report to openedx | ||
| api. | ||
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| """ | ||
| 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_recently_active_users, | ||
| get_generated_certificates, | ||
| get_registered_learners, | ||
| get_unique_courses_offered | ||
| ) | ||
|
|
||
| MAX_WEEKS_SINCE_LAST_LOGIN: int = 4 | ||
|
|
||
|
|
||
| def generate_report() -> None: | ||
| """ Generate a report with relevant data.""" | ||
| courses_offered = get_unique_courses_offered() | ||
| learners = get_recently_active_users(weeks=MAX_WEEKS_SINCE_LAST_LOGIN) | ||
| registered_learners = get_registered_learners() | ||
| 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_learners, | ||
| generated_certificates=certificates, | ||
| enrollments=enrollments, | ||
| extra_data=extra_data, | ||
| ) | ||
|
|
||
| survey_report.save() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| """ | ||
| Survey Report App Configuration. | ||
| """ | ||
| from django.apps import AppConfig | ||
|
|
||
|
|
||
| class SurveyReportConfig(AppConfig): | ||
| """ | ||
| Configuration for the survey report Django app. | ||
| """ | ||
| default_auto_field = 'django.db.models.BigAutoField' | ||
| name = 'openedx.features.survey_report' |
29 changes: 29 additions & 0 deletions
29
openedx/features/survey_report/docs/decisions/0001-addition-to-core-repo.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| Addition of the Survey Report App to edx-platform | ||
| ================================================= | ||
|
|
||
| Status | ||
| ------ | ||
| Accepted | ||
|
|
||
| Context | ||
| ------- | ||
| The transition to a more modular architecture for edx-platorm has been | ||
| strengthened by the acceptance of the `No new Django apps ADR`_. | ||
|
|
||
| .. _No new Django apps ADR: https://github.com/openedx/edx-platform/tree/master/docs/decisions/0014-no-new-apps.rst | ||
|
|
||
|
|
||
| Rationale | ||
| --------- | ||
|
|
||
| This feature was considered for inclusion into the edx-platform code because it | ||
| imports several models from the inner workings of the core functionality in | ||
| order to query them. This goes in accordance with the section further guidance | ||
| of the ADR. | ||
|
|
||
|
|
||
| Decision | ||
| -------- | ||
|
|
||
| Locate the Survey Report Application in the edx-platform repository under | ||
| `openedx/features`. |
Empty file.
Empty file.
31 changes: 31 additions & 0 deletions
31
openedx/features/survey_report/management/commands/generate_report.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| """ | ||
| CLI command to generate survey report. | ||
| """ | ||
|
|
||
| from django.core.management.base import BaseCommand, CommandError | ||
|
|
||
| from openedx.features.survey_report.api 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: | ||
| - Total number of 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 survey report was generating. {error}') from error | ||
|
|
||
| self.stdout.write(self.style.SUCCESS('Survey report has been generated successfully.')) |
Empty file.
44 changes: 44 additions & 0 deletions
44
openedx/features/survey_report/management/commands/tests/test_generate_report.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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_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): | ||
| """ | ||
| 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 | ||
| 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'} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Generated by Django 3.2.16 on 2022-11-03 20:07 | ||
|
|
||
| from django.db import migrations, models | ||
| import jsonfield.fields | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| initial = True | ||
|
|
||
| dependencies = [ | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name='SurveyReport', | ||
| fields=[ | ||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
| ('courses_offered', models.BigIntegerField()), | ||
| ('learners', models.BigIntegerField()), | ||
| ('registered_learners', models.BigIntegerField()), | ||
| ('enrollments', models.BigIntegerField()), | ||
| ('generated_certificates', models.BigIntegerField()), | ||
| ('extra_data', jsonfield.fields.JSONField(blank=True, default=dict, help_text='Extra information for instance data')), | ||
| ('created_at', models.DateTimeField(auto_now=True)), | ||
| ], | ||
| options={ | ||
| 'ordering': ['-created_at'], | ||
| 'get_latest_by': 'created_at', | ||
| }, | ||
| ), | ||
| ] |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| """ | ||
| Survey Report models. | ||
| """ | ||
|
|
||
| from django.db import models | ||
| from jsonfield import JSONField | ||
|
|
||
|
|
||
| class SurveyReport(models.Model): | ||
| """ | ||
| This model stores information to automate the way of gathering impact data from the openedx project. | ||
|
|
||
|
felipemontoya marked this conversation as resolved.
|
||
| .. no_pii: | ||
|
|
||
| fields: | ||
| - courses_offered: Total number of active unique courses. | ||
| - learner: Recently active users with login in some weeks. | ||
| - registered_learners: Total number of users ever registered in the platform. | ||
| - 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. | ||
| """ | ||
| courses_offered = models.BigIntegerField() | ||
| learners = models.BigIntegerField() | ||
| registered_learners = models.BigIntegerField() | ||
|
felipemontoya marked this conversation as resolved.
|
||
| enrollments = models.BigIntegerField() | ||
| generated_certificates = models.BigIntegerField() | ||
| extra_data = JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| help_text="Extra information for instance data", | ||
| ) | ||
| created_at = models.DateTimeField(auto_now=True) | ||
|
felipemontoya marked this conversation as resolved.
|
||
|
|
||
| class Meta: | ||
| ordering = ["-created_at"] | ||
| get_latest_by = 'created_at' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """ | ||
| Queries to get data from database. | ||
| """ | ||
|
|
||
| import logging | ||
| 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.util.query import read_replica_or_default | ||
| from common.djangoapps.student.models import CourseEnrollment | ||
| from lms.djangoapps.grades.models import PersistentCourseGrade | ||
| from openedx.core.djangoapps.content.course_overviews.models import CourseOverview | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
| MIN_ENROLLS_ACTIVE_COURSE: int = 5 | ||
|
|
||
|
|
||
| def get_unique_courses_offered() -> int: | ||
| """ | ||
| Get total number of unique course that started before today and have an open date, | ||
| or have not finished yet, whose number of enrollments is greater than MIN_ENROLLS_ACTIVE_COURSE. | ||
| """ | ||
|
ormsbee marked this conversation as resolved.
|
||
| log.info("Getting the total number of unique courses offered...") | ||
| total = CourseOverview.objects.annotate( | ||
| count=Subquery( | ||
| CourseEnrollment.objects | ||
| .filter(course_id=OuterRef('id')) | ||
| .values('course_id') | ||
| .annotate(count=Count('course_id')) | ||
| .values('count') | ||
|
ormsbee marked this conversation as resolved.
|
||
| ))\ | ||
| .filter(count__gt=MIN_ENROLLS_ACTIVE_COURSE)\ | ||
| .filter(start__lt=datetime.now())\ | ||
| .filter(Q(end__isnull=True) | Q(end__gt=datetime.now()))\ | ||
| .using(read_replica_or_default())\ | ||
| .count() | ||
|
felipemontoya marked this conversation as resolved.
|
||
| log.info("Getting the total number of unique courses offered... DONE") | ||
| return total | ||
|
|
||
|
|
||
| def get_recently_active_users(weeks: int) -> int: | ||
| """ | ||
| Get total number of users with last login in the last weeks. | ||
|
|
||
| Args: | ||
| weeks (int): number of weeks since the last login to considerate as an active learner. | ||
| """ | ||
| log.info("Getting the total number of recently active users...") | ||
| total = User.objects.filter(last_login__gte=datetime.now() - timedelta(weeks=weeks))\ | ||
| .using(read_replica_or_default())\ | ||
| .count() | ||
| log.info("Getting total number of recently active users... DONE") | ||
| return total | ||
|
|
||
|
|
||
| def get_registered_learners() -> int: | ||
| """ | ||
| Get total number of active learners registered. | ||
| """ | ||
| log.info("Getting the total number of ever registered learners...") | ||
| total = User.objects.filter(is_active=True)\ | ||
| .using(read_replica_or_default())\ | ||
| .count() | ||
| log.info("Getting the total number of ever registered learners... DONE") | ||
| return total | ||
|
|
||
|
|
||
| def get_generated_certificates() -> int: | ||
| """ | ||
| Get total number of generated certificates. | ||
| """ | ||
| log.info("Getting the total number of generated certificates...") | ||
| total = PersistentCourseGrade.objects.filter(passed_timestamp__isnull=False)\ | ||
| .using(read_replica_or_default())\ | ||
| .count() | ||
| log.info("Getting the total number of generated certificates... DONE") | ||
| return total | ||
|
|
||
|
|
||
| def get_course_enrollments() -> int: | ||
| """ | ||
| Get total number of enrollments from users that aren't staff. Course staff members will be included. | ||
| """ | ||
| log.info("Getting the total number of course enrollments...") | ||
| total = CourseEnrollment.objects.filter(is_active=True, user__is_superuser=False, user__is_staff=False)\ | ||
| .using(read_replica_or_default())\ | ||
| .count() | ||
| log.info("Getting the total number of course enrollments... DONE") | ||
| return total | ||
Empty file.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.