learning-credentials 0.2.2rc2__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.
Files changed (34) hide show
  1. learning_credentials/__init__.py +1 -0
  2. learning_credentials/admin.py +265 -0
  3. learning_credentials/apps.py +24 -0
  4. learning_credentials/compat.py +119 -0
  5. learning_credentials/conf/locale/config.yaml +85 -0
  6. learning_credentials/exceptions.py +9 -0
  7. learning_credentials/generators.py +225 -0
  8. learning_credentials/migrations/0001_initial.py +205 -0
  9. learning_credentials/migrations/0002_migrate_to_learning_credentials.py +40 -0
  10. learning_credentials/migrations/0003_rename_certificates_to_credentials.py +128 -0
  11. learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +59 -0
  12. learning_credentials/migrations/0005_rename_processors_and_generators.py +106 -0
  13. learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +21 -0
  14. learning_credentials/migrations/__init__.py +0 -0
  15. learning_credentials/models.py +381 -0
  16. learning_credentials/processors.py +378 -0
  17. learning_credentials/settings/__init__.py +1 -0
  18. learning_credentials/settings/common.py +9 -0
  19. learning_credentials/settings/production.py +13 -0
  20. learning_credentials/tasks.py +53 -0
  21. learning_credentials/templates/learning_credentials/base.html +22 -0
  22. learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +22 -0
  23. learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +13 -0
  24. learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +1 -0
  25. learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
  26. learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +4 -0
  27. learning_credentials/urls.py +9 -0
  28. learning_credentials/views.py +1 -0
  29. learning_credentials-0.2.2rc2.dist-info/METADATA +212 -0
  30. learning_credentials-0.2.2rc2.dist-info/RECORD +34 -0
  31. learning_credentials-0.2.2rc2.dist-info/WHEEL +5 -0
  32. learning_credentials-0.2.2rc2.dist-info/entry_points.txt +2 -0
  33. learning_credentials-0.2.2rc2.dist-info/licenses/LICENSE.txt +664 -0
  34. learning_credentials-0.2.2rc2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,225 @@
1
+ """
2
+ This module provides functions to generate credentials.
3
+
4
+ The functions prefixed with `generate_` are automatically detected by the admin page and are used to generate the
5
+ credentials for the users.
6
+
7
+ We will move this module to an external repository (a plugin).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import io
13
+ import logging
14
+ import secrets
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from django.conf import settings
18
+ from django.core.files.base import ContentFile
19
+ from django.core.files.storage import FileSystemStorage, default_storage
20
+ from pypdf import PdfReader, PdfWriter
21
+ from pypdf.constants import UserAccessPermissions
22
+ from reportlab.pdfbase import pdfmetrics
23
+ from reportlab.pdfbase.ttfonts import TTFont
24
+ from reportlab.pdfgen import canvas
25
+
26
+ from .compat import get_default_storage_url, get_learning_context_name, get_localized_credential_date
27
+ from .models import CredentialAsset
28
+
29
+ log = logging.getLogger(__name__)
30
+
31
+ if TYPE_CHECKING: # pragma: no cover
32
+ from uuid import UUID
33
+
34
+ from django.contrib.auth.models import User
35
+ from opaque_keys.edx.keys import CourseKey
36
+
37
+
38
+ def _get_user_name(user: User) -> str:
39
+ """
40
+ Retrieve the user's name.
41
+
42
+ :param user: The user to generate the credential for.
43
+ :return: Username.
44
+ """
45
+ return user.profile.name or f"{user.first_name} {user.last_name}"
46
+
47
+
48
+ def _register_font(options: dict[str, Any]) -> str:
49
+ """
50
+ Register a custom font if specified in options. If not specified, use the default font (Helvetica).
51
+
52
+ :param options: A dictionary containing the font.
53
+ :returns: The font name.
54
+ """
55
+ if font := options.get('font'):
56
+ pdfmetrics.registerFont(TTFont(font, CredentialAsset.get_asset_by_slug(font)))
57
+
58
+ return font or 'Helvetica'
59
+
60
+
61
+ def _write_text_on_template(template: any, font: str, username: str, context_name: str, options: dict[str, Any]) -> any:
62
+ """
63
+ Prepare a new canvas and write the user and course name onto it.
64
+
65
+ :param template: Pdf template.
66
+ :param font: Font name.
67
+ :param username: The name of the user to generate the credential for.
68
+ :param context_name: The name of the learning context.
69
+ :param options: A dictionary documented in the `generate_pdf_credential` function.
70
+ :returns: A canvas with written data.
71
+ """
72
+
73
+ def hex_to_rgb(hex_color: str) -> tuple[float, float, float]:
74
+ """
75
+ Convert a hexadecimal color code to an RGB tuple with floating-point values.
76
+
77
+ :param hex_color: A hexadecimal color string, which can start with '#' and be either 3 or 6 characters long.
78
+ :returns: A tuple representing the RGB color as (red, green, blue), with each value ranging from 0.0 to 1.0.
79
+ """
80
+ hex_color = hex_color.lstrip('#')
81
+ # Expand shorthand form (e.g. "158" to "115588")
82
+ if len(hex_color) == 3:
83
+ hex_color = ''.join([c * 2 for c in hex_color])
84
+
85
+ # noinspection PyTypeChecker
86
+ return tuple(int(hex_color[i : i + 2], 16) / 255 for i in range(0, 6, 2))
87
+
88
+ template_width, template_height = template.mediabox[2:]
89
+ pdf_canvas = canvas.Canvas(io.BytesIO(), pagesize=(template_width, template_height))
90
+
91
+ # Write the learner name.
92
+ pdf_canvas.setFont(font, 32)
93
+ name_color = options.get('name_color', '#000')
94
+ pdf_canvas.setFillColorRGB(*hex_to_rgb(name_color))
95
+
96
+ name_x = (template_width - pdf_canvas.stringWidth(username)) / 2
97
+ name_y = options.get('name_y', 290)
98
+ pdf_canvas.drawString(name_x, name_y, username)
99
+
100
+ # Write the learning context name.
101
+ pdf_canvas.setFont(font, options.get('context_name_size', 28))
102
+ context_name_color = options.get('context_name_color', '#000')
103
+ pdf_canvas.setFillColorRGB(*hex_to_rgb(context_name_color))
104
+
105
+ context_name_y = options.get('context_name_y', 220)
106
+ context_name_line_height = 28 * 1.1
107
+
108
+ # Split the learning context name into lines and write each of them in the center of the template.
109
+ for line_number, line in enumerate(context_name.split('\n')):
110
+ line_x = (template_width - pdf_canvas.stringWidth(line)) / 2
111
+ line_y = context_name_y - (line_number * context_name_line_height)
112
+ pdf_canvas.drawString(line_x, line_y, line)
113
+
114
+ # Write the issue date.
115
+ issue_date = get_localized_credential_date()
116
+ pdf_canvas.setFont(font, 12)
117
+ issue_date_color = options.get('issue_date_color', '#000')
118
+ pdf_canvas.setFillColorRGB(*hex_to_rgb(issue_date_color))
119
+
120
+ issue_date_x = (template_width - pdf_canvas.stringWidth(issue_date)) / 2
121
+ issue_date_y = options.get('issue_date_y', 120)
122
+ pdf_canvas.drawString(issue_date_x, issue_date_y, issue_date)
123
+
124
+ return pdf_canvas
125
+
126
+
127
+ def _save_credential(credential: PdfWriter, credential_uuid: UUID) -> str:
128
+ """
129
+ Save the final PDF file to BytesIO and upload it using Django default storage.
130
+
131
+ :param credential: Pdf credential.
132
+ :param credential_uuid: The UUID of the credential.
133
+ :returns: The URL of the saved credential.
134
+ """
135
+ # Save the final PDF file to BytesIO.
136
+ output_path = f'external_certificates/{credential_uuid}.pdf'
137
+
138
+ view_print_extract_permission = (
139
+ UserAccessPermissions.PRINT
140
+ | UserAccessPermissions.PRINT_TO_REPRESENTATION
141
+ | UserAccessPermissions.EXTRACT_TEXT_AND_GRAPHICS
142
+ )
143
+ credential.encrypt('', secrets.token_hex(32), permissions_flag=view_print_extract_permission, algorithm='AES-256')
144
+
145
+ pdf_bytes = io.BytesIO()
146
+ credential.write(pdf_bytes)
147
+ pdf_bytes.seek(0) # Rewind to start.
148
+ # Upload with Django default storage.
149
+ credential_file = ContentFile(pdf_bytes.read())
150
+ # Delete the file if it already exists.
151
+ if default_storage.exists(output_path):
152
+ default_storage.delete(output_path)
153
+ default_storage.save(output_path, credential_file)
154
+ if isinstance(default_storage, FileSystemStorage):
155
+ url = f"{get_default_storage_url()}{output_path}"
156
+ else:
157
+ url = default_storage.url(output_path)
158
+
159
+ if custom_domain := getattr(settings, 'LEARNING_CREDENTIALS_CUSTOM_DOMAIN', None):
160
+ url = f"{custom_domain}/{credential_uuid}.pdf"
161
+
162
+ return url
163
+
164
+
165
+ def generate_pdf_credential(
166
+ learning_context_key: CourseKey,
167
+ user: User,
168
+ credential_uuid: UUID,
169
+ options: dict[str, Any],
170
+ ) -> str:
171
+ """
172
+ Generate a PDF credential.
173
+
174
+ :param learning_context_key: The ID of the course or learning path the credential is for.
175
+ :param user: The user to generate the credential for.
176
+ :param credential_uuid: The UUID of the credential to generate.
177
+ :param options: The custom options for the credential.
178
+ :returns: The URL of the saved credential.
179
+
180
+ Options:
181
+ - template: The path to the PDF template file.
182
+ - template_two_lines: The path to the PDF template file for two-line context names.
183
+ A two-line context name is specified by using a semicolon as a separator.
184
+ - font: The name of the font to use.
185
+ - name_y: The Y coordinate of the name on the credential (vertical position on the template).
186
+ - name_color: The color of the name on the credential (hexadecimal color code).
187
+ - context_name: Specify the custom course or Learning Path name.
188
+ - context_name_y: The Y coordinate of the context name on the credential (vertical position on the template).
189
+ - context_name_color: The color of the context name on the credential (hexadecimal color code).
190
+ - context_name_size: The font size of the context name on the credential. The default value is 28.
191
+ - issue_date_y: The Y coordinate of the issue date on the credential (vertical position on the template).
192
+ - issue_date_color: The color of the issue date on the credential (hexadecimal color code).
193
+ """
194
+ log.info("Starting credential generation for user %s", user.id)
195
+
196
+ username = _get_user_name(user)
197
+ context_name = options.get('context_name') or get_learning_context_name(learning_context_key)
198
+
199
+ # Get template from the CredentialAsset.
200
+ # HACK: We support two-line strings by using a semicolon as a separator.
201
+ if ';' in context_name and (template_path := options.get('template_two_lines')):
202
+ template_file = CredentialAsset.get_asset_by_slug(template_path)
203
+ context_name = context_name.replace(';', '\n')
204
+ else:
205
+ template_file = CredentialAsset.get_asset_by_slug(options['template'])
206
+
207
+ font = _register_font(options)
208
+
209
+ # Load the PDF template.
210
+ with template_file.open('rb') as template_file:
211
+ template = PdfReader(template_file).pages[0]
212
+
213
+ credential = PdfWriter()
214
+
215
+ # Create a new canvas, prepare the page and write the data
216
+ pdf_canvas = _write_text_on_template(template, font, username, context_name, options)
217
+
218
+ overlay_pdf = PdfReader(io.BytesIO(pdf_canvas.getpdfdata()))
219
+ template.merge_page(overlay_pdf.pages[0])
220
+ credential.add_page(template)
221
+
222
+ url = _save_credential(credential, credential_uuid)
223
+
224
+ log.info("Credential saved to %s", url)
225
+ return url
@@ -0,0 +1,205 @@
1
+ from django.db import migrations, models
2
+ import django.utils.timezone
3
+ import jsonfield.fields
4
+ import model_utils.fields
5
+ import opaque_keys.edx.django.models
6
+ import uuid
7
+
8
+ import learning_credentials.models
9
+
10
+
11
+ class Migration(migrations.Migration):
12
+
13
+ initial = True
14
+
15
+ dependencies = [
16
+ ('django_celery_beat', '0019_alter_periodictasks_options'),
17
+ ]
18
+
19
+ operations = [
20
+ migrations.CreateModel(
21
+ name='ExternalCertificateAsset',
22
+ fields=[
23
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24
+ (
25
+ 'created',
26
+ model_utils.fields.AutoCreatedField(
27
+ default=django.utils.timezone.now, editable=False, verbose_name='created'
28
+ ),
29
+ ),
30
+ (
31
+ 'modified',
32
+ model_utils.fields.AutoLastModifiedField(
33
+ default=django.utils.timezone.now, editable=False, verbose_name='modified'
34
+ ),
35
+ ),
36
+ ('description', models.CharField(blank=True, help_text='Description of the asset.', max_length=255)),
37
+ (
38
+ 'asset',
39
+ models.FileField(
40
+ help_text='Asset file. It could be a PDF template, image or font file.',
41
+ max_length=255,
42
+ upload_to=learning_credentials.models.CredentialAsset.template_assets_path,
43
+ ),
44
+ ),
45
+ (
46
+ 'asset_slug',
47
+ models.SlugField(
48
+ help_text="Asset's unique slug. We can reference the asset in templates using this value.",
49
+ max_length=255,
50
+ unique=True,
51
+ ),
52
+ ),
53
+ ],
54
+ options={
55
+ 'get_latest_by': 'created',
56
+ },
57
+ ),
58
+ migrations.CreateModel(
59
+ name='ExternalCertificateType',
60
+ fields=[
61
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
62
+ (
63
+ 'created',
64
+ model_utils.fields.AutoCreatedField(
65
+ default=django.utils.timezone.now, editable=False, verbose_name='created'
66
+ ),
67
+ ),
68
+ (
69
+ 'modified',
70
+ model_utils.fields.AutoLastModifiedField(
71
+ default=django.utils.timezone.now, editable=False, verbose_name='modified'
72
+ ),
73
+ ),
74
+ ('name', models.CharField(help_text='Name of the certificate type.', max_length=255, unique=True)),
75
+ (
76
+ 'retrieval_func',
77
+ models.CharField(help_text='A name of the function to retrieve eligible users.', max_length=200),
78
+ ),
79
+ (
80
+ 'generation_func',
81
+ models.CharField(help_text='A name of the function to generate certificates.', max_length=200),
82
+ ),
83
+ (
84
+ 'custom_options',
85
+ jsonfield.fields.JSONField(blank=True, default=dict, help_text='Custom options for the functions.'),
86
+ ),
87
+ ],
88
+ options={
89
+ 'abstract': False,
90
+ },
91
+ ),
92
+ migrations.CreateModel(
93
+ name='ExternalCertificate',
94
+ fields=[
95
+ (
96
+ 'created',
97
+ model_utils.fields.AutoCreatedField(
98
+ default=django.utils.timezone.now, editable=False, verbose_name='created'
99
+ ),
100
+ ),
101
+ (
102
+ 'modified',
103
+ model_utils.fields.AutoLastModifiedField(
104
+ default=django.utils.timezone.now, editable=False, verbose_name='modified'
105
+ ),
106
+ ),
107
+ (
108
+ 'uuid',
109
+ models.UUIDField(
110
+ default=uuid.uuid4,
111
+ editable=False,
112
+ help_text='Auto-generated UUID of the certificate',
113
+ primary_key=True,
114
+ serialize=False,
115
+ ),
116
+ ),
117
+ ('user_id', models.IntegerField(help_text='ID of the user receiving the certificate')),
118
+ ('user_full_name', models.CharField(help_text='User receiving the certificate', max_length=255)),
119
+ (
120
+ 'course_id',
121
+ opaque_keys.edx.django.models.CourseKeyField(
122
+ help_text='ID of a course for which the certificate was issued', max_length=255
123
+ ),
124
+ ),
125
+ ('certificate_type', models.CharField(help_text='Type of the certificate', max_length=255)),
126
+ (
127
+ 'status',
128
+ models.CharField(
129
+ choices=[
130
+ ('generating', 'Generating'),
131
+ ('available', 'Available'),
132
+ ('error', 'Error'),
133
+ ('invalidated', 'Invalidated'),
134
+ ],
135
+ default='generating',
136
+ help_text='Status of the certificate generation task',
137
+ max_length=32,
138
+ ),
139
+ ),
140
+ (
141
+ 'download_url',
142
+ models.URLField(blank=True, help_text='URL of the generated certificate PDF (e.g., to S3)'),
143
+ ),
144
+ (
145
+ 'legacy_id',
146
+ models.IntegerField(
147
+ help_text='Legacy ID of the certificate imported from another system', null=True
148
+ ),
149
+ ),
150
+ ('generation_task_id', models.CharField(help_text='Task ID from the Celery queue', max_length=255)),
151
+ ],
152
+ options={
153
+ 'unique_together': {('user_id', 'course_id', 'certificate_type')},
154
+ },
155
+ ),
156
+ migrations.CreateModel(
157
+ name='ExternalCertificateCourseConfiguration',
158
+ fields=[
159
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
160
+ (
161
+ 'created',
162
+ model_utils.fields.AutoCreatedField(
163
+ default=django.utils.timezone.now, editable=False, verbose_name='created'
164
+ ),
165
+ ),
166
+ (
167
+ 'modified',
168
+ model_utils.fields.AutoLastModifiedField(
169
+ default=django.utils.timezone.now, editable=False, verbose_name='modified'
170
+ ),
171
+ ),
172
+ (
173
+ 'course_id',
174
+ opaque_keys.edx.django.models.CourseKeyField(help_text='The ID of the course.', max_length=255),
175
+ ),
176
+ (
177
+ 'custom_options',
178
+ jsonfield.fields.JSONField(
179
+ blank=True,
180
+ default=dict,
181
+ help_text='Custom options for the functions. If specified, they are merged with the options defined in the certificate type.',
182
+ ),
183
+ ),
184
+ (
185
+ 'certificate_type',
186
+ models.ForeignKey(
187
+ help_text='Associated certificate type.',
188
+ on_delete=django.db.models.deletion.CASCADE,
189
+ to='learning_credentials.externalcertificatetype',
190
+ ),
191
+ ),
192
+ (
193
+ 'periodic_task',
194
+ models.OneToOneField(
195
+ help_text='Associated periodic task.',
196
+ on_delete=django.db.models.deletion.CASCADE,
197
+ to='django_celery_beat.periodictask',
198
+ ),
199
+ ),
200
+ ],
201
+ options={
202
+ 'unique_together': {('course_id', 'certificate_type')},
203
+ },
204
+ ),
205
+ ]
@@ -0,0 +1,40 @@
1
+ from django.db import migrations
2
+
3
+
4
+ def migrate_data_if_tables_exist(apps, schema_editor):
5
+ """Migrate data from openedx_certificates to learning_credentials tables if the source tables exist."""
6
+ connection = schema_editor.connection
7
+ cursor = connection.cursor()
8
+ table_names = connection.introspection.table_names(cursor)
9
+
10
+ tables_to_migrate = [
11
+ (
12
+ "openedx_certificates_externalcertificatetype",
13
+ "learning_credentials_externalcertificatetype",
14
+ "id, created, modified, name, retrieval_func, generation_func, custom_options",
15
+ ),
16
+ (
17
+ "openedx_certificates_externalcertificatecourseconfiguration",
18
+ "learning_credentials_externalcertificatecourseconfiguration",
19
+ "id, created, modified, course_id, custom_options, certificate_type_id, periodic_task_id",
20
+ ),
21
+ (
22
+ "openedx_certificates_externalcertificateasset",
23
+ "learning_credentials_externalcertificateasset",
24
+ "id, created, modified, description, asset, asset_slug",
25
+ ),
26
+ (
27
+ "openedx_certificates_externalcertificate",
28
+ "learning_credentials_externalcertificate",
29
+ "uuid, created, modified, user_id, user_full_name, course_id, certificate_type, status, download_url, legacy_id, generation_task_id",
30
+ ),
31
+ ]
32
+
33
+ for source_table, target_table, fields in tables_to_migrate:
34
+ if source_table in table_names:
35
+ cursor.execute(f"INSERT INTO {target_table} ({fields}) SELECT {fields} FROM {source_table};")
36
+
37
+
38
+ class Migration(migrations.Migration):
39
+ dependencies = [("learning_credentials", "0001_initial")]
40
+ operations = [migrations.RunPython(migrate_data_if_tables_exist, migrations.RunPython.noop)]
@@ -0,0 +1,128 @@
1
+ from django.db import migrations, models
2
+ import django.utils.timezone
3
+ import jsonfield.fields
4
+ import opaque_keys.edx.django.models
5
+ import uuid
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('django_celery_beat', '0019_alter_periodictasks_options'),
12
+ ('learning_credentials', '0002_migrate_to_learning_credentials'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.RenameModel(
17
+ old_name='ExternalCertificateCourseConfiguration',
18
+ new_name='CredentialConfiguration',
19
+ ),
20
+ migrations.RenameModel(
21
+ old_name='ExternalCertificateAsset',
22
+ new_name='CredentialAsset',
23
+ ),
24
+ migrations.RenameModel(
25
+ old_name='ExternalCertificateType',
26
+ new_name='CredentialType',
27
+ ),
28
+ migrations.RenameModel(
29
+ old_name='ExternalCertificate',
30
+ new_name='Credential',
31
+ ),
32
+ migrations.RenameField(
33
+ model_name='Credential',
34
+ old_name='certificate_type',
35
+ new_name='credential_type',
36
+ ),
37
+ migrations.RenameField(
38
+ model_name='CredentialConfiguration',
39
+ old_name='certificate_type',
40
+ new_name='credential_type',
41
+ ),
42
+ migrations.AlterField(
43
+ model_name='credential',
44
+ name='course_id',
45
+ field=opaque_keys.edx.django.models.CourseKeyField(
46
+ help_text='ID of a course for which the credential was issued', max_length=255
47
+ ),
48
+ ),
49
+ migrations.AlterField(
50
+ model_name='credential',
51
+ name='credential_type',
52
+ field=models.CharField(help_text='Type of the credential', max_length=255),
53
+ ),
54
+ migrations.AlterField(
55
+ model_name='credential',
56
+ name='download_url',
57
+ field=models.URLField(blank=True, help_text='URL of the generated credential PDF (e.g., to S3)'),
58
+ ),
59
+ migrations.AlterField(
60
+ model_name='credential',
61
+ name='legacy_id',
62
+ field=models.IntegerField(help_text='Legacy ID of the credential imported from another system', null=True),
63
+ ),
64
+ migrations.AlterField(
65
+ model_name='credential',
66
+ name='status',
67
+ field=models.CharField(
68
+ choices=[
69
+ ('generating', 'Generating'),
70
+ ('available', 'Available'),
71
+ ('error', 'Error'),
72
+ ('invalidated', 'Invalidated'),
73
+ ],
74
+ default='generating',
75
+ help_text='Status of the credential generation task',
76
+ max_length=32,
77
+ ),
78
+ ),
79
+ migrations.AlterField(
80
+ model_name='credential',
81
+ name='user_full_name',
82
+ field=models.CharField(help_text='User receiving the credential', max_length=255),
83
+ ),
84
+ migrations.AlterField(
85
+ model_name='credential',
86
+ name='user_id',
87
+ field=models.IntegerField(help_text='ID of the user receiving the credential'),
88
+ ),
89
+ migrations.AlterField(
90
+ model_name='credential',
91
+ name='uuid',
92
+ field=models.UUIDField(
93
+ default=uuid.uuid4,
94
+ editable=False,
95
+ help_text='Auto-generated UUID of the credential',
96
+ primary_key=True,
97
+ serialize=False,
98
+ ),
99
+ ),
100
+ migrations.AlterField(
101
+ model_name='credentialconfiguration',
102
+ name='credential_type',
103
+ field=models.ForeignKey(
104
+ help_text='Associated credential type.',
105
+ on_delete=django.db.models.deletion.CASCADE,
106
+ to='learning_credentials.credentialtype',
107
+ ),
108
+ ),
109
+ migrations.AlterField(
110
+ model_name='credentialconfiguration',
111
+ name='custom_options',
112
+ field=jsonfield.fields.JSONField(
113
+ blank=True,
114
+ default=dict,
115
+ help_text='Custom options for the functions. If specified, they are merged with the options defined in the credential type.',
116
+ ),
117
+ ),
118
+ migrations.AlterField(
119
+ model_name='credentialtype',
120
+ name='generation_func',
121
+ field=models.CharField(help_text='A name of the function to generate credentials.', max_length=200),
122
+ ),
123
+ migrations.AlterField(
124
+ model_name='credentialtype',
125
+ name='name',
126
+ field=models.CharField(help_text='Name of the credential type.', max_length=255, unique=True),
127
+ ),
128
+ ]
@@ -0,0 +1,59 @@
1
+ # Generated by Django 4.2.20 on 2025-03-27 17:36
2
+
3
+ from django.db import migrations
4
+ import opaque_keys.edx.django.models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('learning_credentials', '0003_rename_certificates_to_credentials'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterUniqueTogether(
15
+ name='credential',
16
+ unique_together=set(),
17
+ ),
18
+ migrations.AlterUniqueTogether(
19
+ name='credentialconfiguration',
20
+ unique_together=set(),
21
+ ),
22
+
23
+ migrations.RenameField(
24
+ model_name='credential',
25
+ old_name='course_id',
26
+ new_name='learning_context_key',
27
+ ),
28
+ migrations.RenameField(
29
+ model_name='credentialconfiguration',
30
+ old_name='course_id',
31
+ new_name='learning_context_key',
32
+ ),
33
+
34
+ migrations.AlterField(
35
+ model_name='credential',
36
+ name='learning_context_key',
37
+ field=opaque_keys.edx.django.models.LearningContextKeyField(
38
+ help_text='ID of a learning context (e.g., a course or a Learning Path) for which the credential was issued',
39
+ max_length=255
40
+ ),
41
+ ),
42
+ migrations.AlterField(
43
+ model_name='credentialconfiguration',
44
+ name='learning_context_key',
45
+ field=opaque_keys.edx.django.models.LearningContextKeyField(
46
+ help_text='ID of a learning context (e.g., a course or a Learning Path).',
47
+ max_length=255
48
+ ),
49
+ ),
50
+
51
+ migrations.AlterUniqueTogether(
52
+ name='credential',
53
+ unique_together={('user_id', 'learning_context_key', 'credential_type')},
54
+ ),
55
+ migrations.AlterUniqueTogether(
56
+ name='credentialconfiguration',
57
+ unique_together={('learning_context_key', 'credential_type')},
58
+ ),
59
+ ]