From 66d7a174bbe409e03107fc8d187cc7b6e28c1cc1 Mon Sep 17 00:00:00 2001 From: Nathan Levesque Date: Wed, 2 Oct 2024 16:27:07 -0400 Subject: [PATCH 1/5] Restore program letter intercept view --- profiles/serializers.py | 26 +++++++++++++++++++++++++ profiles/urls.py | 6 ++++++ profiles/views.py | 27 ++++++++++++++++++++++++-- profiles/views_test.py | 43 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 99 insertions(+), 3 deletions(-) diff --git a/profiles/serializers.py b/profiles/serializers.py index 2e0e1b7672..102ddfbddc 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 @@ -368,6 +369,31 @@ 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) -> str: + request = self.context.get("request") + letter_url = reverse( + "profile:program-letter-intercept", + kwargs={"program_id": instance.micromasters_program_id}, + ) + if request: + return request.build_absolute_uri(letter_url) + return letter_url + + def get_program_letter_share_url(self, instance) -> str: + 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 87f54ac7c5..e5a18a0425 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -6,6 +6,7 @@ from profiles.views import ( CurrentUserRetrieveViewSet, ProfileViewSet, + ProgramLetterInterceptView, ProgramLetterViewSet, UserProgramCertificateViewSet, UserViewSet, @@ -52,4 +53,9 @@ name_initials_avatar_view, name="name-initials-avatar", ), + re_path( + r"^program_letter/(?P[0-9]+)/", + ProgramLetterInterceptView.as_view(), + name="program-letter-intercept", + ), ] diff --git a/profiles/views.py b/profiles/views.py index ef28fb4db2..c157209d67 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -2,9 +2,12 @@ from cairosvg import svg2png # pylint:disable=no-name-in-module from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.http import HttpResponse -from django.shortcuts import redirect +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect +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 rest_framework import mixins, viewsets @@ -139,3 +142,23 @@ def name_initials_avatar_view( return redirect(DEFAULT_PROFILE_IMAGE) svg = generate_svg_avatar(user.profile.name, int(size), color, bgcolor) return HttpResponse(svg2png(bytestring=svg), content_type="image/png") + + +@method_decorator(login_required, name="dispatch") +class ProgramLetterInterceptView(View): + """ + View that generates a uuid (via ProgramLetter instance) + and then passes the user along to the shareable letter view + """ + + def get(self, request, **kwargs): + program_id = kwargs.get("program_id") + certificate = get_object_or_404( + ProgramCertificate, + user_email=request.user.email, + micromasters_program_id=program_id, + ) + letter, created = ProgramLetter.objects.get_or_create( + user=request.user, certificate=certificate + ) + return HttpResponseRedirect(letter.get_absolute_url()) diff --git a/profiles/views_test.py b/profiles/views_test.py index 4192c2ee84..6e71e2f3c6 100644 --- a/profiles/views_test.py +++ b/profiles/views_test.py @@ -14,7 +14,7 @@ from learning_resources.serializers import LearningResourceTopicSerializer from learning_resources_search.serializers_test import get_request_object from profiles.factories import ProgramCertificateFactory, ProgramLetterFactory -from profiles.models import Profile +from profiles.models import Profile, ProgramLetter from profiles.serializers import ( ProfileSerializer, ProgramCertificateSerializer, @@ -392,6 +392,47 @@ def test_get_user_by_me(mocker, client, user, is_anonymous): } +@pytest.mark.parametrize("is_anonymous", [True, False]) +def test_letter_intercept_view_generates_program_letter( + mocker, client, user, is_anonymous, settings +): + """ + Test that the letter intercept view generates a + ProgramLetter and then passes the user along to the display. + Also test that anonymous users do not generate letters and cant access this page + """ + settings.DATABASE_ROUTERS = [] + micromasters_program_id = 1 + if not is_anonymous: + client.force_login(user) + cert = ProgramCertificateFactory( + user_email=user.email, micromasters_program_id=micromasters_program_id + ) + assert ProgramLetter.objects.filter(user=user).count() == 0 + + response = client.get( + reverse( + "profile:program-letter-intercept", + kwargs={"program_id": micromasters_program_id}, + ) + ) + assert ProgramLetter.objects.filter(user=user).count() == 1 + letter_id = ProgramLetter.objects.get(user=user, certificate=cert).id + assert response.url == f"/program_letter/{letter_id}/view" + else: + cert = ProgramCertificateFactory( + user_email=user.email, micromasters_program_id=micromasters_program_id + ) + ProgramLetterFactory(user=user, certificate=cert) + response = client.get( + reverse( + "profile:program-letter-intercept", + kwargs={"program_id": micromasters_program_id}, + ) + ) + assert response.status_code == 302 + + @pytest.mark.parametrize("is_anonymous", [True, False]) def test_program_letter_api_view(mocker, client, rf, user, is_anonymous, settings): # noqa: PLR0913 """ From 05e2e52e2c1221840655136a179b15f6fbc0c566 Mon Sep 17 00:00:00 2001 From: Nathan Levesque Date: Wed, 2 Oct 2024 21:43:54 -0400 Subject: [PATCH 2/5] Generate schema --- frontends/api/src/generated/v0/api.ts | 12 ++++++++++++ frontends/api/src/generated/v1/api.ts | 12 ++++++++++++ openapi/specs/v0.yaml | 8 ++++++++ openapi/specs/v1.yaml | 8 ++++++++ 4 files changed, 40 insertions(+) diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 694b9b5086..6d2b7f1290 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -2145,6 +2145,18 @@ export interface ProgramCertificate { * @memberof ProgramCertificate */ record_hash: string + /** + * + * @type {string} + * @memberof ProgramCertificate + */ + program_letter_generate_url: string + /** + * + * @type {string} + * @memberof ProgramCertificate + */ + program_letter_share_url: string /** * * @type {string} diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index 62385b9658..7a04a4410d 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -4834,6 +4834,18 @@ export interface ProgramCertificate { * @memberof ProgramCertificate */ record_hash: string + /** + * + * @type {string} + * @memberof ProgramCertificate + */ + program_letter_generate_url: string + /** + * + * @type {string} + * @memberof ProgramCertificate + */ + program_letter_share_url: string /** * * @type {string} diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index 98ff99fe1f..bea72ed810 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -2249,6 +2249,12 @@ components: record_hash: type: string readOnly: true + program_letter_generate_url: + type: string + readOnly: true + program_letter_share_url: + type: string + readOnly: true program_title: type: string maxLength: 256 @@ -2322,6 +2328,8 @@ components: format: date-time nullable: true required: + - program_letter_generate_url + - program_letter_share_url - program_title - record_hash - user_email diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 1fe6d76a0e..91f44cd28c 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -11123,6 +11123,12 @@ components: record_hash: type: string readOnly: true + program_letter_generate_url: + type: string + readOnly: true + program_letter_share_url: + type: string + readOnly: true program_title: type: string maxLength: 256 @@ -11196,6 +11202,8 @@ components: format: date-time nullable: true required: + - program_letter_generate_url + - program_letter_share_url - program_title - record_hash - user_email From d2945273b903a6b9a7191cede0d1e243ac722f68 Mon Sep 17 00:00:00 2001 From: Nathan Levesque Date: Wed, 9 Oct 2024 09:49:23 -0400 Subject: [PATCH 3/5] Fix test --- profiles/views_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/profiles/views_test.py b/profiles/views_test.py index 6e71e2f3c6..99a7a51043 100644 --- a/profiles/views_test.py +++ b/profiles/views_test.py @@ -13,6 +13,7 @@ from learning_resources.factories import LearningResourceTopicFactory from learning_resources.serializers import LearningResourceTopicSerializer from learning_resources_search.serializers_test import get_request_object +from main.utils import frontend_absolute_url from profiles.factories import ProgramCertificateFactory, ProgramLetterFactory from profiles.models import Profile, ProgramLetter from profiles.serializers import ( @@ -418,7 +419,9 @@ def test_letter_intercept_view_generates_program_letter( ) assert ProgramLetter.objects.filter(user=user).count() == 1 letter_id = ProgramLetter.objects.get(user=user, certificate=cert).id - assert response.url == f"/program_letter/{letter_id}/view" + assert response.url == frontend_absolute_url( + f"/program_letter/{letter_id}/view" + ) else: cert = ProgramCertificateFactory( user_email=user.email, micromasters_program_id=micromasters_program_id From cf568077c45352aa02ed34bde52bc4b4a73d22e7 Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Wed, 9 Oct 2024 15:37:05 -0400 Subject: [PATCH 4/5] fixing import --- ...c_mappings_edx_add_programming_coding_to_computer_science.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_fixtures/migrations/0007_topic_mappings_edx_add_programming_coding_to_computer_science.py b/data_fixtures/migrations/0007_topic_mappings_edx_add_programming_coding_to_computer_science.py index c1056a3153..393b3098c9 100644 --- a/data_fixtures/migrations/0007_topic_mappings_edx_add_programming_coding_to_computer_science.py +++ b/data_fixtures/migrations/0007_topic_mappings_edx_add_programming_coding_to_computer_science.py @@ -5,7 +5,7 @@ from django.db import migrations -from learning_resources.utils import upsert_topic_data_string +from data_fixtures.utils import upsert_topic_data_string map_changes = """ --- From 6e3fa5267e6dd5ccb23dc1a1ab9f9595e0fb3ad9 Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Wed, 9 Oct 2024 15:37:18 -0400 Subject: [PATCH 5/5] Revert "fixing import" This reverts commit cf568077c45352aa02ed34bde52bc4b4a73d22e7. --- ...c_mappings_edx_add_programming_coding_to_computer_science.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_fixtures/migrations/0007_topic_mappings_edx_add_programming_coding_to_computer_science.py b/data_fixtures/migrations/0007_topic_mappings_edx_add_programming_coding_to_computer_science.py index 393b3098c9..c1056a3153 100644 --- a/data_fixtures/migrations/0007_topic_mappings_edx_add_programming_coding_to_computer_science.py +++ b/data_fixtures/migrations/0007_topic_mappings_edx_add_programming_coding_to_computer_science.py @@ -5,7 +5,7 @@ from django.db import migrations -from data_fixtures.utils import upsert_topic_data_string +from learning_resources.utils import upsert_topic_data_string map_changes = """ ---