learning-credentials 0.3.0rc2__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.
Files changed (58) hide show
  1. {learning_credentials-0.3.0rc2/learning_credentials.egg-info → learning_credentials-0.3.0rc4}/PKG-INFO +1 -1
  2. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/serializers.py +1 -1
  3. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/urls.py +7 -1
  4. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/views.py +80 -6
  5. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/apps.py +4 -4
  6. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4/learning_credentials.egg-info}/PKG-INFO +1 -1
  7. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/entry_points.txt +0 -3
  8. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/pyproject.toml +3 -3
  9. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/tests/test_views.py +269 -0
  10. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/CHANGELOG.rst +0 -0
  11. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/LICENSE.txt +0 -0
  12. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/MANIFEST.in +0 -0
  13. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/README.rst +0 -0
  14. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/__init__.py +0 -0
  15. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/admin.py +0 -0
  16. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/api/__init__.py +0 -0
  17. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/api/urls.py +0 -0
  18. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/__init__.py +0 -0
  19. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/api/v1/permissions.py +0 -0
  20. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/compat.py +0 -0
  21. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/conf/locale/config.yaml +0 -0
  22. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/core_api.py +0 -0
  23. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/exceptions.py +0 -0
  24. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/generators.py +0 -0
  25. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0001_initial.py +0 -0
  26. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +0 -0
  27. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0003_rename_certificates_to_credentials.py +0 -0
  28. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +0 -0
  29. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0005_rename_processors_and_generators.py +0 -0
  30. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +0 -0
  31. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/migrations/__init__.py +0 -0
  32. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/models.py +0 -0
  33. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/processors.py +0 -0
  34. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/public/css/credentials_xblock.css +0 -0
  35. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/public/html/credentials_xblock.html +0 -0
  36. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/public/js/credentials_xblock.js +0 -0
  37. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/settings/__init__.py +0 -0
  38. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/settings/common.py +0 -0
  39. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/settings/production.py +0 -0
  40. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/tasks.py +0 -0
  41. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/base.html +0 -0
  42. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +0 -0
  43. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +0 -0
  44. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +0 -0
  45. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
  46. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +0 -0
  47. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/urls.py +0 -0
  48. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials/xblocks.py +0 -0
  49. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/SOURCES.txt +0 -0
  50. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/dependency_links.txt +0 -0
  51. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/requires.txt +0 -0
  52. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/learning_credentials.egg-info/top_level.txt +0 -0
  53. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/setup.cfg +0 -0
  54. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/tests/test_generators.py +0 -0
  55. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/tests/test_models.py +0 -0
  56. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/tests/test_processors.py +0 -0
  57. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/tests/test_serializers.py +0 -0
  58. {learning_credentials-0.3.0rc2 → learning_credentials-0.3.0rc4}/tests/test_tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.3.0rc2
3
+ Version: 0.3.0rc4
4
4
  Summary: A pluggable service for preparing Open edX credentials.
5
5
  Author-email: OpenCraft <help@opencraft.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -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)
@@ -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(
@@ -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(request.user, config) for config in configurations]
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=request.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=request.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, request.user.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:**
@@ -26,9 +26,9 @@ class LearningCredentialsConfig(AppConfig):
26
26
  'common': {'relative_path': 'settings.common'},
27
27
  'production': {'relative_path': 'settings.production'},
28
28
  },
29
- 'cms.djangoapp': {
30
- 'common': {'relative_path': 'settings.common'},
31
- 'production': {'relative_path': 'settings.production'},
32
- },
29
+ # 'cms.djangoapp': {
30
+ # 'common': {'relative_path': 'settings.common'},
31
+ # 'production': {'relative_path': 'settings.production'},
32
+ # },
33
33
  },
34
34
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.3.0rc2
3
+ Version: 0.3.0rc4
4
4
  Summary: A pluggable service for preparing Open edX credentials.
5
5
  Author-email: OpenCraft <help@opencraft.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -1,6 +1,3 @@
1
- [cms.djangoapp]
2
- learning_credentials = learning_credentials.apps:LearningCredentialsConfig
3
-
4
1
  [lms.djangoapp]
5
2
  learning_credentials = learning_credentials.apps:LearningCredentialsConfig
6
3
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "learning-credentials"
3
- version = "0.3.0-rc2"
3
+ version = "0.3.0-rc4"
4
4
  description = "A pluggable service for preparing Open edX credentials."
5
5
  dynamic = ["readme"]
6
6
  requires-python = ">=3.11"
@@ -44,8 +44,8 @@ Documentation = "https://learning-credentials.readthedocs.io/"
44
44
  [project.entry-points."lms.djangoapp"]
45
45
  learning_credentials = "learning_credentials.apps:LearningCredentialsConfig"
46
46
 
47
- [project.entry-points."cms.djangoapp"]
48
- learning_credentials = "learning_credentials.apps:LearningCredentialsConfig"
47
+ # [project.entry-points."cms.djangoapp"]
48
+ # learning_credentials = "learning_credentials.apps:LearningCredentialsConfig"
49
49
 
50
50
  [project.entry-points."xblock.v1"]
51
51
  certificates = "learning_credentials.xblocks:CredentialsXBlock"
@@ -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."""