LMS-11137 Course Action State Django models.
This commit is contained in:
@@ -547,6 +547,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# Monitoring signals
|
||||
'monitoring',
|
||||
|
||||
# Course action state
|
||||
'course_action_state'
|
||||
)
|
||||
|
||||
|
||||
|
||||
0
common/djangoapps/course_action_state/__init__.py
Normal file
0
common/djangoapps/course_action_state/__init__.py
Normal file
150
common/djangoapps/course_action_state/managers.py
Normal file
150
common/djangoapps/course_action_state/managers.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Model Managers for Course Actions
|
||||
"""
|
||||
from django.db import models, transaction
|
||||
|
||||
|
||||
class CourseActionStateManager(models.Manager):
|
||||
"""
|
||||
An abstract Model Manager class for Course Action State models.
|
||||
This abstract class expects child classes to define the ACTION (string) field.
|
||||
"""
|
||||
class Meta:
|
||||
"""Abstract manager class, with subclasses defining the ACTION (string) field."""
|
||||
abstract = True
|
||||
|
||||
def find_all(self, exclude_args=None, **kwargs):
|
||||
"""
|
||||
Finds and returns all entries for this action and the given field names-and-values in kwargs.
|
||||
The exclude_args dict allows excluding entries with the field names-and-values in exclude_args.
|
||||
"""
|
||||
return self.filter(action=self.ACTION, **kwargs).exclude(**(exclude_args or {})) # pylint: disable=no-member
|
||||
|
||||
def find_first(self, exclude_args=None, **kwargs):
|
||||
"""
|
||||
Returns the first entry for the this action and the given fields in kwargs, if found.
|
||||
The exclude_args dict allows excluding entries with the field names-and-values in exclude_args.
|
||||
|
||||
Raises ItemNotFoundError if more than 1 entry is found.
|
||||
|
||||
There may or may not be greater than one entry, depending on the usage pattern for this Action.
|
||||
"""
|
||||
objects = self.find_all(exclude_args=exclude_args, **kwargs)
|
||||
if len(objects) == 0:
|
||||
raise CourseActionStateItemNotFoundError(
|
||||
"No entry found for action {action} with filter {filter}, excluding {exclude}".format(
|
||||
action=self.ACTION, # pylint: disable=no-member
|
||||
filter=kwargs,
|
||||
exclude=exclude_args,
|
||||
))
|
||||
else:
|
||||
return objects[0]
|
||||
|
||||
def delete(self, entry_id):
|
||||
"""
|
||||
Deletes the entry with given id.
|
||||
"""
|
||||
self.filter(id=entry_id).delete()
|
||||
|
||||
|
||||
class CourseActionUIStateManager(CourseActionStateManager):
|
||||
"""
|
||||
A Model Manager subclass of the CourseActionStateManager class that is aware of UI-related fields related
|
||||
to state management, including "should_display" and "message".
|
||||
"""
|
||||
|
||||
# add transaction protection to revert changes by get_or_create if an exception is raised before the final save.
|
||||
@transaction.commit_on_success
|
||||
def update_state(
|
||||
self, course_key, new_state, should_display=True, message="", user=None, allow_not_found=False, **kwargs
|
||||
):
|
||||
"""
|
||||
Updates the state of the given course for this Action with the given data.
|
||||
If allow_not_found is True, automatically creates an entry if it doesn't exist.
|
||||
Raises CourseActionStateException if allow_not_found is False and an entry for the given course
|
||||
for this Action doesn't exist.
|
||||
"""
|
||||
state_object, created = self.get_or_create(course_key=course_key, action=self.ACTION) # pylint: disable=no-member
|
||||
|
||||
if created:
|
||||
if allow_not_found:
|
||||
state_object.created_user = user
|
||||
else:
|
||||
raise CourseActionStateItemNotFoundError(
|
||||
"Cannot update non-existent entry for course_key {course_key} and action {action}".format(
|
||||
action=self.ACTION, # pylint: disable=no-member
|
||||
course_key=course_key,
|
||||
))
|
||||
|
||||
# some state changes may not be user-initiated so override the user field only when provided
|
||||
if user:
|
||||
state_object.updated_user = user
|
||||
|
||||
state_object.state = new_state
|
||||
state_object.should_display = should_display
|
||||
state_object.message = message
|
||||
|
||||
# update any additional fields in kwargs
|
||||
if kwargs:
|
||||
for key, value in kwargs.iteritems():
|
||||
setattr(state_object, key, value)
|
||||
|
||||
state_object.save()
|
||||
return state_object
|
||||
|
||||
def update_should_display(self, entry_id, user, should_display):
|
||||
"""
|
||||
Updates the should_display field with the given value for the entry for the given id.
|
||||
"""
|
||||
self.update(id=entry_id, updated_user=user, should_display=should_display)
|
||||
|
||||
|
||||
class CourseRerunUIStateManager(CourseActionUIStateManager):
|
||||
"""
|
||||
A concrete model Manager for the Reruns Action.
|
||||
"""
|
||||
ACTION = "rerun"
|
||||
|
||||
class State(object):
|
||||
"""
|
||||
An Enum class for maintaining the list of possible states for Reruns.
|
||||
"""
|
||||
IN_PROGRESS = "in_progress"
|
||||
FAILED = "failed"
|
||||
SUCCEEDED = "succeeded"
|
||||
|
||||
def initiated(self, source_course_key, destination_course_key, user):
|
||||
"""
|
||||
To be called when a new rerun is initiated for the given course by the given user.
|
||||
"""
|
||||
self.update_state(
|
||||
course_key=destination_course_key,
|
||||
new_state=self.State.IN_PROGRESS,
|
||||
user=user,
|
||||
allow_not_found=True,
|
||||
source_course_key=source_course_key,
|
||||
)
|
||||
|
||||
def succeeded(self, course_key):
|
||||
"""
|
||||
To be called when an existing rerun for the given course has successfully completed.
|
||||
"""
|
||||
self.update_state(
|
||||
course_key=course_key,
|
||||
new_state=self.State.SUCCEEDED,
|
||||
)
|
||||
|
||||
def failed(self, course_key, exception):
|
||||
"""
|
||||
To be called when an existing rerun for the given course has failed with the given exception.
|
||||
"""
|
||||
self.update_state(
|
||||
course_key=course_key,
|
||||
new_state=self.State.FAILED,
|
||||
message=exception.message,
|
||||
)
|
||||
|
||||
|
||||
class CourseActionStateItemNotFoundError(Exception):
|
||||
"""An exception class for errors specific to Course Action states."""
|
||||
pass
|
||||
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'CourseRerunState'
|
||||
db.create_table('course_action_state_coursererunstate', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('created_time', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('updated_time', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
|
||||
('created_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='created_by_user+', null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
|
||||
('updated_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='updated_by_user+', null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
|
||||
('course_key', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||
('action', self.gf('django.db.models.fields.CharField')(max_length=100, db_index=True)),
|
||||
('state', self.gf('django.db.models.fields.CharField')(max_length=50)),
|
||||
('should_display', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
('message', self.gf('django.db.models.fields.CharField')(max_length=1000)),
|
||||
('source_course_key', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||
))
|
||||
db.send_create_signal('course_action_state', ['CourseRerunState'])
|
||||
|
||||
# Adding unique constraint on 'CourseRerunState', fields ['course_key', 'action']
|
||||
db.create_unique('course_action_state_coursererunstate', ['course_key', 'action'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'CourseRerunState', fields ['course_key', 'action']
|
||||
db.delete_unique('course_action_state_coursererunstate', ['course_key', 'action'])
|
||||
|
||||
# Deleting model 'CourseRerunState'
|
||||
db.delete_table('course_action_state_coursererunstate')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'course_action_state.coursererunstate': {
|
||||
'Meta': {'unique_together': "(('course_key', 'action'),)", 'object_name': 'CourseRerunState'},
|
||||
'action': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}),
|
||||
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'created_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['auth.User']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
|
||||
'should_display': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'source_course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'state': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
|
||||
'updated_time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||
'updated_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'updated_by_user+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['course_action_state']
|
||||
114
common/djangoapps/course_action_state/models.py
Normal file
114
common/djangoapps/course_action_state/models.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Models for course action state
|
||||
|
||||
If you make changes to this model, be sure to create an appropriate migration
|
||||
file and check it in at the same time as your model changes. To do that,
|
||||
|
||||
1. Go to the edx-platform dir
|
||||
2. ./manage.py cms schemamigration course_action_state --auto description_of_your_change
|
||||
3. It adds the migration file to edx-platform/common/djangoapps/course_action_state/migrations/
|
||||
|
||||
"""
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from xmodule_django.models import CourseKeyField
|
||||
from course_action_state.managers import CourseActionStateManager, CourseRerunUIStateManager
|
||||
|
||||
|
||||
class CourseActionState(models.Model):
|
||||
"""
|
||||
A django model for maintaining state data for course actions that take a long time.
|
||||
For example: course copying (reruns), import, export, and validation.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
For performance reasons, we disable "concrete inheritance", by making the Model base class abstract.
|
||||
With the "abstract base class" inheritance model, tables are only created for derived models, not for
|
||||
the parent classes. This way, we don't have extra overhead of extra tables and joins that would
|
||||
otherwise happen with the multi-table inheritance model.
|
||||
"""
|
||||
abstract = True
|
||||
|
||||
# FIELDS
|
||||
|
||||
# Created is the time this action was initiated
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# Updated is the last time this entry was modified
|
||||
updated_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
# User who initiated the course action
|
||||
created_user = models.ForeignKey(
|
||||
User,
|
||||
# allow NULL values in case the action is not initiated by a user (e.g., a background thread)
|
||||
null=True,
|
||||
# set on_delete to SET_NULL to prevent this model from being deleted in the event the user is deleted
|
||||
on_delete=models.SET_NULL,
|
||||
# add a '+' at the end to prevent a backward relation from the User model
|
||||
related_name='created_by_user+'
|
||||
)
|
||||
|
||||
# User who last updated the course action
|
||||
updated_user = models.ForeignKey(
|
||||
User,
|
||||
# allow NULL values in case the action is not updated by a user (e.g., a background thread)
|
||||
null=True,
|
||||
# set on_delete to SET_NULL to prevent this model from being deleted in the event the user is deleted
|
||||
on_delete=models.SET_NULL,
|
||||
# add a '+' at the end to prevent a backward relation from the User model
|
||||
related_name='updated_by_user+'
|
||||
)
|
||||
|
||||
# Course that is being acted upon
|
||||
course_key = CourseKeyField(max_length=255, db_index=True)
|
||||
|
||||
# Action that is being taken on the course
|
||||
action = models.CharField(max_length=100, db_index=True)
|
||||
|
||||
# Current state of the action.
|
||||
state = models.CharField(max_length=50)
|
||||
|
||||
# MANAGERS
|
||||
objects = CourseActionStateManager()
|
||||
|
||||
|
||||
class CourseActionUIState(CourseActionState):
|
||||
"""
|
||||
An abstract django model that is a sub-class of CourseActionState with additional fields related to UI.
|
||||
"""
|
||||
class Meta:
|
||||
"""
|
||||
See comment in CourseActionState on disabling "concrete inheritance".
|
||||
"""
|
||||
abstract = True
|
||||
|
||||
# FIELDS
|
||||
|
||||
# Whether or not the status should be displayed to users
|
||||
should_display = models.BooleanField()
|
||||
|
||||
# Message related to the status
|
||||
message = models.CharField(max_length=1000)
|
||||
|
||||
|
||||
# Rerun courses also need these fields. All rerun course actions will have a row here as well.
|
||||
class CourseRerunState(CourseActionUIState):
|
||||
"""
|
||||
A concrete django model for maintaining state specifically for the Action Course Reruns.
|
||||
"""
|
||||
class Meta:
|
||||
"""
|
||||
Set the (destination) course_key field to be unique for the rerun action
|
||||
Although multiple reruns can be in progress simultaneously for a particular source course_key,
|
||||
only a single rerun action can be in progress for the destination course_key.
|
||||
"""
|
||||
unique_together = ("course_key", "action")
|
||||
|
||||
# FIELDS
|
||||
# Original course that is being rerun
|
||||
source_course_key = CourseKeyField(max_length=255, db_index=True)
|
||||
|
||||
# MANAGERS
|
||||
# Override the abstract class' manager with a Rerun-specific manager that inherits from the base class' manager.
|
||||
objects = CourseRerunUIStateManager()
|
||||
159
common/djangoapps/course_action_state/tests/test_managers.py
Normal file
159
common/djangoapps/course_action_state/tests/test_managers.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# pylint: disable=invalid-name, attribute-defined-outside-init
|
||||
"""
|
||||
Tests for basic common operations related to Course Action State managers
|
||||
"""
|
||||
from ddt import ddt, data
|
||||
from django.test import TestCase
|
||||
from collections import namedtuple
|
||||
from opaque_keys.edx.locations import CourseLocator
|
||||
from course_action_state.models import CourseRerunState
|
||||
from course_action_state.managers import CourseActionStateItemNotFoundError
|
||||
|
||||
|
||||
# Sequence of Action models to be tested with ddt.
|
||||
COURSE_ACTION_STATES = (CourseRerunState, )
|
||||
|
||||
|
||||
class TestCourseActionStateManagerBase(TestCase):
|
||||
"""
|
||||
Base class for testing Course Action State Managers.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.course_key = CourseLocator("test_org", "test_course_num", "test_run")
|
||||
|
||||
|
||||
@ddt
|
||||
class TestCourseActionStateManager(TestCourseActionStateManagerBase):
|
||||
"""
|
||||
Test class for testing the CourseActionStateManager.
|
||||
"""
|
||||
@data(*COURSE_ACTION_STATES)
|
||||
def test_update_state_allow_not_found_is_false(self, action_class):
|
||||
with self.assertRaises(CourseActionStateItemNotFoundError):
|
||||
action_class.objects.update_state(self.course_key, "fake_state", allow_not_found=False)
|
||||
|
||||
@data(*COURSE_ACTION_STATES)
|
||||
def test_update_state_allow_not_found(self, action_class):
|
||||
action_class.objects.update_state(self.course_key, "initial_state", allow_not_found=True)
|
||||
self.assertIsNotNone(
|
||||
action_class.objects.find_first(course_key=self.course_key)
|
||||
)
|
||||
|
||||
@data(*COURSE_ACTION_STATES)
|
||||
def test_delete(self, action_class):
|
||||
obj = action_class.objects.update_state(self.course_key, "initial_state", allow_not_found=True)
|
||||
action_class.objects.delete(obj.id)
|
||||
with self.assertRaises(CourseActionStateItemNotFoundError):
|
||||
action_class.objects.find_first(course_key=self.course_key)
|
||||
|
||||
|
||||
@ddt
|
||||
class TestCourseActionUIStateManager(TestCourseActionStateManagerBase):
|
||||
"""
|
||||
Test class for testing the CourseActionUIStateManager.
|
||||
"""
|
||||
def init_course_action_states(self, action_class):
|
||||
"""
|
||||
Creates course action state entries with different states for the given action model class.
|
||||
Creates both displayable (should_display=True) and non-displayable (should_display=False) entries.
|
||||
"""
|
||||
def create_course_states(starting_course_num, ending_course_num, state, should_display=True):
|
||||
"""
|
||||
Creates a list of course state tuples by creating unique course locators with course-numbers
|
||||
from starting_course_num to ending_course_num.
|
||||
"""
|
||||
CourseState = namedtuple('CourseState', 'course_key, state, should_display')
|
||||
return [
|
||||
CourseState(CourseLocator("org", "course", "run" + str(num)), state, should_display)
|
||||
for num in range(starting_course_num, ending_course_num)
|
||||
]
|
||||
|
||||
NUM_COURSES_WITH_STATE1 = 3
|
||||
NUM_COURSES_WITH_STATE2 = 3
|
||||
NUM_COURSES_WITH_STATE3 = 3
|
||||
NUM_COURSES_NON_DISPLAYABLE = 3
|
||||
|
||||
# courses with state1 and should_display=True
|
||||
self.courses_with_state1 = create_course_states(
|
||||
0,
|
||||
NUM_COURSES_WITH_STATE1,
|
||||
'state1'
|
||||
)
|
||||
# courses with state2 and should_display=True
|
||||
self.courses_with_state2 = create_course_states(
|
||||
NUM_COURSES_WITH_STATE1,
|
||||
NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2,
|
||||
'state2'
|
||||
)
|
||||
# courses with state3 and should_display=True
|
||||
self.courses_with_state3 = create_course_states(
|
||||
NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2,
|
||||
NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2 + NUM_COURSES_WITH_STATE3,
|
||||
'state3'
|
||||
)
|
||||
# all courses with should_display=True
|
||||
self.course_actions_displayable_states = (
|
||||
self.courses_with_state1 + self.courses_with_state2 + self.courses_with_state3
|
||||
)
|
||||
# courses with state3 and should_display=False
|
||||
self.courses_with_state3_non_displayable = create_course_states(
|
||||
NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2 + NUM_COURSES_WITH_STATE3,
|
||||
NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2 + NUM_COURSES_WITH_STATE3 + NUM_COURSES_NON_DISPLAYABLE,
|
||||
'state3',
|
||||
should_display=False,
|
||||
)
|
||||
|
||||
# create course action states for all courses
|
||||
for CourseState in (self.course_actions_displayable_states + self.courses_with_state3_non_displayable):
|
||||
action_class.objects.update_state(
|
||||
CourseState.course_key,
|
||||
CourseState.state,
|
||||
should_display=CourseState.should_display,
|
||||
allow_not_found=True
|
||||
)
|
||||
|
||||
def assertCourseActionStatesEqual(self, expected, found):
|
||||
"""Asserts that the set of course keys in the expected state equal those that are found"""
|
||||
self.assertSetEqual(
|
||||
set(course_action_state.course_key for course_action_state in expected),
|
||||
set(course_action_state.course_key for course_action_state in found))
|
||||
|
||||
@data(*COURSE_ACTION_STATES)
|
||||
def test_find_all_for_display(self, action_class):
|
||||
self.init_course_action_states(action_class)
|
||||
self.assertCourseActionStatesEqual(
|
||||
self.course_actions_displayable_states,
|
||||
action_class.objects.find_all(should_display=True),
|
||||
)
|
||||
|
||||
@data(*COURSE_ACTION_STATES)
|
||||
def test_find_all_for_display_filter_exclude(self, action_class):
|
||||
self.init_course_action_states(action_class)
|
||||
for course_action_state, filter_state, exclude_state in (
|
||||
(self.courses_with_state1, 'state1', None), # filter for state1
|
||||
(self.courses_with_state2, 'state2', None), # filter for state2
|
||||
(self.courses_with_state2 + self.courses_with_state3, None, 'state1'), # exclude state1
|
||||
(self.courses_with_state1 + self.courses_with_state3, None, 'state2'), # exclude state2
|
||||
(self.courses_with_state1, 'state1', 'state2'), # filter for state1, exclude state2
|
||||
([], 'state1', 'state1'), # filter for state1, exclude state1
|
||||
):
|
||||
self.assertCourseActionStatesEqual(
|
||||
course_action_state,
|
||||
action_class.objects.find_all(
|
||||
exclude_args=({'state': exclude_state} if exclude_state else None),
|
||||
should_display=True,
|
||||
**({'state': filter_state} if filter_state else {})
|
||||
)
|
||||
)
|
||||
|
||||
def test_kwargs_in_update_state(self):
|
||||
destination_course_key = CourseLocator("org", "course", "run")
|
||||
source_course_key = CourseLocator("source_org", "source_course", "source_run")
|
||||
CourseRerunState.objects.update_state(
|
||||
course_key=destination_course_key,
|
||||
new_state='state1',
|
||||
allow_not_found=True,
|
||||
source_course_key=source_course_key,
|
||||
)
|
||||
found_action_state = CourseRerunState.objects.find_first(course_key=destination_course_key)
|
||||
self.assertEquals(source_course_key, found_action_state.source_course_key)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Tests specific to the CourseRerunState Model and Manager.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from opaque_keys.edx.locations import CourseLocator
|
||||
from course_action_state.models import CourseRerunState
|
||||
from course_action_state.managers import CourseRerunUIStateManager
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class TestCourseRerunStateManager(TestCase):
|
||||
"""
|
||||
Test class for testing the CourseRerunUIStateManager.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.source_course_key = CourseLocator("source_org", "source_course_num", "source_run")
|
||||
self.course_key = CourseLocator("test_org", "test_course_num", "test_run")
|
||||
self.created_user = UserFactory()
|
||||
self.expected_rerun_state = {
|
||||
'created_user': self.created_user,
|
||||
'updated_user': self.created_user,
|
||||
'course_key': self.course_key,
|
||||
'action': CourseRerunUIStateManager.ACTION,
|
||||
'should_display': True,
|
||||
'message': "",
|
||||
}
|
||||
|
||||
def verify_rerun_state(self):
|
||||
"""
|
||||
Gets the rerun state object for self.course_key and verifies that the values
|
||||
of its fields equal self.expected_rerun_state.
|
||||
"""
|
||||
found_rerun = CourseRerunState.objects.find_first(course_key=self.course_key)
|
||||
found_rerun_state = {key: getattr(found_rerun, key) for key in self.expected_rerun_state}
|
||||
self.assertDictEqual(found_rerun_state, self.expected_rerun_state)
|
||||
return found_rerun
|
||||
|
||||
def dismiss_ui_and_verify(self, rerun):
|
||||
"""
|
||||
Updates the should_display field of the rerun state object for self.course_key
|
||||
and verifies its new state.
|
||||
"""
|
||||
user_who_dismisses_ui = UserFactory()
|
||||
CourseRerunState.objects.update_should_display(
|
||||
entry_id=rerun.id,
|
||||
user=user_who_dismisses_ui,
|
||||
should_display=False,
|
||||
)
|
||||
self.expected_rerun_state.update({
|
||||
'updated_user': user_who_dismisses_ui,
|
||||
'should_display': False,
|
||||
})
|
||||
self.verify_rerun_state()
|
||||
|
||||
def test_rerun_initiated(self):
|
||||
CourseRerunState.objects.initiated(
|
||||
source_course_key=self.source_course_key, destination_course_key=self.course_key, user=self.created_user
|
||||
)
|
||||
self.expected_rerun_state.update(
|
||||
{'state': CourseRerunUIStateManager.State.IN_PROGRESS}
|
||||
)
|
||||
self.verify_rerun_state()
|
||||
|
||||
def test_rerun_succeeded(self):
|
||||
# initiate
|
||||
CourseRerunState.objects.initiated(
|
||||
source_course_key=self.source_course_key, destination_course_key=self.course_key, user=self.created_user
|
||||
)
|
||||
|
||||
# set state to succeed
|
||||
CourseRerunState.objects.succeeded(course_key=self.course_key)
|
||||
self.expected_rerun_state.update({
|
||||
'state': CourseRerunUIStateManager.State.SUCCEEDED,
|
||||
})
|
||||
rerun = self.verify_rerun_state()
|
||||
|
||||
# dismiss ui and verify
|
||||
self.dismiss_ui_and_verify(rerun)
|
||||
|
||||
def test_rerun_failed(self):
|
||||
# initiate
|
||||
CourseRerunState.objects.initiated(
|
||||
source_course_key=self.source_course_key, destination_course_key=self.course_key, user=self.created_user
|
||||
)
|
||||
|
||||
# set state to fail
|
||||
exception = Exception("failure in rerunning")
|
||||
CourseRerunState.objects.failed(course_key=self.course_key, exception=exception)
|
||||
self.expected_rerun_state.update({
|
||||
'state': CourseRerunUIStateManager.State.FAILED,
|
||||
'message': exception.message,
|
||||
})
|
||||
rerun = self.verify_rerun_state()
|
||||
|
||||
# dismiss ui and verify
|
||||
self.dismiss_ui_and_verify(rerun)
|
||||
@@ -1317,6 +1317,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# Monitoring functionality
|
||||
'monitoring',
|
||||
|
||||
# Course action state
|
||||
'course_action_state'
|
||||
)
|
||||
|
||||
######################### MARKETING SITE ###############################
|
||||
|
||||
Reference in New Issue
Block a user