diff --git a/credentials/apps/badges/distribution/credly/credly_badges/admin.py b/credentials/apps/badges/distribution/credly/credly_badges/admin.py index fcda4be4d6..6601caa789 100644 --- a/credentials/apps/badges/distribution/credly/credly_badges/admin.py +++ b/credentials/apps/badges/distribution/credly/credly_badges/admin.py @@ -6,16 +6,38 @@ from credentials.apps.badges.toggles import is_badges_enabled -from .models import CredlyOrganization +from .models import CredlyOrganization, BadgeTemplate +from .utils import sync_badge_templates_for_organization +from .forms import CredlyOrganizationAdminForm class CredlyOrganizationAdmin(admin.ModelAdmin): """ Credly organization admin setup. """ + form = CredlyOrganizationAdminForm list_display = ("name", "uuid", "api_key",) + 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. + """ + for organization in queryset: + sync_badge_templates_for_organization(organization.uuid) + + +class BadgeTemplateAdmin(admin.ModelAdmin): + """ + Badge template admin setup. + """ + list_display = ("name", "uuid", "organization", "state",) + list_filter = ("state", "organization",) + search_fields = ("name", "uuid",) # register admin configurations with respect to the feature flag if is_badges_enabled(): admin.site.register(CredlyOrganization, CredlyOrganizationAdmin) + admin.site.register(BadgeTemplate, BadgeTemplateAdmin) diff --git a/credentials/apps/badges/distribution/credly/credly_badges/apps.py b/credentials/apps/badges/distribution/credly/credly_badges/apps.py index 6314fc2570..e73e85989a 100644 --- a/credentials/apps/badges/distribution/credly/credly_badges/apps.py +++ b/credentials/apps/badges/distribution/credly/credly_badges/apps.py @@ -1,5 +1,5 @@ from credentials.apps.badges.apps import BadgesAppConfig -from credentials.apps.badges.toggles import is_badges_enabled +from credentials.apps.badges.toggles import is_badges_enabled, check_badges_enabled from credentials.apps.plugins.constants import PROJECT_TYPE, PluginSettings, PluginURLs, SettingsType @@ -35,10 +35,11 @@ class CredlyBadgesConfig(BadgesAppConfig): } } if is_badges_enabled() else {} # TODO: improve this + @ check_badges_enabled def ready(self): """ - Activate installed badges plugins if they are enabled. - Performs initial registrations for checks, signals, etc. """ super().ready() + from .utils import sync_badge_templates_for_organization + sync_badge_templates_for_organization("c117c179-81b1-4f7e-a3a1-e6ae30568c13") diff --git a/credentials/apps/badges/distribution/credly/credly_badges/forms.py b/credentials/apps/badges/distribution/credly/credly_badges/forms.py new file mode 100644 index 0000000000..15f320f285 --- /dev/null +++ b/credentials/apps/badges/distribution/credly/credly_badges/forms.py @@ -0,0 +1,32 @@ +""" +Django forms for the credly badges +""" + +from django import forms +from django.utils.translation import gettext_lazy as _ + +from .models import CredlyOrganization +from .rest_api import CredlyAPIClient +from .exceptions import CredlyAPIError + +class CredlyOrganizationAdminForm(forms.ModelForm): + class Meta: + model = CredlyOrganization + fields = "__all__" + + def clean(self): + """ + Validate that organization is existing on Credly services. + """ + cleaned_data = super().clean() + + uuid = cleaned_data.get("uuid") + api_key = cleaned_data.get("api_key") + + try: + credly_api_client = CredlyAPIClient(uuid, api_key) + credly_api_client.fetch_organization() + except CredlyAPIError: + raise forms.ValidationError(_('Invalid organization ID or API key. Organization not found on Credly services.')) + + return cleaned_data diff --git a/credentials/apps/badges/distribution/credly/credly_badges/management/__init__.py b/credentials/apps/badges/distribution/credly/credly_badges/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/credentials/apps/badges/distribution/credly/credly_badges/management/commands/__init__.py b/credentials/apps/badges/distribution/credly/credly_badges/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/credentials/apps/badges/distribution/credly/credly_badges/management/commands/sync_organization_badge_templates.py b/credentials/apps/badges/distribution/credly/credly_badges/management/commands/sync_organization_badge_templates.py new file mode 100644 index 0000000000..a1330f91c2 --- /dev/null +++ b/credentials/apps/badges/distribution/credly/credly_badges/management/commands/sync_organization_badge_templates.py @@ -0,0 +1,35 @@ +import logging +from django.core.management.base import BaseCommand +from credly_badges.utils import sync_badge_templates_for_organization +from credly_badges.models import CredlyOrganization + + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = 'Sync badge templates for a specific organization or all organizations' + + def add_arguments(self, parser): + parser.add_argument('--organization_id', type=str, help='UUID of the organization.') + + def handle(self, *args, **options): + """ + Sync badge templates for a specific organization or all organizations. + + Usage: + ./manage.py sync_organization_badge_templates + ./manage.py sync_organization_badge_templates --organization_id c117c179-81b1-4f7e-a3a1-e6ae30568c13 + """ + organization_id = options.get('organization_id') + + if organization_id: + logger.info(f'Syncing badge templates for single organization: {organization_id}') + sync_badge_templates_for_organization(organization_id) + else: + all_organization_ids = CredlyOrganization.get_all_organization_ids() + logger.info(f'Organization id was not provided. Syncing badge templates for all organizations: {all_organization_ids}') + for organization_id in all_organization_ids: + sync_badge_templates_for_organization(organization_id) + + logger.info('Done.') diff --git a/credentials/apps/badges/distribution/credly/credly_badges/migrations/0002_alter_credlyorganization_api_key_and_more.py b/credentials/apps/badges/distribution/credly/credly_badges/migrations/0002_alter_credlyorganization_api_key_and_more.py new file mode 100644 index 0000000000..c6ae6d1a08 --- /dev/null +++ b/credentials/apps/badges/distribution/credly/credly_badges/migrations/0002_alter_credlyorganization_api_key_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.7 on 2024-01-26 11:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('credly_badges', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='credlyorganization', + name='api_key', + field=models.CharField(help_text='Credly API shared secret for organization.', max_length=255), + ), + migrations.AlterField( + model_name='credlyorganization', + name='name', + field=models.CharField(help_text='Organization display name.', max_length=255), + ), + migrations.AlterField( + model_name='credlyorganization', + name='uuid', + field=models.UUIDField(help_text='Unique organization ID.', unique=True), + ), + migrations.CreateModel( + name='BadgeTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(help_text='Unique badge template ID.', unique=True)), + ('name', models.CharField(help_text='Badge template name.', max_length=255)), + ('state', models.CharField(choices=[('active', 'Active'), ('archived', 'Archived'), ('draft', 'Draft'), ('inactive', 'Inactive')], default='inactive', help_text='State of the badge template.', max_length=255)), + ('organization', models.ForeignKey(help_text='Organization of the badge template.', on_delete=django.db.models.deletion.CASCADE, to='credly_badges.credlyorganization')), + ], + ), + ] diff --git a/credentials/apps/badges/distribution/credly/credly_badges/models.py b/credentials/apps/badges/distribution/credly/credly_badges/models.py index 2f25ceb446..ffdd722963 100644 --- a/credentials/apps/badges/distribution/credly/credly_badges/models.py +++ b/credentials/apps/badges/distribution/credly/credly_badges/models.py @@ -14,3 +14,42 @@ class CredlyOrganization(TimeStampedModel): uuid = models.UUIDField(unique=True, help_text=_('Unique organization ID.')) name = models.CharField(max_length=255, help_text=_('Organization display name.')) api_key = models.CharField(max_length=255, help_text=_('Credly API shared secret for organization.')) + + def __str__(self): + return self.name + + @classmethod + def get_all_organization_ids(cls): + """ + Get all organization IDs. + """ + return cls.objects.values_list('uuid', flat=True) + + +class BadgeTemplate(models.Model): + """ + Badge template model. + """ + STATE_CHOICES = ( + ('active', _('Active')), + ('archived', _('Archived')), + ('draft', _('Draft')), + ('inactive', _('Inactive')), + ) + + uuid = models.UUIDField(unique=True, help_text=_('Unique badge template ID.')) + name = models.CharField(max_length=255, help_text=_('Badge template name.')) + organization = models.ForeignKey( + CredlyOrganization, + on_delete=models.CASCADE, + help_text=_('Organization of the badge template.') + ) + state = models.CharField( + max_length=255, + choices=STATE_CHOICES, + default='inactive', + help_text=_('State of the badge template.') + ) + + def __str__(self): + return self.name diff --git a/credentials/apps/badges/distribution/credly/credly_badges/rest_api.py b/credentials/apps/badges/distribution/credly/credly_badges/rest_api.py index 8bc21504d7..c434edbea3 100644 --- a/credentials/apps/badges/distribution/credly/credly_badges/rest_api.py +++ b/credentials/apps/badges/distribution/credly/credly_badges/rest_api.py @@ -6,8 +6,7 @@ import requests from attrs import asdict from django.conf import settings -from requests.packages.urllib3.exceptions import HTTPError - +from requests.exceptions import HTTPError from .exceptions import CredlyAPIError @@ -21,6 +20,8 @@ class CredlyAPIClient: This class provides methods for performing various operations on the Credly API, such as fetching organization details, fetching badge templates, issuing badges, and revoking badges. + + TODO: improve client to return data in a more usable format """ def __init__(self, organization_id, api_key): diff --git a/credentials/apps/badges/distribution/credly/credly_badges/utils.py b/credentials/apps/badges/distribution/credly/credly_badges/utils.py new file mode 100644 index 0000000000..de9d054c5a --- /dev/null +++ b/credentials/apps/badges/distribution/credly/credly_badges/utils.py @@ -0,0 +1,42 @@ +from django.shortcuts import get_object_or_404 + +from .rest_api import CredlyAPIClient +from .models import CredlyOrganization, BadgeTemplate + + +def sync_badge_templates_for_organization(organization_id): + """ + Sync badge templates for a specific organization and create records in the database. + + Args: + organization_id (str): UUID of the organization. + + Raises: + Http404: If organization is not found. + """ + organization = get_object_or_404(CredlyOrganization, uuid=organization_id) + + credly_api_client = CredlyAPIClient(organization_id, organization.api_key) + badge_templates_data = credly_api_client.fetch_badge_templates() + + for badge_template_data in badge_templates_data.get('data', []): + BadgeTemplate.objects.update_or_create( + uuid=badge_template_data.get('id'), + defaults={ + 'name': badge_template_data.get('name'), + 'organization': organization, + } + ) + + +def validate_organization(organization_id): + """ + Validate organization ID. + + Args: + organization_id (str): UUID of the organization. + """ + + organization = get_object_or_404(CredlyOrganization, uuid=organization_id) + credly_api_client = CredlyAPIClient(organization_id, organization.api_key) + credly_api_client.fetch_organization()