Skip to content

Commit 80653fa

Browse files
committed
feat: add Accredible integration
1 parent f7ff165 commit 80653fa

34 files changed

+1425
-89
lines changed

.annotation_safe_list.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ badges.Fulfillment:
2929
".. no_pii:": "This model has no PII"
3030
badges.PenaltyDataRule:
3131
".. no_pii:": "This model has no PII"
32+
badges.AccredibleAPIConfig:
33+
".. no_pii:": "This model has no PII"
3234
credentials.HistoricalProgramCompletionEmailConfiguration:
3335
".. no_pii:": "This model has no PII"
3436
contenttypes.ContentType:
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import logging
2+
3+
from attrs import asdict
4+
from django.conf import settings
5+
from django.contrib.sites.models import Site
6+
7+
from credentials.apps.badges.accredible.data import AccredibleBadgeData, AccredibleExpireBadgeData
8+
from credentials.apps.badges.accredible.exceptions import AccredibleError
9+
from credentials.apps.badges.accredible.utils import get_accredible_api_base_url
10+
from credentials.apps.badges.base_api_client import BaseBadgeProviderClient
11+
from credentials.apps.badges.models import AccredibleAPIConfig, AccredibleGroup
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class AccredibleAPIClient(BaseBadgeProviderClient):
18+
"""
19+
A client for interacting with the Accredible API.
20+
21+
This class provides methods for performing various operations on the Accredible API.
22+
"""
23+
24+
PROVIDER_NAME = "Accredible"
25+
26+
def __init__(self, api_config_id: int):
27+
"""
28+
Initializes a AccredibleAPIClient object.
29+
30+
Args:
31+
api_config (AccredibleAPIConfig): Configuration object for the Accredible API.
32+
"""
33+
34+
self.api_config_id = api_config_id
35+
self.api_config = self.get_api_config()
36+
37+
def get_api_config(self) -> AccredibleAPIConfig:
38+
"""
39+
Returns the API configuration object for the Accredible API.
40+
"""
41+
try:
42+
return AccredibleAPIConfig.objects.get(id=self.api_config_id)
43+
except AccredibleAPIConfig.DoesNotExist:
44+
raise AccredibleError(f"AccredibleAPIConfig with the id {self.api_config_id} does not exist!")
45+
46+
def _get_base_api_url(self) -> str:
47+
return get_accredible_api_base_url(settings)
48+
49+
def _get_headers(self) -> dict:
50+
"""
51+
Returns the headers for making API requests to Accredible.
52+
"""
53+
return {
54+
"Accept": "application/json",
55+
"Content-Type": "application/json",
56+
"Authorization": f"Bearer {self.api_config.api_key}",
57+
}
58+
59+
def fetch_all_groups(self) -> dict:
60+
"""
61+
Fetch all groups.
62+
"""
63+
return self.perform_request("get", "issuer/all_groups")
64+
65+
def fetch_design_image(self, design_id: int) -> str:
66+
"""
67+
Fetches the design and return the URL of image.
68+
"""
69+
design_raw = self.perform_request("get", f"designs/{design_id}")
70+
return design_raw.get("design", {}).get("rasterized_content_url")
71+
72+
def issue_badge(self, issue_badge_data: AccredibleBadgeData) -> dict:
73+
"""
74+
Issues a badge using the Accredible REST API.
75+
76+
Args:
77+
issue_badge_data (IssueBadgeData): Data required to issue the badge.
78+
"""
79+
return self.perform_request("post", "credentials", asdict(issue_badge_data))
80+
81+
def revoke_badge(self, badge_id, data: AccredibleExpireBadgeData) -> dict:
82+
"""
83+
Revoke a badge with the given badge ID.
84+
85+
Args:
86+
badge_id (str): ID of the badge to revoke.
87+
data (dict): Additional data for the revocation.
88+
"""
89+
return self.perform_request("patch", f"credentials/{badge_id}", asdict(data))
90+
91+
def sync_groups(self, site_id: int) -> int:
92+
"""
93+
Pull all groups for a given Accredible API config.
94+
95+
Args:
96+
site_id (int): ID of the site.
97+
98+
Returns:
99+
int | None: processed items.
100+
"""
101+
try:
102+
site = Site.objects.get(id=site_id)
103+
except Site.DoesNotExist:
104+
logger.error(f"Site with the id {site_id} does not exist!")
105+
raise
106+
107+
groups_data = self.fetch_all_groups()
108+
raw_groups = groups_data.get("groups", [])
109+
110+
all_group_ids = [group.get("id") for group in raw_groups]
111+
AccredibleGroup.objects.exclude(id__in=all_group_ids).delete()
112+
113+
for raw_group in raw_groups:
114+
AccredibleGroup.objects.update_or_create(
115+
id=raw_group.get("id"),
116+
api_config=self.api_config,
117+
defaults={
118+
"site": site,
119+
"name": raw_group.get("course_name"),
120+
"description": raw_group.get("course_description"),
121+
"icon": self.fetch_design_image(raw_group.get("primary_design_id")),
122+
"created": raw_group.get("created_at"),
123+
"state": AccredibleGroup.STATES.active,
124+
},
125+
)
126+
127+
return len(raw_groups)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from datetime import datetime
2+
3+
import attr
4+
5+
6+
@attr.s(auto_attribs=True, frozen=True)
7+
class AccredibleRecipient:
8+
"""
9+
Represents the recipient data in the credential.
10+
11+
Attributes:
12+
name (str): The recipient's name.
13+
email (str): The recipient's email address.
14+
"""
15+
16+
name: str
17+
email: str
18+
19+
20+
@attr.s(auto_attribs=True, frozen=True)
21+
class AccredibleCredential:
22+
"""
23+
Represents the credential data.
24+
25+
Attributes:
26+
recipient (RecipientData): Information about the recipient.
27+
group_id (int): ID of the credential group.
28+
name (str): Title of the credential.
29+
issued_on (datetime): Date when the credential was issued.
30+
complete (bool): Whether the credential process is complete.
31+
"""
32+
33+
recipient: AccredibleRecipient
34+
group_id: int
35+
name: str
36+
issued_on: datetime
37+
complete: bool
38+
39+
40+
@attr.s(auto_attribs=True, frozen=True)
41+
class AccredibleExpiredCredential:
42+
"""
43+
Represents the data required to expire a credential.
44+
"""
45+
46+
expired_on: datetime
47+
48+
49+
@attr.s(auto_attribs=True, frozen=True)
50+
class AccredibleBadgeData:
51+
"""
52+
Represents the data required to issue a badge.
53+
"""
54+
55+
credential: AccredibleCredential
56+
57+
58+
@attr.s(auto_attribs=True, frozen=True)
59+
class AccredibleExpireBadgeData:
60+
"""
61+
Represents the data required to expire a badge.
62+
"""
63+
64+
credential: AccredibleExpiredCredential
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Specific for Accredible exceptions.
3+
"""
4+
5+
from credentials.apps.badges.exceptions import BadgesError
6+
7+
8+
class AccredibleError(BadgesError):
9+
"""
10+
Accredible backend generic error.
11+
"""
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
Accredible utility functions.
3+
"""
4+
5+
6+
def get_accredible_api_base_url(settings) -> str:
7+
"""
8+
Determines the base URL for the Accredible service based on application settings.
9+
10+
Parameters:
11+
- settings: A configuration object containing the application's settings.
12+
13+
Returns:
14+
- str: The base URL for the Accredible service (web site).
15+
This will be the URL for the sandbox environment if `USE_SANDBOX` is
16+
set to a truthy value in the configuration;
17+
otherwise, it will be the production environment's URL.
18+
"""
19+
accredible_config = settings.BADGES_CONFIG["accredible"]
20+
21+
if accredible_config.get("USE_SANDBOX"):
22+
return accredible_config["ACCREDIBLE_SANDBOX_API_BASE_URL"]
23+
24+
return accredible_config["ACCREDIBLE_API_BASE_URL"]
25+
26+
27+
def get_accredible_base_url(settings) -> str:
28+
"""
29+
Determines the base URL for the Accredible service based on application settings.
30+
31+
Parameters:
32+
- settings: A configuration object containing the application's settings.
33+
34+
Returns:
35+
- str: The base URL for the Accredible service (web site).
36+
This will be the URL for the sandbox environment if `USE_SANDBOX` is
37+
set to a truthy value in the configuration;
38+
otherwise, it will be the production environment's URL.
39+
"""
40+
accredible_config = settings.BADGES_CONFIG["accredible"]
41+
42+
if accredible_config.get("USE_SANDBOX"):
43+
return accredible_config["ACCREDIBLE_SANDBOX_BASE_URL"]
44+
45+
return accredible_config["ACCREDIBLE_BASE_URL"]

0 commit comments

Comments
 (0)