learning-credentials 0.3.0rc4__py3-none-any.whl → 0.3.0rc10__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/api/v1/permissions.py +42 -24
- learning_credentials/api/v1/urls.py +1 -11
- learning_credentials/api/v1/views.py +19 -367
- learning_credentials/apps.py +7 -10
- learning_credentials/compat.py +1 -7
- learning_credentials/generators.py +0 -1
- learning_credentials/models.py +10 -44
- learning_credentials/processors.py +40 -148
- learning_credentials/settings/common.py +5 -2
- learning_credentials/settings/production.py +5 -2
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/METADATA +19 -3
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/RECORD +16 -22
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/entry_points.txt +0 -3
- learning_credentials/api/v1/serializers.py +0 -74
- learning_credentials/core_api.py +0 -77
- learning_credentials/public/css/credentials_xblock.css +0 -7
- learning_credentials/public/html/credentials_xblock.html +0 -48
- learning_credentials/public/js/credentials_xblock.js +0 -23
- learning_credentials/xblocks.py +0 -85
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/WHEEL +0 -0
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/licenses/LICENSE.txt +0 -0
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/top_level.txt +0 -0
learning_credentials/core_api.py
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"""API functions for the Learning Credentials app."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
from .models import Credential, CredentialConfiguration
|
|
7
|
-
from .tasks import generate_credential_for_user_task
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from django.contrib.auth.models import User
|
|
11
|
-
from opaque_keys.edx.keys import CourseKey
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def get_eligible_users_by_credential_type(
|
|
17
|
-
course_id: 'CourseKey', user_id: int | None = None
|
|
18
|
-
) -> dict[str, list['User']]:
|
|
19
|
-
"""
|
|
20
|
-
Retrieve eligible users for each credential type in the given course.
|
|
21
|
-
|
|
22
|
-
:param course_id: The key of the course for which to check eligibility.
|
|
23
|
-
:param user_id: Optional. If provided, will check eligibility for the specific user.
|
|
24
|
-
:return: A dictionary with credential type as the key and eligible users as the value.
|
|
25
|
-
"""
|
|
26
|
-
credential_configs = CredentialConfiguration.objects.filter(course_id=course_id)
|
|
27
|
-
|
|
28
|
-
if not credential_configs:
|
|
29
|
-
return {}
|
|
30
|
-
|
|
31
|
-
eligible_users_by_type = {}
|
|
32
|
-
for credential_config in credential_configs:
|
|
33
|
-
user_ids = credential_config.get_eligible_user_ids(user_id)
|
|
34
|
-
filtered_user_ids = credential_config.filter_out_user_ids_with_credentials(user_ids)
|
|
35
|
-
|
|
36
|
-
if user_id:
|
|
37
|
-
eligible_users_by_type[credential_config.credential_type.name] = list(set(filtered_user_ids) & {user_id})
|
|
38
|
-
else:
|
|
39
|
-
eligible_users_by_type[credential_config.credential_type.name] = filtered_user_ids
|
|
40
|
-
|
|
41
|
-
return eligible_users_by_type
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def get_user_credentials_by_type(course_id: 'CourseKey', user_id: int) -> dict[str, dict[str, str]]:
|
|
45
|
-
"""
|
|
46
|
-
Retrieve the available credentials for a given user in a course.
|
|
47
|
-
|
|
48
|
-
:param course_id: The course ID for which to retrieve credentials.
|
|
49
|
-
:param user_id: The ID of the user for whom credentials are being retrieved.
|
|
50
|
-
:return: A dict where keys are credential types and values are dicts with the download link and status.
|
|
51
|
-
"""
|
|
52
|
-
credentials = Credential.objects.filter(user_id=user_id, course_id=course_id)
|
|
53
|
-
|
|
54
|
-
return {cred.credential_type: {'download_url': cred.download_url, 'status': cred.status} for cred in credentials}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def generate_credential_for_user(course_id: 'CourseKey', credential_type: str, user_id: int, force: bool = False):
|
|
58
|
-
"""
|
|
59
|
-
Generate a credential for a user in a course.
|
|
60
|
-
|
|
61
|
-
:param course_id: The course ID for which to generate the credential.
|
|
62
|
-
:param credential_type: The type of credential to generate.
|
|
63
|
-
:param user_id: The ID of the user for whom the credential is being generated.
|
|
64
|
-
:param force: If True, will generate the credential even if the user is not eligible.
|
|
65
|
-
"""
|
|
66
|
-
credential_config = CredentialConfiguration.objects.get(course_id=course_id, credential_type__name=credential_type)
|
|
67
|
-
|
|
68
|
-
if not credential_config:
|
|
69
|
-
logger.error('No course configuration found for course %s', course_id)
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
if not force and not credential_config.get_eligible_user_ids(user_id):
|
|
73
|
-
logger.error('User %s is not eligible for the credential in course %s', user_id, course_id)
|
|
74
|
-
msg = 'User is not eligible for the credential.'
|
|
75
|
-
raise ValueError(msg)
|
|
76
|
-
|
|
77
|
-
generate_credential_for_user_task.delay(credential_config.id, user_id)
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
<div class="credentials-block">
|
|
2
|
-
{% if is_author_mode %}
|
|
3
|
-
<p>The Studio view of this XBlock is not supported yet. Please preview the XBlock in the LMS.</p>
|
|
4
|
-
{% else %}
|
|
5
|
-
<h3>Check Your Certificate Eligibility Status</h3>
|
|
6
|
-
<ul class="credentials-list">
|
|
7
|
-
{% if credentials %}
|
|
8
|
-
{% for credential_type, credential in credentials.items %}
|
|
9
|
-
<li class="credential">
|
|
10
|
-
<strong>Type:</strong> {{ credential_type }}
|
|
11
|
-
{% if credential.download_url %}
|
|
12
|
-
<p class="credential-status">Congratulations on finishing strong!</p>
|
|
13
|
-
<strong>Download Link:</strong> <a href="{{ credential.download_url }}">Download Certificate</a>
|
|
14
|
-
{% elif credential.status == credential.Status.ERROR %}
|
|
15
|
-
<p class="credential-status">Something went wrong. Please contact us via the Help page for assistance.</p>
|
|
16
|
-
{% endif %}
|
|
17
|
-
<button class="btn-brand generate-credential" data-credential-type="{{ credential_type }}" disabled>
|
|
18
|
-
Certificate Claimed
|
|
19
|
-
</button>
|
|
20
|
-
<div id="message-area-{{ credential_type }}"></div>
|
|
21
|
-
</li>
|
|
22
|
-
{% endfor %}
|
|
23
|
-
{% endif %}
|
|
24
|
-
|
|
25
|
-
{% if eligible_types %}
|
|
26
|
-
{% for credential_type, is_eligible in eligible_types.items %}
|
|
27
|
-
{% if not credentials or credential_type not in credentials %}
|
|
28
|
-
<li class="credential">
|
|
29
|
-
<strong>Type:</strong> {{ credential_type }}
|
|
30
|
-
{% if is_eligible %}
|
|
31
|
-
<p class="credential-status">Congratulations! You have earned this certificate. Please claim it below.</p>
|
|
32
|
-
<button class="btn-brand generate-credential" data-credential-type="{{ credential_type }}">
|
|
33
|
-
Claim Certificate
|
|
34
|
-
</button>
|
|
35
|
-
{% else %}
|
|
36
|
-
<p class="certificate-status">You are not yet eligible for this certificate.</p>
|
|
37
|
-
<button class="btn-brand generate-certificate" data-certificate-type="{{ credential_type }}" disabled>
|
|
38
|
-
Claim Certificate
|
|
39
|
-
</button>
|
|
40
|
-
{% endif %}
|
|
41
|
-
<div id="message-area-{{ credential_type }}"></div>
|
|
42
|
-
</li>
|
|
43
|
-
{% endif %}
|
|
44
|
-
{% endfor %}
|
|
45
|
-
{% endif %}
|
|
46
|
-
</ul>
|
|
47
|
-
{% endif %}
|
|
48
|
-
</div>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
function CredentialsXBlock(runtime, element) {
|
|
2
|
-
function generateCredential(event) {
|
|
3
|
-
const button = event.target;
|
|
4
|
-
const credentialType = $(button).data('credential-type');
|
|
5
|
-
const handlerUrl = runtime.handlerUrl(element, 'generate_credential');
|
|
6
|
-
|
|
7
|
-
$.post(handlerUrl, JSON.stringify({ credential_type: credentialType }))
|
|
8
|
-
.done(function(data) {
|
|
9
|
-
const messageArea = $(element).find('#message-area-' + credentialType);
|
|
10
|
-
if (data.status === 'success') {
|
|
11
|
-
messageArea.html('<p style="color:green;">Certificate generation initiated successfully.</p>');
|
|
12
|
-
} else {
|
|
13
|
-
messageArea.html('<p style="color:red;">' + data.message + '</p>');
|
|
14
|
-
}
|
|
15
|
-
})
|
|
16
|
-
.fail(function() {
|
|
17
|
-
const messageArea = $(element).find('#message-area-' + credentialType);
|
|
18
|
-
messageArea.html('<p style="color:red;">An error occurred while processing your request.</p>');
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
$(element).find('.generate-credential').on('click', generateCredential);
|
|
23
|
-
}
|
learning_credentials/xblocks.py
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
"""XBlocks for Learning Credentials."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
|
-
from xblock.core import XBlock
|
|
6
|
-
from xblock.fields import Scope, String
|
|
7
|
-
from xblock.fragment import Fragment
|
|
8
|
-
from xblock.utils.resources import ResourceLoader
|
|
9
|
-
from xblock.utils.studio_editable import StudioEditableXBlockMixin
|
|
10
|
-
|
|
11
|
-
from .core_api import generate_credential_for_user, get_eligible_users_by_credential_type, get_user_credentials_by_type
|
|
12
|
-
|
|
13
|
-
loader = ResourceLoader(__name__)
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class CredentialsXBlock(StudioEditableXBlockMixin, XBlock):
|
|
18
|
-
"""XBlock that displays the credential eligibility status and allows eligible users to generate credentials."""
|
|
19
|
-
|
|
20
|
-
display_name = String(
|
|
21
|
-
help='The display name for this component.',
|
|
22
|
-
scope=Scope.content,
|
|
23
|
-
display_name="Display name",
|
|
24
|
-
default='Credentials',
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
def student_view(self, context) -> Fragment: # noqa: ANN001, ARG002
|
|
28
|
-
"""Main view for the student. Displays the credential eligibility or ineligibility status."""
|
|
29
|
-
fragment = Fragment()
|
|
30
|
-
eligible_types = False
|
|
31
|
-
credentials = []
|
|
32
|
-
|
|
33
|
-
if not (is_author_mode := getattr(self.runtime, 'is_author_mode', False)):
|
|
34
|
-
credentials = self.get_credentials()
|
|
35
|
-
eligible_types = self.get_eligible_credential_types()
|
|
36
|
-
|
|
37
|
-
# Filter out the eligible types that already have a credential generated
|
|
38
|
-
for cred_type in credentials:
|
|
39
|
-
if cred_type in eligible_types:
|
|
40
|
-
del eligible_types[cred_type]
|
|
41
|
-
|
|
42
|
-
fragment.add_content(
|
|
43
|
-
loader.render_django_template(
|
|
44
|
-
'public/html/credentials_xblock.html',
|
|
45
|
-
{
|
|
46
|
-
'credentials': credentials,
|
|
47
|
-
'eligible_types': eligible_types,
|
|
48
|
-
'is_author_mode': is_author_mode,
|
|
49
|
-
},
|
|
50
|
-
)
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
fragment.add_css_url(self.runtime.local_resource_url(self, "public/css/credentials_xblock.css"))
|
|
54
|
-
fragment.add_javascript_url(self.runtime.local_resource_url(self, "public/js/credentials_xblock.js"))
|
|
55
|
-
fragment.initialize_js('CredentialsXBlock')
|
|
56
|
-
return fragment
|
|
57
|
-
|
|
58
|
-
def get_eligible_credential_types(self) -> dict[str, bool]:
|
|
59
|
-
"""Retrieve the eligibility status for each credential type."""
|
|
60
|
-
eligible_users = get_eligible_users_by_credential_type(self.runtime.course_id, user_id=self.scope_ids.user_id)
|
|
61
|
-
|
|
62
|
-
return {credential_type: bool(users) for credential_type, users in eligible_users.items()}
|
|
63
|
-
|
|
64
|
-
def get_credentials(self) -> dict[str, dict[str, str]]:
|
|
65
|
-
"""Retrieve the credentials for the current user in the current course."""
|
|
66
|
-
return get_user_credentials_by_type(self.runtime.course_id, self.scope_ids.user_id)
|
|
67
|
-
|
|
68
|
-
@XBlock.json_handler
|
|
69
|
-
def generate_credential(self, data: dict, suffix: str = '') -> dict[str, str]: # noqa: ARG002
|
|
70
|
-
"""Handler for generating a credential for a specific type."""
|
|
71
|
-
credential_type = data.get('credential_type')
|
|
72
|
-
if not credential_type:
|
|
73
|
-
return {'status': 'error', 'message': 'No credential type specified.'}
|
|
74
|
-
|
|
75
|
-
course_id = self.runtime.course_id
|
|
76
|
-
user_id = self.scope_ids.user_id
|
|
77
|
-
logger.info(
|
|
78
|
-
'Generating a credential for user %s in course %s with type %s.', user_id, course_id, credential_type
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
try:
|
|
82
|
-
generate_credential_for_user(course_id, credential_type, user_id)
|
|
83
|
-
except ValueError as e:
|
|
84
|
-
return {'status': 'error', 'message': str(e)}
|
|
85
|
-
return {'status': 'success'}
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/top_level.txt
RENAMED
|
File without changes
|