Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ omit =
*/templates/*
*/tests/*
*/settings/*
core_api.py
xblocks.py
1 change: 1 addition & 0 deletions learning_credentials/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Learning Credentials API package."""
12 changes: 12 additions & 0 deletions learning_credentials/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""API URLs."""

from django.urls import include, path

urlpatterns = [
path(
"v1/",
include(
("learning_credentials.api.v1.urls", "learning_credentials_api_v1"), namespace="learning_credentials_api_v1"
),
),
]
1 change: 1 addition & 0 deletions learning_credentials/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Learning Credentials API v1 package."""
63 changes: 63 additions & 0 deletions learning_credentials/api/v1/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Django REST framework permissions."""

from typing import TYPE_CHECKING

from learning_paths.models import LearningPathEnrollment
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import LearningContextKey
from rest_framework.exceptions import NotFound, ParseError
from rest_framework.permissions import BasePermission

from learning_credentials.compat import get_course_enrollments

if TYPE_CHECKING:
from rest_framework.request import Request
from rest_framework.views import APIView


class IsAdminOrSelf(BasePermission):
"""
Permission to allow only admins or the user themselves to access the API.

Non-staff users cannot pass "username" that is not their own.
"""

def has_permission(self, request: "Request", view: "APIView") -> bool: # noqa: ARG002
"""Check if the user is admin or accessing their own data."""
if request.user.is_staff:
return True

username = request.query_params.get("username") if request.method == "GET" else request.data.get("username")

# For learners, the username passed should match the logged in user.
if username:
return request.user.username == username
return True


class CanAccessLearningContext(BasePermission):
"""Permission to allow access to learning context if the user is enrolled."""

def has_permission(self, request: "Request", view: "APIView") -> bool:
"""Check if the user is enrolled in the learning context."""
try:
key = view.kwargs.get("learning_context_key") or request.query_params.get("learning_context_key")
learning_context_key = LearningContextKey.from_string(key)
except InvalidKeyError as e:
msg = "Invalid learning context key."
raise ParseError(msg) from e

if request.user.is_staff:
return True

if learning_context_key.is_course:
if bool(get_course_enrollments(learning_context_key, request.user.id)):
return True
msg = "Course not found or user does not have access."
raise NotFound(msg)

if LearningPathEnrollment.objects.filter(learning_path__key=learning_context_key, user=request.user).exists():
return True

msg = "Learning path not found or user does not have access."
raise NotFound(msg)
74 changes: 74 additions & 0 deletions learning_credentials/api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Serializers for the Learning Credentials API."""

from typing import Any, ClassVar

from rest_framework import serializers

from learning_credentials.models import Credential


class CredentialModelSerializer(serializers.ModelSerializer):
"""Model serializer for Credential instances."""

credential_id = serializers.UUIDField(source='uuid', read_only=True)
credential_type = serializers.CharField(read_only=True)
context_key = serializers.CharField(source='learning_context_key', read_only=True)
created_date = serializers.DateTimeField(source='created', read_only=True)
download_url = serializers.URLField(read_only=True)

class Meta:
"""Meta configuration for CredentialModelSerializer."""

model = Credential
fields: ClassVar[list[str]] = [
'credential_id',
'credential_type',
'context_key',
'status',
'created_date',
'download_url',
]
read_only_fields: ClassVar[list[str]] = [
'credential_id',
'credential_type',
'context_key',
'status',
'created_date',
'download_url',
]


class CredentialEligibilitySerializer(serializers.Serializer):
"""Serializer for credential eligibility information with dynamic fields."""

credential_type_id = serializers.IntegerField()
name = serializers.CharField()
is_eligible = serializers.BooleanField()
existing_credential = serializers.UUIDField(required=False, allow_null=True)
existing_credential_url = serializers.URLField(required=False, allow_blank=True, allow_null=True)

current_grades = serializers.DictField(required=False)
required_grades = serializers.DictField(required=False)

current_completion = serializers.FloatField(required=False, allow_null=True)
required_completion = serializers.FloatField(required=False, allow_null=True)

steps = serializers.DictField(required=False)

def to_representation(self, instance: dict) -> dict[str, Any]:
"""Remove null/empty fields from representation."""
data = super().to_representation(instance)
return {key: value for key, value in data.items() if value is not None and value not in ({}, [])}


class CredentialEligibilityResponseSerializer(serializers.Serializer):
"""Serializer for the complete credential eligibility response."""

context_key = serializers.CharField()
credentials = CredentialEligibilitySerializer(many=True)


class CredentialListResponseSerializer(serializers.Serializer):
"""Serializer for credential list response."""

credentials = CredentialModelSerializer(many=True)
23 changes: 23 additions & 0 deletions learning_credentials/api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""API v1 URLs."""

from django.urls import path

from .views import CredentialConfigurationCheckView, CredentialEligibilityView, CredentialListView

urlpatterns = [
# Credential configuration check endpoint
path(
'configured/<str:learning_context_key>/',
CredentialConfigurationCheckView.as_view(),
name='credential-configuration-check',
),
# Credential eligibility endpoints
path('eligibility/<str:learning_context_key>/', CredentialEligibilityView.as_view(), name='credential-eligibility'),
path(
'eligibility/<str:learning_context_key>/<int:credential_type_id>/',
CredentialEligibilityView.as_view(),
name='credential-generation',
),
# Credential listing endpoints
path('credentials/', CredentialListView.as_view(), name='credential-list'),
]
Loading
Loading