diff --git a/enterprise_access/apps/api/serializers/__init__.py b/enterprise_access/apps/api/serializers/__init__.py index e8d2090e..d4207239 100644 --- a/enterprise_access/apps/api/serializers/__init__.py +++ b/enterprise_access/apps/api/serializers/__init__.py @@ -4,7 +4,8 @@ from .content_assignments.assignment import ( ContentMetadataForAssignmentSerializer, LearnerContentAssignmentAdminResponseSerializer, - LearnerContentAssignmentResponseSerializer + LearnerContentAssignmentResponseSerializer, + LearnerContentAssignmentUpdateRequestSerializer, ) from .content_assignments.assignment_configuration import ( AssignmentConfigurationCreateRequestSerializer, diff --git a/enterprise_access/apps/api/serializers/content_assignments/assignment.py b/enterprise_access/apps/api/serializers/content_assignments/assignment.py index 006a82e7..85fbcfb9 100644 --- a/enterprise_access/apps/api/serializers/content_assignments/assignment.py +++ b/enterprise_access/apps/api/serializers/content_assignments/assignment.py @@ -94,6 +94,7 @@ class Meta: 'transaction_uuid', 'last_notification_at', 'actions', + 'has_dismissed', ] read_only_fields = fields @@ -258,3 +259,33 @@ def get_content_metadata(self, obj): if metadata_lookup and (assignment_content_metadata := metadata_lookup.get(obj.content_key)): return ContentMetadataForAssignmentSerializer(assignment_content_metadata).data return None + +class LearnerContentAssignmentUpdateRequestSerializer(serializers.ModelSerializer): + """ + Request Serializer for PUT or PATCH requests to update a LearnerContentAssignment. + + For views: LearnerContentAssignmentAdminViewSet.update and LearnerContentAssignmentAdminViewSet.partial_update. + """ + class Meta: + model = LearnerContentAssignment + fields = ( + 'has_dismissed', + ) + + def validate(self, attrs): + """ + Raises a ValidationError if any field not explicitly declared as a field in this serializer definition is + provided as input. + """ + unknown = sorted(set(self.initial_data) - set(self.fields)) + if unknown: + raise serializers.ValidationError("Field(s) are not updatable: {}".format(", ".join(unknown))) + return attrs + + def to_representation(self, instance): + """ + Once an LearnerContentAssignment has been updated, we want to serialize more fields from the instance than are + required in this, the input serializer. + """ + read_serializer = LearnerContentAssignmentResponseSerializer(instance) + return read_serializer.data \ No newline at end of file diff --git a/enterprise_access/apps/api/serializers/subsidy_access_policy.py b/enterprise_access/apps/api/serializers/subsidy_access_policy.py index 0f285dae..499e19a7 100644 --- a/enterprise_access/apps/api/serializers/subsidy_access_policy.py +++ b/enterprise_access/apps/api/serializers/subsidy_access_policy.py @@ -543,7 +543,9 @@ def get_assignments_serializer(self, obj): return [] assignments = obj.assignment_configuration.assignments.prefetch_related('actions').filter( - lms_user_id=self.context.get('lms_user_id') + lms_user_id=self.context.get('lms_user_id'), + # only return assignments that have not been dismissed + has_dismissed=False ) content_metadata_lookup = get_content_metadata_for_assignments(obj.catalog_uuid, assignments) context = {'content_metadata': content_metadata_lookup} diff --git a/enterprise_access/apps/api/v1/views/content_assignments/assignments.py b/enterprise_access/apps/api/v1/views/content_assignments/assignments.py index 6300fefa..0469e91c 100644 --- a/enterprise_access/apps/api/v1/views/content_assignments/assignments.py +++ b/enterprise_access/apps/api/v1/views/content_assignments/assignments.py @@ -11,7 +11,7 @@ from enterprise_access.apps.api import filters, serializers, utils from enterprise_access.apps.api.v1.views.utils import PaginationWithPageCount from enterprise_access.apps.content_assignments.models import LearnerContentAssignment -from enterprise_access.apps.core.constants import CONTENT_ASSIGNMENT_LEARNER_READ_PERMISSION +from enterprise_access.apps.core.constants import (CONTENT_ASSIGNMENT_LEARNER_READ_PERMISSION, CONTENT_ASSIGNMENT_LEARNER_WRITE_PERMISSION) logger = logging.getLogger(__name__) @@ -32,6 +32,7 @@ class LearnerContentAssignmentViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet, + mixins.UpdateModelMixin, ): """ Viewset supporting all learner-facing CRUD operations on ``LearnerContentAssignment`` records. @@ -43,6 +44,16 @@ class LearnerContentAssignmentViewSet( pagination_class = PaginationWithPageCount lookup_field = 'uuid' + def get_serializer_class(self): + """ + Overrides the default behavior to return different serializers depending on the request action. + """ + + if self.action in ('update', 'partial_update'): + return serializers.LearnerContentAssignmentUpdateRequestSerializer + # list and retrieve use the default serializer. + return self.serializer_class + @property def requesting_user_email(self): """ @@ -98,3 +109,20 @@ def list(self, request, *args, **kwargs): configuration. """ return super().list(request, *args, **kwargs) + + @extend_schema( + tags=[CONTENT_ASSIGNMENT_CRUD_API_TAG], + summary='Partially update (with a PATCH) a learner assignment by UUID.', + request=serializers.LearnerContentAssignmentUpdateRequestSerializer, + responses={ + status.HTTP_200_OK: None, + status.HTTP_404_NOT_FOUND: None, + status.HTTP_422_UNPROCESSABLE_ENTITY: None, + }, + ) + @permission_required(CONTENT_ASSIGNMENT_LEARNER_WRITE_PERMISSION, fn=assignment_permission_fn) + def partial_update(self, request, *args, uuid=None, **kwargs): + """ + Updates a single ``LearnerContentAssignment`` record by uuid. All fields for the update are optional. + """ + return super().partial_update(request, *args, uuid=uuid, **kwargs) diff --git a/enterprise_access/apps/content_assignments/migrations/0013_historicallearnercontentassignment_has_dismissed_and_more.py b/enterprise_access/apps/content_assignments/migrations/0013_historicallearnercontentassignment_has_dismissed_and_more.py new file mode 100644 index 00000000..07697448 --- /dev/null +++ b/enterprise_access/apps/content_assignments/migrations/0013_historicallearnercontentassignment_has_dismissed_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.6 on 2023-12-13 08:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('content_assignments', '0012_assignmentaction_add_automatic_cancellation_option'), + ] + + operations = [ + migrations.AddField( + model_name='historicallearnercontentassignment', + name='has_dismissed', + field=models.BooleanField(default=False, help_text='True if the learner dismissed alert for cancelled or expired assignments. Default to false'), + ), + migrations.AddField( + model_name='learnercontentassignment', + name='has_dismissed', + field=models.BooleanField(default=False, help_text='True if the learner dismissed alert for cancelled or expired assignments. Default to false'), + ), + ] diff --git a/enterprise_access/apps/content_assignments/models.py b/enterprise_access/apps/content_assignments/models.py index 93098cb7..18ecb55e 100644 --- a/enterprise_access/apps/content_assignments/models.py +++ b/enterprise_access/apps/content_assignments/models.py @@ -184,6 +184,10 @@ class Meta: "been notified." ), ) + has_dismissed = models.BooleanField( + default=False, + help_text='True if the learner dismissed alert for cancelled or expired assignments. Default to false', + ) history = HistoricalRecords() def __str__(self): diff --git a/enterprise_access/apps/core/constants.py b/enterprise_access/apps/core/constants.py index de9d1297..2dc56869 100644 --- a/enterprise_access/apps/core/constants.py +++ b/enterprise_access/apps/core/constants.py @@ -26,6 +26,7 @@ CONTENT_ASSIGNMENT_ADMIN_READ_PERMISSION = 'content_assignment.has_admin_read_access' CONTENT_ASSIGNMENT_ADMIN_WRITE_PERMISSION = 'content_assignment.has_admin_write_access' CONTENT_ASSIGNMENT_LEARNER_READ_PERMISSION = 'content_assignment.has_learner_read_access' +CONTENT_ASSIGNMENT_LEARNER_WRITE_PERMISSION = 'content_assignment.has_learner_write_access' ALL_ACCESS_CONTEXT = '*' diff --git a/enterprise_access/apps/core/rules.py b/enterprise_access/apps/core/rules.py index ef7335f4..9f615c05 100644 --- a/enterprise_access/apps/core/rules.py +++ b/enterprise_access/apps/core/rules.py @@ -330,6 +330,15 @@ def has_explicit_access_to_content_assignments_learner(user, enterprise_customer ), ) +rules.add_perm( + constants.CONTENT_ASSIGNMENT_LEARNER_WRITE_PERMISSION, + ( + has_content_assignments_operator_access | + has_content_assignments_admin_access | + has_content_assignments_learner_access + ), +) + # Grants permission to allocate assignments from a policy if the user is a content assignment configuration admin. rules.add_perm(