feat: implement course access role history table (#37260)
Co-authored-by: Muhammad Faraz Maqsood <faraz.maqsood@A006-01130.local>
This commit is contained in:
committed by
GitHub
parent
73b19832aa
commit
37b2b0d0d9
@@ -36,6 +36,7 @@ from common.djangoapps.student.models import (
|
||||
BulkChangeEnrollmentConfiguration,
|
||||
BulkUnenrollConfiguration,
|
||||
CourseAccessRole,
|
||||
CourseAccessRoleHistory,
|
||||
CourseEnrollment,
|
||||
CourseEnrollmentAllowed,
|
||||
CourseEnrollmentCelebration,
|
||||
@@ -229,6 +230,131 @@ class CourseAccessRoleAdmin(admin.ModelAdmin):
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(CourseAccessRoleHistory)
|
||||
class CourseAccessRoleHistoryAdmin(admin.ModelAdmin):
|
||||
"""Admin panel for the Course Access Role History."""
|
||||
list_display = (
|
||||
'id', 'user', 'org', 'course_id', 'role', 'action_type', 'changed_by', 'created'
|
||||
)
|
||||
list_filter = (
|
||||
'action_type', 'org', 'role'
|
||||
)
|
||||
search_fields = (
|
||||
'user__username', 'org', 'course_id', 'role', 'action_type', 'changed_by__username'
|
||||
)
|
||||
readonly_fields = (
|
||||
'user', 'org', 'course_id', 'role', 'action_type', 'changed_by', 'created', 'modified'
|
||||
)
|
||||
actions = ['revert_selected_history', 'delete_selected_history_entries']
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def revert_selected_history(self, request, queryset):
|
||||
"""
|
||||
Admin action to revert selected CourseAccessRoleHistory entries.
|
||||
"""
|
||||
if not request.user.has_perm('student.can_revert_course_access_role'):
|
||||
self.message_user(request, "You do not have permission to revert course access roles.", level='ERROR')
|
||||
return
|
||||
|
||||
reverted_count = 0
|
||||
for history_record in queryset:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
if history_record.action_type == 'created':
|
||||
CourseAccessRole.objects.filter(
|
||||
user=history_record.user,
|
||||
org=history_record.org,
|
||||
course_id=history_record.course_id,
|
||||
role=history_record.role
|
||||
).delete()
|
||||
self.message_user(
|
||||
request, f"Successfully reverted creation of role for "
|
||||
f"{history_record.user.username} in {history_record.course_id}"
|
||||
)
|
||||
elif history_record.action_type == 'updated':
|
||||
if history_record.old_values:
|
||||
CourseAccessRole.objects.update_or_create(
|
||||
user_id=history_record.old_values['user_id'],
|
||||
org=history_record.old_values['org'],
|
||||
course_id=history_record.old_values['course_id'],
|
||||
defaults={'role': history_record.old_values['role']}
|
||||
)
|
||||
self.message_user(
|
||||
request, f"Successfully reverted update of role for "
|
||||
f"{history_record.user.username} to {history_record.old_values['role']} "
|
||||
f"in {history_record.course_id}"
|
||||
)
|
||||
else:
|
||||
self.message_user(
|
||||
request, f"Cannot revert update for record {history_record.id}: "
|
||||
f"old_values not found.", level='WARNING'
|
||||
)
|
||||
elif history_record.action_type == 'deleted':
|
||||
CourseAccessRole.objects.update_or_create(
|
||||
user=history_record.user,
|
||||
org=history_record.org,
|
||||
course_id=history_record.course_id,
|
||||
role=history_record.role
|
||||
)
|
||||
self.message_user(
|
||||
request, f"Successfully reverted deletion of role for "
|
||||
f"{history_record.user.username} in {history_record.course_id}"
|
||||
)
|
||||
reverted_count += 1
|
||||
except Exception as e: # lint-amnesty, pylint: disable=broad-except
|
||||
self.message_user(request, f"Error reverting record {history_record.id}: {e}", level='ERROR')
|
||||
|
||||
if reverted_count > 0:
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"Successfully reverted %(count)d selected history entry.",
|
||||
"Successfully reverted %(count)d selected history entries.",
|
||||
reverted_count
|
||||
) % {'count': reverted_count},
|
||||
)
|
||||
revert_selected_history.short_description = "Revert selected history entries"
|
||||
|
||||
def delete_selected_history_entries(self, request, queryset):
|
||||
"""
|
||||
Admin action to delete selected CourseAccessRoleHistory entries.
|
||||
"""
|
||||
if not request.user.has_perm('student.can_delete_course_access_role_history'):
|
||||
self.message_user(
|
||||
request, "You do not have permission to delete course access role history entries.",
|
||||
level='ERROR'
|
||||
)
|
||||
return
|
||||
|
||||
deleted_count = 0
|
||||
for history_record in queryset:
|
||||
try:
|
||||
history_record.delete()
|
||||
deleted_count += 1
|
||||
except Exception as e: # lint-amnesty, pylint: disable=broad-except
|
||||
self.message_user(request, f"Error deleting record {history_record.id}: {e}", level='ERROR')
|
||||
|
||||
if deleted_count > 0:
|
||||
self.message_user(
|
||||
request,
|
||||
ngettext(
|
||||
"Successfully deleted %(count)d selected history entry.",
|
||||
"Successfully deleted %(count)d selected history entries.",
|
||||
deleted_count
|
||||
) % {'count': deleted_count},
|
||||
level='SUCCESS',
|
||||
)
|
||||
delete_selected_history_entries.short_description = "Delete selected history entries"
|
||||
|
||||
|
||||
@admin.register(LinkedInAddToProfileConfiguration)
|
||||
class LinkedInAddToProfileConfigurationAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for the LinkedIn Add to Profile configuration. """
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-22 10:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
import opaque_keys.edx.django.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('student', '0046_alter_userprofile_phone_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CourseAccessRoleHistory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||
('org', models.CharField(blank=True, db_index=True, max_length=64)),
|
||||
('course_id', opaque_keys.edx.django.models.CourseKeyField(blank=True, db_index=True, max_length=255)),
|
||||
('role', models.CharField(db_index=True, max_length=64)),
|
||||
('action_type', models.CharField(choices=[('created', 'Created'), ('updated', 'Updated'), ('deleted', 'Deleted')], db_index=True, max_length=10)),
|
||||
('old_values', models.JSONField(blank=True, help_text="Stores old values of fields for 'updated' actions.", null=True)),
|
||||
('changed_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='courseaccessrole_history_changer', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'permissions': (('can_revert_course_access_role', 'Can revert course access role changes'), ('can_delete_course_access_role_history', 'Can delete course access role history')),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -32,7 +32,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.validators import FileExtensionValidator, RegexValidator
|
||||
from django.db import IntegrityError, models
|
||||
from django.db.models import Q
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.db.models.signals import post_save, pre_save, post_delete
|
||||
from django.db.utils import ProgrammingError
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -1103,6 +1103,41 @@ class CourseAccessRole(models.Model):
|
||||
return f"[CourseAccessRole] user: {self.user.username} role: {self.role} org: {self.org} course: {self.course_id}" # lint-amnesty, pylint: disable=line-too-long
|
||||
|
||||
|
||||
class CourseAccessRoleHistory(TimeStampedModel):
|
||||
"""
|
||||
Stores the change history for CourseAccessRole objects.
|
||||
"""
|
||||
ACTION_CHOICES = (
|
||||
('created', 'Created'),
|
||||
('updated', 'Updated'),
|
||||
('deleted', 'Deleted'),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
org = models.CharField(max_length=64, db_index=True, blank=True)
|
||||
course_id = CourseKeyField(max_length=255, db_index=True, blank=True)
|
||||
role = models.CharField(max_length=64, db_index=True)
|
||||
action_type = models.CharField(max_length=10, choices=ACTION_CHOICES, db_index=True)
|
||||
changed_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='courseaccessrole_history_changer',
|
||||
)
|
||||
old_values = models.JSONField(null=True, blank=True, help_text="Stores old values of fields for 'updated' actions.")
|
||||
|
||||
class Meta:
|
||||
permissions = (("can_revert_course_access_role", "Can revert course access role changes"),
|
||||
("can_delete_course_access_role_history", "Can delete course access role history"),)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"[CourseAccessRoleHistory] user: {self.user.username} role: {self.role} "
|
||||
f"org: {self.org} course: {self.course_id} action: {self.action_type} "
|
||||
f"changed_by: {self.changed_by.username if self.changed_by else 'N/A'} at {self.created}"
|
||||
)
|
||||
|
||||
|
||||
#### Helper methods for use from python manage.py shell and other classes.
|
||||
|
||||
def strip_if_string(value):
|
||||
@@ -1879,3 +1914,58 @@ class UserPasswordToggleHistory(TimeStampedModel):
|
||||
|
||||
def __str__(self):
|
||||
return self.comment
|
||||
|
||||
|
||||
@receiver(pre_save, sender=CourseAccessRole)
|
||||
def pre_save_course_access_role(sender, instance, **kwargs):
|
||||
"""
|
||||
Captures the current state of a CourseAccessRole before it is saved for update tracking.
|
||||
"""
|
||||
if instance.pk:
|
||||
try:
|
||||
old_instance = sender.objects.get(pk=instance.pk)
|
||||
# pylint: disable=protected-access
|
||||
instance._old_values = {
|
||||
'user_id': old_instance.user_id,
|
||||
'org': old_instance.org,
|
||||
'course_id': str(old_instance.course_id) if old_instance.course_id else None,
|
||||
'role': old_instance.role,
|
||||
}
|
||||
except sender.DoesNotExist:
|
||||
# pylint: disable=protected-access
|
||||
instance._old_values = None
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseAccessRole)
|
||||
def create_course_access_role_history_on_save(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Handle create and update actions for CourseAccessRole objects.
|
||||
"""
|
||||
action_type = 'created' if created else 'updated'
|
||||
current_user = crum.get_current_user()
|
||||
old_values = getattr(instance, '_old_values', None) if not created else None
|
||||
CourseAccessRoleHistory.objects.create(
|
||||
user=instance.user,
|
||||
org=instance.org,
|
||||
course_id=instance.course_id,
|
||||
role=instance.role,
|
||||
action_type=action_type,
|
||||
changed_by=current_user if current_user and current_user.is_authenticated else None,
|
||||
old_values=old_values
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CourseAccessRole)
|
||||
def create_course_access_role_history_on_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Handle delete actions for CourseAccessRole objects.
|
||||
"""
|
||||
current_user = crum.get_current_user()
|
||||
CourseAccessRoleHistory.objects.create(
|
||||
user=instance.user,
|
||||
org=instance.org,
|
||||
course_id=instance.course_id,
|
||||
role=instance.role,
|
||||
action_type='deleted',
|
||||
changed_by=current_user if current_user and current_user.is_authenticated else None
|
||||
)
|
||||
|
||||
@@ -4,10 +4,13 @@ Tests of student.roles
|
||||
|
||||
|
||||
import ddt
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.test import TestCase
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
|
||||
from common.djangoapps.student.admin import CourseAccessRoleHistoryAdmin
|
||||
from common.djangoapps.student.models import CourseAccessRoleHistory, User
|
||||
from common.djangoapps.student.roles import (
|
||||
CourseAccessRole,
|
||||
CourseBetaTesterRole,
|
||||
@@ -309,3 +312,248 @@ class RoleCacheTestCase(TestCase): # lint-amnesty, pylint: disable=missing-clas
|
||||
assert roles_dict.get('library-v1:edX+quizzes').pop().course_id.course == 'quizzes'
|
||||
assert roles_dict.get('course-v1:edX+toy+2012_Summer').pop().course_id.course == 'toy'
|
||||
assert roles_dict.get('course-v1:edX+toy2+2013_Fall').pop().course_id.course == 'toy2'
|
||||
|
||||
|
||||
class CourseAccessRoleHistoryTest(TestCase):
|
||||
"""
|
||||
Tests for the CourseAccessRoleHistory model and associated signals/admin actions.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = UserFactory(username="test_user", email="test@example.com")
|
||||
self.admin_user = UserFactory(
|
||||
username="admin_user",
|
||||
email="admin@example.com",
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
self.course_key = CourseKey.from_string("course-v1:OrgX+CourseY+2023_Fall")
|
||||
self.org = "OrgX"
|
||||
|
||||
revert_permission = Permission.objects.get(
|
||||
codename="can_revert_course_access_role", content_type__app_label="student"
|
||||
)
|
||||
delete_history_permission = Permission.objects.get(
|
||||
codename="can_delete_course_access_role_history",
|
||||
content_type__app_label="student",
|
||||
)
|
||||
self.admin_user.user_permissions.add(
|
||||
revert_permission, delete_history_permission
|
||||
)
|
||||
self.admin_user = User.objects.get(pk=self.admin_user.pk)
|
||||
|
||||
def test_create_logs_history(self):
|
||||
"""
|
||||
Test that creating a CourseAccessRole logs a history entry.
|
||||
"""
|
||||
CourseAccessRole.objects.create(
|
||||
user=self.user, org=self.org, course_id=self.course_key, role="student"
|
||||
)
|
||||
|
||||
history = CourseAccessRoleHistory.objects.first()
|
||||
self.assertIsNotNone(history)
|
||||
self.assertEqual(history.user, self.user)
|
||||
self.assertEqual(history.org, self.org)
|
||||
self.assertEqual(history.course_id, self.course_key)
|
||||
self.assertEqual(history.role, "student")
|
||||
self.assertEqual(history.action_type, "created")
|
||||
self.assertIsNone(history.old_values)
|
||||
|
||||
def test_update_logs_history(self):
|
||||
"""
|
||||
Test that updating a CourseAccessRole logs a history entry with old_values.
|
||||
"""
|
||||
role_instance = CourseAccessRole.objects.create(
|
||||
user=self.user, org=self.org, course_id=self.course_key, role="student"
|
||||
)
|
||||
role_instance.role = "staff"
|
||||
role_instance.save()
|
||||
|
||||
history_entries = CourseAccessRoleHistory.objects.filter(
|
||||
user=self.user, course_id=self.course_key
|
||||
).order_by("created")
|
||||
self.assertEqual(history_entries.count(), 2)
|
||||
|
||||
update_history = history_entries.last()
|
||||
self.assertEqual(update_history.action_type, "updated")
|
||||
self.assertIsNotNone(update_history.old_values)
|
||||
self.assertEqual(update_history.old_values["role"], "student")
|
||||
self.assertEqual(update_history.role, "staff")
|
||||
|
||||
def test_delete_logs_history(self):
|
||||
"""
|
||||
Test that deleting a CourseAccessRole logs a history entry.
|
||||
"""
|
||||
role_instance = CourseAccessRole.objects.create(
|
||||
user=self.user, org=self.org, course_id=self.course_key, role="student"
|
||||
)
|
||||
|
||||
role_instance.delete()
|
||||
|
||||
history_entries = CourseAccessRoleHistory.objects.filter(
|
||||
user=self.user, course_id=self.course_key
|
||||
).order_by("created")
|
||||
self.assertEqual(history_entries.count(), 2)
|
||||
|
||||
delete_history = history_entries.last()
|
||||
self.assertEqual(delete_history.action_type, "deleted")
|
||||
self.assertIsNone(delete_history.old_values)
|
||||
self.assertEqual(delete_history.role, "student")
|
||||
|
||||
|
||||
class CourseAccessRoleAdminActionsTest(TestCase):
|
||||
"""
|
||||
Tests for the admin actions (revert, delete) on CourseAccessRoleHistory.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = UserFactory(
|
||||
username="test_user_admin", email="test_admin@example.com"
|
||||
)
|
||||
self.admin_user = UserFactory(
|
||||
username="admin_action_user",
|
||||
email="admin_action@example.com",
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
self.course_key = CourseKey.from_string(
|
||||
"course-v1:AdminOrg+AdminCourse+2024_Spring"
|
||||
)
|
||||
self.org = "AdminOrg"
|
||||
revert_permission = Permission.objects.get(
|
||||
codename="can_revert_course_access_role", content_type__app_label="student"
|
||||
)
|
||||
delete_history_permission = Permission.objects.get(
|
||||
codename="can_delete_course_access_role_history",
|
||||
content_type__app_label="student",
|
||||
)
|
||||
self.admin_user.user_permissions.add(
|
||||
revert_permission, delete_history_permission
|
||||
)
|
||||
self.admin_user = User.objects.get(pk=self.admin_user.pk)
|
||||
self.messages = []
|
||||
|
||||
def _get_admin_action_response(self, action, queryset):
|
||||
"""Helper to call admin actions and capture messages."""
|
||||
from django.contrib.admin import AdminSite
|
||||
|
||||
model_admin = CourseAccessRoleHistoryAdmin(CourseAccessRoleHistory, AdminSite())
|
||||
request = self.client.get("/")
|
||||
request.user = self.admin_user
|
||||
|
||||
def mock_message_user(request, message, level=None):
|
||||
self.messages.append(message)
|
||||
|
||||
model_admin.message_user = mock_message_user
|
||||
|
||||
response = action(model_admin, request, queryset)
|
||||
return response
|
||||
|
||||
def test_revert_created_action(self):
|
||||
"""
|
||||
Test reverting a 'created' history entry should delete the CourseAccessRole.
|
||||
"""
|
||||
CourseAccessRole.objects.create(
|
||||
user=self.user, org=self.org, course_id=self.course_key, role="beta_tester"
|
||||
)
|
||||
self.assertEqual(CourseAccessRole.objects.count(), 1)
|
||||
created_history = CourseAccessRoleHistory.objects.filter(
|
||||
action_type="created"
|
||||
).first()
|
||||
self.assertIsNotNone(created_history)
|
||||
|
||||
self._get_admin_action_response(
|
||||
CourseAccessRoleHistoryAdmin.revert_selected_history,
|
||||
CourseAccessRoleHistory.objects.filter(pk=created_history.pk),
|
||||
)
|
||||
|
||||
self.assertEqual(CourseAccessRole.objects.count(), 0)
|
||||
self.assertIn(
|
||||
f"Successfully reverted creation of role for {self.user.username} in {self.course_key}",
|
||||
self.messages[0],
|
||||
)
|
||||
|
||||
def test_revert_updated_action(self):
|
||||
"""
|
||||
Test reverting an 'updated' history entry should restore the CourseAccessRole to its old_values.
|
||||
"""
|
||||
role_instance = CourseAccessRole.objects.create(
|
||||
user=self.user, org=self.org, course_id=self.course_key, role="old_role"
|
||||
)
|
||||
|
||||
role_instance.role = "new_role"
|
||||
role_instance.save()
|
||||
|
||||
self.assertEqual(CourseAccessRole.objects.get().role, "new_role")
|
||||
updated_history = CourseAccessRoleHistory.objects.filter(
|
||||
action_type="updated"
|
||||
).first()
|
||||
self.assertIsNotNone(updated_history)
|
||||
self.assertEqual(updated_history.old_values["role"], "old_role")
|
||||
|
||||
self._get_admin_action_response(
|
||||
CourseAccessRoleHistoryAdmin.revert_selected_history,
|
||||
CourseAccessRoleHistory.objects.filter(pk=updated_history.pk),
|
||||
)
|
||||
|
||||
self.assertEqual(CourseAccessRole.objects.get().role, "old_role")
|
||||
self.assertIn(
|
||||
f"Successfully reverted update of role for {self.user.username} to old_role in {self.course_key}",
|
||||
self.messages[0],
|
||||
)
|
||||
|
||||
def test_revert_deleted_action(self):
|
||||
"""
|
||||
Test reverting a 'deleted' history entry should recreate the CourseAccessRole.
|
||||
"""
|
||||
role_instance = CourseAccessRole.objects.create(
|
||||
user=self.user,
|
||||
org=self.org,
|
||||
course_id=self.course_key,
|
||||
role="to_be_deleted",
|
||||
)
|
||||
self.assertEqual(CourseAccessRole.objects.count(), 1)
|
||||
initial_history_count = CourseAccessRoleHistory.objects.count()
|
||||
|
||||
role_instance.delete()
|
||||
self.assertEqual(CourseAccessRole.objects.count(), 0)
|
||||
deleted_history = CourseAccessRoleHistory.objects.filter(
|
||||
action_type="deleted"
|
||||
).first()
|
||||
self.assertIsNotNone(deleted_history)
|
||||
|
||||
self._get_admin_action_response(
|
||||
CourseAccessRoleHistoryAdmin.revert_selected_history,
|
||||
CourseAccessRoleHistory.objects.filter(pk=deleted_history.pk),
|
||||
)
|
||||
|
||||
self.assertEqual(CourseAccessRole.objects.count(), 1)
|
||||
reverted_role = CourseAccessRole.objects.first()
|
||||
self.assertEqual(reverted_role.user, self.user)
|
||||
self.assertEqual(reverted_role.org, self.org)
|
||||
self.assertEqual(reverted_role.course_id, self.course_key)
|
||||
self.assertEqual(reverted_role.role, "to_be_deleted")
|
||||
self.assertIn(
|
||||
f"Successfully reverted deletion of role for {self.user.username} in {self.course_key}",
|
||||
self.messages[0],
|
||||
)
|
||||
|
||||
def test_delete_history_action(self):
|
||||
"""
|
||||
Test the admin action to delete selected history entries.
|
||||
"""
|
||||
CourseAccessRole.objects.create(
|
||||
user=self.user, org=self.org, course_id=self.course_key, role="some_role"
|
||||
)
|
||||
self.assertEqual(CourseAccessRoleHistory.objects.count(), 1)
|
||||
history_entry = CourseAccessRoleHistory.objects.first()
|
||||
|
||||
self._get_admin_action_response(
|
||||
CourseAccessRoleHistoryAdmin.delete_selected_history_entries,
|
||||
CourseAccessRoleHistory.objects.filter(pk=history_entry.pk),
|
||||
)
|
||||
|
||||
self.assertEqual(CourseAccessRoleHistory.objects.count(), 0)
|
||||
self.assertIn("Successfully deleted 1 selected history entry.", self.messages[0])
|
||||
|
||||
Reference in New Issue
Block a user