Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [badges] add Badges feature #116

Open
wants to merge 1 commit into
base: aci.main.master-based
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ coverage:
target: auto
project:
default:
target: auto
target: 95
ignore:
- credentials/settings/production.py
- credentials/settings/devstack.py
Empty file.
245 changes: 245 additions & 0 deletions credentials/apps/badges/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
"""
Admin section configuration.
"""

from django.contrib import admin, messages
from django.contrib.sites.shortcuts import get_current_site
from django.core.management import call_command
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _

from .admin_forms import BadgePenaltyForm, BadgeRequirementForm, CredlyOrganizationAdminForm, DataRuleForm, PenaltyDataRuleForm

from .models import (
BadgePenalty,
BadgeProgress,
BadgeRequirement,
CredlyBadge,
CredlyBadgeTemplate,
CredlyOrganization,
DataRule,
Fulfillment,
PenaltyDataRule,
)
from .toggles import is_badges_enabled


class BadgeRequirementInline(admin.TabularInline):
model = BadgeRequirement
show_change_link = True
extra = 0
form = BadgeRequirementForm


class BadgePenaltyInline(admin.TabularInline):
model = BadgePenalty
show_change_link = True
extra = 0
form = BadgePenaltyForm


class FulfillmentInline(admin.TabularInline):
model = Fulfillment
extra = 0


class DataRuleInline(admin.TabularInline):
model = DataRule
extra = 0
form = DataRuleForm


class CredlyOrganizationAdmin(admin.ModelAdmin):
"""
Credly organization admin setup.
"""

form = CredlyOrganizationAdminForm
list_display = (
"name",
"uuid",
"api_key",
)
readonly_fields = [
"name",
]
actions = ("sync_organization_badge_templates",)

@admin.action(description="Sync organization badge templates")
def sync_organization_badge_templates(self, request, queryset):
"""
Sync badge templates for selected organizations.
"""
site = get_current_site(request)
for organization in queryset:
call_command(
"sync_organization_badge_templates",
organization_id=organization.uuid,
site_id=site.id,
)

messages.success(request, _("Badge templates were successfully updated."))


class CredlyBadgeTemplateAdmin(admin.ModelAdmin):
"""
Badge template admin setup.
"""

exclude = [
"icon",
]
list_display = (
"organization",
"state",
"name",
"uuid",
"is_active",
"image",
)
list_filter = (
"organization",
"is_active",
"state",
)
search_fields = (
"name",
"uuid",
)
readonly_fields = [
"organization",
"origin",
"state",
"dashboard_link",
"image",
]
inlines = [
BadgeRequirementInline,
BadgePenaltyInline,
]

def has_add_permission(self, request):
return False

def dashboard_link(self, obj):
url = obj.management_url
return format_html("<a href='{url}'>{url}</a>", url=url)

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

image.short_description = _("icon")


class DataRulePenaltyInline(admin.TabularInline):
model = PenaltyDataRule
extra = 0
form = PenaltyDataRuleForm


class BadgeRequirementAdmin(admin.ModelAdmin):
"""
Badge template requirement admin setup.
"""

inlines = [
DataRuleInline,
]

list_display = [
"id",
"template",
"event_type",
]
list_display_links = (
"id",
"template",
)
list_filter = [
"template",
"event_type",
]
form = BadgeRequirementForm


class BadgePenaltyAdmin(admin.ModelAdmin):
"""
Badge requirement penalty setup admin.
"""
inlines = [
DataRulePenaltyInline,
]

list_display = [
"id",
"template",
]
list_display_links = (
"id",
"template",
)
list_filter = [
"template",
"requirements",
]
form = BadgePenaltyForm


class BadgeProgressAdmin(admin.ModelAdmin):
"""
Badge template progress admin setup.
"""

inlines = [
FulfillmentInline,
]
list_display = [
"id",
"template",
"username",
"complete",
]
list_display_links = (
"id",
"template",
)

@admin.display(boolean=True)
def complete(self, obj):
"""
TODO: switch dedicated `is_complete` bool field
"""
return bool(getattr(obj, "credential", False))


class CredlyBadgeAdmin(admin.ModelAdmin):
"""
Credly badge admin setup.
"""

list_display = (
"username",
"state",
"uuid",
)
list_filter = ("state",)
search_fields = (
"username",
"uuid",
)
readonly_fields = (
"state",
"uuid",
)


# 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)
admin.site.register(BadgeProgress, BadgeProgressAdmin)
113 changes: 113 additions & 0 deletions credentials/apps/badges/admin_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Badges admin forms.
"""

from django import forms
from django.utils.translation import gettext_lazy as _

from .credly.api_client import CredlyAPIClient
from .credly.exceptions import CredlyAPIError
from .models import BadgePenalty, BadgeRequirement, CredlyOrganization, DataRule, PenaltyDataRule


class CredlyOrganizationAdminForm(forms.ModelForm):
"""
Additional actions for Credly Organization items.
"""

api_data = {}

class Meta:
model = CredlyOrganization
fields = "__all__"

def clean(self):
"""
Perform Credly API check for given organization ID.

- Credly Organization exists;
- fetch additional data for such organization;
"""
cleaned_data = super().clean()

uuid = cleaned_data.get("uuid")
api_key = cleaned_data.get("api_key")

credly_api_client = CredlyAPIClient(uuid, api_key)
self._ensure_organization_exists(credly_api_client)

return cleaned_data

def save(self, commit=True):
"""
Auto-fill addition properties.
"""
instance = super().save(commit=False)
instance.name = self.api_data.get("name")
instance.save()

return instance

def _ensure_organization_exists(self, api_client):
"""
Try to fetch organization data by the configured Credly Organization ID.
"""
try:
response_json = api_client.fetch_organization()
if org_data := response_json.get("data"):
self.api_data = org_data
except CredlyAPIError as err:
raise forms.ValidationError(message=str(err))



class BadgePenaltyForm(forms.ModelForm):
class Meta:
model = BadgePenalty
fields = "__all__"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and hasattr(self.instance, 'template') and self.instance.template.is_active:
for field_name in self.fields:
if field_name in ("template", "requirements", "description"):
self.fields[field_name].disabled = True


class PenaltyDataRuleForm(forms.ModelForm):
class Meta:
model = PenaltyDataRule
fields = "__all__"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and hasattr(self.instance, 'penalty') and self.instance.penalty.template.is_active:
for field_name in self.fields:
if field_name in ("data_path", "operator", "value"):
self.fields[field_name].disabled = True


class BadgeRequirementForm(forms.ModelForm):
class Meta:
model = BadgeRequirement
fields = "__all__"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and hasattr(self.instance, 'template') and self.instance.template.is_active:
for field_name in self.fields:
if field_name in ("template", "event_type", "description"):
self.fields[field_name].disabled = True


class DataRuleForm(forms.ModelForm):
class Meta:
model = DataRule
fields = "__all__"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and hasattr(self.instance, 'requirement') and self.instance.requirement.template.is_active:
for field_name in self.fields:
if field_name in ("data_path", "operator", "value"):
self.fields[field_name].disabled = True
Empty file added credentials/apps/badges/api.py
Empty file.
Loading
Loading