Merge pull request #19287 from edx/schen/EDUCATOR-3653
Create the persistentsubsectiongradeoverride audit records
This commit is contained in:
@@ -21,7 +21,7 @@ from lms.djangoapps.grades.api.v1.views import CourseGradesView
|
||||
from lms.djangoapps.grades.config.waffle import waffle_flags, WRITABLE_GRADEBOOK
|
||||
from lms.djangoapps.grades.course_data import CourseData
|
||||
from lms.djangoapps.grades.course_grade import CourseGrade
|
||||
from lms.djangoapps.grades.models import PersistentSubsectionGrade
|
||||
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverrideHistory
|
||||
from lms.djangoapps.grades.subsection_grade import ReadSubsectionGrade
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||||
@@ -1308,3 +1308,11 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
expected_value = getattr(expected_grades, field_name)
|
||||
self.assertEqual(expected_value, getattr(grade, field_name))
|
||||
self.assertEqual(expected_value, getattr(grade.override, field_name + '_override'))
|
||||
|
||||
update_records = PersistentSubsectionGradeOverrideHistory.objects.filter(user=self.global_staff)
|
||||
self.assertEqual(update_records.count(), 3)
|
||||
for audit_item in update_records:
|
||||
self.assertEqual(audit_item.user, self.global_staff)
|
||||
self.assertIsNotNone(audit_item.created)
|
||||
self.assertEqual(audit_item.feature, PersistentSubsectionGradeOverrideHistory.GRADEBOOK)
|
||||
self.assertEqual(audit_item.action, PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE)
|
||||
|
||||
@@ -24,7 +24,11 @@ from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum
|
||||
from lms.djangoapps.grades.course_data import CourseData
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
from lms.djangoapps.grades.events import SUBSECTION_GRADE_CALCULATED, subsection_grade_calculated
|
||||
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
|
||||
from lms.djangoapps.grades.models import (
|
||||
PersistentSubsectionGrade,
|
||||
PersistentSubsectionGradeOverride,
|
||||
PersistentSubsectionGradeOverrideHistory
|
||||
)
|
||||
from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade
|
||||
from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3, are_grades_frozen
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -803,7 +807,7 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
continue
|
||||
|
||||
if subsection_grade_model:
|
||||
self._create_override(subsection_grade_model, **user_data['grade'])
|
||||
self._create_override(request.user, subsection_grade_model, **user_data['grade'])
|
||||
result.append(GradebookUpdateResponseItem(
|
||||
user_id=user.id,
|
||||
usage_id=text_type(usage_key),
|
||||
@@ -826,7 +830,7 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
subsection_grade = CreateSubsectionGrade(subsection, course_data.structure, {}, {})
|
||||
return subsection_grade.update_or_create_model(user, force_update_subsections=True)
|
||||
|
||||
def _create_override(self, subsection_grade_model, **override_data):
|
||||
def _create_override(self, request_user, subsection_grade_model, **override_data):
|
||||
"""
|
||||
Helper method to create a `PersistentSubsectionGradeOverride` object
|
||||
and send a `SUBSECTION_OVERRIDE_CHANGED` signal.
|
||||
@@ -836,6 +840,13 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
defaults=self._clean_override_data(override_data),
|
||||
)
|
||||
|
||||
_ = PersistentSubsectionGradeOverrideHistory.objects.create(
|
||||
override_id=override.id,
|
||||
user=request_user,
|
||||
feature=PersistentSubsectionGradeOverrideHistory.GRADEBOOK,
|
||||
action=PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE,
|
||||
)
|
||||
|
||||
set_event_transaction_type(SUBSECTION_GRADE_CALCULATED)
|
||||
create_new_event_transaction_id()
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.16 on 2018-11-27 20:53
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('grades', '0013_persistentsubsectiongradeoverride'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PersistentSubsectionGradeOverrideHistory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('override_id', models.IntegerField(db_index=True)),
|
||||
('feature', models.CharField(choices=[(b'PROCTORING', b'proctoring'), (b'GRADEBOOK', b'gradebook')], default=b'PROCTORING', max_length=32)),
|
||||
('action', models.CharField(choices=[(b'CREATEORUPDATE', b'create_or_update'), (b'DELETE', b'delete')], default=b'CREATEORUPDATE', max_length=32)),
|
||||
('comments', models.CharField(blank=True, max_length=300, null=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -14,6 +14,7 @@ from base64 import b64encode
|
||||
from collections import namedtuple
|
||||
from hashlib import sha1
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from lazy import lazy
|
||||
@@ -636,3 +637,55 @@ class PersistentSubsectionGradeOverride(models.Model):
|
||||
def prefetch(user, course_key):
|
||||
PersistentSubsectionGradeOverride.prefetch(user.id, course_key)
|
||||
VisibleBlocks.bulk_read(user.id, course_key)
|
||||
|
||||
|
||||
class PersistentSubsectionGradeOverrideHistory(models.Model):
|
||||
"""
|
||||
A django model tracking persistent grades override audit records.
|
||||
"""
|
||||
PROCTORING = 'PROCTORING'
|
||||
GRADEBOOK = 'GRADEBOOK'
|
||||
OVERRIDE_FEATURES = (
|
||||
(PROCTORING, 'proctoring'),
|
||||
(GRADEBOOK, 'gradebook'),
|
||||
)
|
||||
|
||||
CREATE_OR_UPDATE = 'CREATEORUPDATE'
|
||||
DELETE = 'DELETE'
|
||||
OVERRIDE_ACTIONS = (
|
||||
(CREATE_OR_UPDATE, 'create_or_update'),
|
||||
(DELETE, 'delete')
|
||||
)
|
||||
|
||||
class Meta(object):
|
||||
app_label = "grades"
|
||||
|
||||
override_id = models.IntegerField(db_index=True)
|
||||
feature = models.CharField(
|
||||
max_length=32,
|
||||
choices=OVERRIDE_FEATURES,
|
||||
default=PROCTORING
|
||||
)
|
||||
action = models.CharField(
|
||||
max_length=32,
|
||||
choices=OVERRIDE_ACTIONS,
|
||||
default=CREATE_OR_UPDATE
|
||||
)
|
||||
user = models.ForeignKey(User, blank=True, null=True)
|
||||
comments = models.CharField(max_length=300, blank=True, null=True)
|
||||
created = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
String representation of this model.
|
||||
"""
|
||||
return (
|
||||
u"{} override_id: {}, user_id: {}, feature: {}, action: {}, created: {}"
|
||||
).format(
|
||||
type(self).__name__,
|
||||
self.override_id,
|
||||
self.user,
|
||||
self.feature,
|
||||
self.action,
|
||||
self.created
|
||||
)
|
||||
|
||||
@@ -12,7 +12,11 @@ from track.event_transaction_utils import create_new_event_transaction_id, set_e
|
||||
from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE
|
||||
from .constants import ScoreDatabaseTableEnum
|
||||
from .events import SUBSECTION_OVERRIDE_EVENT_TYPE
|
||||
from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
|
||||
from .models import (
|
||||
PersistentSubsectionGrade,
|
||||
PersistentSubsectionGradeOverride,
|
||||
PersistentSubsectionGradeOverrideHistory
|
||||
)
|
||||
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED
|
||||
|
||||
|
||||
@@ -78,6 +82,12 @@ class GradesService(object):
|
||||
earned_graded_override=earned_graded
|
||||
)
|
||||
|
||||
_ = PersistentSubsectionGradeOverrideHistory.objects.create(
|
||||
override_id=override.id,
|
||||
feature=PersistentSubsectionGradeOverrideHistory.PROCTORING,
|
||||
action=PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE
|
||||
)
|
||||
|
||||
# Cache a new event id and event type which the signal handler will use to emit a tracking log event.
|
||||
create_new_event_transaction_id()
|
||||
set_event_transaction_type(SUBSECTION_OVERRIDE_EVENT_TYPE)
|
||||
@@ -112,6 +122,11 @@ class GradesService(object):
|
||||
|
||||
# Older rejected exam attempts that transition to verified might not have an override created
|
||||
if override is not None:
|
||||
_ = PersistentSubsectionGradeOverrideHistory.objects.create(
|
||||
override_id=override.id,
|
||||
feature=PersistentSubsectionGradeOverrideHistory.PROCTORING,
|
||||
action=PersistentSubsectionGradeOverrideHistory.DELETE
|
||||
)
|
||||
override.delete()
|
||||
|
||||
# Cache a new event id and event type which the signal handler will use to emit a tracking log event.
|
||||
|
||||
@@ -5,7 +5,11 @@ from datetime import datetime
|
||||
import ddt
|
||||
import pytz
|
||||
from freezegun import freeze_time
|
||||
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
|
||||
from lms.djangoapps.grades.models import (
|
||||
PersistentSubsectionGrade,
|
||||
PersistentSubsectionGradeOverride,
|
||||
PersistentSubsectionGradeOverrideHistory,
|
||||
)
|
||||
from lms.djangoapps.grades.services import GradesService
|
||||
from mock import patch, call
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -129,6 +133,12 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
'earned_graded_override': override.earned_graded_override
|
||||
})
|
||||
|
||||
def _verify_override_history(self, override_history, history_action):
|
||||
self.assertIsNone(override_history.user)
|
||||
self.assertIsNotNone(override_history.created)
|
||||
self.assertEqual(override_history.feature, PersistentSubsectionGradeOverrideHistory.PROCTORING)
|
||||
self.assertEqual(override_history.action, history_action)
|
||||
|
||||
@ddt.data(
|
||||
[{
|
||||
'earned_all': 0.0,
|
||||
@@ -191,11 +201,13 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
score_db_table=ScoreDatabaseTableEnum.overrides
|
||||
)
|
||||
)
|
||||
override_history = PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override_obj.id).first()
|
||||
self._verify_override_history(override_history, PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE)
|
||||
|
||||
@freeze_time('2017-01-01')
|
||||
def test_undo_override_subsection_grade(self):
|
||||
override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(grade=self.grade)
|
||||
|
||||
override_id = override.id
|
||||
self.service.undo_override_subsection_grade(
|
||||
user_id=self.user.id,
|
||||
course_key_or_id=self.course.id,
|
||||
@@ -218,6 +230,8 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
score_db_table=ScoreDatabaseTableEnum.overrides
|
||||
)
|
||||
)
|
||||
override_history = PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override_id).first()
|
||||
self._verify_override_history(override_history, PersistentSubsectionGradeOverrideHistory.DELETE)
|
||||
|
||||
@freeze_time('2018-01-01')
|
||||
def test_undo_override_subsection_grade_without_grade(self):
|
||||
|
||||
Reference in New Issue
Block a user