diff --git a/frontends/api/src/generated/api.ts b/frontends/api/src/generated/api.ts index 2416d47c89..9d003a8ac3 100644 --- a/frontends/api/src/generated/api.ts +++ b/frontends/api/src/generated/api.ts @@ -3018,6 +3018,18 @@ export interface ProgramCertificate { * @memberof ProgramCertificate */ id: number + /** + * + * @type {string} + * @memberof ProgramCertificate + */ + program_letter_generate_url: string + /** + * + * @type {string} + * @memberof ProgramCertificate + */ + program_letter_share_url: string /** * * @type {number} diff --git a/frontends/api/src/test-utils/factories/programLetters.ts b/frontends/api/src/test-utils/factories/programLetters.ts index 514230882b..c84178be41 100644 --- a/frontends/api/src/test-utils/factories/programLetters.ts +++ b/frontends/api/src/test-utils/factories/programLetters.ts @@ -39,6 +39,8 @@ const programLetter: Factory = (overrides = {}) => ({ user_first_name: faker.name.firstName(), user_last_name: faker.name.lastName(), user_full_name: faker.name.fullName(), + program_letter_generate_url: new URL(faker.internet.url()).toString(), + program_letter_share_url: new URL(faker.internet.url()).toString(), }, ...overrides, }) diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 19a8c5824d..aec293bc81 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -9765,6 +9765,12 @@ components: id: type: integer readOnly: true + program_letter_generate_url: + type: string + readOnly: true + program_letter_share_url: + type: string + readOnly: true user_edxorg_id: type: integer maximum: 2147483647 @@ -9828,6 +9834,8 @@ components: nullable: true required: - id + - program_letter_generate_url + - program_letter_share_url - program_title - user_email ProgramLetter: diff --git a/profiles/factories.py b/profiles/factories.py index dbeb6eac20..49e34f5c13 100644 --- a/profiles/factories.py +++ b/profiles/factories.py @@ -59,6 +59,7 @@ class Meta: class ProgramCertificateFactory(DjangoModelFactory): user_full_name = Faker("name") user_email = Faker("email") + micromasters_program_id = Faker("random_int") class Meta: model = ProgramCertificate diff --git a/profiles/serializers.py b/profiles/serializers.py index 7583dce2a8..fcb30f3f59 100644 --- a/profiles/serializers.py +++ b/profiles/serializers.py @@ -7,6 +7,7 @@ import ulid from django.contrib.auth import get_user_model from django.db import transaction +from django.urls import reverse from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -259,6 +260,30 @@ class ProgramCertificateSerializer(serializers.ModelSerializer): Serializer for Program Certificates """ + program_letter_generate_url = serializers.SerializerMethodField() + program_letter_share_url = serializers.SerializerMethodField() + + def get_program_letter_generate_url(self, instance): + request = self.context.get("request") + letter_url = reverse( + "profile:program-letter-intercept", args=[instance.micromasters_program_id] + ) + if request: + return request.build_absolute_uri(letter_url) + return letter_url + + def get_program_letter_share_url(self, instance): + request = self.context.get("request") + + user = User.objects.get(email=instance.user_email) + letter, created = ProgramLetter.objects.get_or_create( + user=user, certificate=instance + ) + letter_url = letter.get_absolute_url() + if request: + return request.build_absolute_uri(letter_url) + return letter_url + class Meta: model = ProgramCertificate fields = "__all__" diff --git a/profiles/urls.py b/profiles/urls.py index 40440e54ac..e099c46b22 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -7,6 +7,7 @@ CurrentUserRetrieveViewSet, ProfileViewSet, ProgramLetterInterceptView, + UserProgramCertificateViewSet, UserViewSet, UserWebsiteViewSet, name_initials_avatar_view, @@ -16,6 +17,11 @@ router.register(r"users", UserViewSet, basename="user_api") router.register(r"profiles", ProfileViewSet, basename="profile_api") router.register(r"websites", UserWebsiteViewSet, basename="user_websites_api") +router.register( + r"program_certificates", + UserProgramCertificateViewSet, + basename="user_program_certificates_api", +) v0_urls = [ diff --git a/profiles/views.py b/profiles/views.py index 0c96c9563c..e109701aac 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -9,6 +9,7 @@ from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.cache import cache_page +from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import mixins, viewsets from rest_framework.permissions import IsAuthenticated @@ -22,6 +23,7 @@ from profiles.permissions import HasEditPermission, HasSiteEditPermission from profiles.serializers import ( ProfileSerializer, + ProgramCertificateSerializer, ProgramLetterSerializer, UserSerializer, UserWebsiteSerializer, @@ -109,6 +111,31 @@ class UserWebsiteViewSet( queryset = UserWebsite.objects.select_related("profile__user") +@extend_schema(exclude=True) +class UserProgramCertificateViewSet(viewsets.ViewSet): + """ + View for listing program certificates for a user + (includes program letter links) + """ + + permission_classes = (IsAuthenticated,) + serializer_class = ProgramCertificateSerializer + filter_backends = (DjangoFilterBackend,) + filterset_fields = ["micromasters_program_id", "program_title"] + + def list(self, request): + queryset = ProgramCertificate.objects.filter(user_email=request.user.email) + serializer = ProgramCertificateSerializer( + self.filter_queryset(queryset), many=True, context={"request": request} + ) + return Response(serializer.data) + + def filter_queryset(self, queryset): + for backend in list(self.filter_backends): + queryset = backend().filter_queryset(self.request, queryset, view=self) + return queryset + + @cache_page(60 * 60 * 24) def name_initials_avatar_view( request, # noqa: ARG001 diff --git a/profiles/views_test.py b/profiles/views_test.py index 0f204fe24a..d9b4a1d7a1 100644 --- a/profiles/views_test.py +++ b/profiles/views_test.py @@ -9,9 +9,10 @@ from django.urls import reverse from rest_framework import status +from learning_resources_search.serializers_test import get_request_object from profiles.factories import ProgramCertificateFactory, ProgramLetterFactory from profiles.models import ProgramLetter -from profiles.serializers import ProgramLetterSerializer +from profiles.serializers import ProgramCertificateSerializer, ProgramLetterSerializer from profiles.utils import DEFAULT_PROFILE_IMAGE, make_temp_image_file pytestmark = [pytest.mark.django_db] @@ -413,3 +414,29 @@ def test_program_letter_api_view_returns_404_for_invalid_id( ) ) assert response.status_code == 404 + + +@pytest.mark.parametrize("is_anonymous", [True, False]) +def test_list_user_program_certificates(mocker, client, user, is_anonymous): + """ + Test listing program certificates for a user + """ + if not is_anonymous: + client.force_login(user) + certs = ProgramCertificateFactory.create_batch( + 3, + user_email=user.email, + ) + url = reverse("profile:v0:user_program_certificates_api-list") + resp = client.get(url) + if not is_anonymous: + request = get_request_object(url) + assert resp.status_code == 200 + assert ( + resp.json() + == ProgramCertificateSerializer( + certs, many=True, context={"request": request} + ).data + ) + else: + assert resp.status_code == 403