learning-credentials 0.3.0rc3__tar.gz → 0.3.0rc4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {learning_credentials-0.3.0rc3/learning_credentials.egg-info → learning_credentials-0.3.0rc4}/PKG-INFO +1 -1
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/serializers.py +1 -1
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/urls.py +7 -1
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/views.py +80 -6
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4/learning_credentials.egg-info}/PKG-INFO +1 -1
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/pyproject.toml +1 -1
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/tests/test_views.py +269 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/CHANGELOG.rst +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/LICENSE.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/MANIFEST.in +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/README.rst +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/__init__.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/admin.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/__init__.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/urls.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/__init__.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/permissions.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/apps.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/compat.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/conf/locale/config.yaml +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/core_api.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/exceptions.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/generators.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0001_initial.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0003_rename_certificates_to_credentials.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0005_rename_processors_and_generators.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/__init__.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/models.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/processors.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/public/css/credentials_xblock.css +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/public/html/credentials_xblock.html +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/public/js/credentials_xblock.js +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/settings/__init__.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/settings/common.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/settings/production.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/tasks.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/base.html +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/urls.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/xblocks.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/SOURCES.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/dependency_links.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/entry_points.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/requires.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/top_level.txt +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/setup.cfg +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/tests/test_generators.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/tests/test_models.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/tests/test_processors.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/tests/test_serializers.py +0 -0
- {learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/tests/test_tasks.py +0 -0
|
@@ -45,7 +45,7 @@ class CredentialEligibilitySerializer(serializers.Serializer):
|
|
|
45
45
|
name = serializers.CharField()
|
|
46
46
|
is_eligible = serializers.BooleanField()
|
|
47
47
|
existing_credential = serializers.UUIDField(required=False, allow_null=True)
|
|
48
|
-
existing_credential_url = serializers.URLField(required=False, allow_null=True)
|
|
48
|
+
existing_credential_url = serializers.URLField(required=False, allow_blank=True, allow_null=True)
|
|
49
49
|
|
|
50
50
|
current_grades = serializers.DictField(required=False)
|
|
51
51
|
required_grades = serializers.DictField(required=False)
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/urls.py
RENAMED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from django.urls import path
|
|
4
4
|
|
|
5
|
-
from .views import CredentialEligibilityView, CredentialListView
|
|
5
|
+
from .views import CredentialConfigurationCheckView, CredentialEligibilityView, CredentialListView
|
|
6
6
|
|
|
7
7
|
urlpatterns = [
|
|
8
|
+
# Credential configuration check endpoint
|
|
9
|
+
path(
|
|
10
|
+
'configured/<str:learning_context_key>/',
|
|
11
|
+
CredentialConfigurationCheckView.as_view(),
|
|
12
|
+
name='credential-configuration-check',
|
|
13
|
+
),
|
|
8
14
|
# Credential eligibility endpoints
|
|
9
15
|
path('eligibility/<str:learning_context_key>/', CredentialEligibilityView.as_view(), name='credential-eligibility'),
|
|
10
16
|
path(
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/views.py
RENAMED
|
@@ -28,6 +28,66 @@ if TYPE_CHECKING:
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
class CredentialConfigurationCheckView(APIView):
|
|
32
|
+
"""API view to check if any credentials are configured for a specific learning context."""
|
|
33
|
+
|
|
34
|
+
permission_classes = (IsAuthenticated, IsAdminOrSelf, CanAccessLearningContext)
|
|
35
|
+
|
|
36
|
+
@apidocs.schema(
|
|
37
|
+
parameters=[
|
|
38
|
+
apidocs.string_parameter(
|
|
39
|
+
"learning_context_key",
|
|
40
|
+
ParameterLocation.PATH,
|
|
41
|
+
description=(
|
|
42
|
+
"Learning context identifier. Can be a course key (course-v1:OpenedX+DemoX+DemoCourse) "
|
|
43
|
+
"or learning path key (path-v1:OpenedX+DemoX+DemoPath+Demo)"
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
responses={
|
|
48
|
+
200: "Boolean indicating if credentials are configured.",
|
|
49
|
+
400: "Invalid context key format.",
|
|
50
|
+
404: "Learning context not found or user does not have access.",
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
def get(self, _request: "Request", learning_context_key: str) -> Response:
|
|
54
|
+
"""
|
|
55
|
+
Check if any credentials are configured for the given learning context.
|
|
56
|
+
|
|
57
|
+
**Example Request**
|
|
58
|
+
|
|
59
|
+
GET /api/learning_credentials/v1/configured/course-v1:OpenedX+DemoX+DemoCourse/
|
|
60
|
+
|
|
61
|
+
**Response Values**
|
|
62
|
+
|
|
63
|
+
If the request is successful, an HTTP 200 "OK" response is returned.
|
|
64
|
+
|
|
65
|
+
**Example Response**
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"has_credentials": true,
|
|
70
|
+
"credential_count": 2
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Response Fields**
|
|
75
|
+
- `has_credentials`: Boolean indicating if any credentials are configured
|
|
76
|
+
- `credential_count`: Number of credential configurations available
|
|
77
|
+
|
|
78
|
+
**Note**
|
|
79
|
+
This endpoint does not perform learning context existence validation, so it will not return 404 for staff users.
|
|
80
|
+
"""
|
|
81
|
+
credential_count = CredentialConfiguration.objects.filter(learning_context_key=learning_context_key).count()
|
|
82
|
+
|
|
83
|
+
response_data = {
|
|
84
|
+
'has_credentials': credential_count > 0,
|
|
85
|
+
'credential_count': credential_count,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Response(response_data, status=status.HTTP_200_OK)
|
|
89
|
+
|
|
90
|
+
|
|
31
91
|
class CredentialEligibilityView(APIView):
|
|
32
92
|
"""
|
|
33
93
|
API view for credential eligibility and generation.
|
|
@@ -37,6 +97,10 @@ class CredentialEligibilityView(APIView):
|
|
|
37
97
|
Supported Learning Contexts:
|
|
38
98
|
- Course keys: `course-v1:org+course+run`
|
|
39
99
|
- Learning path keys: `path-v1:org+path+run+group`
|
|
100
|
+
|
|
101
|
+
**Staff Features**:
|
|
102
|
+
- Staff users can view eligibility for any user by providing `username` parameter
|
|
103
|
+
- Non-staff users can only view their own eligibility
|
|
40
104
|
"""
|
|
41
105
|
|
|
42
106
|
permission_classes = (IsAuthenticated, IsAdminOrSelf, CanAccessLearningContext)
|
|
@@ -93,6 +157,9 @@ class CredentialEligibilityView(APIView):
|
|
|
93
157
|
- Step-by-step progress for learning paths
|
|
94
158
|
- Eligibility status for each credential type
|
|
95
159
|
|
|
160
|
+
**Query Parameters:**
|
|
161
|
+
- `username` (staff only): View eligibility for a specific user
|
|
162
|
+
|
|
96
163
|
**Example Request**
|
|
97
164
|
|
|
98
165
|
GET /api/learning_credentials/v1/eligibility/course-v1:OpenedX+DemoX+DemoCourse/
|
|
@@ -150,11 +217,14 @@ class CredentialEligibilityView(APIView):
|
|
|
150
217
|
}
|
|
151
218
|
```
|
|
152
219
|
"""
|
|
220
|
+
username = request.query_params.get('username')
|
|
221
|
+
user = get_object_or_404(User, username=username) if username else request.user
|
|
222
|
+
|
|
153
223
|
configurations = CredentialConfiguration.objects.filter(
|
|
154
224
|
learning_context_key=learning_context_key
|
|
155
225
|
).select_related('credential_type')
|
|
156
226
|
|
|
157
|
-
eligibility_data = [self._get_eligibility_data(
|
|
227
|
+
eligibility_data = [self._get_eligibility_data(user, config) for config in configurations]
|
|
158
228
|
|
|
159
229
|
response_data = {
|
|
160
230
|
'context_key': learning_context_key,
|
|
@@ -210,6 +280,9 @@ class CredentialEligibilityView(APIView):
|
|
|
210
280
|
**Notification:**
|
|
211
281
|
Users will receive an email notification when credential generation completes.
|
|
212
282
|
|
|
283
|
+
**Query Parameters:**
|
|
284
|
+
- `username` (staff only): Trigger credential generation for a specific user
|
|
285
|
+
|
|
213
286
|
**Example Request**
|
|
214
287
|
|
|
215
288
|
POST /api/learning_credentials/v1/eligibility/course-v1:OpenedX+DemoX+DemoCourse/1/
|
|
@@ -228,6 +301,9 @@ class CredentialEligibilityView(APIView):
|
|
|
228
301
|
}
|
|
229
302
|
```
|
|
230
303
|
"""
|
|
304
|
+
username = request.query_params.get('username')
|
|
305
|
+
user = get_object_or_404(User, username=username) if username else request.user
|
|
306
|
+
|
|
231
307
|
config = get_object_or_404(
|
|
232
308
|
CredentialConfiguration.objects.select_related('credential_type'),
|
|
233
309
|
learning_context_key=learning_context_key,
|
|
@@ -236,7 +312,7 @@ class CredentialEligibilityView(APIView):
|
|
|
236
312
|
|
|
237
313
|
existing_credential = (
|
|
238
314
|
Credential.objects.filter(
|
|
239
|
-
user_id=
|
|
315
|
+
user_id=user.id,
|
|
240
316
|
learning_context_key=learning_context_key,
|
|
241
317
|
credential_type=config.credential_type.name,
|
|
242
318
|
)
|
|
@@ -247,10 +323,10 @@ class CredentialEligibilityView(APIView):
|
|
|
247
323
|
if existing_credential:
|
|
248
324
|
return Response({"detail": "User already has a credential of this type."}, status=status.HTTP_409_CONFLICT)
|
|
249
325
|
|
|
250
|
-
if not config.get_eligible_user_ids(user_id=
|
|
326
|
+
if not config.get_eligible_user_ids(user_id=user.id):
|
|
251
327
|
return Response({"detail": "User is not eligible for this credential."}, status=status.HTTP_400_BAD_REQUEST)
|
|
252
328
|
|
|
253
|
-
generate_credential_for_user_task.delay(config.id,
|
|
329
|
+
generate_credential_for_user_task.delay(config.id, user.id)
|
|
254
330
|
return Response({"detail": "Credential generation started."}, status=status.HTTP_201_CREATED)
|
|
255
331
|
|
|
256
332
|
|
|
@@ -307,8 +383,6 @@ class CredentialListView(APIView):
|
|
|
307
383
|
|
|
308
384
|
**Query Parameters:**
|
|
309
385
|
- `username` (staff only): View credentials for a specific user
|
|
310
|
-
|
|
311
|
-
**Path Parameters:**
|
|
312
386
|
- `learning_context_key` (optional): Filter credentials by learning context
|
|
313
387
|
|
|
314
388
|
**Response includes:**
|
|
@@ -128,6 +128,275 @@ def credential_instance(user: User, course_key: CourseKey) -> Credential:
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
# Test classes
|
|
131
|
+
@pytest.mark.django_db
|
|
132
|
+
class TestCredentialConfigurationCheckViewAuthentication:
|
|
133
|
+
"""Test authentication requirements for credential configuration check endpoint."""
|
|
134
|
+
|
|
135
|
+
def test_unauthenticated_user_gets_403(self, api_client: APIClient, course_key: CourseKey):
|
|
136
|
+
"""Test that unauthenticated user gets 403."""
|
|
137
|
+
url = reverse(
|
|
138
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
139
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
140
|
+
)
|
|
141
|
+
response = api_client.get(url)
|
|
142
|
+
assert response.status_code == status.HTTP_403_FORBIDDEN
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.mark.django_db
|
|
146
|
+
class TestCredentialConfigurationCheckViewPermissions:
|
|
147
|
+
"""Test permission requirements for credential configuration check endpoint."""
|
|
148
|
+
|
|
149
|
+
@patch('learning_credentials.api.v1.permissions.get_course_enrollments')
|
|
150
|
+
def test_enrolled_user_can_access_course_check(
|
|
151
|
+
self, mock_course_enrollments: Mock, authenticated_client: APIClient, user: User, course_key: CourseKey
|
|
152
|
+
):
|
|
153
|
+
"""Test that enrolled user can access course configuration check."""
|
|
154
|
+
mock_course_enrollments.return_value = [user]
|
|
155
|
+
|
|
156
|
+
url = reverse(
|
|
157
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
158
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
159
|
+
)
|
|
160
|
+
response = authenticated_client.get(url)
|
|
161
|
+
|
|
162
|
+
assert response.status_code == status.HTTP_200_OK
|
|
163
|
+
data = response.json()
|
|
164
|
+
assert data['has_credentials'] is False
|
|
165
|
+
assert data['credential_count'] == 0
|
|
166
|
+
mock_course_enrollments.assert_called_once_with(course_key, user.id)
|
|
167
|
+
|
|
168
|
+
@patch('learning_credentials.api.v1.permissions.get_course_enrollments')
|
|
169
|
+
def test_non_enrolled_user_denied_course_access(
|
|
170
|
+
self, mock_course_enrollments: Mock, authenticated_client: APIClient, course_key: CourseKey
|
|
171
|
+
):
|
|
172
|
+
"""Test that non-enrolled user is denied course access."""
|
|
173
|
+
mock_course_enrollments.return_value = []
|
|
174
|
+
|
|
175
|
+
url = reverse(
|
|
176
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
177
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
178
|
+
)
|
|
179
|
+
response = authenticated_client.get(url)
|
|
180
|
+
|
|
181
|
+
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
182
|
+
assert 'Course not found or user does not have access' in str(response.data)
|
|
183
|
+
|
|
184
|
+
@patch('learning_paths.models.LearningPathEnrollment.objects')
|
|
185
|
+
def test_enrolled_user_can_access_learning_path_check(
|
|
186
|
+
self, mock_learning_path_enrollment: Mock, authenticated_client: APIClient, learning_path_key: LearningPathKey
|
|
187
|
+
):
|
|
188
|
+
"""Test that enrolled user can access learning path configuration check."""
|
|
189
|
+
mock_learning_path_enrollment.filter.return_value.exists.return_value = True
|
|
190
|
+
|
|
191
|
+
url = reverse(
|
|
192
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
193
|
+
kwargs={'learning_context_key': str(learning_path_key)},
|
|
194
|
+
)
|
|
195
|
+
response = authenticated_client.get(url)
|
|
196
|
+
|
|
197
|
+
assert response.status_code == status.HTTP_200_OK
|
|
198
|
+
data = response.json()
|
|
199
|
+
assert data['has_credentials'] is False
|
|
200
|
+
assert data['credential_count'] == 0
|
|
201
|
+
|
|
202
|
+
@patch('learning_paths.models.LearningPathEnrollment.objects')
|
|
203
|
+
def test_non_enrolled_user_denied_learning_path_access(
|
|
204
|
+
self, mock_learning_path_enrollment: Mock, authenticated_client: APIClient, learning_path_key: LearningPathKey
|
|
205
|
+
):
|
|
206
|
+
"""Test that non-enrolled user is denied learning path access."""
|
|
207
|
+
mock_learning_path_enrollment.filter.return_value.exists.return_value = False
|
|
208
|
+
|
|
209
|
+
url = reverse(
|
|
210
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
211
|
+
kwargs={'learning_context_key': str(learning_path_key)},
|
|
212
|
+
)
|
|
213
|
+
response = authenticated_client.get(url)
|
|
214
|
+
|
|
215
|
+
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
216
|
+
assert 'Learning path not found or user does not have access' in str(response.data)
|
|
217
|
+
|
|
218
|
+
def test_invalid_learning_context_key_returns_400(self, authenticated_client: APIClient):
|
|
219
|
+
"""Test that invalid learning context key returns 400."""
|
|
220
|
+
url = reverse(
|
|
221
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
222
|
+
kwargs={'learning_context_key': 'invalid-key'},
|
|
223
|
+
)
|
|
224
|
+
response = authenticated_client.get(url)
|
|
225
|
+
|
|
226
|
+
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
227
|
+
assert 'Invalid learning context key' in str(response.data)
|
|
228
|
+
|
|
229
|
+
@patch('learning_credentials.api.v1.permissions.get_course_enrollments')
|
|
230
|
+
def test_staff_can_view_any_context_check(
|
|
231
|
+
self, mock_course_enrollments: Mock, staff_client: APIClient, course_key: CourseKey
|
|
232
|
+
):
|
|
233
|
+
"""Test that staff can view configuration check for any context without enrollment check."""
|
|
234
|
+
# Staff users bypass enrollment checks, so we don't need to mock enrollment
|
|
235
|
+
url = reverse(
|
|
236
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
237
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
238
|
+
)
|
|
239
|
+
response = staff_client.get(url)
|
|
240
|
+
|
|
241
|
+
assert response.status_code == status.HTTP_200_OK
|
|
242
|
+
data = response.json()
|
|
243
|
+
assert data['has_credentials'] is False
|
|
244
|
+
assert data['credential_count'] == 0
|
|
245
|
+
# Staff users don't trigger enrollment checks
|
|
246
|
+
mock_course_enrollments.assert_not_called()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@pytest.mark.django_db
|
|
250
|
+
class TestCredentialConfigurationCheckView:
|
|
251
|
+
"""Test the CredentialConfigurationCheckView functionality."""
|
|
252
|
+
|
|
253
|
+
@patch('learning_credentials.api.v1.permissions.get_course_enrollments')
|
|
254
|
+
def test_no_credentials_configured(
|
|
255
|
+
self, mock_course_enrollments: Mock, authenticated_client: APIClient, user: User, course_key: CourseKey
|
|
256
|
+
):
|
|
257
|
+
"""Test response when no credentials are configured for a learning context."""
|
|
258
|
+
mock_course_enrollments.return_value = [user]
|
|
259
|
+
|
|
260
|
+
url = reverse(
|
|
261
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
262
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
263
|
+
)
|
|
264
|
+
response = authenticated_client.get(url)
|
|
265
|
+
|
|
266
|
+
assert response.status_code == status.HTTP_200_OK
|
|
267
|
+
data = response.json()
|
|
268
|
+
|
|
269
|
+
assert data['has_credentials'] is False
|
|
270
|
+
assert data['credential_count'] == 0
|
|
271
|
+
|
|
272
|
+
@patch('learning_credentials.api.v1.permissions.get_course_enrollments')
|
|
273
|
+
def test_single_credential_configured(
|
|
274
|
+
self,
|
|
275
|
+
mock_course_enrollments: Mock,
|
|
276
|
+
authenticated_client: APIClient,
|
|
277
|
+
user: User,
|
|
278
|
+
course_key: CourseKey,
|
|
279
|
+
grade_config: CredentialConfiguration,
|
|
280
|
+
):
|
|
281
|
+
"""Test response when one credential is configured for a learning context."""
|
|
282
|
+
mock_course_enrollments.return_value = [user]
|
|
283
|
+
|
|
284
|
+
url = reverse(
|
|
285
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
286
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
287
|
+
)
|
|
288
|
+
response = authenticated_client.get(url)
|
|
289
|
+
|
|
290
|
+
assert response.status_code == status.HTTP_200_OK
|
|
291
|
+
data = response.json()
|
|
292
|
+
|
|
293
|
+
assert data['has_credentials'] is True
|
|
294
|
+
assert data['credential_count'] == 1
|
|
295
|
+
|
|
296
|
+
@patch('learning_credentials.api.v1.permissions.get_course_enrollments')
|
|
297
|
+
def test_multiple_credentials_configured(
|
|
298
|
+
self,
|
|
299
|
+
mock_course_enrollments: Mock,
|
|
300
|
+
authenticated_client: APIClient,
|
|
301
|
+
user: User,
|
|
302
|
+
course_key: CourseKey,
|
|
303
|
+
grade_config: CredentialConfiguration,
|
|
304
|
+
completion_config: CredentialConfiguration,
|
|
305
|
+
):
|
|
306
|
+
"""Test response when multiple credentials are configured for a learning context."""
|
|
307
|
+
mock_course_enrollments.return_value = [user]
|
|
308
|
+
|
|
309
|
+
url = reverse(
|
|
310
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
311
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
312
|
+
)
|
|
313
|
+
response = authenticated_client.get(url)
|
|
314
|
+
|
|
315
|
+
assert response.status_code == status.HTTP_200_OK
|
|
316
|
+
data = response.json()
|
|
317
|
+
|
|
318
|
+
assert data['has_credentials'] is True
|
|
319
|
+
assert data['credential_count'] == 2
|
|
320
|
+
|
|
321
|
+
@patch('learning_paths.models.LearningPathEnrollment.objects')
|
|
322
|
+
def test_learning_path_credentials_configured(
|
|
323
|
+
self, mock_learning_path_enrollment: Mock, authenticated_client: APIClient, learning_path_key: LearningPathKey
|
|
324
|
+
):
|
|
325
|
+
"""Test response for learning path context with configured credentials."""
|
|
326
|
+
mock_learning_path_enrollment.filter.return_value.exists.return_value = True
|
|
327
|
+
|
|
328
|
+
# Create a credential configuration for the learning path
|
|
329
|
+
credential_type = CredentialType.objects.create(
|
|
330
|
+
name="Learning Path Certificate",
|
|
331
|
+
retrieval_func="learning_credentials.processors.retrieve_completions",
|
|
332
|
+
generation_func="learning_credentials.generators.generate_pdf_credential",
|
|
333
|
+
)
|
|
334
|
+
CredentialConfiguration.objects.create(
|
|
335
|
+
learning_context_key=learning_path_key,
|
|
336
|
+
credential_type=credential_type,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
url = reverse(
|
|
340
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
341
|
+
kwargs={'learning_context_key': str(learning_path_key)},
|
|
342
|
+
)
|
|
343
|
+
response = authenticated_client.get(url)
|
|
344
|
+
|
|
345
|
+
assert response.status_code == status.HTTP_200_OK
|
|
346
|
+
data = response.json()
|
|
347
|
+
|
|
348
|
+
assert data['has_credentials'] is True
|
|
349
|
+
assert data['credential_count'] == 1
|
|
350
|
+
|
|
351
|
+
@patch('learning_credentials.api.v1.permissions.get_course_enrollments')
|
|
352
|
+
def test_response_structure(
|
|
353
|
+
self,
|
|
354
|
+
mock_course_enrollments: Mock,
|
|
355
|
+
authenticated_client: APIClient,
|
|
356
|
+
user: User,
|
|
357
|
+
course_key: CourseKey,
|
|
358
|
+
grade_config: CredentialConfiguration,
|
|
359
|
+
):
|
|
360
|
+
"""Test that response has the correct structure and field types."""
|
|
361
|
+
mock_course_enrollments.return_value = [user]
|
|
362
|
+
|
|
363
|
+
url = reverse(
|
|
364
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
365
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
366
|
+
)
|
|
367
|
+
response = authenticated_client.get(url)
|
|
368
|
+
|
|
369
|
+
assert response.status_code == status.HTTP_200_OK
|
|
370
|
+
data = response.json()
|
|
371
|
+
|
|
372
|
+
# Verify all expected fields are present
|
|
373
|
+
assert 'has_credentials' in data
|
|
374
|
+
assert 'credential_count' in data
|
|
375
|
+
|
|
376
|
+
# Verify field types
|
|
377
|
+
assert isinstance(data['has_credentials'], bool)
|
|
378
|
+
assert isinstance(data['credential_count'], int)
|
|
379
|
+
|
|
380
|
+
# Verify values
|
|
381
|
+
assert data['has_credentials'] is True
|
|
382
|
+
assert data['credential_count'] == 1
|
|
383
|
+
|
|
384
|
+
def test_staff_can_check_any_context(
|
|
385
|
+
self, staff_client: APIClient, course_key: CourseKey, grade_config: CredentialConfiguration
|
|
386
|
+
):
|
|
387
|
+
"""Test that staff can check configuration for any context without enrollment."""
|
|
388
|
+
url = reverse(
|
|
389
|
+
'learning_credentials_api_v1:credential-configuration-check',
|
|
390
|
+
kwargs={'learning_context_key': str(course_key)},
|
|
391
|
+
)
|
|
392
|
+
response = staff_client.get(url)
|
|
393
|
+
|
|
394
|
+
assert response.status_code == status.HTTP_200_OK
|
|
395
|
+
data = response.json()
|
|
396
|
+
assert data['has_credentials'] is True
|
|
397
|
+
assert data['credential_count'] == 1
|
|
398
|
+
|
|
399
|
+
|
|
131
400
|
@pytest.mark.django_db
|
|
132
401
|
class TestCredentialEligibilityViewAuthentication:
|
|
133
402
|
"""Test authentication requirements for credential eligibility endpoints."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/__init__.py
RENAMED
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/admin.py
RENAMED
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/__init__.py
RENAMED
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/api/urls.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/apps.py
RENAMED
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/compat.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/core_api.py
RENAMED
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/exceptions.py
RENAMED
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/generators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/models.py
RENAMED
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/processors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/urls.py
RENAMED
|
File without changes
|
{learning_credentials-0.3.0rc3 → learning_credentials-0.3.0rc4}/learning_credentials/xblocks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|