diff --git a/lms/djangoapps/program_enrollments/admin.py b/lms/djangoapps/program_enrollments/admin.py
index f39b82c757..18d0c54b35 100644
--- a/lms/djangoapps/program_enrollments/admin.py
+++ b/lms/djangoapps/program_enrollments/admin.py
@@ -5,7 +5,11 @@ from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
-from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
+from lms.djangoapps.program_enrollments.models import (
+ CourseAccessRoleAssignment,
+ ProgramCourseEnrollment,
+ ProgramEnrollment
+)
class ProgramEnrollmentAdmin(admin.ModelAdmin):
@@ -67,7 +71,6 @@ def _pce_ce(pce):
enrollment=enrollment, active_string=active_string
)
-
_pce_pe_id.short_description = "Program Enrollment"
_pce_pe_user.short_description = "Pgm Enrollment: User"
_pce_pe_external_user_key.short_description = "Pgm Enrollment: Ext User Key"
@@ -102,5 +105,45 @@ class ProgramCourseEnrollmentAdmin(admin.ModelAdmin):
raw_id_fields = ('program_enrollment', 'course_enrollment')
+def _pending_role_assignment_enrollment_id(pending_role_assignment):
+ """
+ Generate a link to edit enrollment, with ID in link text.
+ """
+ pce = pending_role_assignment.enrollment
+ if not pce:
+ return None
+ link_url = reverse(
+ "admin:program_enrollments_programcourseenrollment_change",
+ args=[pce.id],
+ )
+ link_text = "id={pce.id:05}".format(pce=pce)
+ return format_html("{}", link_url, link_text)
+
+
+def _pending_role_assignment_external_user_key(pending_role_assignment):
+ """
+ Generate the external user key for a pending role assignment
+ """
+ pce = pending_role_assignment.enrollment
+ return _pce_pe_external_user_key(pce)
+
+_pending_role_assignment_enrollment_id.short_description = "Program Course Enrollment"
+_pending_role_assignment_external_user_key.short_description = "Pgm Enrollment: Ext User Key"
+
+
+class CourseAccessRoleAssignmentAdmin(admin.ModelAdmin):
+ """
+ Admin tool for the CourseAccessRoleAssignment model
+ """
+ list_display = (
+ 'id',
+ 'role',
+ _pending_role_assignment_enrollment_id,
+ _pending_role_assignment_external_user_key
+ )
+ list_filter = ('role',)
+ raw_id_fields = ('enrollment',)
+
admin.site.register(ProgramEnrollment, ProgramEnrollmentAdmin)
admin.site.register(ProgramCourseEnrollment, ProgramCourseEnrollmentAdmin)
+admin.site.register(CourseAccessRoleAssignment, CourseAccessRoleAssignmentAdmin)
diff --git a/lms/djangoapps/program_enrollments/constants.py b/lms/djangoapps/program_enrollments/constants.py
index cd544f2f18..5857f576db 100644
--- a/lms/djangoapps/program_enrollments/constants.py
+++ b/lms/djangoapps/program_enrollments/constants.py
@@ -2,6 +2,7 @@
Constants used throughout the program_enrollments app and exposed to other
in-process apps through api.py.
"""
+from student.roles import CourseStaffRole
class ProgramEnrollmentStatuses(object):
@@ -113,3 +114,17 @@ class ProgramCourseOperationStatuses(
__OK__ = ProgramCourseEnrollmentStatuses.__ALL__
__ERRORS__ = (NOT_FOUND,) + _EnrollmentErrorStatuses.__ALL__
__ALL__ = __OK__ + __ERRORS__
+
+
+class ProgramCourseEnrollmentRoles(object):
+ """
+ Valid roles that can be assigned as part of a ProgramCourseEnrollment
+ """
+ COURSE_STAFF = CourseStaffRole.ROLE
+ __ALL__ = (COURSE_STAFF,)
+
+ # Note: Any changes to this value will trigger a migration on
+ # CourseAccessRoleAssignment!
+ __MODEL_CHOICES__ = (
+ (role, role) for role in __ALL__
+ )
diff --git a/lms/djangoapps/program_enrollments/migrations/0010_add_courseaccessroleassignment.py b/lms/djangoapps/program_enrollments/migrations/0010_add_courseaccessroleassignment.py
new file mode 100644
index 0000000000..2f0affb92b
--- /dev/null
+++ b/lms/djangoapps/program_enrollments/migrations/0010_add_courseaccessroleassignment.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2020-03-25 19:28
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import model_utils.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('program_enrollments', '0009_update_course_enrollment_field_to_foreign_key'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CourseAccessRoleAssignment',
+ 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')),
+ ('role', models.CharField(choices=[('staff', 'staff')], max_length=64)),
+ ('enrollment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='program_enrollments.ProgramCourseEnrollment')),
+ ],
+ ),
+ migrations.AlterUniqueTogether(
+ name='CourseAccessRoleAssignment',
+ unique_together=set([('role', 'enrollment')]),
+ ),
+ ]
diff --git a/lms/djangoapps/program_enrollments/models.py b/lms/djangoapps/program_enrollments/models.py
index d552656abc..7aca257591 100644
--- a/lms/djangoapps/program_enrollments/models.py
+++ b/lms/djangoapps/program_enrollments/models.py
@@ -14,7 +14,7 @@ from simple_history.models import HistoricalRecords
from student.models import CourseEnrollment
-from .constants import ProgramCourseEnrollmentStatuses, ProgramEnrollmentStatuses
+from .constants import ProgramCourseEnrollmentRoles, ProgramCourseEnrollmentStatuses, ProgramEnrollmentStatuses
class ProgramEnrollment(TimeStampedModel):
@@ -149,3 +149,28 @@ class ProgramCourseEnrollment(TimeStampedModel):
" status={self.status!r}"
">"
).format(self=self)
+
+
+class CourseAccessRoleAssignment(TimeStampedModel):
+ """
+ This model represents a role that should be assigned to the eventual user of a pending enrollment.
+
+ .. no_pii:
+ """
+ class Meta(object):
+ unique_together = ('role', 'enrollment')
+
+ role = models.CharField(max_length=64, choices=ProgramCourseEnrollmentRoles.__MODEL_CHOICES__)
+ enrollment = models.ForeignKey(ProgramCourseEnrollment, on_delete=models.CASCADE)
+
+ def __str__(self):
+ return '[CourseAccessRoleAssignment id={}]'.format(self.id)
+
+ def __repr__(self):
+ return (
+ ""
+ ).format(self=self)
diff --git a/lms/djangoapps/program_enrollments/tests/factories.py b/lms/djangoapps/program_enrollments/tests/factories.py
index cb2e5e4844..1fef90518f 100644
--- a/lms/djangoapps/program_enrollments/tests/factories.py
+++ b/lms/djangoapps/program_enrollments/tests/factories.py
@@ -45,3 +45,12 @@ class ProgramCourseEnrollmentFactory(DjangoModelFactory):
)
)
status = 'active'
+
+
+class CourseAccessRoleAssignmentFactory(DjangoModelFactory):
+ """ A factory for the CourseAccessRoleAssignment model. """
+ class Meta(object):
+ model = models.CourseAccessRoleAssignment
+
+ enrollment = factory.SubFactory(ProgramCourseEnrollmentFactory)
+ role = 'staff'
diff --git a/lms/djangoapps/program_enrollments/tests/test_models.py b/lms/djangoapps/program_enrollments/tests/test_models.py
index 7610b3c7e5..01f363623b 100644
--- a/lms/djangoapps/program_enrollments/tests/test_models.py
+++ b/lms/djangoapps/program_enrollments/tests/test_models.py
@@ -15,8 +15,13 @@ from course_modes.models import CourseMode
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from student.tests.factories import CourseEnrollmentFactory, UserFactory
+from ..constants import ProgramCourseEnrollmentRoles
from ..models import ProgramEnrollment
-from .factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
+from .factories import (
+ CourseAccessRoleAssignmentFactory,
+ ProgramCourseEnrollmentFactory,
+ ProgramEnrollmentFactory
+)
class ProgramEnrollmentModelTests(TestCase):
@@ -200,3 +205,41 @@ class ProgramCourseEnrollmentModelTests(TestCase):
course_enrollment=None,
status="active"
)
+
+
+class CourseAccessRoleAssignmentTests(TestCase):
+ """
+ Tests for the CourseAccessRoleAssignment model.
+ """
+ def setUp(self):
+ super(CourseAccessRoleAssignmentTests, self).setUp()
+ self.program_course_enrollment = ProgramCourseEnrollmentFactory()
+ self.pending_role_assignment = CourseAccessRoleAssignmentFactory(
+ enrollment=self.program_course_enrollment,
+ role=ProgramCourseEnrollmentRoles.COURSE_STAFF,
+ )
+
+ def test_str_and_repr(self):
+ """
+ Make sure str() and repr() work correctly on instances of this model.
+ """
+ assert str(self.pending_role_assignment) == "[CourseAccessRoleAssignment id=1]"
+
+ # The record contains timestamp information, and a repeat of the ProgramCourseEnrollment repr()
+ # already tested above, let's just test the parts of the repr()
+ # that come before that.
+ assert (
+ "