From a57a4991fbc32a8044452b7f978b46f105fc95b3 Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Tue, 12 Mar 2024 16:17:52 -0400 Subject: [PATCH 1/8] adding program certificate endpoint and tests --- profiles/factories.py | 1 + profiles/serializers.py | 8 ++++++++ profiles/urls.py | 6 ++++++ profiles/views.py | 17 +++++++++++++++++ profiles/views_test.py | 22 +++++++++++++++++++++- 5 files changed, 53 insertions(+), 1 deletion(-) 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..e3afc05796 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,13 @@ class ProgramCertificateSerializer(serializers.ModelSerializer): Serializer for Program Certificates """ + program_letter_url = serializers.SerializerMethodField() + + def get_program_letter_url(self, instance): + return reverse( + "profile:program-letter-intercept", args=[instance.micromasters_program_id] + ) + 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..3dc60c5b04 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -22,6 +22,7 @@ from profiles.permissions import HasEditPermission, HasSiteEditPermission from profiles.serializers import ( ProfileSerializer, + ProgramCertificateSerializer, ProgramLetterSerializer, UserSerializer, UserWebsiteSerializer, @@ -109,6 +110,22 @@ 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 + + def list(self, request): + queryset = ProgramCertificate.objects.filter(user_email=request.user.email) + serializer = ProgramCertificateSerializer(queryset, many=True) + return Response(serializer.data) + + @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..0fbe138459 100644 --- a/profiles/views_test.py +++ b/profiles/views_test.py @@ -11,7 +11,7 @@ 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 +413,23 @@ 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: + assert resp.status_code == 200 + assert resp.json() == ProgramCertificateSerializer(certs, many=True).data + else: + assert resp.status_code == 403 From 2c4b5d5009bd7001700363705db8b3f38cd8ca76 Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Tue, 12 Mar 2024 16:34:16 -0400 Subject: [PATCH 2/8] making view filterable --- profiles/views.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/profiles/views.py b/profiles/views.py index 3dc60c5b04..041e8bc508 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 @@ -119,12 +120,21 @@ class UserProgramCertificateViewSet(viewsets.ViewSet): 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(queryset, many=True) + serializer = ProgramCertificateSerializer( + self.filter_queryset(queryset), many=True + ) 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( From 84fb215e244c239ceec9ce5436d213b1a545621c Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Tue, 12 Mar 2024 16:36:41 -0400 Subject: [PATCH 3/8] updating open api schema --- frontends/api/src/generated/api.ts | 6 ++++++ openapi/specs/v1.yaml | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/frontends/api/src/generated/api.ts b/frontends/api/src/generated/api.ts index 2416d47c89..ad54ec454a 100644 --- a/frontends/api/src/generated/api.ts +++ b/frontends/api/src/generated/api.ts @@ -3018,6 +3018,12 @@ export interface ProgramCertificate { * @memberof ProgramCertificate */ id: number + /** + * + * @type {string} + * @memberof ProgramCertificate + */ + program_letter_url: string /** * * @type {number} diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 19a8c5824d..074864e214 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -9765,6 +9765,9 @@ components: id: type: integer readOnly: true + program_letter_url: + type: string + readOnly: true user_edxorg_id: type: integer maximum: 2147483647 @@ -9828,6 +9831,7 @@ components: nullable: true required: - id + - program_letter_url - program_title - user_email ProgramLetter: From b36d05bd42474b58d1f222dd056c5dc61767e84e Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Tue, 12 Mar 2024 16:48:38 -0400 Subject: [PATCH 4/8] lint fix --- frontends/api/src/test-utils/factories/programLetters.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontends/api/src/test-utils/factories/programLetters.ts b/frontends/api/src/test-utils/factories/programLetters.ts index 514230882b..3aba56f82d 100644 --- a/frontends/api/src/test-utils/factories/programLetters.ts +++ b/frontends/api/src/test-utils/factories/programLetters.ts @@ -39,6 +39,7 @@ const programLetter: Factory = (overrides = {}) => ({ user_first_name: faker.name.firstName(), user_last_name: faker.name.lastName(), user_full_name: faker.name.fullName(), + program_letter_url: new URL(faker.internet.url()).toString(), }, ...overrides, }) From 8fec3cb1b3594a74f015f973ba354149d804edd6 Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Wed, 13 Mar 2024 12:59:38 -0400 Subject: [PATCH 5/8] making program letter urls absolute and adding share urls --- profiles/serializers.py | 23 ++++++++++++++++++++--- profiles/views.py | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/profiles/serializers.py b/profiles/serializers.py index e3afc05796..fcb30f3f59 100644 --- a/profiles/serializers.py +++ b/profiles/serializers.py @@ -260,12 +260,29 @@ class ProgramCertificateSerializer(serializers.ModelSerializer): Serializer for Program Certificates """ - program_letter_url = serializers.SerializerMethodField() + program_letter_generate_url = serializers.SerializerMethodField() + program_letter_share_url = serializers.SerializerMethodField() - def get_program_letter_url(self, instance): - return reverse( + 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 diff --git a/profiles/views.py b/profiles/views.py index 041e8bc508..e109701aac 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -126,7 +126,7 @@ class UserProgramCertificateViewSet(viewsets.ViewSet): def list(self, request): queryset = ProgramCertificate.objects.filter(user_email=request.user.email) serializer = ProgramCertificateSerializer( - self.filter_queryset(queryset), many=True + self.filter_queryset(queryset), many=True, context={"request": request} ) return Response(serializer.data) From 59b2e7f68e9131dec1b1676c4614d1b92fa5babf Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Wed, 13 Mar 2024 13:00:40 -0400 Subject: [PATCH 6/8] updating openapi spec --- frontends/api/src/generated/api.ts | 8 +++++++- openapi/specs/v1.yaml | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontends/api/src/generated/api.ts b/frontends/api/src/generated/api.ts index ad54ec454a..9d003a8ac3 100644 --- a/frontends/api/src/generated/api.ts +++ b/frontends/api/src/generated/api.ts @@ -3023,7 +3023,13 @@ export interface ProgramCertificate { * @type {string} * @memberof ProgramCertificate */ - program_letter_url: string + program_letter_generate_url: string + /** + * + * @type {string} + * @memberof ProgramCertificate + */ + program_letter_share_url: string /** * * @type {number} diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 074864e214..aec293bc81 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -9765,7 +9765,10 @@ components: id: type: integer readOnly: true - program_letter_url: + program_letter_generate_url: + type: string + readOnly: true + program_letter_share_url: type: string readOnly: true user_edxorg_id: @@ -9831,7 +9834,8 @@ components: nullable: true required: - id - - program_letter_url + - program_letter_generate_url + - program_letter_share_url - program_title - user_email ProgramLetter: From 50eb5b1e6a1e255ce0ae5decde19a69a783f0e4c Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Wed, 13 Mar 2024 13:06:30 -0400 Subject: [PATCH 7/8] updating factory for new fields --- frontends/api/src/test-utils/factories/programLetters.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontends/api/src/test-utils/factories/programLetters.ts b/frontends/api/src/test-utils/factories/programLetters.ts index 3aba56f82d..c84178be41 100644 --- a/frontends/api/src/test-utils/factories/programLetters.ts +++ b/frontends/api/src/test-utils/factories/programLetters.ts @@ -39,7 +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_url: new URL(faker.internet.url()).toString(), + program_letter_generate_url: new URL(faker.internet.url()).toString(), + program_letter_share_url: new URL(faker.internet.url()).toString(), }, ...overrides, }) From 20d3eb7310783d796352cb9c360e95363b305cea Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Wed, 13 Mar 2024 13:38:49 -0400 Subject: [PATCH 8/8] fixing test --- profiles/views_test.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/profiles/views_test.py b/profiles/views_test.py index 0fbe138459..d9b4a1d7a1 100644 --- a/profiles/views_test.py +++ b/profiles/views_test.py @@ -9,6 +9,7 @@ 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 ProgramCertificateSerializer, ProgramLetterSerializer @@ -429,7 +430,13 @@ def test_list_user_program_certificates(mocker, client, user, is_anonymous): 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).data + assert ( + resp.json() + == ProgramCertificateSerializer( + certs, many=True, context={"request": request} + ).data + ) else: assert resp.status_code == 403