Skip to content

Commit

Permalink
Merge branch 'aci.main' into hantkovskyi/aci-931/collect-only-active-…
Browse files Browse the repository at this point in the history
…penalties
  • Loading branch information
wowkalucky authored Apr 17, 2024
2 parents e73a91f + 485e5ca commit 37b48c2
Show file tree
Hide file tree
Showing 16 changed files with 324 additions and 86 deletions.
119 changes: 105 additions & 14 deletions credentials/apps/badges/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.core.management import call_command
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.urls import reverse

from credentials.apps.badges.admin_forms import (
BadgePenaltyForm,
Expand All @@ -32,6 +33,11 @@ class BadgeRequirementInline(admin.TabularInline):
show_change_link = True
extra = 0

# FIXME: disable until "Release VI"
exclude = [
"group",
]


class BadgePenaltyInline(admin.TabularInline):
model = BadgePenalty
Expand All @@ -50,6 +56,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 @@ -128,7 +137,14 @@ class CredlyBadgeTemplateAdmin(admin.ModelAdmin):
"fields": (
"site",
"is_active",
)
),
"description": _(
"""
WARNING: avoid configuration updates on activated badges.
Active badge templates are continuously processed and learners may already have partial progress on them.
Any changes in badge template requirements (including data rules) will affect learners' experience!
"""
),
},
),
(
Expand Down Expand Up @@ -156,7 +172,8 @@ class CredlyBadgeTemplateAdmin(admin.ModelAdmin):
)
inlines = [
BadgeRequirementInline,
BadgePenaltyInline,
# FIXME: disable until "Release V"
# BadgePenaltyInline,
]

def has_add_permission(self, request):
Expand Down Expand Up @@ -187,9 +204,12 @@ def delete_queryset(self, request, queryset):
super().delete_queryset(request, queryset)

def image(self, obj):
"""
Badge template preview image.
"""
if obj.icon:
return format_html('<img src="{}" width="50" height="auto" />', obj.icon)
return "-"
return None

image.short_description = _("icon")

Expand Down Expand Up @@ -224,21 +244,44 @@ class BadgeRequirementAdmin(admin.ModelAdmin):

list_display = [
"id",
"template",
"__str__",
"event_type",
"template_link",
]
list_display_links = (
"id",
"template",
"__str__",
)
list_filter = [
"template",
"event_type",
]
readonly_fields = [
"template",
"event_type",
"template_link",
]

fields = [
"template_link",
"event_type",
"description",
# FIXME: disable until "Release VI"
# "group",
]

def has_add_permission(self, request):
return False

def template_link(self, instance):
"""
Interactive link to parent (badge template).
"""
url = reverse("admin:badges_credlybadgetemplate_change", args=[instance.template.pk])
return format_html('<a href="{}">{}</a>', url, instance.template)

template_link.short_description = _("badge template")


class BadgePenaltyAdmin(admin.ModelAdmin):
"""
Expand All @@ -249,13 +292,19 @@ class BadgePenaltyAdmin(admin.ModelAdmin):
DataRulePenaltyInline,
]

list_display = [
list_display_links = (
"id",
"template",
)
list_display = [
"id",
"__str__",
"event_type",
"template_link",
]
list_display_links = (
"id",
"template",
"__str__",
)
list_filter = [
"template",
Expand All @@ -273,6 +322,15 @@ def formfield_for_manytomany(self, db_field, request, **kwargs):
kwargs["queryset"] = BadgeRequirement.objects.filter(template_id=template_id)
return super().formfield_for_manytomany(db_field, request, **kwargs)

def template_link(self, instance):
"""
Interactive link to parent (badge template).
"""
url = reverse("admin:badges_credlybadgetemplate_change", args=[instance.template.pk])
return format_html('<a href="{}">{}</a>', url, instance.template)

template_link.short_description = _("badge template")


class BadgeProgressAdmin(admin.ModelAdmin):
"""
Expand All @@ -284,21 +342,39 @@ class BadgeProgressAdmin(admin.ModelAdmin):
]
list_display = [
"id",
"template",
"username",
"template",
"complete",
]
list_display_links = (
"id",
"username",
"template",
)
readonly_fields = (
"username",
"template",
"complete",
"ratio",
)

@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 bool(getattr(obj, "credential", False))
return obj.completed

def ratio(self, obj):
"""
Displays progress value.
"""
return obj.ratio

def has_add_permission(self, request):
return False


class CredlyBadgeAdmin(admin.ModelAdmin):
Expand All @@ -307,26 +383,41 @@ class CredlyBadgeAdmin(admin.ModelAdmin):
"""

list_display = (
"uuid",
"username",
"credential",
"status",
"state",
"external_uuid",
)
list_filter = (
"status",
"state",
"uuid",
)
list_filter = ("state",)
search_fields = (
"username",
"uuid",
"external_uuid",
)
readonly_fields = (
"credential_id",
"credential_content_type",
"username",
"download_url",
"state",
"uuid",
"external_uuid",
)

def has_add_permission(self, request):
return False


# register admin configurations with respect to the feature flag
if is_badges_enabled():
admin.site.register(CredlyOrganization, CredlyOrganizationAdmin)
admin.site.register(CredlyBadgeTemplate, CredlyBadgeTemplateAdmin)
admin.site.register(CredlyBadge, CredlyBadgeAdmin)
admin.site.register(BadgeRequirement, BadgeRequirementAdmin)
admin.site.register(BadgePenalty, BadgePenaltyAdmin)
# FIXME: disable until "Release V"
# admin.site.register(BadgePenalty, BadgePenaltyAdmin)
admin.site.register(BadgeProgress, BadgeProgressAdmin)
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
59 changes: 43 additions & 16 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 @@ -69,7 +69,16 @@ def issue_credential(

return user_credential

def award(self, credential_id, username):
def award(self, *, username, credential_id):
"""
Awards a badge.
Creates user credential record for the given badge template, for a given user.
Notifies about the awarded badge (public signal).
Returns: UserCredential
"""

credential = self.get_credential(credential_id)
user_credential = self.issue_credential(credential, username)

Expand All @@ -92,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 All @@ -131,14 +145,27 @@ def revoke_credly_badge(self, credential_id, user_credential):
user_credential.state = response.get("data").get("state")
user_credential.save()

def award(self, credential_id, username):
user_credential = super().award(credential_id, username)
if not user_credential.is_issued:
self.issue_credly_badge(credential_id, user_credential)
return user_credential
def award(self, *, username, credential_id):
"""
Awards a Credly badge.
- Creates user credential record for the given badge template, for a given user;
- Notifies about the awarded badge (public signal);
- Issues external Credly badge (Credly API);
Returns: (CredlyBadge) user credential
"""

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

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

return credly_badge

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

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


class Migration(migrations.Migration):

dependencies = [
('badges', '0013_alter_datarule_requirement'),
]

operations = [
migrations.AlterField(
model_name='fulfillment',
name='requirement',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fulfillments', to='badges.badgerequirement'),
),
]
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),
),
]
Loading

0 comments on commit 37b48c2

Please sign in to comment.