Files
edx-platform/lms/djangoapps/instructor/views/serializer.py
Daniel Wong cd6faeb966 Follow-up to PR 36789 (#37751)
* refactor(certificates): replace direct model imports with data classes and APIs

* fix: use Certificates API to create certificates

* docs: update docstring for get_certificate_for_user

* fix: remove trailing whitespace

---------

Co-authored-by: coder1918 <ram.chandra@wgu.edu>
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2026-01-08 13:03:46 -05:00

573 lines
19 KiB
Python

""" Instructor apis serializers. """
import re
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from rest_framework import serializers
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.instructor.access import ROLES
from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_COMMUNITY_TA,
FORUM_ROLE_GROUP_MODERATOR,
FORUM_ROLE_MODERATOR,
Role
)
from lms.djangoapps.discussion.django_comment_client.utils import (
get_group_id_for_user,
get_group_name
)
from .tools import get_student_from_identifier
class RoleNameSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer that describes the response of the problem response report generation API.
"""
rolename = serializers.CharField(help_text=_("Role name"))
def validate_rolename(self, value):
"""
Check that the rolename is valid.
"""
if value not in ROLES:
raise ValidationError(_("Invalid role name."))
return value
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
class UniqueStudentIdentifierSerializer(serializers.Serializer):
"""
Serializer for identifying unique_student.
"""
unique_student_identifier = serializers.CharField(
max_length=255,
help_text="Email or username of user to change access"
)
def validate_unique_student_identifier(self, value):
"""
Validate that the unique_student_identifier corresponds to an existing user.
"""
try:
user = get_student_from_identifier(value)
except User.DoesNotExist:
return None
return user
class AccessSerializer(UniqueStudentIdentifierSerializer):
"""
Serializer for managing user access changes.
This serializer validates and processes the data required to modify
user access within a system.
"""
rolename = serializers.CharField(
help_text="Role name to assign to the user"
)
action = serializers.ChoiceField(
choices=['allow', 'revoke'],
help_text="Action to perform on the user's access"
)
class ForumRoleNameSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for forum rolename.
"""
rolename = serializers.CharField(help_text=_("Role name"))
users = serializers.SerializerMethodField()
def validate_rolename(self, value):
"""
Check that the rolename is valid.
"""
if value not in [
FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_GROUP_MODERATOR, FORUM_ROLE_MODERATOR
]:
raise ValidationError(_("Invalid role name."))
return value
def get_users(self, obj):
"""
Retrieve a list of users associated with the specified role and course.
Args:
obj (dict): A dictionary containing the 'rolename' for which to retrieve users.
This dictionary is the data passed to the serializer.
Returns:
list: A list of dictionaries, each representing a user associated with the specified role.
Each user dictionary contains 'username', 'email', 'first_name', 'last_name', and 'group_name'.
If no users are found, an empty list is returned.
"""
course_id = self.context.get('course_id')
rolename = obj['rolename']
try:
role = Role.objects.get(name=rolename, course_id=course_id)
users = role.users.all().order_by('username')
except Role.DoesNotExist:
users = []
return [extract_user_info(user, self.context.get('course_discussion_settings')) for user in users]
def extract_user_info(user, course_discussion_settings):
""" utility method to convert user into dict for JSON rendering. """
group_id = get_group_id_for_user(user, course_discussion_settings)
group_name = get_group_name(group_id, course_discussion_settings)
return {
'username': user.username,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'group_name': group_name,
}
class ListInstructorTaskInputSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for handling the input data for the problem response report generation API.
Attributes:
unique_student_identifier (str): The email or username of the student.
This field is optional, but if provided, the `problem_location_str`
must also be provided.
problem_location_str (str): The string representing the location of the problem within the course.
This field is optional, unless `unique_student_identifier` is provided.
"""
unique_student_identifier = serializers.CharField(
max_length=255,
help_text="Email or username of student",
required=False
)
problem_location_str = serializers.CharField(
help_text="Problem location",
required=False
)
def validate(self, data):
"""
Validate the data to ensure that if unique_student_identifier is provided,
problem_location_str must also be provided.
"""
unique_student_identifier = data.get('unique_student_identifier')
problem_location_str = data.get('problem_location_str')
if unique_student_identifier and not problem_location_str:
raise serializers.ValidationError(
"unique_student_identifier must accompany problem_location_str"
)
return data
class ShowStudentExtensionSerializer(serializers.Serializer):
"""
Serializer for validating and processing the student identifier.
"""
student = serializers.CharField(write_only=True, required=True)
def validate_student(self, value):
"""
Validate that the student corresponds to an existing user.
"""
try:
user = get_student_from_identifier(value)
except User.DoesNotExist:
return None
return user
class ShowUnitExtensionsSerializer(serializers.Serializer):
"""
Serializer for showing all students who have due date extensions
for a specific unit (block).
Fields:
url (str): The URL (block ID) of the unit for which student extensions should be retrieved.
"""
url = serializers.CharField(
required=True,
max_length=2048,
help_text="The unit URL (block ID) to retrieve student extensions for."
)
class StudentAttemptsSerializer(serializers.Serializer):
"""
Serializer for resetting a students attempts counter or starts a task to reset all students
attempts counters.
"""
problem_to_reset = serializers.CharField(
help_text="The identifier or description of the problem that needs to be reset."
)
# following are optional params.
unique_student_identifier = serializers.CharField(
help_text="Email or username of student.", required=False
)
all_students = serializers.CharField(required=False)
delete_module = serializers.CharField(required=False)
def validate_all_students(self, value):
"""
converts the all_student params value to bool.
"""
return self.verify_bool(value)
def validate_delete_module(self, value):
"""
converts the all_student params value.
"""
return self.verify_bool(value)
def validate_unique_student_identifier(self, value):
"""
Validate that the student corresponds to an existing user.
"""
try:
user = get_student_from_identifier(value)
except User.DoesNotExist:
return None
return user
def verify_bool(self, value):
"""
Returns the value of the boolean parameter with the given
name in the POST request. Handles translation from string
values to boolean values.
"""
if value is not None:
return value in ['true', 'True', True]
class UpdateForumRoleMembershipSerializer(AccessSerializer):
"""
Serializer for managing user's forum role.
This serializer extends the AccessSerializer to allow for different action
choices specific to this API. It validates and processes the data required
to modify user access within a system.
Attributes:
unique_student_identifier (str): The email or username of the user whose access is being modified.
rolename (str): The role name to assign to the user.
action (str): The specific action to perform on the user's access, with options 'activate' or 'deactivate'.
"""
rolename = serializers.ChoiceField(
choices=[
FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR,
FORUM_ROLE_GROUP_MODERATOR, FORUM_ROLE_COMMUNITY_TA
],
help_text="Rolename assign to given user."
)
class SendEmailSerializer(serializers.Serializer):
"""
Serializer for sending an email with optional scheduling.
Fields:
send_to (str): The email address of the recipient. This field is required.
subject (str): The subject line of the email. This field is required.
message (str): The body of the email. This field is required.
schedule (str, optional):
An optional field to specify when the email should be sent.
If provided, this should be a string that can be parsed into a
datetime format or some other scheduling logic.
"""
send_to = serializers.CharField(write_only=True, required=True)
# set max length as per model field.
subject = serializers.CharField(max_length=128, write_only=True, required=True)
message = serializers.CharField(required=True)
schedule = serializers.CharField(required=False)
class BlockDueDateSerializer(serializers.Serializer):
"""
Serializer for handling block due date updates for a specific student.
Fields:
url (str): The URL related to the block that needs the due date update.
due_datetime (str): The new due date and time for the block.
student (str): The email or username of the student whose access is being modified.
reason (str): Reason why updating this.
"""
url = serializers.CharField()
due_datetime = serializers.CharField()
student = serializers.CharField(
max_length=255,
help_text="Email or username of user to change access"
)
reason = serializers.CharField(required=False)
def validate_student(self, value):
"""
Validate that the student corresponds to an existing user.
"""
try:
user = get_student_from_identifier(value)
except User.DoesNotExist:
return None
return user
def __init__(self, *args, **kwargs):
# Get context to check if `due_datetime` should be optional
disable_due_datetime = kwargs.get('context', {}).get('disable_due_datetime', False)
super().__init__(*args, **kwargs)
if disable_due_datetime:
self.fields['due_datetime'].required = False
class ProblemResetSerializer(UniqueStudentIdentifierSerializer):
"""
serializer for resetting problem.
"""
problem_to_reset = serializers.CharField(
help_text=_("The URL name of the problem to reset."),
error_messages={
'blank': _("Problem URL name cannot be blank."),
}
)
all_students = serializers.BooleanField(
default=False,
help_text=_("Whether to reset the problem for all students."),
)
only_if_higher = serializers.BooleanField(
default=False,
)
# Override the unique_student_identifier field to make it optional
unique_student_identifier = serializers.CharField(
required=False, # Make this field optional
allow_null=True,
help_text=_("unique student identifier.")
)
class ModifyAccessSerializer(serializers.Serializer):
"""
serializers for enroll or un-enroll users in beta testing program.
"""
identifiers = serializers.CharField(
help_text="A comma separated list of emails or usernames.",
required=True
)
action = serializers.ChoiceField(
choices=["add", "remove"],
help_text="Action to perform: add or remove.",
required=True
)
email_students = serializers.BooleanField(
default=False,
help_text="Boolean flag to indicate if students should be emailed."
)
auto_enroll = serializers.BooleanField(
default=False,
help_text="Boolean flag to indicate if the user should be auto-enrolled."
)
def validate_identifiers(self, value):
"""
Validate the 'identifiers' field which is now a list of strings.
"""
# Iterate over the list of identifiers and validate each one
validated_list = _split_input_list(value)
if not validated_list:
raise serializers.ValidationError("The identifiers list cannot be empty.")
return validated_list
def validate_email_students(self, value):
"""
handle string values like 'true' or 'false'.
"""
if isinstance(value, str):
return value.lower() == 'true'
return bool(value)
def validate_auto_enroll(self, value):
"""
handle string values like 'true' or 'false'.
"""
if isinstance(value, str):
return value.lower() == 'true'
return bool(value)
def _split_input_list(str_list):
"""
Separate out individual student email from the comma, or space separated string.
e.g.
in: "Lorem@ipsum.dolor, sit@amet.consectetur\nadipiscing@elit.Aenean\r convallis@at.lacus\r, ut@lacinia.Sed"
out: ['Lorem@ipsum.dolor', 'sit@amet.consectetur', 'adipiscing@elit.Aenean', 'convallis@at.lacus', 'ut@lacinia.Sed']
`str_list` is a string coming from an input text area
returns a list of separated values
"""
new_list = re.split(r'[,\s\n\r]+', str_list)
new_list = [s.strip() for s in new_list]
new_list = [s for s in new_list if s != '']
return new_list
class CertificateStatusesSerializer(serializers.Serializer):
"""
Serializer for validating and serializing certificate status inputs.
This serializer is used to ensure that the provided certificate statuses
conform to the predefined set of valid statuses defined in the
`CertificateStatuses` enumeration.
"""
certificate_statuses = serializers.ListField(
child=serializers.ChoiceField(choices=[
CertificateStatuses.downloadable,
CertificateStatuses.error,
CertificateStatuses.notpassing,
CertificateStatuses.audit_passing,
CertificateStatuses.audit_notpassing,
]),
allow_empty=False # Set to True if you want to allow empty lists
)
class CertificateSerializer(serializers.Serializer):
"""
Serializer for multiple operations related with certificates.
resetting a students attempts counter or starts a task to reset all students
attempts counters
Also Add/Remove students to/from the certificate allowlist.
Also For resetting a students attempts counter or starts a task to reset all students
attempts counters.
"""
user = serializers.CharField(
help_text="Email or username of student.", required=True
)
notes = serializers.CharField(required=False, allow_null=True, allow_blank=True)
def validate_user(self, value):
"""
Validate that the user corresponds to an existing user.
"""
try:
user = get_student_from_identifier(value)
except User.DoesNotExist:
return None
return user
class RescoreEntranceExamSerializer(serializers.Serializer):
"""Serializer for entrance exam rescoring"""
unique_student_identifier = serializers.CharField(required=False, allow_null=True)
all_students = serializers.BooleanField(required=False)
only_if_higher = serializers.BooleanField(required=False, allow_null=True)
class ResetEntranceExamAttemptsSerializer(UniqueStudentIdentifierSerializer):
"""
Serializer for resetting entrance exam attempts or deleting entrance exam state.
Inherits user validation from UniqueStudentIdentifierSerializer.
"""
all_students = serializers.BooleanField(
required=False,
default=False,
help_text=_("Whether to reset for all students."),
)
delete_module = serializers.BooleanField(
required=False,
default=False,
help_text=_("Whether to delete entrance exam state for the student."),
)
# Override the unique_student_identifier field to make it optional
unique_student_identifier = serializers.CharField(
required=False, # Make this field optional
allow_null=True,
help_text=_("unique student identifier.")
)
def validate_unique_student_identifier(self, value):
"""
Validate that the unique_student_identifier corresponds to an existing user,
if a value is provided.
"""
if not value:
return None
user = super().validate_unique_student_identifier(value)
if user is None:
raise serializers.ValidationError(
"No user found with the provided identifier."
)
return user
def validate(self, attrs):
all_students = attrs.get("all_students", False)
student = attrs.get("unique_student_identifier")
delete_module = attrs.get("delete_module", False)
errors = []
if all_students and student:
errors.append(_("all_students and unique_student_identifier are mutually exclusive."))
if all_students and delete_module:
errors.append(_(
"all_students and delete_module are mutually exclusive."
))
if not (student or all_students):
errors.append(_("You must provide either unique_student_identifier or set all_students to True."))
if errors:
raise serializers.ValidationError({"non_field_errors": errors})
return attrs
class StudentsUpdateEnrollmentSerializer(serializers.Serializer):
"""Serializer for student enroll/unenroll actions."""
action = serializers.ChoiceField(choices=["enroll", "unenroll"])
identifiers = serializers.CharField()
auto_enroll = serializers.BooleanField(default=False)
email_students = serializers.BooleanField(default=False)
reason = serializers.CharField(required=False, allow_blank=True)
class OverrideProblemScoreSerializer(UniqueStudentIdentifierSerializer):
"""
Serializer for overriding a student's score for a specific problem.
"""
problem_to_reset = serializers.CharField(
help_text=_("The URL name of the problem to override the score for."),
error_messages={
'blank': _("Problem URL name cannot be blank."),
}
)
score = serializers.FloatField(
help_text=_("The overriding score to set."),
)