Skip to content

Commit 51587f1

Browse files
authored
Endpoint for user program certificate info and program letter links (#608)
* adding program certificate endpoint and tests * making view filterable * updating open api schema * lint fix * making program letter urls absolute and adding share urls * updating openapi spec * updating factory for new fields * fixing test
1 parent 5a30881 commit 51587f1

File tree

8 files changed

+109
-1
lines changed

8 files changed

+109
-1
lines changed

frontends/api/src/generated/api.ts

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontends/api/src/test-utils/factories/programLetters.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ const programLetter: Factory<ProgramLetter> = (overrides = {}) => ({
3939
user_first_name: faker.name.firstName(),
4040
user_last_name: faker.name.lastName(),
4141
user_full_name: faker.name.fullName(),
42+
program_letter_generate_url: new URL(faker.internet.url()).toString(),
43+
program_letter_share_url: new URL(faker.internet.url()).toString(),
4244
},
4345
...overrides,
4446
})

openapi/specs/v1.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9765,6 +9765,12 @@ components:
97659765
id:
97669766
type: integer
97679767
readOnly: true
9768+
program_letter_generate_url:
9769+
type: string
9770+
readOnly: true
9771+
program_letter_share_url:
9772+
type: string
9773+
readOnly: true
97689774
user_edxorg_id:
97699775
type: integer
97709776
maximum: 2147483647
@@ -9828,6 +9834,8 @@ components:
98289834
nullable: true
98299835
required:
98309836
- id
9837+
- program_letter_generate_url
9838+
- program_letter_share_url
98319839
- program_title
98329840
- user_email
98339841
ProgramLetter:

profiles/factories.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class Meta:
5959
class ProgramCertificateFactory(DjangoModelFactory):
6060
user_full_name = Faker("name")
6161
user_email = Faker("email")
62+
micromasters_program_id = Faker("random_int")
6263

6364
class Meta:
6465
model = ProgramCertificate

profiles/serializers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import ulid
88
from django.contrib.auth import get_user_model
99
from django.db import transaction
10+
from django.urls import reverse
1011
from drf_spectacular.utils import extend_schema_field
1112
from rest_framework import serializers
1213
from rest_framework.exceptions import ValidationError
@@ -259,6 +260,30 @@ class ProgramCertificateSerializer(serializers.ModelSerializer):
259260
Serializer for Program Certificates
260261
"""
261262

263+
program_letter_generate_url = serializers.SerializerMethodField()
264+
program_letter_share_url = serializers.SerializerMethodField()
265+
266+
def get_program_letter_generate_url(self, instance):
267+
request = self.context.get("request")
268+
letter_url = reverse(
269+
"profile:program-letter-intercept", args=[instance.micromasters_program_id]
270+
)
271+
if request:
272+
return request.build_absolute_uri(letter_url)
273+
return letter_url
274+
275+
def get_program_letter_share_url(self, instance):
276+
request = self.context.get("request")
277+
278+
user = User.objects.get(email=instance.user_email)
279+
letter, created = ProgramLetter.objects.get_or_create(
280+
user=user, certificate=instance
281+
)
282+
letter_url = letter.get_absolute_url()
283+
if request:
284+
return request.build_absolute_uri(letter_url)
285+
return letter_url
286+
262287
class Meta:
263288
model = ProgramCertificate
264289
fields = "__all__"

profiles/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
CurrentUserRetrieveViewSet,
88
ProfileViewSet,
99
ProgramLetterInterceptView,
10+
UserProgramCertificateViewSet,
1011
UserViewSet,
1112
UserWebsiteViewSet,
1213
name_initials_avatar_view,
@@ -16,6 +17,11 @@
1617
router.register(r"users", UserViewSet, basename="user_api")
1718
router.register(r"profiles", ProfileViewSet, basename="profile_api")
1819
router.register(r"websites", UserWebsiteViewSet, basename="user_websites_api")
20+
router.register(
21+
r"program_certificates",
22+
UserProgramCertificateViewSet,
23+
basename="user_program_certificates_api",
24+
)
1925

2026

2127
v0_urls = [

profiles/views.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.utils.decorators import method_decorator
1010
from django.views import View
1111
from django.views.decorators.cache import cache_page
12+
from django_filters.rest_framework import DjangoFilterBackend
1213
from drf_spectacular.utils import extend_schema, extend_schema_view
1314
from rest_framework import mixins, viewsets
1415
from rest_framework.permissions import IsAuthenticated
@@ -22,6 +23,7 @@
2223
from profiles.permissions import HasEditPermission, HasSiteEditPermission
2324
from profiles.serializers import (
2425
ProfileSerializer,
26+
ProgramCertificateSerializer,
2527
ProgramLetterSerializer,
2628
UserSerializer,
2729
UserWebsiteSerializer,
@@ -109,6 +111,31 @@ class UserWebsiteViewSet(
109111
queryset = UserWebsite.objects.select_related("profile__user")
110112

111113

114+
@extend_schema(exclude=True)
115+
class UserProgramCertificateViewSet(viewsets.ViewSet):
116+
"""
117+
View for listing program certificates for a user
118+
(includes program letter links)
119+
"""
120+
121+
permission_classes = (IsAuthenticated,)
122+
serializer_class = ProgramCertificateSerializer
123+
filter_backends = (DjangoFilterBackend,)
124+
filterset_fields = ["micromasters_program_id", "program_title"]
125+
126+
def list(self, request):
127+
queryset = ProgramCertificate.objects.filter(user_email=request.user.email)
128+
serializer = ProgramCertificateSerializer(
129+
self.filter_queryset(queryset), many=True, context={"request": request}
130+
)
131+
return Response(serializer.data)
132+
133+
def filter_queryset(self, queryset):
134+
for backend in list(self.filter_backends):
135+
queryset = backend().filter_queryset(self.request, queryset, view=self)
136+
return queryset
137+
138+
112139
@cache_page(60 * 60 * 24)
113140
def name_initials_avatar_view(
114141
request, # noqa: ARG001

profiles/views_test.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
from django.urls import reverse
1010
from rest_framework import status
1111

12+
from learning_resources_search.serializers_test import get_request_object
1213
from profiles.factories import ProgramCertificateFactory, ProgramLetterFactory
1314
from profiles.models import ProgramLetter
14-
from profiles.serializers import ProgramLetterSerializer
15+
from profiles.serializers import ProgramCertificateSerializer, ProgramLetterSerializer
1516
from profiles.utils import DEFAULT_PROFILE_IMAGE, make_temp_image_file
1617

1718
pytestmark = [pytest.mark.django_db]
@@ -413,3 +414,29 @@ def test_program_letter_api_view_returns_404_for_invalid_id(
413414
)
414415
)
415416
assert response.status_code == 404
417+
418+
419+
@pytest.mark.parametrize("is_anonymous", [True, False])
420+
def test_list_user_program_certificates(mocker, client, user, is_anonymous):
421+
"""
422+
Test listing program certificates for a user
423+
"""
424+
if not is_anonymous:
425+
client.force_login(user)
426+
certs = ProgramCertificateFactory.create_batch(
427+
3,
428+
user_email=user.email,
429+
)
430+
url = reverse("profile:v0:user_program_certificates_api-list")
431+
resp = client.get(url)
432+
if not is_anonymous:
433+
request = get_request_object(url)
434+
assert resp.status_code == 200
435+
assert (
436+
resp.json()
437+
== ProgramCertificateSerializer(
438+
certs, many=True, context={"request": request}
439+
).data
440+
)
441+
else:
442+
assert resp.status_code == 403

0 commit comments

Comments
 (0)