learning-credentials 0.4.1rc6__py3-none-any.whl → 0.5.0rc1__py3-none-any.whl

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.
@@ -7,8 +7,9 @@ import inspect
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from django import forms
10
- from django.contrib import admin
10
+ from django.contrib import admin, messages
11
11
  from django.core.exceptions import ValidationError
12
+ from django.urls import reverse
12
13
  from django.utils.html import format_html
13
14
  from django_object_actions import DjangoObjectActions, action
14
15
  from django_reverse_admin import ReverseModelAdmin
@@ -225,7 +226,7 @@ class CredentialConfigurationAdmin(DjangoObjectActions, ReverseModelAdmin):
225
226
 
226
227
 
227
228
  @admin.register(Credential)
228
- class CredentialAdmin(admin.ModelAdmin): # noqa: D101
229
+ class CredentialAdmin(DjangoObjectActions, admin.ModelAdmin): # noqa: D101
229
230
  list_display = (
230
231
  'user_id',
231
232
  'user_full_name',
@@ -237,19 +238,32 @@ class CredentialAdmin(admin.ModelAdmin): # noqa: D101
237
238
  'modified',
238
239
  )
239
240
  readonly_fields = (
241
+ 'uuid',
242
+ 'verify_uuid',
240
243
  'user_id',
241
244
  'created',
242
245
  'modified',
243
246
  'user_full_name',
244
247
  'learning_context_key',
248
+ 'learning_context_name',
245
249
  'credential_type',
246
250
  'status',
247
251
  'url',
248
252
  'legacy_id',
249
253
  'generation_task_id',
250
254
  )
251
- search_fields = ("learning_context_key", "user_id", "user_full_name")
255
+ search_fields = ("learning_context_key", "user_id", "user_full_name", "uuid")
252
256
  list_filter = ("learning_context_key", "credential_type", "status")
257
+ change_actions = ('reissue_credential',)
258
+
259
+ def save_model(self, request: HttpRequest, obj: Credential, _form: forms.ModelForm, _change: bool): # noqa: FBT001
260
+ """Display validation errors as messages in the admin interface."""
261
+ try:
262
+ obj.save()
263
+ except ValidationError as e:
264
+ self.message_user(request, e.message or "Invalid data", level=messages.ERROR)
265
+ # Optionally, redirect to the change form with the error message
266
+ return
253
267
 
254
268
  def get_form(self, request: HttpRequest, obj: Credential | None = None, **kwargs) -> forms.ModelForm:
255
269
  """Hide the download_url field."""
@@ -263,3 +277,16 @@ class CredentialAdmin(admin.ModelAdmin): # noqa: D101
263
277
  if obj.download_url:
264
278
  return format_html("<a href='{url}'>{url}</a>", url=obj.download_url)
265
279
  return "-"
280
+
281
+ @action(label="Reissue credential", description="Reissue the credential for the user.")
282
+ def reissue_credential(self, request: HttpRequest, obj: Credential):
283
+ """Reissue the credential for the user."""
284
+ try:
285
+ new_credential = obj.reissue()
286
+ admin_url = reverse('admin:learning_credentials_credential_change', args=[new_credential.pk])
287
+ message = format_html(
288
+ 'The credential has been reissued as <a href="{}">{}</a>.', admin_url, new_credential.uuid
289
+ )
290
+ messages.success(request, message)
291
+ except CredentialConfiguration.DoesNotExist:
292
+ messages.error(request, "The configuration does not exist.")
@@ -0,0 +1,13 @@
1
+ """API serializers for learning credentials."""
2
+
3
+ from rest_framework import serializers
4
+
5
+ from learning_credentials.models import Credential
6
+
7
+
8
+ class CredentialSerializer(serializers.ModelSerializer):
9
+ """Serializer that returns credential metadata."""
10
+
11
+ class Meta: # noqa: D106
12
+ model = Credential
13
+ fields = ('user_full_name', 'created', 'learning_context_name', 'status', 'invalidation_reason')
@@ -2,7 +2,7 @@
2
2
 
3
3
  from django.urls import path
4
4
 
5
- from .views import CredentialConfigurationCheckView
5
+ from .views import CredentialConfigurationCheckView, CredentialMetadataView
6
6
 
7
7
  urlpatterns = [
8
8
  path(
@@ -10,4 +10,5 @@ urlpatterns = [
10
10
  CredentialConfigurationCheckView.as_view(),
11
11
  name='credential_configuration_check',
12
12
  ),
13
+ path('metadata/<uuid:uuid>/', CredentialMetadataView.as_view(), name='credential-metadata'),
13
14
  ]
@@ -9,9 +9,10 @@ from rest_framework.permissions import IsAuthenticated
9
9
  from rest_framework.response import Response
10
10
  from rest_framework.views import APIView
11
11
 
12
- from learning_credentials.models import CredentialConfiguration
12
+ from learning_credentials.models import Credential, CredentialConfiguration
13
13
 
14
14
  from .permissions import CanAccessLearningContext
15
+ from .serializers import CredentialSerializer
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from rest_framework.request import Request
@@ -81,3 +82,62 @@ class CredentialConfigurationCheckView(APIView):
81
82
  }
82
83
 
83
84
  return Response(response_data, status=status.HTTP_200_OK)
85
+
86
+
87
+ class CredentialMetadataView(APIView):
88
+ """API view to retrieve credential metadata by UUID."""
89
+
90
+ @apidocs.schema(
91
+ parameters=[
92
+ apidocs.string_parameter(
93
+ "uuid",
94
+ ParameterLocation.PATH,
95
+ description="The UUID of the credential to retrieve.",
96
+ ),
97
+ ],
98
+ responses={
99
+ 200: "Successfully retrieved the credential metadata.",
100
+ 404: "Credential not found or not valid.",
101
+ },
102
+ )
103
+ def get(self, _request: "Request", uuid: str) -> Response:
104
+ """
105
+ Retrieve credential metadata by its UUID.
106
+
107
+ **Example Request**
108
+
109
+ ``GET /api/learning_credentials/v1/metadata/123e4567-e89b-12d3-a456-426614174000/``
110
+
111
+ **Response Values**
112
+
113
+ - **200 OK**: Successfully retrieved the credential metadata.
114
+ - **404 Not Found**: Credential not found or not valid.
115
+
116
+ **Example Response**
117
+
118
+ .. code-block:: json
119
+
120
+ {
121
+ "user_full_name": "John Doe",
122
+ "created": "2023-01-01",
123
+ "learning_context_name": "Demo Course",
124
+ "status": "available",
125
+ "invalidation_reason": ""
126
+ }
127
+
128
+
129
+ {
130
+ "user_full_name": "John Doe",
131
+ "created": "2023-01-01",
132
+ "learning_context_name": "Demo Course",
133
+ "status": "invalidated",
134
+ "invalidation_reason": "Reissued due to name change."
135
+ }
136
+ """
137
+ try:
138
+ credential = Credential.objects.get(verify_uuid=uuid)
139
+ except Credential.DoesNotExist:
140
+ return Response({'error': 'Credential not found.'}, status=status.HTTP_404_NOT_FOUND)
141
+
142
+ serializer = CredentialSerializer(credential)
143
+ return Response(serializer.data, status=status.HTTP_200_OK)
@@ -0,0 +1,94 @@
1
+ # Generated by Django 4.2.25 on 2025-10-31 17:43
2
+
3
+ from django.db import migrations, models
4
+ import uuid
5
+
6
+
7
+ def backfill_credential_fields(apps, schema_editor):
8
+ """Generate verification UUIDs and backfill learning_context_name for all existing credentials."""
9
+ from learning_credentials.compat import get_learning_context_name
10
+
11
+ Credential = apps.get_model("learning_credentials", "Credential")
12
+ for credential in Credential.objects.all():
13
+ credential.verify_uuid = uuid.uuid4()
14
+ credential.learning_context_name = get_learning_context_name(credential.learning_context_key)
15
+ credential.save(update_fields=["verify_uuid", "learning_context_name"])
16
+
17
+
18
+ class Migration(migrations.Migration):
19
+ dependencies = [
20
+ ("learning_credentials", "0007_migrate_to_text_elements_format"),
21
+ ]
22
+
23
+ operations = [
24
+ migrations.AlterUniqueTogether(
25
+ name="credential",
26
+ unique_together=set(),
27
+ ),
28
+ migrations.AlterField(
29
+ model_name="credential",
30
+ name="user_full_name",
31
+ field=models.CharField(
32
+ editable=False,
33
+ help_text="User receiving the credential. This field is used for validation purposes.",
34
+ max_length=255,
35
+ ),
36
+ ),
37
+ migrations.AddField(
38
+ model_name="credential",
39
+ name="invalidated_at",
40
+ field=models.DateTimeField(
41
+ editable=False,
42
+ help_text="Timestamp when the credential was invalidated",
43
+ null=True,
44
+ ),
45
+ ),
46
+ migrations.AddField(
47
+ model_name="credential",
48
+ name="invalidation_reason",
49
+ field=models.CharField(
50
+ blank=True,
51
+ help_text="Reason for invalidating the credential",
52
+ max_length=255,
53
+ ),
54
+ ),
55
+ migrations.AddField(
56
+ model_name="credential",
57
+ name="learning_context_name",
58
+ field=models.CharField(
59
+ editable=False,
60
+ help_text="Name of the learning context for which the credential was issued. This field is used for validation purposes.",
61
+ max_length=255,
62
+ null=True,
63
+ ),
64
+ ),
65
+ migrations.AddField(
66
+ model_name="credential",
67
+ name="verify_uuid",
68
+ field=models.UUIDField(
69
+ default=uuid.uuid4,
70
+ editable=False,
71
+ help_text="UUID used for verifying the credential",
72
+ null=True,
73
+ ),
74
+ ),
75
+ migrations.RunPython(backfill_credential_fields, reverse_code=migrations.RunPython.noop),
76
+ migrations.AlterField(
77
+ model_name="credential",
78
+ name="learning_context_name",
79
+ field=models.CharField(
80
+ editable=False,
81
+ help_text="Name of the learning context for which the credential was issued. This field is used for validation purposes.",
82
+ max_length=255,
83
+ ),
84
+ ),
85
+ migrations.AlterField(
86
+ model_name="credential",
87
+ name="verify_uuid",
88
+ field=models.UUIDField(
89
+ default=uuid.uuid4,
90
+ editable=False,
91
+ help_text="UUID used for verifying the credential",
92
+ ),
93
+ ),
94
+ ]
@@ -4,10 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import logging
7
- import uuid
7
+ import uuid as uuid_lib
8
8
  from importlib import import_module
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING
10
+ from typing import TYPE_CHECKING, Self
11
11
 
12
12
  import jsonfield
13
13
  from django.conf import settings
@@ -16,6 +16,7 @@ from django.core.exceptions import ValidationError
16
16
  from django.db import models
17
17
  from django.db.models.signals import post_delete
18
18
  from django.dispatch import receiver
19
+ from django.utils import timezone
19
20
  from django.utils.translation import gettext_lazy as _
20
21
  from django_celery_beat.models import IntervalSchedule, PeriodicTask
21
22
  from edx_ace import Message, Recipient, ace
@@ -147,9 +148,8 @@ class CredentialConfiguration(TimeStampedModel):
147
148
  self.periodic_task.args = json.dumps([self.id])
148
149
  self.periodic_task.save()
149
150
 
150
- # Replace the return type with `QuerySet[Self]` after migrating to Python 3.10+.
151
151
  @classmethod
152
- def get_enabled_configurations(cls) -> QuerySet[CredentialConfiguration]:
152
+ def get_enabled_configurations(cls) -> QuerySet[Self]:
153
153
  """
154
154
  Get the list of enabled configurations.
155
155
 
@@ -198,7 +198,7 @@ class CredentialConfiguration(TimeStampedModel):
198
198
  custom_options = _deep_merge(self.credential_type.custom_options, self.custom_options)
199
199
  return func(self.learning_context_key, custom_options)
200
200
 
201
- def generate_credential_for_user(self, user_id: int, celery_task_id: int = 0):
201
+ def generate_credential_for_user(self, user_id: int, celery_task_id: int = 0) -> Credential:
202
202
  """
203
203
  Celery task for processing a single user's credential.
204
204
 
@@ -209,19 +209,24 @@ class CredentialConfiguration(TimeStampedModel):
209
209
  Args:
210
210
  user_id: The ID of the user to process the credential for.
211
211
  celery_task_id (optional): The ID of the Celery task that is running this function.
212
+
213
+ Returns:
214
+ The generated Credential object.
212
215
  """
213
216
  user = get_user_model().objects.get(id=user_id)
214
217
  # Use the name from the profile if it is not empty. Otherwise, use the first and last name.
215
218
  # We check if the profile exists because it may not exist in some cases (e.g., when a User is created manually).
216
219
  user_full_name = getattr(getattr(user, 'profile', None), 'name', f"{user.first_name} {user.last_name}")
220
+ learning_context_name = get_learning_context_name(self.learning_context_key)
217
221
  custom_options = _deep_merge(self.credential_type.custom_options, self.custom_options)
218
222
 
219
- credential, _ = Credential.objects.update_or_create(
223
+ credential, _ = Credential.objects.exclude(status=Credential.Status.INVALIDATED).update_or_create(
220
224
  user_id=user_id,
221
225
  learning_context_key=self.learning_context_key,
222
226
  credential_type=self.credential_type.name,
223
227
  defaults={
224
228
  'user_full_name': user_full_name,
229
+ 'learning_context_name': learning_context_name,
225
230
  'status': Credential.Status.GENERATING,
226
231
  'generation_task_id': celery_task_id,
227
232
  },
@@ -247,6 +252,8 @@ class CredentialConfiguration(TimeStampedModel):
247
252
  if user.is_active and user.has_usable_password():
248
253
  credential.send_email()
249
254
 
255
+ return credential
256
+
250
257
 
251
258
  # noinspection PyUnusedLocal
252
259
  @receiver(post_delete, sender=CredentialConfiguration)
@@ -281,16 +288,33 @@ class Credential(TimeStampedModel):
281
288
 
282
289
  uuid = models.UUIDField(
283
290
  primary_key=True,
284
- default=uuid.uuid4,
291
+ default=uuid_lib.uuid4,
285
292
  editable=False,
286
293
  help_text=_('Auto-generated UUID of the credential'),
287
294
  )
295
+ verify_uuid = models.UUIDField(
296
+ default=uuid_lib.uuid4,
297
+ editable=False,
298
+ help_text=_('UUID used for verifying the credential'),
299
+ )
288
300
  user_id = models.IntegerField(help_text=_('ID of the user receiving the credential'))
289
- user_full_name = models.CharField(max_length=255, help_text=_('User receiving the credential'))
301
+ user_full_name = models.CharField(
302
+ max_length=255,
303
+ editable=False,
304
+ help_text=_('User receiving the credential. This field is used for validation purposes.'),
305
+ )
290
306
  learning_context_key = LearningContextKeyField(
291
307
  max_length=255,
292
308
  help_text=_('ID of a learning context (e.g., a course or a Learning Path) for which the credential was issued'),
293
309
  )
310
+ learning_context_name = models.CharField(
311
+ max_length=255,
312
+ editable=False,
313
+ help_text=_(
314
+ 'Name of the learning context for which the credential was issued. '
315
+ 'This field is used for validation purposes.'
316
+ ),
317
+ )
294
318
  credential_type = models.CharField(max_length=255, help_text=_('Type of the credential'))
295
319
  status = models.CharField(
296
320
  max_length=32,
@@ -301,16 +325,26 @@ class Credential(TimeStampedModel):
301
325
  download_url = models.URLField(blank=True, help_text=_('URL of the generated credential PDF (e.g., to S3)'))
302
326
  legacy_id = models.IntegerField(null=True, help_text=_('Legacy ID of the credential imported from another system'))
303
327
  generation_task_id = models.CharField(max_length=255, help_text=_('Task ID from the Celery queue'))
304
-
305
- class Meta: # noqa: D106
306
- unique_together = (('user_id', 'learning_context_key', 'credential_type'),)
328
+ invalidated_at = models.DateTimeField(
329
+ null=True, editable=False, help_text=_('Timestamp when the credential was invalidated')
330
+ )
331
+ invalidation_reason = models.CharField(
332
+ max_length=255, blank=True, help_text=_('Reason for invalidating the credential')
333
+ )
307
334
 
308
335
  def __str__(self): # noqa: D105
309
336
  return f"{self.credential_type} for {self.user_full_name} in {self.learning_context_key}"
310
337
 
338
+ def save(self, *args, **kwargs):
339
+ """If the invalidation reason is set, update the status and timestamp."""
340
+ if self.invalidation_reason and self.status != Credential.Status.INVALIDATED:
341
+ self.status = Credential.Status.INVALIDATED
342
+ if self.status == Credential.Status.INVALIDATED and not self.invalidated_at:
343
+ self.invalidated_at = timezone.now()
344
+ super().save(*args, **kwargs)
345
+
311
346
  def send_email(self):
312
347
  """Send a credential link to the student."""
313
- learning_context_name = get_learning_context_name(self.learning_context_key)
314
348
  user = get_user_model().objects.get(id=self.user_id)
315
349
  msg = Message(
316
350
  name="certificate_generated",
@@ -319,12 +353,26 @@ class Credential(TimeStampedModel):
319
353
  language='en',
320
354
  context={
321
355
  'certificate_link': self.download_url,
322
- 'course_name': learning_context_name,
356
+ 'course_name': self.learning_context_name,
323
357
  'platform_name': settings.PLATFORM_NAME,
324
358
  },
325
359
  )
326
360
  ace.send(msg)
327
361
 
362
+ def reissue(self) -> Self:
363
+ """Invalidate the current credential and create a new one."""
364
+ config = CredentialConfiguration.objects.get(
365
+ learning_context_key=self.learning_context_key,
366
+ credential_type__name=self.credential_type,
367
+ )
368
+
369
+ if self.invalidation_reason:
370
+ self.invalidation_reason += '\n'
371
+ self.invalidation_reason += 'Reissued'
372
+ self.save()
373
+
374
+ return config.generate_credential_for_user(self.user_id)
375
+
328
376
 
329
377
  class CredentialAsset(TimeStampedModel):
330
378
  """
@@ -0,0 +1,83 @@
1
+ {% extends "main_django.html" %}
2
+ {% load i18n %}
3
+ {% load static %}
4
+
5
+ {% block bodyextra %}
6
+ <div id="content" class="credential-verification-container content-wrapper main-container">
7
+ <section class="container">
8
+ <h1>{% trans "Credential verification page" %}</h1>
9
+ <form id="credentialVerificationForm" method="post">
10
+ {% csrf_token %}
11
+ <div class="form-group">
12
+ <label for="credentialID">{% trans 'Credential ID' %}</label>
13
+ <input type="text" id="credentialID" class="form-control" name="credentialID" required>
14
+ <div id="formError" class="text-danger mt-2" style="display: none;"></div>
15
+ </div>
16
+ <button type="submit" class="btn btn-primary">
17
+ {% trans 'Verify' %}
18
+ </button>
19
+ </form>
20
+
21
+ <div id="verificationResults" class="p-0 border-0 mt-5"></div>
22
+ </section>
23
+ </div>
24
+
25
+ <script>
26
+ document.getElementById('credentialVerificationForm').onsubmit = async function(event) {
27
+ event.preventDefault();
28
+ let formError = document.getElementById('formError');
29
+ let verificationResults = document.getElementById('verificationResults');
30
+
31
+ let credentialId = document.getElementById('credentialID').value;
32
+ var url = '/api/learning_credentials/v1/metadata/' + encodeURIComponent(credentialId);
33
+
34
+ // Hide previous error messages.
35
+ formError.style.display = 'none';
36
+
37
+ await fetch(url)
38
+ .then(response => response.json())
39
+ .then(data => {
40
+ const table = document.createElement('table');
41
+ table.className = 'table table-striped table-bordered';
42
+ const tbody = document.createElement('tbody');
43
+ Object.entries(data).forEach(([key, value]) => {
44
+ if (!!value) {
45
+ const row = tbody.insertRow();
46
+ const cellKey = row.insertCell();
47
+ const cellValue = row.insertCell();
48
+ cellKey.textContent = key.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
49
+ cellValue.textContent = value;
50
+ }
51
+ });
52
+ table.appendChild(tbody);
53
+ verificationResults.textContent = '';
54
+ verificationResults.appendChild(table);
55
+ })
56
+ .catch((error) => {
57
+ console.error('Error:', error);
58
+ verificationResults.textContent = '';
59
+ formError.innerHTML = "An error occurred during verification. Please check the Credential ID and try again.";
60
+ formError.style.display = 'block';
61
+ });
62
+ }
63
+
64
+
65
+ $(document).ready(function(){
66
+ // Fill the credential ID field with URL 'credential' query param.
67
+ let urlParams = new URLSearchParams(window.location.search);
68
+ const uuid = urlParams.get('credential');
69
+
70
+ if (uuid) {
71
+ $('#credentialID').val(uuid);
72
+ }
73
+
74
+ // If the field is filled, submit the form automatically.
75
+ if ($('#credentialID').val()) {
76
+ $('#credentialVerificationForm').submit();
77
+ }
78
+
79
+ // Focus the field for user convenience.
80
+ $('#credentialID').focus();
81
+ });
82
+ </script>
83
+ {% endblock %}
@@ -1,9 +1,11 @@
1
1
  """URLs for learning_credentials."""
2
2
 
3
3
  from django.urls import include, path
4
+ from django.views.generic import TemplateView
4
5
 
5
6
  from .api import urls as api_urls
6
7
 
7
8
  urlpatterns = [
8
9
  path('api/learning_credentials/', include(api_urls)),
10
+ path('learning_credentials/verify/', TemplateView.as_view(template_name="learning_credentials/verify.html")),
9
11
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.4.1rc6
3
+ Version: 0.5.0rc1
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
@@ -11,6 +11,7 @@ Keywords: Python,edx,credentials,django
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Framework :: Django
13
13
  Classifier: Framework :: Django :: 4.2
14
+ Classifier: Framework :: Django :: 5.2
14
15
  Classifier: Intended Audience :: Developers
15
16
  Classifier: Natural Language :: English
16
17
  Classifier: Programming Language :: Python :: 3
@@ -176,7 +177,16 @@ Unreleased
176
177
 
177
178
  *
178
179
 
179
- 0.4.1 - 2025-12-31
180
+ 0.5.0 - 2026-01-29
181
+ ******************
182
+
183
+ Added
184
+ =====
185
+
186
+ * Frontend form and backend API endpoint for verifying credentials.
187
+ * Option to invalidate issued credentials.
188
+
189
+ 0.4.0 - 2026-01-28
180
190
  ******************
181
191
 
182
192
  Added
@@ -1,19 +1,20 @@
1
1
  learning_credentials/__init__.py,sha256=8Q0-3Hdnfmcj41EKu1GSfzEfwWcYNDlItyEEke2r9bs,62
2
- learning_credentials/admin.py,sha256=ynK3tVJwLsIeV7Jk66t1FAVyVsU1G-KRIAdRkycVTmA,10439
2
+ learning_credentials/admin.py,sha256=gLVpCn5oOHLL3u-wnx4R1yJXfar1Z32vk8zcTVjtBFY,11791
3
3
  learning_credentials/apps.py,sha256=trdQxe-JRhUdUaOQoQWiGL1sn6I1sfDiTvdCwy8yGuw,1037
4
4
  learning_credentials/compat.py,sha256=bTAB6bTh99ZyhUqOsDtM_BuIPzFxCjySFtfvc-_fCd4,4731
5
5
  learning_credentials/exceptions.py,sha256=UaqBVXFMWR2Iob7_LMb3j4NNVmWQFAgLi_MNMRUvGsI,290
6
6
  learning_credentials/generators.py,sha256=gaw3zoEzVhSjo96QM6Q6K70e_iK44LxCuQIQ05p7lP0,14895
7
- learning_credentials/models.py,sha256=t_FahXhxa6IsIQl2nqzgcE4XVGdyb5jVGXMLWjJd5cI,16679
7
+ learning_credentials/models.py,sha256=6ObPBesQW7RPXOH1bnQf4ZEWoXwHfwN3K_OmvZZxW8E,18476
8
8
  learning_credentials/processors.py,sha256=LkdjmkLBnXc9qeMcksB1T8AQ5ZhYaECyQO__KfHB_aU,15212
9
9
  learning_credentials/tasks.py,sha256=byoFEUvN_ayVaU5K5SlEiA7vu9BRPaSSmKnB9g5toec,1927
10
- learning_credentials/urls.py,sha256=gO_c930rzMylP-riQ9SGHXH9JIMF7ajySDT2Tc-E8x4,188
10
+ learning_credentials/urls.py,sha256=KXZtvPXXl2X_nTREWaCFxcAgY2XET1eWRbcx2rq_6eI,348
11
11
  learning_credentials/api/__init__.py,sha256=q8sLFfwo5RwQu8FY6BJUL_Jrt3TUojbZK-Zlw9v08EM,40
12
12
  learning_credentials/api/urls.py,sha256=wW27hrrJ7D_h8PbFDbSxzeaneNla0R-56gjKy9zISG8,216
13
13
  learning_credentials/api/v1/__init__.py,sha256=A7ZqENtM4QM1A7j_cAfnzw4zn0kuyfXSWtylFIE0_f8,43
14
14
  learning_credentials/api/v1/permissions.py,sha256=TqM50TpR3JGUgZgIgKZF0-R_g1_P2V9bqKzYXgk-VvY,3436
15
- learning_credentials/api/v1/urls.py,sha256=6YqLS4aVfA1cuLOgVe4lFUFa38wVehYKleXBF8ImMm0,287
16
- learning_credentials/api/v1/views.py,sha256=rkdj1AfRBDzrpRC5uGMAxTUf4P1zs-MSF9lpNMIgYLw,3005
15
+ learning_credentials/api/v1/serializers.py,sha256=H7l-vRTwLBplveCBjnNgSawJqpSVskeHTz7wpUiNB3g,417
16
+ learning_credentials/api/v1/urls.py,sha256=RytArViuKZQkWs46sk58VfaVCwLV-QrgTG7cQLE_NtU,408
17
+ learning_credentials/api/v1/views.py,sha256=CJEVPwCXs_ii463agPRpJeX6NCgyyFX9ZIBJh0BAc9I,4926
17
18
  learning_credentials/conf/locale/config.yaml,sha256=jPen2DmckNDKK30axCKEd2Q2ha9oOG3IBxrJ63Pvznk,2280
18
19
  learning_credentials/migrations/0001_initial.py,sha256=61EvThCv-0UAnhCE5feyQVfjRodbp-6cDaAr4CY5PMA,8435
19
20
  learning_credentials/migrations/0002_migrate_to_learning_credentials.py,sha256=vUhcnQKDdwOsppkXsjz2zZwOGMwIJ-fkQRsaj-K7l1o,1779
@@ -22,19 +23,21 @@ learning_credentials/migrations/0004_replace_course_keys_with_learning_context_k
22
23
  learning_credentials/migrations/0005_rename_processors_and_generators.py,sha256=5UCqjq-CBJnRo1qBAoWs91ngyEuSMN8_tQtfzsuR5SI,5271
23
24
  learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py,sha256=aJs_gOP4TmW9J-Dmr21m94jBfLQxzjAu6-ua7x4uYLE,727
24
25
  learning_credentials/migrations/0007_migrate_to_text_elements_format.py,sha256=_olkaxPPuRys2c2X5fnyQIFVvqEfdoYu-JlApmXuHEM,4758
26
+ learning_credentials/migrations/0008_validation.py,sha256=jcTg4Lnlmcyp1Czc9b-52gPJ_s8W7Dwodvi_LggpVjw,3387
25
27
  learning_credentials/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
28
  learning_credentials/settings/__init__.py,sha256=tofc5eg3Q2lV13Ff_jjg1ggGgWpKYoeESkP1qxl3H_A,29
27
29
  learning_credentials/settings/common.py,sha256=Cck-nyFt11G1NLiz-bHfKJp8MV6sDZGqTwdbC8_1WE0,360
28
30
  learning_credentials/settings/production.py,sha256=6P0P7JxbpWNsk4Lk8lfyxHirOWMgU4UWOb3EYKLjiVQ,542
29
31
  learning_credentials/templates/learning_credentials/base.html,sha256=wtjBYqfHmOnyEY5tN3VGOmzYLsOD24MXdEUhTZ7OmwI,662
32
+ learning_credentials/templates/learning_credentials/verify.html,sha256=vXTiZMZkTLhp6cnqKBJcQnDu7qUxLvrTzN2m1KM8_9k,3414
30
33
  learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html,sha256=t-i1Ra9AC4pX-rPRifDJIvBBZuxCxdrFqg1NKTjHBOk,813
31
34
  learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt,sha256=IF_x8aF_-dORlQB-RCh0IkJDl2ktD489E8qGgLe9M3Y,677
32
35
  learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt,sha256=-n8tjPSwfwAfeOSZ1WhcCTrpOah4VswzMZ5mh63Pxow,20
33
36
  learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
37
  learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt,sha256=S7Hc5T_sZSsSBXm5_H5HBNNv16Ohl0oZn0nVqqeWL0g,132
35
- learning_credentials-0.4.1rc6.dist-info/licenses/LICENSE.txt,sha256=GDpsPnW_1NKhPvZpZL9imz25P2nIpbwJPEhrlq4vPAU,34523
36
- learning_credentials-0.4.1rc6.dist-info/METADATA,sha256=_3vW2RqkLIUwho5al3zefc877eWnW9qLPOB-ZjsOvDg,8258
37
- learning_credentials-0.4.1rc6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- learning_credentials-0.4.1rc6.dist-info/entry_points.txt,sha256=hHqqLUEdzAN24v5OGBX9Fr-wh3ATDPjQjByKz03eC2Y,91
39
- learning_credentials-0.4.1rc6.dist-info/top_level.txt,sha256=Ce-4_leZe_nny7CpmkeRiemcDV6jIHpIvLjlcQBuf18,21
40
- learning_credentials-0.4.1rc6.dist-info/RECORD,,
38
+ learning_credentials-0.5.0rc1.dist-info/licenses/LICENSE.txt,sha256=GDpsPnW_1NKhPvZpZL9imz25P2nIpbwJPEhrlq4vPAU,34523
39
+ learning_credentials-0.5.0rc1.dist-info/METADATA,sha256=8O8HFW30e2DKY_0Yzirkh_Nt0jQV7aWyCEBluO_Erfg,8461
40
+ learning_credentials-0.5.0rc1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
41
+ learning_credentials-0.5.0rc1.dist-info/entry_points.txt,sha256=hHqqLUEdzAN24v5OGBX9Fr-wh3ATDPjQjByKz03eC2Y,91
42
+ learning_credentials-0.5.0rc1.dist-info/top_level.txt,sha256=Ce-4_leZe_nny7CpmkeRiemcDV6jIHpIvLjlcQBuf18,21
43
+ learning_credentials-0.5.0rc1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5