Skip to content

Commit

Permalink
feat: [ACI-387, ACI-393] badge templates synchronization
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrylo-kh committed Jan 29, 2024
1 parent 060df14 commit 97880c1
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

from __future__ import unicode_literals


__version__ = "0.0.1"
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

from credentials.apps.badges.toggles import is_badges_enabled

from .models import CredlyOrganization, BadgeTemplate
from .utils import sync_badge_templates_for_organization
from .forms import CredlyOrganizationAdminForm
from .models import BadgeTemplate, CredlyOrganization
from .utils import sync_badge_templates_for_organization


class CredlyOrganizationAdmin(admin.ModelAdmin):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import base64
import logging
from functools import lru_cache
from urllib.parse import urljoin

import requests
from attrs import asdict
from django.conf import settings
from requests.exceptions import HTTPError

from .exceptions import CredlyAPIError


logger = logging.getLogger(__name__)


class CredlyAPIClient:
"""
A client for interacting with the Credly API.
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.
"""

def __init__(self, organization_id, api_key):
"""
Initializes a CredlyRestAPI object.
Args:
organization_id (str): ID of the organization.
api_key (str): API key for authentication.
"""

self.organization_id = organization_id
self.api_key = api_key
self.base_api_url = urljoin(settings.CREDLY_API_BASE_URL, f"organizations/{self.organization_id}/")

def perform_request(self, method, url_suffix, data=None):
"""
Perform an HTTP request to the specified URL suffix.
Args:
method (str): HTTP method to use for the request.
url_suffix (str): URL suffix to append to the base Credly API URL.
data (dict, optional): Data to send with the request.
Returns:
dict: JSON response from the API.
Raises:
requests.HTTPError: If the API returns an error response.
"""
url = urljoin(self.base_api_url, url_suffix)
response = requests.request(method.upper(), url, headers=self._get_headers(), data=data)
self._raise_for_error(response)
return response.json()

def fetch_organization(self):
"""
Fetches the organization from the Credly API.
"""
return self.perform_request("get", "")

def fetch_badge_templates(self):
"""
Fetches the badge templates from the Credly API.
"""
return self.perform_request("get", "badge_templates/")

def fetch_event_information(self, event_id):
"""
Fetches the event information from the Credly API.
Args:
event_id (str): ID of the event.
"""
return self.perform_request("get", f"events/{event_id}/")

def issue_badge(self, issue_badge_data):
"""
Issues a badge using the Credly REST API.
Args:
issue_badge_data (IssueBadgeData): Data required to issue the badge.
"""
return self.perform_request("post", "badges/", asdict(issue_badge_data))

def revoke_badge(self, badge_id):
"""
Revoke a badge with the given badge ID.
Args:
badge_id (str): ID of the badge to revoke.
"""
return self.perform_request("put", f"badges/{badge_id}/revoke/")

def _raise_for_error(self, response):
"""
Raises a CredlyAPIError if the response status code indicates an error.
Args:
response (requests.Response): Response object from the Credly API request.
Raises:
CredlyAPIError: If the response status code indicates an error.
"""
try:
response.raise_for_status()
except HTTPError:
logger.error(f"Error while processing credly api request: {response.status_code} - {response.text}")
raise CredlyAPIError

def _get_headers(self):
"""
Returns the headers for making API requests to Credly.
"""
return {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Basic {self._build_authorization_token()}",
}

@lru_cache
def _build_authorization_token(self):
"""
Build the authorization token for the Credly API.
Returns:
str: Authorization token.
"""
return base64.b64encode(self.api_key.encode("ascii")).decode("ascii")
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from credentials.apps.badges.apps import BadgesAppConfig
from credentials.apps.badges.toggles import is_badges_enabled, check_badges_enabled
from credentials.apps.badges.toggles import check_badges_enabled, is_badges_enabled
from credentials.apps.plugins.constants import PROJECT_TYPE, PluginSettings, PluginURLs, SettingsType


Expand Down
21 changes: 15 additions & 6 deletions credentials/apps/badges/distribution/credly/credly_badges/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import attr
from datetime import datetime

import attr


@attr.s(auto_attribs=True, frozen=True)
class IssueBadgeData:
Expand All @@ -15,8 +16,16 @@ class IssueBadgeData:
issued_at (datetime): Timestamp when the badge was issued.
"""

recipient_email: attr.ib(type=str)
issued_to_first_name: attr.ib(type=str)
issued_to_last_name: attr.ib(type=str)
badge_template_id: attr.ib(type=str)
issued_at: attr.ib(type=datetime)
recipient_email: str
issued_to_first_name: str
issued_to_last_name: str
badge_template_id: str
issued_at: datetime


@attr.s(auto_attribs=True, frozen=True)
class CredlyEventInfoData:
id: str
organization_id: str
event_type: str
occurred_at: datetime
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from django import forms
from django.utils.translation import gettext_lazy as _

from .models import CredlyOrganization
from .rest_api import CredlyAPIClient
from .api_client import CredlyAPIClient
from .exceptions import CredlyAPIError
from .models import CredlyOrganization


class CredlyOrganizationAdminForm(forms.ModelForm):
class Meta:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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
from credly_badges.utils import sync_badge_templates_for_organization
from django.core.management.base import BaseCommand


logger = logging.getLogger(__name__)
Expand Down
142 changes: 33 additions & 109 deletions credentials/apps/badges/distribution/credly/credly_badges/rest_api.py
Original file line number Diff line number Diff line change
@@ -1,124 +1,48 @@
import base64
import logging
from functools import lru_cache
from urllib.parse import urljoin

import requests
from attrs import asdict
from django.conf import settings
from requests.exceptions import HTTPError
from .exceptions import CredlyAPIError
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from .api_client import CredlyAPIClient
from .data import CredlyEventInfoData
from .models import CredlyOrganization
from .utils import (
handle_badge_template_changed_event,
handle_badge_template_created_event,
handle_badge_template_deleted_event,
)


logger = logging.getLogger(__name__)


class CredlyAPIClient:
class CredlyWebhook(APIView):
"""
A client for interacting with the Credly API.
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.
Public API that handle Credly webhooks.
TODO: improve client to return data in a more usable format
Usage:
POST /edx_badges/api/credly/v1/webhook
"""

def __init__(self, organization_id, api_key):
"""
Initializes a CredlyRestAPI object.
Args:
organization_id (str): ID of the organization.
api_key (str): API key for authentication.
"""

self.organization_id = organization_id
self.api_key = api_key
self.base_api_url = urljoin(settings.CREDLY_API_BASE_URL, f"organizations/{self.organization_id}/")

def perform_request(self, method, url_suffix, data=None):
"""
Perform an HTTP request to the specified URL suffix.
Args:
method (str): HTTP method to use for the request.
url_suffix (str): URL suffix to append to the base Credly API URL.
data (dict, optional): Data to send with the request.
Returns:
dict: JSON response from the API.
Raises:
requests.HTTPError: If the API returns an error response.
"""
url = urljoin(self.base_api_url, url_suffix)
response = requests.request(method.upper(), url, headers=self._get_headers(), data=data)
self._raise_for_error(response)
return response.json()

def fetch_organization(self):
"""
Fetches the organization from the Credly API.
"""
return self.perform_request("get", "")

def fetch_badge_templates(self):
"""
Fetches the badge templates from the Credly API.
"""
return self.perform_request("get", "badge_templates/")

def issue_badge(self, issue_badge_data):
"""
Issues a badge using the Credly REST API.
Args:
issue_badge_data (IssueBadgeData): Data required to issue the badge.
"""
return self.perform_request("post", "badges/", asdict(issue_badge_data))

def revoke_badge(self, badge_id):
"""
Revoke a badge with the given badge ID.
Args:
badge_id (str): ID of the badge to revoke.
"""
return self.perform_request("put", f"badges/{badge_id}/revoke/")

def _raise_for_error(self, response):
"""
Raises a CredlyAPIError if the response status code indicates an error.
Args:
response (requests.Response): Response object from the Credly API request.
authentication_classes = []
permission_classes = []

Raises:
CredlyAPIError: If the response status code indicates an error.
"""
try:
response.raise_for_status()
except HTTPError:
logger.error(f"Error while processing credly api request: {response.status_code} - {response.text}")
raise CredlyAPIError
def post(self, request):
event_info_data = CredlyEventInfoData(**request.data)
organization = get_object_or_404(CredlyOrganization, uuid=event_info_data.organization_id)
credly_api_client = CredlyAPIClient(organization.uuid, organization.api_key)

def _get_headers(self):
"""
Returns the headers for making API requests to Credly.
"""
return {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Basic {self._build_authorization_token()}",
}
event_info_response = credly_api_client.fetch_event_information(event_info_data.id)

@lru_cache
def _build_authorization_token(self):
"""
Build the authorization token for the Credly API.
if event_info_data.event_type == "badge_template.created":
handle_badge_template_created_event(event_info_response)
elif event_info_data.event_type == "badge_template.changed":
handle_badge_template_changed_event(event_info_response)
elif event_info_data.event_type == "badge_template.deleted":
handle_badge_template_deleted_event(event_info_response)
else:
logger.error(f"Unknown event type: {event_info_data.event_type}")

Returns:
str: Authorization token.
"""
return base64.b64encode(self.api_key.encode("ascii")).decode("ascii")
return Response(status=status.HTTP_204_NO_CONTENT)
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
Credly Badges routing configuration.
"""

from django.urls import path

from credentials.apps.badges.toggles import is_badges_enabled

from .rest_api import CredlyWebhook


urlpatterns = []

if is_badges_enabled():
urlpatterns = [
# Define urls here
path('api/webhook/', CredlyWebhook.as_view(), name='credly-webhook'),
]
Loading

0 comments on commit 97880c1

Please sign in to comment.