learning-credentials 0.4.1rc6__py3-none-any.whl → 0.5.0rc3__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.
- learning_credentials/admin.py +59 -10
- learning_credentials/api/v1/serializers.py +13 -0
- learning_credentials/api/v1/urls.py +2 -1
- learning_credentials/api/v1/views.py +61 -1
- learning_credentials/compat.py +13 -7
- learning_credentials/generators.py +81 -39
- learning_credentials/migrations/0001_squashed_0010.py +265 -0
- learning_credentials/migrations/0008_validation.py +94 -0
- learning_credentials/migrations/0009_credential_user_fk.py +63 -0
- learning_credentials/migrations/0010_credential_configuration_fk.py +79 -0
- learning_credentials/models.py +84 -27
- learning_credentials/templates/learning_credentials/verify.html +83 -0
- learning_credentials/urls.py +2 -0
- {learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/METADATA +12 -2
- {learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/RECORD +19 -13
- {learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/WHEEL +1 -1
- {learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/entry_points.txt +0 -0
- {learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/licenses/LICENSE.txt +0 -0
- {learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/top_level.txt +0 -0
learning_credentials/models.py
CHANGED
|
@@ -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[
|
|
152
|
+
def get_enabled_configurations(cls) -> QuerySet[Self]:
|
|
153
153
|
"""
|
|
154
154
|
Get the list of enabled configurations.
|
|
155
155
|
|
|
@@ -176,8 +176,7 @@ class CredentialConfiguration(TimeStampedModel):
|
|
|
176
176
|
2. Have such a credential with an error status.
|
|
177
177
|
"""
|
|
178
178
|
users_ids_with_credentials = Credential.objects.filter(
|
|
179
|
-
models.Q(
|
|
180
|
-
models.Q(credential_type=self.credential_type),
|
|
179
|
+
models.Q(configuration=self),
|
|
181
180
|
~(models.Q(status=Credential.Status.ERROR)),
|
|
182
181
|
).values_list('user_id', flat=True)
|
|
183
182
|
|
|
@@ -198,7 +197,7 @@ class CredentialConfiguration(TimeStampedModel):
|
|
|
198
197
|
custom_options = _deep_merge(self.credential_type.custom_options, self.custom_options)
|
|
199
198
|
return func(self.learning_context_key, custom_options)
|
|
200
199
|
|
|
201
|
-
def generate_credential_for_user(self, user_id: int, celery_task_id: int = 0):
|
|
200
|
+
def generate_credential_for_user(self, user_id: int, celery_task_id: int = 0) -> Credential:
|
|
202
201
|
"""
|
|
203
202
|
Celery task for processing a single user's credential.
|
|
204
203
|
|
|
@@ -209,19 +208,23 @@ class CredentialConfiguration(TimeStampedModel):
|
|
|
209
208
|
Args:
|
|
210
209
|
user_id: The ID of the user to process the credential for.
|
|
211
210
|
celery_task_id (optional): The ID of the Celery task that is running this function.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
The generated Credential object.
|
|
212
214
|
"""
|
|
213
215
|
user = get_user_model().objects.get(id=user_id)
|
|
214
216
|
# Use the name from the profile if it is not empty. Otherwise, use the first and last name.
|
|
215
217
|
# We check if the profile exists because it may not exist in some cases (e.g., when a User is created manually).
|
|
216
218
|
user_full_name = getattr(getattr(user, 'profile', None), 'name', f"{user.first_name} {user.last_name}")
|
|
219
|
+
learning_context_name = get_learning_context_name(self.learning_context_key)
|
|
217
220
|
custom_options = _deep_merge(self.credential_type.custom_options, self.custom_options)
|
|
218
221
|
|
|
219
|
-
credential, _ = Credential.objects.update_or_create(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
credential_type=self.credential_type.name,
|
|
222
|
+
credential, _ = Credential.objects.exclude(status=Credential.Status.INVALIDATED).update_or_create(
|
|
223
|
+
user=user,
|
|
224
|
+
configuration=self,
|
|
223
225
|
defaults={
|
|
224
226
|
'user_full_name': user_full_name,
|
|
227
|
+
'learning_context_name': learning_context_name,
|
|
225
228
|
'status': Credential.Status.GENERATING,
|
|
226
229
|
'generation_task_id': celery_task_id,
|
|
227
230
|
},
|
|
@@ -233,13 +236,13 @@ class CredentialConfiguration(TimeStampedModel):
|
|
|
233
236
|
generation_func = getattr(generation_module, generation_func_name)
|
|
234
237
|
|
|
235
238
|
# Run the functions. We do not validate them here, as they are validated in the model's clean() method.
|
|
236
|
-
credential.download_url = generation_func(
|
|
239
|
+
credential.download_url = generation_func(credential, custom_options)
|
|
237
240
|
credential.status = Credential.Status.AVAILABLE
|
|
238
241
|
credential.save()
|
|
239
242
|
except Exception as exc:
|
|
240
243
|
credential.status = Credential.Status.ERROR
|
|
241
244
|
credential.save()
|
|
242
|
-
msg = f'Failed to generate the {credential.uuid=} for {user_id=} with {self.id=}
|
|
245
|
+
msg = f'Failed to generate the {credential.uuid=} for {user_id=} with {self.id=}.\nReason: {exc}'
|
|
243
246
|
raise CredentialGenerationError(msg) from exc
|
|
244
247
|
else:
|
|
245
248
|
# TODO: In the future, we want to check this before generating the credential.
|
|
@@ -247,6 +250,8 @@ class CredentialConfiguration(TimeStampedModel):
|
|
|
247
250
|
if user.is_active and user.has_usable_password():
|
|
248
251
|
credential.send_email()
|
|
249
252
|
|
|
253
|
+
return credential
|
|
254
|
+
|
|
250
255
|
|
|
251
256
|
# noinspection PyUnusedLocal
|
|
252
257
|
@receiver(post_delete, sender=CredentialConfiguration)
|
|
@@ -281,17 +286,38 @@ class Credential(TimeStampedModel):
|
|
|
281
286
|
|
|
282
287
|
uuid = models.UUIDField(
|
|
283
288
|
primary_key=True,
|
|
284
|
-
default=
|
|
289
|
+
default=uuid_lib.uuid4,
|
|
285
290
|
editable=False,
|
|
286
291
|
help_text=_('Auto-generated UUID of the credential'),
|
|
287
292
|
)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
293
|
+
verify_uuid = models.UUIDField(
|
|
294
|
+
default=uuid_lib.uuid4,
|
|
295
|
+
editable=False,
|
|
296
|
+
help_text=_('UUID used for verifying the credential'),
|
|
297
|
+
)
|
|
298
|
+
user = models.ForeignKey(
|
|
299
|
+
settings.AUTH_USER_MODEL,
|
|
300
|
+
on_delete=models.CASCADE,
|
|
301
|
+
help_text=_('User receiving the credential'),
|
|
302
|
+
)
|
|
303
|
+
user_full_name = models.CharField(
|
|
291
304
|
max_length=255,
|
|
292
|
-
|
|
305
|
+
editable=False,
|
|
306
|
+
help_text=_('User receiving the credential. This field is used for validation purposes.'),
|
|
307
|
+
)
|
|
308
|
+
learning_context_name = models.CharField(
|
|
309
|
+
max_length=255,
|
|
310
|
+
editable=False,
|
|
311
|
+
help_text=_(
|
|
312
|
+
'Name of the learning context for which the credential was issued. '
|
|
313
|
+
'This field is used for validation purposes.'
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
configuration = models.ForeignKey(
|
|
317
|
+
'CredentialConfiguration',
|
|
318
|
+
on_delete=models.PROTECT,
|
|
319
|
+
help_text=_('Associated credential configuration'),
|
|
293
320
|
)
|
|
294
|
-
credential_type = models.CharField(max_length=255, help_text=_('Type of the credential'))
|
|
295
321
|
status = models.CharField(
|
|
296
322
|
max_length=32,
|
|
297
323
|
choices=Status.choices,
|
|
@@ -301,30 +327,61 @@ class Credential(TimeStampedModel):
|
|
|
301
327
|
download_url = models.URLField(blank=True, help_text=_('URL of the generated credential PDF (e.g., to S3)'))
|
|
302
328
|
legacy_id = models.IntegerField(null=True, help_text=_('Legacy ID of the credential imported from another system'))
|
|
303
329
|
generation_task_id = models.CharField(max_length=255, help_text=_('Task ID from the Celery queue'))
|
|
330
|
+
invalidated_at = models.DateTimeField(
|
|
331
|
+
null=True, editable=False, help_text=_('Timestamp when the credential was invalidated')
|
|
332
|
+
)
|
|
333
|
+
invalidation_reason = models.CharField(
|
|
334
|
+
max_length=255, blank=True, help_text=_('Reason for invalidating the credential')
|
|
335
|
+
)
|
|
304
336
|
|
|
305
|
-
|
|
306
|
-
|
|
337
|
+
def __str__(self):
|
|
338
|
+
"""Get a string representation of this model's instance."""
|
|
339
|
+
return (
|
|
340
|
+
f"{self.configuration.credential_type.name} for {self.user_full_name} "
|
|
341
|
+
f"in {self.configuration.learning_context_key}"
|
|
342
|
+
)
|
|
307
343
|
|
|
308
|
-
def
|
|
309
|
-
|
|
344
|
+
def save(self, *args, **kwargs):
|
|
345
|
+
"""If the invalidation reason is set, trigger the invalidation and update the timestamp."""
|
|
346
|
+
if self.invalidation_reason and self.status != Credential.Status.INVALIDATED:
|
|
347
|
+
self._invalidate()
|
|
348
|
+
if self.status == Credential.Status.INVALIDATED and not self.invalidated_at:
|
|
349
|
+
self.invalidated_at = timezone.now()
|
|
350
|
+
super().save(*args, **kwargs)
|
|
351
|
+
|
|
352
|
+
def _invalidate(self):
|
|
353
|
+
"""Trigger the invalidation process for the credential."""
|
|
354
|
+
generation_module_name, generation_func_name = self.configuration.credential_type.generation_func.rsplit('.', 1)
|
|
355
|
+
generation_module = import_module(generation_module_name)
|
|
356
|
+
generation_func = getattr(generation_module, generation_func_name)
|
|
357
|
+
|
|
358
|
+
self.download_url = generation_func(self, {}, invalidate=True)
|
|
359
|
+
self.status = Credential.Status.INVALIDATED
|
|
310
360
|
|
|
311
361
|
def send_email(self):
|
|
312
362
|
"""Send a credential link to the student."""
|
|
313
|
-
learning_context_name = get_learning_context_name(self.learning_context_key)
|
|
314
|
-
user = get_user_model().objects.get(id=self.user_id)
|
|
315
363
|
msg = Message(
|
|
316
364
|
name="certificate_generated",
|
|
317
365
|
app_label="learning_credentials",
|
|
318
|
-
recipient=Recipient(lms_user_id=user.id, email_address=user.email),
|
|
366
|
+
recipient=Recipient(lms_user_id=self.user.id, email_address=self.user.email),
|
|
319
367
|
language='en',
|
|
320
368
|
context={
|
|
321
369
|
'certificate_link': self.download_url,
|
|
322
|
-
'course_name': learning_context_name,
|
|
370
|
+
'course_name': self.learning_context_name,
|
|
323
371
|
'platform_name': settings.PLATFORM_NAME,
|
|
324
372
|
},
|
|
325
373
|
)
|
|
326
374
|
ace.send(msg)
|
|
327
375
|
|
|
376
|
+
def reissue(self) -> Self:
|
|
377
|
+
"""Invalidate the current credential and create a new one."""
|
|
378
|
+
if self.invalidation_reason:
|
|
379
|
+
self.invalidation_reason += '\n'
|
|
380
|
+
self.invalidation_reason += 'Reissued'
|
|
381
|
+
self.save()
|
|
382
|
+
|
|
383
|
+
return self.configuration.generate_credential_for_user(self.user.id)
|
|
384
|
+
|
|
328
385
|
|
|
329
386
|
class CredentialAsset(TimeStampedModel):
|
|
330
387
|
"""
|
|
@@ -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 %}
|
learning_credentials/urls.py
CHANGED
|
@@ -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
|
]
|
{learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-credentials
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0rc3
|
|
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.
|
|
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,40 +1,46 @@
|
|
|
1
1
|
learning_credentials/__init__.py,sha256=8Q0-3Hdnfmcj41EKu1GSfzEfwWcYNDlItyEEke2r9bs,62
|
|
2
|
-
learning_credentials/admin.py,sha256=
|
|
2
|
+
learning_credentials/admin.py,sha256=AkbLqeFyGEmEsv-EGU4tD3KeCOd6ldvopg2uj_sRUAA,12558
|
|
3
3
|
learning_credentials/apps.py,sha256=trdQxe-JRhUdUaOQoQWiGL1sn6I1sfDiTvdCwy8yGuw,1037
|
|
4
|
-
learning_credentials/compat.py,sha256=
|
|
4
|
+
learning_credentials/compat.py,sha256=EVUxlybL3ugzquPgL0sst7yAQ9qpyAplloTzS52jF5o,4826
|
|
5
5
|
learning_credentials/exceptions.py,sha256=UaqBVXFMWR2Iob7_LMb3j4NNVmWQFAgLi_MNMRUvGsI,290
|
|
6
|
-
learning_credentials/generators.py,sha256=
|
|
7
|
-
learning_credentials/models.py,sha256=
|
|
6
|
+
learning_credentials/generators.py,sha256=2tAeqmEaK5zaygloLXQNHjDXMAalYlwJprx1KmL1pUY,16969
|
|
7
|
+
learning_credentials/models.py,sha256=JF2xtZWDCA7WRY0kBopzxgazPngpqHAM67wdPwdis1M,18602
|
|
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=
|
|
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/
|
|
16
|
-
learning_credentials/api/v1/
|
|
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
|
|
20
|
+
learning_credentials/migrations/0001_squashed_0010.py,sha256=STQ4MRVfW7F-Dfqb8DrdiezQavn0wlwWfjGpGFmDcqc,10944
|
|
19
21
|
learning_credentials/migrations/0002_migrate_to_learning_credentials.py,sha256=vUhcnQKDdwOsppkXsjz2zZwOGMwIJ-fkQRsaj-K7l1o,1779
|
|
20
22
|
learning_credentials/migrations/0003_rename_certificates_to_credentials.py,sha256=YqSaHTB60VNc9k245um2GYVDH6J0l9BrN3ak6WKljjk,4677
|
|
21
23
|
learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py,sha256=5KaXvASl69qbEaHX5_Ty_3Dr7K4WV6p8VWOx72yJnTU,1919
|
|
22
24
|
learning_credentials/migrations/0005_rename_processors_and_generators.py,sha256=5UCqjq-CBJnRo1qBAoWs91ngyEuSMN8_tQtfzsuR5SI,5271
|
|
23
25
|
learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py,sha256=aJs_gOP4TmW9J-Dmr21m94jBfLQxzjAu6-ua7x4uYLE,727
|
|
24
26
|
learning_credentials/migrations/0007_migrate_to_text_elements_format.py,sha256=_olkaxPPuRys2c2X5fnyQIFVvqEfdoYu-JlApmXuHEM,4758
|
|
27
|
+
learning_credentials/migrations/0008_validation.py,sha256=jcTg4Lnlmcyp1Czc9b-52gPJ_s8W7Dwodvi_LggpVjw,3387
|
|
28
|
+
learning_credentials/migrations/0009_credential_user_fk.py,sha256=yT1YdE1ptZ8ZT7bWVQyTpgxs--UifGxK3V10ehHZ5Ig,2233
|
|
29
|
+
learning_credentials/migrations/0010_credential_configuration_fk.py,sha256=z3OZCgs_AJzxxyBvWVvvUSTXKPjYvBN8xd8VajwYBZA,3415
|
|
25
30
|
learning_credentials/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
31
|
learning_credentials/settings/__init__.py,sha256=tofc5eg3Q2lV13Ff_jjg1ggGgWpKYoeESkP1qxl3H_A,29
|
|
27
32
|
learning_credentials/settings/common.py,sha256=Cck-nyFt11G1NLiz-bHfKJp8MV6sDZGqTwdbC8_1WE0,360
|
|
28
33
|
learning_credentials/settings/production.py,sha256=6P0P7JxbpWNsk4Lk8lfyxHirOWMgU4UWOb3EYKLjiVQ,542
|
|
29
34
|
learning_credentials/templates/learning_credentials/base.html,sha256=wtjBYqfHmOnyEY5tN3VGOmzYLsOD24MXdEUhTZ7OmwI,662
|
|
35
|
+
learning_credentials/templates/learning_credentials/verify.html,sha256=vXTiZMZkTLhp6cnqKBJcQnDu7qUxLvrTzN2m1KM8_9k,3414
|
|
30
36
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html,sha256=t-i1Ra9AC4pX-rPRifDJIvBBZuxCxdrFqg1NKTjHBOk,813
|
|
31
37
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt,sha256=IF_x8aF_-dORlQB-RCh0IkJDl2ktD489E8qGgLe9M3Y,677
|
|
32
38
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt,sha256=-n8tjPSwfwAfeOSZ1WhcCTrpOah4VswzMZ5mh63Pxow,20
|
|
33
39
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
40
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt,sha256=S7Hc5T_sZSsSBXm5_H5HBNNv16Ohl0oZn0nVqqeWL0g,132
|
|
35
|
-
learning_credentials-0.
|
|
36
|
-
learning_credentials-0.
|
|
37
|
-
learning_credentials-0.
|
|
38
|
-
learning_credentials-0.
|
|
39
|
-
learning_credentials-0.
|
|
40
|
-
learning_credentials-0.
|
|
41
|
+
learning_credentials-0.5.0rc3.dist-info/licenses/LICENSE.txt,sha256=GDpsPnW_1NKhPvZpZL9imz25P2nIpbwJPEhrlq4vPAU,34523
|
|
42
|
+
learning_credentials-0.5.0rc3.dist-info/METADATA,sha256=eOcIS9drEVJVDExK5zGolz5e3apUQWBEnfIzeTa8-vo,8461
|
|
43
|
+
learning_credentials-0.5.0rc3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
44
|
+
learning_credentials-0.5.0rc3.dist-info/entry_points.txt,sha256=hHqqLUEdzAN24v5OGBX9Fr-wh3ATDPjQjByKz03eC2Y,91
|
|
45
|
+
learning_credentials-0.5.0rc3.dist-info/top_level.txt,sha256=Ce-4_leZe_nny7CpmkeRiemcDV6jIHpIvLjlcQBuf18,21
|
|
46
|
+
learning_credentials-0.5.0rc3.dist-info/RECORD,,
|
{learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{learning_credentials-0.4.1rc6.dist-info → learning_credentials-0.5.0rc3.dist-info}/top_level.txt
RENAMED
|
File without changes
|