Merge pull request #19287 from edx/schen/EDUCATOR-3653

Create the persistentsubsectiongradeoverride audit records
This commit is contained in:
Simon Chen
2018-11-29 10:40:41 -05:00
committed by GitHub
6 changed files with 138 additions and 7 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)),
],
),
]

View File

@@ -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
)

View File

@@ -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.

View File

@@ -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):