Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion courses/serializers/v1/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class ProgramRequirementTreeSerializer(BaseProgramRequirementTreeSerializer):
class PartnerSchoolSerializer(serializers.ModelSerializer):
class Meta:
model = models.PartnerSchool
fields = "__all__"
fields = ["id", "name"]


class LearnerProgramRecordShareSerializer(serializers.ModelSerializer):
Expand Down
21 changes: 17 additions & 4 deletions courses/views/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,9 +633,22 @@ class PartnerSchoolViewSet(viewsets.ReadOnlyModelViewSet):
queryset = PartnerSchool.objects.all()


def get_enrolled_program_or_404(user, program_id: int) -> Program:
"""Return a program only if the user has an active enrollment for it."""

enrollment = get_object_or_404(
ProgramEnrollment.objects.select_related("program"),
user=user,
program_id=program_id,
)
return enrollment.program


class GetLearnerRecordView(APIView):
"""View to get learner record by program ID"""

permission_classes = [IsAuthenticated]

@extend_schema(
operation_id="learner_record_retrieve_by_id",
description="Get learner record using program ID",
Expand All @@ -645,7 +658,7 @@ def get(self, request, pk):
"""
Retrieve the learner record for a specific program.
"""
program = Program.objects.get(pk=pk)
program = get_enrolled_program_or_404(request.user, pk)
serializer = LearnerRecordSerializer(program, context={"request": request})
return Response(serializer.data)

Expand All @@ -662,7 +675,7 @@ def post(self, request, pk):
Sets up a sharing link for the learner's record. Returns back the entire
learner record.
"""
program = Program.objects.get(pk=pk)
program = get_enrolled_program_or_404(request.user, pk)

school = None

Expand All @@ -672,7 +685,7 @@ def post(self, request, pk):
):
try:
school = PartnerSchool.objects.get(pk=request.data["partnerSchool"])
except: # noqa: E722
except PartnerSchool.DoesNotExist:
return Response("Partner school not found.", status.HTTP_404_NOT_FOUND)

(ps_share, _) = LearnerProgramRecordShare.objects.filter(
Expand Down Expand Up @@ -707,7 +720,7 @@ def post(self, request, pk: int):
anonymous ones; shares sent to partner schools are always allowed once they
are sent.
"""
program = Program.objects.get(pk=pk)
program = get_enrolled_program_or_404(request.user, pk)

LearnerProgramRecordShare.objects.filter(
user=request.user, partner_school=None, program=program
Expand Down
170 changes: 170 additions & 0 deletions courses/views/v1/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
CourseRunCertificateFactory,
CourseRunEnrollmentFactory,
CourseRunFactory,
LearnerProgramRecordShareFactory,
PartnerSchoolFactory,
ProgramCertificateFactory,
ProgramEnrollmentFactory,
ProgramFactory,
Expand All @@ -39,6 +41,7 @@
Course,
CourseRun,
CourseRunEnrollment,
LearnerProgramRecordShare,
PaidProgram,
Program,
ProgramEnrollment,
Expand Down Expand Up @@ -1188,3 +1191,170 @@ def test_destroy_program_enrollment_paid_fails(user_drf_client, user):

enrollment.refresh_from_db()
assert enrollment.active is True


def test_get_learner_record_requires_program_enrollment(user_drf_client):
"""The learner record endpoint should return 404 without a program enrollment."""
program = ProgramFactory.create()

resp = user_drf_client.get(reverse("get-learner-record", kwargs={"pk": program.id}))

assert resp.status_code == status.HTTP_404_NOT_FOUND


def test_get_learner_record(user_drf_client, user):
"""The learner record endpoint should return data for an enrolled user."""
enrollment = ProgramEnrollmentFactory.create(user=user)
partner_school = PartnerSchoolFactory.create()

resp = user_drf_client.get(
reverse("get-learner-record", kwargs={"pk": enrollment.program.id})
)

assert resp.status_code == status.HTTP_200_OK
assert resp.json()["program"]["title"] == enrollment.program.title
assert resp.json()["partner_schools"] == [
{"id": partner_school.id, "name": partner_school.name}
]


def test_share_learner_record_requires_program_enrollment(user_drf_client):
"""The learner record share endpoint should return 404 without a program enrollment."""
program = ProgramFactory.create()

resp = user_drf_client.post(
reverse("learner-record-share", kwargs={"pk": program.id}),
data={"partnerSchool": None},
)

assert resp.status_code == status.HTTP_404_NOT_FOUND


def test_share_learner_record(user_drf_client, user, mocker):
"""The learner record share endpoint should create an active anonymous share."""
enrollment = ProgramEnrollmentFactory.create(user=user)
partner_school = PartnerSchoolFactory.create()
patched_send_email = mocker.patch(
"courses.views.v1.send_partner_school_email.delay"
)

resp = user_drf_client.post(
reverse("learner-record-share", kwargs={"pk": enrollment.program.id}),
data={"partnerSchool": None},
)

assert resp.status_code == status.HTTP_200_OK
assert resp.json()["partner_schools"] == [
{"id": partner_school.id, "name": partner_school.name}
]
assert (
LearnerProgramRecordShare.objects.filter(
user=user,
program=enrollment.program,
partner_school=None,
is_active=True,
).count()
== 1
)
patched_send_email.assert_not_called()


def test_share_learner_record_partner_school(user_drf_client, user, mocker):
"""The learner record share endpoint should create an active partner school share."""
enrollment = ProgramEnrollmentFactory.create(user=user)
partner_school = PartnerSchoolFactory.create()
patched_send_email = mocker.patch(
"courses.views.v1.send_partner_school_email.delay"
)

resp = user_drf_client.post(
reverse("learner-record-share", kwargs={"pk": enrollment.program.id}),
data={"partnerSchool": partner_school.id},
)

assert resp.status_code == status.HTTP_200_OK
assert all("email" not in school for school in resp.json()["partner_schools"])
share = LearnerProgramRecordShare.objects.get(
user=user,
program=enrollment.program,
partner_school=partner_school,
)
assert share.is_active is True
patched_send_email.assert_called_once_with(share.share_uuid)


def test_revoke_learner_record_share_requires_program_enrollment(user_drf_client):
"""The learner record revoke endpoint should return 404 without a program enrollment."""
program = ProgramFactory.create()

resp = user_drf_client.post(
reverse("revoke-learner-record-share", kwargs={"pk": program.id})
)

assert resp.status_code == status.HTTP_404_NOT_FOUND


def test_revoke_learner_record_share(user_drf_client, user):
"""The learner record revoke endpoint should disable only anonymous shares."""
enrollment = ProgramEnrollmentFactory.create(user=user)
anonymous_share = LearnerProgramRecordShareFactory.create(
user=user,
program=enrollment.program,
partner_school=None,
is_active=True,
)
partner_school_share = LearnerProgramRecordShareFactory.create(
user=user,
program=enrollment.program,
is_active=True,
)

resp = user_drf_client.post(
reverse("revoke-learner-record-share", kwargs={"pk": enrollment.program.id})
)

assert resp.status_code == status.HTTP_200_OK
anonymous_share.refresh_from_db()
partner_school_share.refresh_from_db()
assert anonymous_share.is_active is False
assert partner_school_share.is_active is True


def test_get_shared_learner_record(user):
"""The shared learner record endpoint should expose active shared records."""
enrollment = ProgramEnrollmentFactory.create(user=user)
share = LearnerProgramRecordShareFactory.create(
user=user,
program=enrollment.program,
partner_school=None,
is_active=True,
)
client = APIClient()

resp = client.get(
reverse("shared_learner_record_from_uuid", kwargs={"uuid": share.share_uuid})
)

assert resp.status_code == status.HTTP_200_OK
assert resp.json()["program"]["title"] == enrollment.program.title
assert resp.json()["sharing"] == []
assert resp.json()["partner_schools"] == []


def test_get_shared_learner_record_inactive_share_returns_404(user):
"""The shared learner record endpoint should return 404 for inactive links."""
enrollment = ProgramEnrollmentFactory.create(user=user)
share = LearnerProgramRecordShareFactory.create(
user=user,
program=enrollment.program,
partner_school=None,
is_active=False,
)
client = APIClient()

resp = client.get(
reverse("shared_learner_record_from_uuid", kwargs={"uuid": share.share_uuid})
)

assert resp.status_code == status.HTTP_404_NOT_FOUND
assert resp.json() == []
5 changes: 2 additions & 3 deletions frontend/public/src/factories/course.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,8 @@ export const makeLearnerRecordProgram = (): LearnerRecordProgram => ({
})

export const makePartnerSchool = (): PartnerSchool => ({
id: genPartnerSchoolId.next().value,
name: casual.company_name,
email: casual.email
id: genPartnerSchoolId.next().value,
name: casual.company_name
})

export const makeLearnerRecordShare = (
Expand Down
3 changes: 1 addition & 2 deletions frontend/public/src/flow/courseTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ export type ProgramEnrollment = {

export type PartnerSchool = {
id: number,
name: string,
email: string
name: string
}

export type RequirementNode = {
Expand Down
21 changes: 0 additions & 21 deletions openapi/specs/v0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6673,41 +6673,20 @@ components:
id:
type: integer
readOnly: true
created_on:
type: string
format: date-time
readOnly: true
updated_on:
type: string
format: date-time
readOnly: true
name:
type: string
maxLength: 255
email:
type: string
is_active:
type: boolean
required:
- created_on
- email
- id
- name
- updated_on
PartnerSchoolRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 255
email:
type: string
minLength: 1
is_active:
type: boolean
required:
- email
- name
PatchedChangeEmailRequestUpdateRequest:
type: object
Expand Down
21 changes: 0 additions & 21 deletions openapi/specs/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6673,41 +6673,20 @@ components:
id:
type: integer
readOnly: true
created_on:
type: string
format: date-time
readOnly: true
updated_on:
type: string
format: date-time
readOnly: true
name:
type: string
maxLength: 255
email:
type: string
is_active:
type: boolean
required:
- created_on
- email
- id
- name
- updated_on
PartnerSchoolRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 255
email:
type: string
minLength: 1
is_active:
type: boolean
required:
- email
- name
PatchedChangeEmailRequestUpdateRequest:
type: object
Expand Down
Loading
Loading