Skip to content

Commit

Permalink
Merge pull request #111 from ts-tech-repo/letterGradeChanges
Browse files Browse the repository at this point in the history
Letter Grade Implementation
  • Loading branch information
sabid-ansari authored Nov 25, 2024
2 parents 487e286 + af072c2 commit 055c787
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 56 deletions.
102 changes: 56 additions & 46 deletions common/djangoapps/student/views/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -1553,58 +1553,68 @@ def extras_update_lti_grades(request):
usage_id = request.POST.get("usage_id", "")
user_object = User.objects.get(email = user_email)
user_id = user_object.id
grade = request.POST.get("user_grade", "")
course_id = request.POST.get("course_id", "")
block_data = get_course_blocks(User.objects.get(id = user_id), modulestore().make_course_usage_key(CourseKey.from_string(str(course_id))), allow_start_dates_in_future=True, include_completion=True)

#SA || letter_grade changes
letter_grade = request.POST.get("letter_grade", "")
grade = 0 if letter_grade else request.POST.get("user_grade", "")

try:
studentmodule = StudentModule.objects.get(student_id = user_id, module_state_key = usage_id)

#Update Grades
studentmodule.grade = grade
student_state = json.loads(studentmodule.state)
student_state["module_score"] = grade
studentmodule.state = json.dumps(student_state)
studentmodule.save()
grades_signals.PROBLEM_RAW_SCORE_CHANGED.send(
sender=None,
raw_earned=grade,
raw_possible=studentmodule.max_grade,
weight=block_data.get_xblock_field(studentmodule.module_state_key, 'weight'),
user_id=user_id,
course_id=str(studentmodule.course_id),
usage_id=str(usage_id),
score_deleted=True,
only_if_higher=False,
modified=datetime.datetime.now().replace(tzinfo=pytz.UTC),
score_db_table=grades_constants.ScoreDatabaseTableEnum.courseware_student_module,
)
except StudentModule.DoesNotExist:
studentmodule = StudentModule.objects.create(student_id=user_id,course_id=request.POST.get("course_id"),module_state_key=usage_id,state=json.dumps({"module_score" : grade, "score_comment" : ""}), max_grade= block_data.get_xblock_field(usage_id, 'weight'))
block_data = get_course_blocks(User.objects.get(id = user_id), modulestore().make_course_usage_key(CourseKey.from_string(str(course_id))), allow_start_dates_in_future=True, include_completion=True)

try:
studentmodule = StudentModule.objects.get(student_id = user_id, module_state_key = usage_id)

#Update Grades
studentmodule.grade = grade
studentmodule.letter_grade = letter_grade
student_state = json.loads(studentmodule.state)
student_state["module_score"] = grade
studentmodule.state = json.dumps(student_state)
studentmodule.save()
grades_signals.PROBLEM_RAW_SCORE_CHANGED.send(
sender=None,
raw_earned=grade,
letter_grade=letter_grade,
raw_possible = 0 if letter_grade else studentmodule.max_grade,
weight=block_data.get_xblock_field(studentmodule.module_state_key, 'weight'),
user_id=user_id,
course_id=str(studentmodule.course_id),
usage_id=str(usage_id),
score_deleted=True,
only_if_higher=False,
modified=datetime.datetime.now().replace(tzinfo=pytz.UTC),
score_db_table=grades_constants.ScoreDatabaseTableEnum.courseware_student_module,
)
except StudentModule.DoesNotExist:
studentmodule = StudentModule.objects.create(student_id=user_id,course_id=request.POST.get("course_id"),module_state_key=usage_id,state=json.dumps({"module_score" : grade, "score_comment" : ""}), max_grade= 0 if letter_grade else block_data.get_xblock_field(usage_id, 'weight'), letter_grade=letter_grade)

log.info("Student module created {0}".format(studentmodule))
log.info("Student module created {0}".format(studentmodule))

grades_signals.SCORE_PUBLISHED.send(
sender=None,
block=modulestore().get_item(studentmodule.module_state_key),
user=user_object,
raw_earned=grade,
raw_possible=block_data.get_xblock_field(studentmodule.module_state_key, 'weight'),
only_if_higher=False,
score_deleted=False,
)

grades_signals.SCORE_PUBLISHED.send(
sender=None,
block=modulestore().get_item(studentmodule.module_state_key),
user=user_object,
raw_earned=grade,
raw_possible=block_data.get_xblock_field(studentmodule.module_state_key, 'weight'),
only_if_higher=False,
score_deleted=False,
)
handlers.scorable_block_completion(
sender="",
user_id=user_id,
course_id=course_id,
usage_id=usage_id,
weighted_earned=grade,
weighted_possible=block_data.get_xblock_field(studentmodule.module_state_key, 'weight'),
modified=datetime.datetime.now().replace(tzinfo=pytz.UTC),
score_db_table=grades_constants.ScoreDatabaseTableEnum.courseware_student_module
)

handlers.scorable_block_completion(
sender="",
user_id=user_id,
course_id=course_id,
usage_id=usage_id,
weighted_earned=grade,
weighted_possible=block_data.get_xblock_field(studentmodule.module_state_key, 'weight'),
modified=datetime.datetime.now().replace(tzinfo=pytz.UTC),
score_db_table=grades_constants.ScoreDatabaseTableEnum.courseware_student_module
)

except Exception as e:
log.error(e)
return JsonResponse({"Status" : "Failed", "message" : "Something went wrong! Unable to update Grades."})

return JsonResponse({"Status" : "Success", "message" : "Grades updated successfully"})

Expand Down
3 changes: 3 additions & 0 deletions lms/djangoapps/course_home_api/progress/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class SubsectionScoresSerializer(ReadOnlySerializer):
show_correctness = serializers.CharField()
show_grades = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()

#SA || letter_grade changes
letter_grade = serializers.CharField()

def get_override(self, subsection):
"""Proctoring or grading score override"""
Expand Down
7 changes: 4 additions & 3 deletions lms/djangoapps/courseware/model_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,8 @@ class ScoresClient:
Eventually, this should read and write scores, but at the moment it only
handles the read side of things.
"""
Score = namedtuple('Score', 'correct total created')
#SA || letter_grade changes
Score = namedtuple('Score', 'correct total created letter_grade')

def __init__(self, course_key, user_id):
self.course_key = course_key
Expand All @@ -938,8 +939,8 @@ def fetch_scores(self, locations):
# attached to them (since old mongo identifiers don't include runs).
# So we have to add that info back in before we put it into our lookup.
self._locations_to_scores.update({
location.map_into_course(self.course_key): self.Score(correct, total, created)
for location, correct, total, created
location.map_into_course(self.course_key): self.Score(correct, total, created, letter_grade)
for location, correct, total, created, letter_grade
in scores_qset.values_list('module_state_key', 'grade', 'max_grade', 'created')
})
self._has_fetched = True
Expand Down
5 changes: 5 additions & 0 deletions lms/djangoapps/courseware/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ class Meta:
created = models.DateTimeField(auto_now_add=True, db_index=True)
modified = models.DateTimeField(auto_now=True, db_index=True)

#SA || letter_grade changes
letter_grade = models.TextField(null=True, blank=True)

@classmethod
def all_submitted_problems_read_only(cls, course_id):
"""
Expand All @@ -139,6 +142,7 @@ def all_submitted_problems_read_only(cls, course_id):
return queryset

def __repr__(self):
#SA || letter_grade changes
return 'StudentModule<{!r}>'.format(
{
'course_id': self.course_id,
Expand All @@ -149,6 +153,7 @@ def __repr__(self):
'student_id': self.student_id,
'module_state_key': self.module_state_key,
'state': str(self.state)[:20],
'letter_grade': self.letter_grade,
})

def __str__(self):
Expand Down
2 changes: 2 additions & 0 deletions lms/djangoapps/grades/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def grade_updated(**kwargs):
if not root_id:
root_id = create_new_event_transaction_id()
set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
#SA || letter_grade changes
tracker.emit(
str(PROBLEM_SUBMITTED_EVENT_TYPE),
{
Expand All @@ -60,6 +61,7 @@ def grade_updated(**kwargs):
'event_transaction_type': str(PROBLEM_SUBMITTED_EVENT_TYPE),
'weighted_earned': kwargs.get('weighted_earned'),
'weighted_possible': kwargs.get('weighted_possible'),
'letter_grade': kwargs.get('letter_grade'),
}
)

Expand Down
7 changes: 6 additions & 1 deletion lms/djangoapps/grades/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ class Meta:
earned_graded = models.FloatField(blank=False)
possible_graded = models.FloatField(blank=False)

#SA || letter_grade changes
letter_grade = models.CharField(max_length=255, blank=True, null=True)

# timestamp for the learner's first attempt at content in
# this subsection. If null, indicates no attempt
# has yet been made.
Expand All @@ -368,8 +371,9 @@ def __str__(self):
"""
Returns a string representation of this model.
"""
#SA || letter_grade changes
return (
"{} user: {}, course version: {}, subsection: {} ({}). {}/{} graded, {}/{} all, first_attempted: {}"
"{} user: {}, course version: {}, subsection: {} ({}). {}/{} graded, {}/{} all, first_attempted: {}, letter_grade: {}"
).format(
type(self).__name__,
self.user_id,
Expand All @@ -381,6 +385,7 @@ def __str__(self):
self.earned_all,
self.possible_all,
self.first_attempted,
self.letter_grade,
)

@classmethod
Expand Down
3 changes: 3 additions & 0 deletions lms/djangoapps/grades/rest_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class SectionBreakdownSerializer(serializers.Serializer):
score_earned = serializers.FloatField()
score_possible = serializers.FloatField()
subsection_name = serializers.CharField()

#SA || letter_grade changes
letter_grade = serializers.CharField()


class StudentGradebookEntrySerializer(serializers.Serializer):
Expand Down
2 changes: 2 additions & 0 deletions lms/djangoapps/grades/rest_api/v1/gradebook_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ def _section_breakdown(self, course, graded_subsections, course_grade):
# TODO: https://openedx.atlassian.net/browse/EDUCATOR-3559 -- Some fields should be renamed, others removed:
# 'displayed_value' should maybe be 'description_percent'
# 'grade_description' should be 'description_ratio'
#SA || letter_grade changes
breakdown.append({
'attempted': attempted,
'category': subsection_grade.format,
Expand All @@ -484,6 +485,7 @@ def _section_breakdown(self, course, graded_subsections, course_grade):
'score_earned': score_earned,
'score_possible': score_possible,
'subsection_name': subsection_grade.display_name,
'letter_grade': subsection_grade.letter_grade,
})
return breakdown

Expand Down
18 changes: 14 additions & 4 deletions lms/djangoapps/grades/scores.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):

# Priority order for retrieving the scores:
# submissions API -> CSM -> grades persisted block -> latest block content
raw_earned, raw_possible, weighted_earned, weighted_possible, first_attempted = (
#SA || letter_grade changes
raw_earned, raw_possible, weighted_earned, weighted_possible, first_attempted, letter_grade = (
_get_score_from_submissions(submissions_scores, block) or
_get_score_from_csm(csm_scores, block, weight) or
_get_score_from_persisted_or_latest_block(persisted_block, block, weight)
Expand All @@ -129,6 +130,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
has_valid_denominator = weighted_possible > 0.0
graded = _get_graded_from_block(persisted_block, block) if has_valid_denominator else False

#SA || letter_grade changes
return ProblemScore(
raw_earned,
raw_possible,
Expand All @@ -137,6 +139,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
weight,
graded,
first_attempted=first_attempted,
letter_grade=letter_grade,
)


Expand Down Expand Up @@ -174,12 +177,14 @@ def _get_score_from_submissions(submissions_scores, block):
"""
if submissions_scores:
submission_value = submissions_scores.get(str(block.location))
#SA || letter_grade changes
if submission_value:
first_attempted = submission_value['created_at']
weighted_earned = submission_value['points_earned']
weighted_possible = submission_value['points_possible']
letter_grade = submission_value.get('letter_grade', '')
assert weighted_earned >= 0.0 and weighted_possible > 0.0 # per contract from submissions API
return (None, None) + (weighted_earned, weighted_possible) + (first_attempted,)
return (None, None) + (weighted_earned, weighted_possible) + (first_attempted, letter_grade,)


def _get_score_from_csm(csm_scores, block, weight):
Expand Down Expand Up @@ -209,7 +214,8 @@ def _get_score_from_csm(csm_scores, block, weight):
raw_earned = 0.0

raw_possible = score.total
return (raw_earned, raw_possible) + weighted_score(raw_earned, raw_possible, weight) + (first_attempted,)
#SA || letter_grade changes
return (raw_earned, raw_possible) + weighted_score(raw_earned, raw_possible, weight) + (first_attempted, score.letter_grade,)


def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
Expand All @@ -227,6 +233,9 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
raw_earned = 0.0
first_attempted = None

#SA || letter_grade changes
letter_grade = None

if persisted_block:
raw_possible = persisted_block.raw_possible
else:
Expand All @@ -242,7 +251,8 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
else:
weighted_scores = weighted_score(raw_earned, raw_possible, weight)

return (raw_earned, raw_possible) + weighted_scores + (first_attempted,)
#SA || letter_grade changes
return (raw_earned, raw_possible) + weighted_scores + (first_attempted, letter_grade,)


def _get_weight_from_block(persisted_block, block):
Expand Down
6 changes: 5 additions & 1 deletion lms/djangoapps/grades/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unus
else: # TODO: remove as part of TNL-5982
weighted_earned, weighted_possible = kwargs['raw_earned'], kwargs['raw_possible']

#SA || letter_grade changes
PROBLEM_WEIGHTED_SCORE_CHANGED.send(
sender=None,
weighted_earned=weighted_earned,
Expand All @@ -218,7 +219,8 @@ def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unus
score_deleted=kwargs.get('score_deleted', False),
modified=kwargs['modified'],
score_db_table=kwargs['score_db_table'],
grader_response=kwargs.get('grader_response', False)
grader_response=kwargs.get('grader_response', False),
letter_grade=kwargs.get('letter_grade', '')
)


Expand All @@ -233,6 +235,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
context_key = LearningContextKey.from_string(kwargs['course_id'])
if not context_key.is_course:
return # If it's not a course, it has no subsections, so skip the subsection grading update
#SA || letter_grade changes
recalculate_subsection_grade_v3.apply_async(
kwargs=dict(
user_id=kwargs['user_id'],
Expand All @@ -246,6 +249,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
event_transaction_type=str(get_event_transaction_type()),
score_db_table=kwargs['score_db_table'],
force_update_subsections=kwargs.get('force_update_subsections', False),
letter_grade=kwargs.get('letter_grade', ''),
),
countdown=RECALCULATE_GRADE_DELAY_SECONDS,
)
Expand Down
Loading

0 comments on commit 055c787

Please sign in to comment.