Skip to content

Commit

Permalink
feat: add custom fields on profile data csv
Browse files Browse the repository at this point in the history
Allow site operators to include on the export of profile information as CSV
custom fields if the platform has an extending User model.
This can be used if you have an extended model that include for example
an university student number and site operator want to export the
student number on the student profile information CSV.
  • Loading branch information
igobranco committed May 10, 2023
1 parent 0c51bec commit f9bfde2
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 6 deletions.
61 changes: 60 additions & 1 deletion lms/djangoapps/instructor_analytics/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,65 @@ def issued_certificates(course_key, features):
return generated_certificates


def get_student_features_with_custom(course_key):
"""
Allow site operators to include on the export custom fields if platform has an extending
User model. This can be used if you have an extended model that include for example
an university student number.
Basic example of adding age:
```python
def get_age(self):
return datetime.datetime.now().year - self.profile.year_of_birth
setattr(User, 'age', property(get_age))
```
Then you have to add `age` to both site configurations:
- `student_profile_download_fields_custom_student_attributes`
- `student_profile_download_fields` site configurations`
```json
"student_profile_download_fields_custom_student_attributes": ["age"],
"student_profile_download_fields": [
"id", "username", "name", "email", "language", "location",
"year_of_birth", "gender", "level_of_education", "mailing_address",
"goals", "enrollment_mode", "last_login", "date_joined", "external_user_key",
"enrollment_date", "age"
]
```
Example if the platform has a custom user extended model like a One-To-One Link
with the User Model:
```python
def get_user_extended_model_custom_field(self):
if hasattr(self, "userextendedmodel"):
return self.userextendedmodel.custom_field
return None
setattr(User, 'user_extended_model_custom_field', property(get_user_extended_model_custom_field))
```
```json
"student_profile_download_fields_custom_student_attributes": ["user_extended_model_custom_field"],
"student_profile_download_fields": [
"id", "username", "name", "email", "language", "location",
"year_of_birth", "gender", "level_of_education", "mailing_address",
"goals", "enrollment_mode", "last_login", "date_joined", "external_user_key",
"enrollment_date", "user_extended_model_custom_field"
]
```
"""
return STUDENT_FEATURES + tuple(
configuration_helpers.get_value_for_org(
course_key.org,
"student_profile_download_fields_custom_student_attributes",
getattr(
settings,
"STUDENT_PROFILE_DOWNLOAD_FIELDS_CUSTOM_STUDENT_ATTRIBUTES",
(),
),
)
)


def enrolled_students_features(course_key, features): # lint-amnesty, pylint: disable=too-many-statements
"""
Return list of student features as dictionaries.
Expand Down Expand Up @@ -117,7 +176,7 @@ def enrolled_students_features(course_key, features): # lint-amnesty, pylint: d

students = [enrollment.user for enrollment in enrollments]

student_features = [x for x in STUDENT_FEATURES if x in features]
student_features = [x for x in get_student_features_with_custom(course_key) if x in features]
profile_features = [x for x in PROFILE_FEATURES if x in features]

if include_program_enrollments and len(students) > 0:
Expand Down
43 changes: 38 additions & 5 deletions lms/djangoapps/instructor_analytics/tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

from unittest.mock import MagicMock, Mock, patch

import random
import datetime
import ddt
import json # lint-amnesty, pylint: disable=wrong-import-order
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from edx_proctoring.api import create_exam
from edx_proctoring.models import ProctoredExamStudentAttempt
from opaque_keys.edx.locator import UsageKey
Expand All @@ -25,6 +28,7 @@
)
from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
from common.djangoapps.student.tests.factories import InstructorFactory
from common.djangoapps.student.tests.factories import UserFactory
Expand Down Expand Up @@ -132,7 +136,7 @@ def test_enrolled_students_features_keys(self):
user.profile.save()
for feature in query_features:
assert feature in AVAILABLE_FEATURES
with self.assertNumQueries(1):
with self.assertNumQueries(2):
userreports = enrolled_students_features(self.course_key, query_features)
assert len(userreports) == len(self.users)

Expand Down Expand Up @@ -160,7 +164,7 @@ def test_enrolled_students_meta_features_keys(self):
Assert that we can query individual fields in the 'meta' field in the UserProfile
"""
query_features = ('meta.position', 'meta.company')
with self.assertNumQueries(1):
with self.assertNumQueries(2):
userreports = enrolled_students_features(self.course_key, query_features)
assert len(userreports) == len(self.users)
for userreport in userreports:
Expand Down Expand Up @@ -217,7 +221,7 @@ def test_enrolled_students_features_keys_cohorted(self):
# enrolled_students_features. The first query comes from the call to
# User.objects.filter(...), and the second comes from
# prefetch_related('course_groups').
with self.assertNumQueries(2):
with self.assertNumQueries(3):
userreports = enrolled_students_features(course.id, query_features)
assert len([r for r in userreports if r['username'] in cohorted_usernames]) == len(cohorted_students)
assert len([r for r in userreports if r['username'] == non_cohorted_student.username]) == 1
Expand All @@ -239,7 +243,7 @@ def test_enrolled_student_features_external_user_keys(self):
ProgramEnrollmentFactory.create(user=user, external_user_key=external_user_key)
username_with_external_user_key_dict[user.username] = external_user_key

with self.assertNumQueries(2):
with self.assertNumQueries(3):
userreports = enrolled_students_features(self.course_key, query_features)
assert len(userreports) == 30
for report in userreports:
Expand Down Expand Up @@ -268,7 +272,7 @@ def test_enrolled_students_enrollment_date(self):
query_features = ('username', 'enrollment_date',)
for feature in query_features:
assert feature in AVAILABLE_FEATURES
with self.assertNumQueries(1):
with self.assertNumQueries(2):
userreports = enrolled_students_features(self.course_key, query_features)
assert len(userreports) == len(self.users)

Expand All @@ -278,6 +282,35 @@ def test_enrolled_students_enrollment_date(self):
assert set(userreport.keys()) == set(query_features)
assert userreport['enrollment_date'] == CourseEnrollment.enrollments_for_user(user)[0].created

def test_enrolled_students_extended_model_age(self):
SiteConfigurationFactory.create(
site_values={
'course_org_filter': ['robot'],
'student_profile_download_fields_custom_student_attributes': ['age'],
}
)

def get_age(self):
return datetime.datetime.now().year - self.profile.year_of_birth
setattr(User, "age", property(get_age)) # lint-amnesty, pylint: disable=literal-used-as-attribute

for user in self.users:
user.profile.year_of_birth = random.randint(1900, 2000)
user.profile.save()

query_features = ('username', 'age',)
with self.assertNumQueries(3):
userreports = enrolled_students_features(self.course_key, query_features)
assert len(userreports) == len(self.users)

userreports = sorted(userreports, key=lambda u: u["username"])
users = sorted(self.users, key=lambda u: u.username)
for userreport, user in zip(userreports, users):
assert set(userreport.keys()) == set(query_features)
assert userreport['age'] == str(user.age)

delattr(User, "age") # lint-amnesty, pylint: disable=literal-used-as-attribute

def test_list_may_enroll(self):
may_enroll = list_may_enroll(self.course_key, ['email'])
assert len(may_enroll) == (len(self.students_who_may_enroll) - len(self.users))
Expand Down

0 comments on commit f9bfde2

Please sign in to comment.