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 ( + "