Skip to content

Commit

Permalink
refactor: add external UUID for Credly badge issuing tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
wowkalucky committed Apr 16, 2024
1 parent 99a6c8c commit d22a50f
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 27 deletions.
29 changes: 26 additions & 3 deletions credentials/apps/badges/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def formfield_for_manytomany(self, db_field, request, **kwargs):
class FulfillmentInline(admin.TabularInline):
model = Fulfillment
extra = 0
readonly_fields = [
"requirement",
]


class DataRuleInline(admin.TabularInline):
Expand Down Expand Up @@ -284,21 +287,41 @@ class BadgeProgressAdmin(admin.ModelAdmin):
]
list_display = [
"id",
"template",
"username",
"template",
"complete",
"issued",
]
list_display_links = (
"id",
"username",
"template",
)
readonly_fields = (
"username",
"template",
"complete",
"issued",
)
exclude = [
"credential",
]

@admin.display(boolean=True)
def complete(self, obj):
"""
TODO: switch dedicated `is_complete` bool field
Identifies if all requirements are already fulfilled.
NOTE: (performance) dynamic evaluation.
"""
return obj.completed

@admin.display(boolean=True)
def issued(self, obj):
"""
Identifies if user credential exists (regardless its current status).
"""
return bool(getattr(obj, "credential", False))
return bool(obj.credential)


class CredlyBadgeAdmin(admin.ModelAdmin):
Expand Down
2 changes: 1 addition & 1 deletion credentials/apps/badges/credly/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


@attr.s(auto_attribs=True, frozen=True)
class IssueBadgeData:
class CredlyBadgeData:
"""
Represents the data required to issue a badge.
Expand Down
29 changes: 17 additions & 12 deletions credentials/apps/badges/issuers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.utils.translation import gettext as _

from credentials.apps.badges.credly.api_client import CredlyAPIClient
from credentials.apps.badges.credly.data import IssueBadgeData
from credentials.apps.badges.credly.data import CredlyBadgeData
from credentials.apps.badges.credly.exceptions import CredlyAPIError
from credentials.apps.badges.models import BadgeTemplate, CredlyBadge, CredlyBadgeTemplate, UserCredential
from credentials.apps.badges.signals.signals import notify_badge_awarded, notify_badge_revoked
Expand Down Expand Up @@ -101,26 +101,31 @@ class CredlyBadgeTemplateIssuer(BadgeTemplateIssuer):
issued_credential_type = CredlyBadgeTemplate
issued_user_credential_type = CredlyBadge

def issue_credly_badge(self, credential_id, user_credential):
def issue_credly_badge(self, *, user_credential):
"""
Requests Credly service for external badge issuing based on internal user credential (CredlyBadge).
"""

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

credential = self.get_credential(credential_id)
credly_api = CredlyAPIClient(credential.organization.uuid)
issue_badge_data = IssueBadgeData(
credly_badge_data = CredlyBadgeData(
recipient_email=user.email,
issued_to_first_name=(user.first_name or user.username),
issued_to_last_name=(user.last_name or user.username),
badge_template_id=str(credential.uuid),
issued_at=credential.created.strftime("%Y-%m-%d %H:%M:%S %z"),
badge_template_id=str(badge_template.uuid),
issued_at=badge_template.created.strftime("%Y-%m-%d %H:%M:%S %z"),
)

try:
response = credly_api.issue_badge(issue_badge_data)
credly_api = CredlyAPIClient(badge_template.organization.uuid)
response = credly_api.issue_badge(credly_badge_data)
except CredlyAPIError:
user_credential.state = "error"
user_credential.save()
raise

user_credential.uuid = response.get("data").get("id")
user_credential.external_uuid = response.get("data").get("id")
user_credential.state = response.get("data").get("state")
user_credential.save()

Expand Down Expand Up @@ -154,13 +159,13 @@ def award(self, *, username, credential_id):
user_credential = super().award(username=username, credential_id=credential_id)

# do not issue new badges if the badge was issued already
if not user_credential.is_issued:
self.issue_credly_badge(credential_id, user_credential)
if not user_credential.issued:
self.issue_credly_badge(user_credential)

return user_credential

def revoke(self, credential_id, username):
user_credential = super().revoke(credential_id, username)
if user_credential.is_issued:
if user_credential.issued:
self.revoke_credly_badge(credential_id, user_credential)
return user_credential
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2024-04-16 15:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('badges', '0014_alter_fulfillment_requirement'),
]

operations = [
migrations.AddField(
model_name='credlybadge',
name='external_uuid',
field=models.UUIDField(blank=True, help_text='Credly service badge identifier', null=True, unique=True),
),
]
49 changes: 39 additions & 10 deletions credentials/apps/badges/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@
Badges DB models.
"""

import logging
import operator
import uuid

from attrs import asdict
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from credentials.apps.badges.signals.signals import (
notify_progress_complete,
notify_progress_incomplete,
notify_requirement_fulfilled,
notify_requirement_regressed,
)
from django_extensions.db.models import TimeStampedModel
from model_utils import Choices
from model_utils.fields import StatusField
Expand All @@ -26,12 +22,20 @@
)

from credentials.apps.badges.credly.utils import get_credly_base_url

from credentials.apps.badges.signals.signals import (
notify_progress_complete,
notify_progress_incomplete,
notify_requirement_fulfilled,
notify_requirement_regressed,
)
from credentials.apps.badges.utils import is_datapath_valid, keypath
from credentials.apps.core.api import get_user_by_username
from credentials.apps.credentials.models import AbstractCredential, UserCredential


logger = logging.getLogger(__name__)


class CredlyOrganization(TimeStampedModel):
"""
Credly Organization configuration.
Expand Down Expand Up @@ -509,14 +513,35 @@ class CredlyBadge(UserCredential):
- tracks distributed (external Credly service) state for Credly badge.
"""

STATES = Choices("created", "no_response", "error", "pending", "accepted", "rejected", "revoked")
STATES = Choices(
"created",
"no_response",
"error",
"pending",
"accepted",
"rejected",
"revoked",
"expired",
)
ISSUING_STATES = {
STATES.pending,
STATES.accepted,
STATES.rejected,
}

state = StatusField(
choices_name="STATES",
help_text=_("Credly badge issuing state"),
default=STATES.created,
)

external_uuid = models.UUIDField(
blank=True,
null=True,
unique=True,
help_text=_("Credly service badge identifier"),
)

def as_badge_data(self) -> BadgeData:
"""
Represents itself as a BadgeData instance.
Expand Down Expand Up @@ -544,8 +569,12 @@ def as_badge_data(self) -> BadgeData:
image_url=str(badge_template.icon),
),
)

return badge_data

@property
def is_issued(self):
return self.uuid and (self.state in ["pending", "accepted", "rejected"])
def issued(self):
"""
Checks if this user credential already has issued (external) Credly badge.
"""
return self.external_uuid and (self.state in self.ISSUING_STATES)
4 changes: 4 additions & 0 deletions credentials/apps/badges/signals/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
Badges internal signals.
"""

import logging
from django.dispatch import Signal
from openedx_events.learning.signals import BADGE_AWARDED, BADGE_REVOKED


logger = logging.getLogger(__name__)


# a single requirements for a badge template was finished
BADGE_REQUIREMENT_FULFILLED = Signal()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.2.20 on 2024-04-16 15:46

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

dependencies = [
('credentials', '0027_auto_20240408_1614'),
]

operations = [
migrations.AlterField(
model_name='usercredential',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
]
2 changes: 1 addition & 1 deletion credentials/apps/credentials/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class UserCredential(TimeStampedModel):
download_url = models.CharField(
max_length=255, blank=True, null=True, help_text=_("URL at which the credential can be downloaded")
)
uuid = models.UUIDField(blank=True, null=True, unique=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

class Meta:
unique_together = (("username", "credential_content_type", "credential_id"),)
Expand Down

0 comments on commit d22a50f

Please sign in to comment.