Skip to content

Commit

Permalink
feat: [AXM-1249] implement accredible issuer level
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrylo-kh committed Dec 19, 2024
1 parent 7439aba commit 36dd538
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 16 deletions.
2 changes: 0 additions & 2 deletions credentials/apps/badges/accredible/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,13 @@ class AccredibleCredential:
recipient (RecipientData): Information about the recipient.
group_id (int): ID of the credential group.
name (str): Title of the credential.
description (str): Description of the credential.
issued_on (datetime): Date when the credential was issued.
complete (bool): Whether the credential process is complete.
"""

recipient: AccredibleRecipient
group_id: int
name: str
description: str
issued_on: datetime
complete: bool

Expand Down
120 changes: 118 additions & 2 deletions credentials/apps/badges/issuers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,31 @@
This module provides classes for issuing badge credentials to users.
"""

from datetime import datetime
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.utils.translation import gettext as _

from credentials.apps.badges.accredible.api_client import AccredibleAPIClient
from credentials.apps.badges.accredible.data import(
AccredibleRecipient,
AccredibleCredential,
AccredibleBadgeData,
AccredibleExpireBadgeData,
AccredibleExpiredCredential,
)
from credentials.apps.badges.credly.api_client import CredlyAPIClient
from credentials.apps.badges.credly.data import CredlyBadgeData
from credentials.apps.badges.credly.exceptions import CredlyAPIError
from credentials.apps.badges.exceptions import BadgeProviderError
from credentials.apps.badges.models import BadgeTemplate, CredlyBadge, CredlyBadgeTemplate, UserCredential
from credentials.apps.badges.models import (
BadgeTemplate,
CredlyBadge,
CredlyBadgeTemplate,
UserCredential,
AccredibleGroup,
AccredibleBadge
)
from credentials.apps.badges.signals.signals import notify_badge_awarded, notify_badge_revoked
from credentials.apps.core.api import get_user_by_username
from credentials.apps.credentials.constants import UserCredentialStatus
Expand Down Expand Up @@ -155,7 +171,7 @@ def revoke_credly_badge(self, credential_id, user_credential):
}
try:
response = credly_api.revoke_badge(user_credential.external_uuid, revoke_data)
except CredlyAPIError:
except BadgeProviderError:
user_credential.state = "error"
user_credential.save()
raise
Expand Down Expand Up @@ -197,3 +213,103 @@ def revoke(self, credential_id, username):
if user_credential.propagated:
self.revoke_credly_badge(credential_id, user_credential)
return user_credential



class AccredibleBadgeTemplateIssuer(BadgeTemplateIssuer):
"""
Issues AccredibleGroup credentials to users.
"""

issued_credential_type = AccredibleGroup
issued_user_credential_type = AccredibleBadge

def issue_accredible_badge(self, *, user_credential):
"""
Requests Accredible service for external badge issuing based on internal user credential (AccredibleBadge).
"""

user = get_user_by_username(user_credential.username)
group = user_credential.credential

accredible_badge_data = AccredibleBadgeData(
credential=AccredibleCredential(
recipient=AccredibleRecipient(
name=user.get_full_name() or user.username,
email=user.email,
),
group_id=group.id,
name=group.name,
issued_on=user_credential.created.strftime("%Y-%m-%d %H:%M:%S %z"),
complete=True,
)
)

try:
accredible_api = AccredibleAPIClient(group.api_config)
response = accredible_api.issue_badge(accredible_badge_data)
except BadgeProviderError:
user_credential.state = "error"
user_credential.save()
raise

user_credential.external_id = response.get("credential").get("id")
user_credential.state = AccredibleBadge.STATES.accepted
user_credential.save()

def revoke_accredible_badge(self, credential_id, user_credential):
"""
Requests Accredible service for external badge expiring based on internal user credential (AccredibleBadge).
"""

credential = self.get_credential(credential_id)
accredible_api_client = AccredibleAPIClient(credential.api_config)
revoke_badge_data = AccredibleExpireBadgeData(
credential=AccredibleExpiredCredential(expired_on=datetime.now().strftime("%Y-%m-%d %H:%M:%S %z"))
)

try:
accredible_api_client.revoke_badge(user_credential.external_id, revoke_badge_data)
except BadgeProviderError:
user_credential.state = "error"
user_credential.save()
raise

user_credential.state = AccredibleBadge.STATES.expired
user_credential.save()


def award(self, *, username, credential_id):
"""
Awards a Accredible badge.
- Creates user credential record for the group, for a given user;
- Notifies about the awarded badge (public signal);
- Issues external Accredible badge (Accredible API);
Returns: (AccredibleBadge) user credential
"""

accredible_badge = super().award(username=username, credential_id=credential_id)

# do not issue new badges if the badge was issued already
if not accredible_badge.propagated:
self.issue_accredible_badge(user_credential=accredible_badge)

return accredible_badge

def revoke(self, credential_id, username):
"""
Revokes a Accredible badge.
- Changes user credential status to REVOKED, for a given user;
- Notifies about the revoked badge (public signal);
- Expire external Accredible badge (Accredible API);
Returns: (AccredibleBadge) user credential
"""

user_credential = super().revoke(credential_id, username)
if user_credential.propagated:
self.revoke_accredible_badge(credential_id, user_credential)
return user_credential
44 changes: 41 additions & 3 deletions credentials/apps/badges/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,15 +553,14 @@ def progress(self):
"""
Notify about the progress.
"""

notify_progress_complete(self, self.username, self.template.id)
notify_progress_complete(self, self.username, self.template.id, self.template.origin)

def regress(self):
"""
Notify about the regression.
"""

notify_progress_incomplete(self, self.username, self.template.id)
notify_progress_incomplete(self, self.username, self.template.id, self.template.origin)

def reset(self):
"""
Expand Down Expand Up @@ -752,3 +751,42 @@ class AccredibleBadge(UserCredential):
unique=True,
help_text=_("Accredible service badge identifier"),
)


def as_badge_data(self) -> BadgeData:
"""
Represents itself as a BadgeData instance.
"""

user = get_user_by_username(self.username)
group = self.credential

badge_data = BadgeData(
uuid=str(self.uuid),
user=UserData(
pii=UserPersonalData(
username=self.username,
email=user.email,
name=user.get_full_name(),
),
id=user.lms_user_id,
is_active=user.is_active,
),
template=BadgeTemplateData(
uuid=str(group.uuid),
origin=group.origin,
name=group.name,
description=group.description,
image_url=str(group.icon),
),
)

return badge_data

@property
def propagated(self):
"""
Checks if this user credential already has issued (external) Credly badge.
"""

return self.external_id and (self.state in self.ISSUING_STATES)
18 changes: 12 additions & 6 deletions credentials/apps/badges/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from django.dispatch import receiver
from openedx_events.tooling import OpenEdxPublicSignal, load_all_signals

from credentials.apps.badges.issuers import CredlyBadgeTemplateIssuer
from credentials.apps.badges.models import BadgeProgress
from credentials.apps.badges.issuers import CredlyBadgeTemplateIssuer, AccredibleBadgeTemplateIssuer
from credentials.apps.badges.models import BadgeProgress, CredlyBadgeTemplate, AccredibleGroup
from credentials.apps.badges.processing.generic import process_event
from credentials.apps.badges.signals import (
BADGE_PROGRESS_COMPLETE,
Expand Down Expand Up @@ -63,7 +63,7 @@ def handle_requirement_regressed(sender, username, **kwargs):


@receiver(BADGE_PROGRESS_COMPLETE)
def handle_badge_completion(sender, username, badge_template_id, **kwargs): # pylint: disable=unused-argument
def handle_badge_completion(sender, username, badge_template_id, origin, **kwargs): # pylint: disable=unused-argument
"""
Fires once ALL requirements for a badge template were marked as "done".
Expand All @@ -73,16 +73,22 @@ def handle_badge_completion(sender, username, badge_template_id, **kwargs): # p

logger.debug("BADGES: progress is complete for %s on the %s", username, badge_template_id)

CredlyBadgeTemplateIssuer().award(username=username, credential_id=badge_template_id)
if origin == CredlyBadgeTemplate.ORIGIN:
CredlyBadgeTemplateIssuer().award(username=username, credential_id=badge_template_id)
elif origin == AccredibleGroup.ORIGIN:
AccredibleBadgeTemplateIssuer().award(username=username, credential_id=badge_template_id)


@receiver(BADGE_PROGRESS_INCOMPLETE)
def handle_badge_regression(sender, username, badge_template_id, **kwargs): # pylint: disable=unused-argument
def handle_badge_regression(sender, username, badge_template_id, origin, **kwargs): # pylint: disable=unused-argument
"""
On user's Badge regression (incompletion).
- username
- badge template ID
"""

CredlyBadgeTemplateIssuer().revoke(badge_template_id, username)
if origin == CredlyBadgeTemplate.ORIGIN:
CredlyBadgeTemplateIssuer().revoke(badge_template_id, username)
elif origin == AccredibleGroup.ORIGIN:
AccredibleBadgeTemplateIssuer().revoke(badge_template_id, username)
6 changes: 4 additions & 2 deletions credentials/apps/badges/signals/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def notify_requirement_regressed(*, sender, username, badge_template_id):
)


def notify_progress_complete(sender, username, badge_template_id):
def notify_progress_complete(sender, username, badge_template_id, origin):
"""
Notifies about user's completion on the badge template.
"""
Expand All @@ -57,17 +57,19 @@ def notify_progress_complete(sender, username, badge_template_id):
sender=sender,
username=username,
badge_template_id=badge_template_id,
origin=origin,
)


def notify_progress_incomplete(sender, username, badge_template_id):
def notify_progress_incomplete(sender, username, badge_template_id, origin):
"""
Notifies about user's regression on the badge template.
"""
BADGE_PROGRESS_INCOMPLETE.send(
sender=sender,
username=username,
badge_template_id=badge_template_id,
origin=origin,
)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.17 on 2024-12-19 11:38

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('credentials', '0030_revoke_certificates_management_command'),
]

operations = [
migrations.AlterField(
model_name='usercredential',
name='credential_content_type',
field=models.ForeignKey(limit_choices_to={'model__in': ('coursecertificate', 'programcertificate', 'credlybadgetemplate', 'accrediblegroup')}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
),
]
2 changes: 1 addition & 1 deletion credentials/apps/credentials/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class UserCredential(TimeStampedModel):

credential_content_type = models.ForeignKey(
ContentType,
limit_choices_to={"model__in": ("coursecertificate", "programcertificate", "credlybadgetemplate")},
limit_choices_to={"model__in": ("coursecertificate", "programcertificate", "credlybadgetemplate", "accrediblegroup")},
on_delete=models.CASCADE,
)
credential_id = models.PositiveIntegerField()
Expand Down

0 comments on commit 36dd538

Please sign in to comment.