Skip to content
Merged
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
12 changes: 12 additions & 0 deletions frontends/api/src/generated/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontends/api/src/test-utils/factories/programLetters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const programLetter: Factory<ProgramLetter> = (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,
})
Expand Down
8 changes: 8 additions & 0 deletions openapi/specs/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -9828,6 +9834,8 @@ components:
nullable: true
required:
- id
- program_letter_generate_url
- program_letter_share_url
- program_title
- user_email
ProgramLetter:
Expand Down
1 change: 1 addition & 0 deletions profiles/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions profiles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is returning urls in the format "/program_letter/1/", which will only work for the certificate's logged-in user. Wondering if maybe it should provide the shareable url instead if it exists yet (/program_letter/<uuid>/view), or both as separate fields (something like create_letter_url, share_letter_url) since it's possible the shareable url hasn't yet been created?

Also, what do you think of returning absolute urls instead (http://<domain>/program_letter/....)? The frontend could create a full url itself, but maybe easier to do it in the backend?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mbertrand - made it generate absolute urls and changed the url params to return both a program_letter_generate_url and a program_letter_share_url. I also made it so the serializer itself does a get_or_create for the program letter. In the future we might consider removing the intercept view altogether but I think it still has a place for now (i'm imagning a case where we send links to view view a program letter from some marketing automation software that is only aware of program ids etc)

class Meta:
model = ProgramCertificate
fields = "__all__"
Expand Down
6 changes: 6 additions & 0 deletions profiles/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
CurrentUserRetrieveViewSet,
ProfileViewSet,
ProgramLetterInterceptView,
UserProgramCertificateViewSet,
UserViewSet,
UserWebsiteViewSet,
name_initials_avatar_view,
Expand All @@ -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 = [
Expand Down
27 changes: 27 additions & 0 deletions profiles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +23,7 @@
from profiles.permissions import HasEditPermission, HasSiteEditPermission
from profiles.serializers import (
ProfileSerializer,
ProgramCertificateSerializer,
ProgramLetterSerializer,
UserSerializer,
UserWebsiteSerializer,
Expand Down Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion profiles/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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