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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to

### Added

- Add admin API to manage offering deep links for organizations
- Add `OfferingDeepLink` model to store links of offering
to purchase a training from an external platform

Expand Down
5 changes: 5 additions & 0 deletions src/backend/joanie/admin_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@
api_admin.NestedOfferingRuleViewSet,
basename="admin_offering_rules",
)
admin_offering_related_router.register(
"offering-deep-links",
api_admin.NestedOfferingDeepLinkViewSet,
basename="admin_offering_deeplink",
)

urlpatterns = [
path(
Expand Down
82 changes: 82 additions & 0 deletions src/backend/joanie/core/api/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,38 @@ def generate_certificates(self, request, pk=None): # pylint:disable=unused-argu

return JsonResponse(cache_data, status=HTTPStatus.CREATED)

@extend_schema(
request={"is_active": OpenApiTypes.BOOL},
responses={
200: OpenApiTypes.NONE,
400: serializers.ErrorResponseSerializer,
},
)
@action(methods=["PATCH"], detail=True, url_path="activate-deep-links")
def activate_deep_links(self, request, pk=None): # pylint: disable=unused-argument
"""
Activate or deactivate all deep links related to the offering.
"""
offering = self.get_object()

Comment thread
jonathanreveille marked this conversation as resolved.
if not "is_active" in request.data:
return Response(
_("is_active boolean value is required"),
status=HTTPStatus.BAD_REQUEST,
)

is_active = request.data.get("is_active")

if not isinstance(is_active, bool):
return Response(
_("is_active must be a boolean value."),
status=HTTPStatus.BAD_REQUEST,
)
Comment thread
jonathanreveille marked this conversation as resolved.

offering.activate_deep_links(is_active)

return Response(status=HTTPStatus.OK)
Comment thread
jonathanreveille marked this conversation as resolved.


class NestedOfferingRuleViewSet(
SerializerPerActionMixin,
Expand Down Expand Up @@ -636,6 +668,56 @@ def create(self, request, *args, **kwargs):
return Response(serializer.data, status=HTTPStatus.CREATED, headers=headers)


class NestedOfferingDeepLinkViewSet(viewsets.ModelViewSet, NestedGenericViewSet):
"""Admin Offering Deep Link ViewSet"""

authentication_classes = [SessionAuthenticationWithAuthenticateHeader]
permission_classes = [permissions.IsAdminUser & permissions.DjangoModelPermissions]
serializer_class = serializers.AdminOfferingDeepLinkSerializer
queryset = models.OfferingDeepLink.objects.all().select_related(
"offering", "organization"
)
lookup_fields = ["offering", "pk"]
lookup_url_kwargs = ["offering_id", "pk"]
filter_backends = [DjangoFilterBackend, AliasOrderingFilter]

def create(self, request, *args, **kwargs):
"""
Create a `OfferingDeepLink` using the `offering_id` from the URL.
"""
data = request.data.copy()
offering_id = kwargs.get("offering_id")
organization_id = data.pop("organization_id", None)
data["offering"] = str(offering_id)
data["organization"] = str(organization_id)

serializer = self.get_serializer(data=data)

if not serializer.is_valid():
return Response(serializer.errors, status=HTTPStatus.BAD_REQUEST)

self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=HTTPStatus.CREATED, headers=headers)

def update(self, request, *args, **kwargs):
"""Override update method to add the offering id from the url"""
instance = self.get_object()

partial = kwargs.pop("partial", False)
offering_id = kwargs.pop("offering_id")
data = request.data.copy()
data["offering"] = str(offering_id)
serializer = self.get_serializer(instance, data=data, partial=partial)

if not serializer.is_valid():
return Response(serializer.errors, status=HTTPStatus.BAD_REQUEST)
Comment thread
jonathanreveille marked this conversation as resolved.

self.perform_update(serializer)

return Response(serializer.data)


Comment thread
jonathanreveille marked this conversation as resolved.
class OrderViewSet(
SerializerPerActionMixin,
mixins.DestroyModelMixin,
Expand Down
16 changes: 16 additions & 0 deletions src/backend/joanie/core/models/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,12 @@ def rules(self):
"has_seats_left": has_seats_left or not offering_rule_is_blocking,
}

def activate_deep_links(self, is_active=True):
"""
Activate or deactivate the deeplinks of the offering.
"""
self.organization_links.update(is_active=is_active)


class OfferingDeepLink(BaseModel):
"""
Expand Down Expand Up @@ -996,6 +1002,16 @@ def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)

def delete(self, using=None, keep_parents=False):
"""
A deep link can be deleted if it's not active only.
Comment thread
jonathanreveille marked this conversation as resolved.
"""
if self.is_active:
raise ValidationError(
_("You cannot delete this offering deep link, it's active.")
)
return super().delete(using=using, keep_parents=keep_parents)


class CourseRun(parler_models.TranslatableModel, BaseModel):
"""
Expand Down
37 changes: 37 additions & 0 deletions src/backend/joanie/core/serializers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,43 @@ def update(self, instance, validated_data):
return super().update(instance, validated_data)


class AdminOfferingDeepLinkSerializer(serializers.ModelSerializer):
"""
Admin serializer for Offering Deep Link model
"""

organization = serializers.SlugRelatedField(
queryset=models.Organization.objects.all(),
slug_field="id",
write_only=True,
)
offering = serializers.SlugRelatedField(
queryset=models.CourseProductRelation.objects.all(),
slug_field="id",
write_only=True,
)

class Meta:
model = models.OfferingDeepLink
fields = [
"id",
"deep_link",
"is_active",
"organization",
"offering",
]
read_only_fields = ["id"]

def to_representation(self, instance):
"""
Override `to_representation` to customize the output with the organization and offering id.
"""
representation = super().to_representation(instance)
representation["organization"] = str(instance.organization.id)
representation["offering"] = str(instance.offering.id)
return representation
Comment thread
jonathanreveille marked this conversation as resolved.


class AdminCourseAccessSerializer(serializers.ModelSerializer):
"""Serializer for CourseAccess model."""

Expand Down
Empty file.
Loading