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: add Accredible integration #2675

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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: 2 additions & 0 deletions .annotation_safe_list.yml
Original file line number Diff line number Diff line change
@@ -29,6 +29,8 @@ badges.Fulfillment:
".. no_pii:": "This model has no PII"
badges.PenaltyDataRule:
".. no_pii:": "This model has no PII"
badges.AccredibleAPIConfig:
".. no_pii:": "This model has no PII"
credentials.HistoricalProgramCompletionEmailConfiguration:
".. no_pii:": "This model has no PII"
contenttypes.ContentType:
127 changes: 127 additions & 0 deletions credentials/apps/badges/accredible/api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import logging

from attrs import asdict
from django.conf import settings
from django.contrib.sites.models import Site

from credentials.apps.badges.accredible.data import AccredibleBadgeData, AccredibleExpireBadgeData
from credentials.apps.badges.accredible.exceptions import AccredibleError
from credentials.apps.badges.accredible.utils import get_accredible_api_base_url
from credentials.apps.badges.base_api_client import BaseBadgeProviderClient
from credentials.apps.badges.models import AccredibleAPIConfig, AccredibleGroup


logger = logging.getLogger(__name__)


class AccredibleAPIClient(BaseBadgeProviderClient):
"""
A client for interacting with the Accredible API.

This class provides methods for performing various operations on the Accredible API.
"""

PROVIDER_NAME = "Accredible"

def __init__(self, api_config_id: int):
"""
Initializes a AccredibleAPIClient object.

Args:
api_config (AccredibleAPIConfig): Configuration object for the Accredible API.
"""

self.api_config_id = api_config_id
self.api_config = self.get_api_config()

def get_api_config(self) -> AccredibleAPIConfig:
"""
Returns the API configuration object for the Accredible API.
"""
try:
return AccredibleAPIConfig.objects.get(id=self.api_config_id)
except AccredibleAPIConfig.DoesNotExist:
raise AccredibleError(f"AccredibleAPIConfig with the id {self.api_config_id} does not exist!")

Check warning on line 44 in credentials/apps/badges/accredible/api_client.py

Codecov / codecov/patch

credentials/apps/badges/accredible/api_client.py#L43-L44

Added lines #L43 - L44 were not covered by tests

def _get_base_api_url(self) -> str:
return get_accredible_api_base_url(settings)

def _get_headers(self) -> dict:
"""
Returns the headers for making API requests to Accredible.
"""
return {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_config.api_key}",
}

def fetch_all_groups(self) -> dict:
"""
Fetch all groups.
"""
return self.perform_request("get", "issuer/all_groups")

def fetch_design_image(self, design_id: int) -> str:
"""
Fetches the design and return the URL of image.
"""
design_raw = self.perform_request("get", f"designs/{design_id}")
return design_raw.get("design", {}).get("rasterized_content_url")

def issue_badge(self, issue_badge_data: AccredibleBadgeData) -> dict:
"""
Issues a badge using the Accredible REST API.

Args:
issue_badge_data (IssueBadgeData): Data required to issue the badge.
"""
return self.perform_request("post", "credentials", asdict(issue_badge_data))

def revoke_badge(self, badge_id, data: AccredibleExpireBadgeData) -> dict:
"""
Revoke a badge with the given badge ID.

Args:
badge_id (str): ID of the badge to revoke.
data (dict): Additional data for the revocation.
"""
return self.perform_request("patch", f"credentials/{badge_id}", asdict(data))

def sync_groups(self, site_id: int) -> int:
"""
Pull all groups for a given Accredible API config.

Args:
site_id (int): ID of the site.

Returns:
int | None: processed items.
"""
try:
site = Site.objects.get(id=site_id)
except Site.DoesNotExist:
logger.error(f"Site with the id {site_id} does not exist!")
raise

Check warning on line 105 in credentials/apps/badges/accredible/api_client.py

Codecov / codecov/patch

credentials/apps/badges/accredible/api_client.py#L103-L105

Added lines #L103 - L105 were not covered by tests

groups_data = self.fetch_all_groups()
raw_groups = groups_data.get("groups", [])

all_group_ids = [group.get("id") for group in raw_groups]
AccredibleGroup.objects.exclude(id__in=all_group_ids).delete()

for raw_group in raw_groups:
AccredibleGroup.objects.update_or_create(
id=raw_group.get("id"),
api_config=self.api_config,
defaults={
"site": site,
"name": raw_group.get("course_name"),
"description": raw_group.get("course_description"),
"icon": self.fetch_design_image(raw_group.get("primary_design_id")),
"created": raw_group.get("created_at"),
"state": AccredibleGroup.STATES.active,
},
)

return len(raw_groups)
64 changes: 64 additions & 0 deletions credentials/apps/badges/accredible/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from datetime import datetime

import attr


@attr.s(auto_attribs=True, frozen=True)
class AccredibleRecipient:
"""
Represents the recipient data in the credential.

Attributes:
name (str): The recipient's name.
email (str): The recipient's email address.
"""

name: str
email: str


@attr.s(auto_attribs=True, frozen=True)
class AccredibleCredential:
"""
Represents the credential data.

Attributes:
recipient (RecipientData): Information about the recipient.
group_id (int): ID of the credential group.
name (str): Title of the credential.
issued_on (datetime): Date when the credential was issued.
complete (bool): Whether the credential process is complete.
"""

recipient: AccredibleRecipient
group_id: int
name: str
issued_on: datetime
complete: bool


@attr.s(auto_attribs=True, frozen=True)
class AccredibleExpiredCredential:
"""
Represents the data required to expire a credential.
"""

expired_on: datetime


@attr.s(auto_attribs=True, frozen=True)
class AccredibleBadgeData:
"""
Represents the data required to issue a badge.
"""

credential: AccredibleCredential


@attr.s(auto_attribs=True, frozen=True)
class AccredibleExpireBadgeData:
"""
Represents the data required to expire a badge.
"""

credential: AccredibleExpiredCredential
11 changes: 11 additions & 0 deletions credentials/apps/badges/accredible/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Specific for Accredible exceptions.
"""

from credentials.apps.badges.exceptions import BadgesError


class AccredibleError(BadgesError):
"""
Accredible backend generic error.
"""
45 changes: 45 additions & 0 deletions credentials/apps/badges/accredible/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Accredible utility functions.
"""


def get_accredible_api_base_url(settings) -> str:
"""
Determines the base URL for the Accredible service based on application settings.

Parameters:
- settings: A configuration object containing the application's settings.

Returns:
- str: The base URL for the Accredible service (web site).
This will be the URL for the sandbox environment if `USE_SANDBOX` is
set to a truthy value in the configuration;
otherwise, it will be the production environment's URL.
"""
accredible_config = settings.BADGES_CONFIG["accredible"]

if accredible_config.get("USE_SANDBOX"):
return accredible_config["ACCREDIBLE_SANDBOX_API_BASE_URL"]

return accredible_config["ACCREDIBLE_API_BASE_URL"]

Check warning on line 24 in credentials/apps/badges/accredible/utils.py

Codecov / codecov/patch

credentials/apps/badges/accredible/utils.py#L24

Added line #L24 was not covered by tests


def get_accredible_base_url(settings) -> str:
"""
Determines the base URL for the Accredible service based on application settings.

Parameters:
- settings: A configuration object containing the application's settings.

Returns:
- str: The base URL for the Accredible service (web site).
This will be the URL for the sandbox environment if `USE_SANDBOX` is
set to a truthy value in the configuration;
otherwise, it will be the production environment's URL.
"""
accredible_config = settings.BADGES_CONFIG["accredible"]

if accredible_config.get("USE_SANDBOX"):
return accredible_config["ACCREDIBLE_SANDBOX_BASE_URL"]

return accredible_config["ACCREDIBLE_BASE_URL"]
Loading