Skip to content

Commit 1f732ca

Browse files
authored
Pull more product data from the Learn API (#193)
1 parent 62279cc commit 1f732ca

File tree

14 files changed

+523
-45
lines changed

14 files changed

+523
-45
lines changed

frontends/api/src/generated/v0/api.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2761,6 +2761,58 @@ export const MetaApiAxiosParamCreator = function (
27612761
options: localVarRequestOptions,
27622762
}
27632763
},
2764+
/**
2765+
* Pre-loads the product metadata for a given SKU, even if the SKU doesn\'t exist yet.
2766+
* @param {string} sku
2767+
* @param {string} system_slug
2768+
* @param {*} [options] Override http request option.
2769+
* @throws {RequiredError}
2770+
*/
2771+
metaProductPreloadRetrieve: async (
2772+
sku: string,
2773+
system_slug: string,
2774+
options: RawAxiosRequestConfig = {},
2775+
): Promise<RequestArgs> => {
2776+
// verify required parameter 'sku' is not null or undefined
2777+
assertParamExists("metaProductPreloadRetrieve", "sku", sku)
2778+
// verify required parameter 'system_slug' is not null or undefined
2779+
assertParamExists(
2780+
"metaProductPreloadRetrieve",
2781+
"system_slug",
2782+
system_slug,
2783+
)
2784+
const localVarPath = `/api/v0/meta/product/preload/{system_slug}/{sku}/`
2785+
.replace(`{${"sku"}}`, encodeURIComponent(String(sku)))
2786+
.replace(`{${"system_slug"}}`, encodeURIComponent(String(system_slug)))
2787+
// use dummy base URL string because the URL constructor only accepts absolute URLs.
2788+
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
2789+
let baseOptions
2790+
if (configuration) {
2791+
baseOptions = configuration.baseOptions
2792+
}
2793+
2794+
const localVarRequestOptions = {
2795+
method: "GET",
2796+
...baseOptions,
2797+
...options,
2798+
}
2799+
const localVarHeaderParameter = {} as any
2800+
const localVarQueryParameter = {} as any
2801+
2802+
setSearchParams(localVarUrlObj, localVarQueryParameter)
2803+
let headersFromBaseOptions =
2804+
baseOptions && baseOptions.headers ? baseOptions.headers : {}
2805+
localVarRequestOptions.headers = {
2806+
...localVarHeaderParameter,
2807+
...headersFromBaseOptions,
2808+
...options.headers,
2809+
}
2810+
2811+
return {
2812+
url: toPathString(localVarUrlObj),
2813+
options: localVarRequestOptions,
2814+
}
2815+
},
27642816
/**
27652817
* Viewset for Product model.
27662818
* @param {number} id A unique integer value identifying this product.
@@ -3185,6 +3237,37 @@ export const MetaApiFp = function (configuration?: Configuration) {
31853237
configuration,
31863238
)(axios, operationBasePath || basePath)
31873239
},
3240+
/**
3241+
* Pre-loads the product metadata for a given SKU, even if the SKU doesn\'t exist yet.
3242+
* @param {string} sku
3243+
* @param {string} system_slug
3244+
* @param {*} [options] Override http request option.
3245+
* @throws {RequiredError}
3246+
*/
3247+
async metaProductPreloadRetrieve(
3248+
sku: string,
3249+
system_slug: string,
3250+
options?: RawAxiosRequestConfig,
3251+
): Promise<
3252+
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Product>
3253+
> {
3254+
const localVarAxiosArgs =
3255+
await localVarAxiosParamCreator.metaProductPreloadRetrieve(
3256+
sku,
3257+
system_slug,
3258+
options,
3259+
)
3260+
const index = configuration?.serverIndex ?? 0
3261+
const operationBasePath =
3262+
operationServerMap["MetaApi.metaProductPreloadRetrieve"]?.[index]?.url
3263+
return (axios, basePath) =>
3264+
createRequestFunction(
3265+
localVarAxiosArgs,
3266+
globalAxios,
3267+
BASE_PATH,
3268+
configuration,
3269+
)(axios, operationBasePath || basePath)
3270+
},
31883271
/**
31893272
* Viewset for Product model.
31903273
* @param {number} id A unique integer value identifying this product.
@@ -3420,6 +3503,24 @@ export const MetaApiFactory = function (
34203503
)
34213504
.then((request) => request(axios, basePath))
34223505
},
3506+
/**
3507+
* Pre-loads the product metadata for a given SKU, even if the SKU doesn\'t exist yet.
3508+
* @param {MetaApiMetaProductPreloadRetrieveRequest} requestParameters Request parameters.
3509+
* @param {*} [options] Override http request option.
3510+
* @throws {RequiredError}
3511+
*/
3512+
metaProductPreloadRetrieve(
3513+
requestParameters: MetaApiMetaProductPreloadRetrieveRequest,
3514+
options?: RawAxiosRequestConfig,
3515+
): AxiosPromise<Product> {
3516+
return localVarFp
3517+
.metaProductPreloadRetrieve(
3518+
requestParameters.sku,
3519+
requestParameters.system_slug,
3520+
options,
3521+
)
3522+
.then((request) => request(axios, basePath))
3523+
},
34233524
/**
34243525
* Viewset for Product model.
34253526
* @param {MetaApiMetaProductRetrieveRequest} requestParameters Request parameters.
@@ -3644,6 +3745,27 @@ export interface MetaApiMetaProductPartialUpdateRequest {
36443745
readonly PatchedProductRequest?: PatchedProductRequest
36453746
}
36463747

3748+
/**
3749+
* Request parameters for metaProductPreloadRetrieve operation in MetaApi.
3750+
* @export
3751+
* @interface MetaApiMetaProductPreloadRetrieveRequest
3752+
*/
3753+
export interface MetaApiMetaProductPreloadRetrieveRequest {
3754+
/**
3755+
*
3756+
* @type {string}
3757+
* @memberof MetaApiMetaProductPreloadRetrieve
3758+
*/
3759+
readonly sku: string
3760+
3761+
/**
3762+
*
3763+
* @type {string}
3764+
* @memberof MetaApiMetaProductPreloadRetrieve
3765+
*/
3766+
readonly system_slug: string
3767+
}
3768+
36473769
/**
36483770
* Request parameters for metaProductRetrieve operation in MetaApi.
36493771
* @export
@@ -3871,6 +3993,26 @@ export class MetaApi extends BaseAPI {
38713993
.then((request) => request(this.axios, this.basePath))
38723994
}
38733995

3996+
/**
3997+
* Pre-loads the product metadata for a given SKU, even if the SKU doesn\'t exist yet.
3998+
* @param {MetaApiMetaProductPreloadRetrieveRequest} requestParameters Request parameters.
3999+
* @param {*} [options] Override http request option.
4000+
* @throws {RequiredError}
4001+
* @memberof MetaApi
4002+
*/
4003+
public metaProductPreloadRetrieve(
4004+
requestParameters: MetaApiMetaProductPreloadRetrieveRequest,
4005+
options?: RawAxiosRequestConfig,
4006+
) {
4007+
return MetaApiFp(this.configuration)
4008+
.metaProductPreloadRetrieve(
4009+
requestParameters.sku,
4010+
requestParameters.system_slug,
4011+
options,
4012+
)
4013+
.then((request) => request(this.axios, this.basePath))
4014+
}
4015+
38744016
/**
38754017
* Viewset for Product model.
38764018
* @param {MetaApiMetaProductRetrieveRequest} requestParameters Request parameters.

openapi/specs/v0.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,33 @@ paths:
304304
responses:
305305
'204':
306306
description: No response body
307+
/api/v0/meta/product/preload/{system_slug}/{sku}/:
308+
get:
309+
operationId: meta_product_preload_retrieve
310+
description: Pre-loads the product metadata for a given SKU, even if the SKU
311+
doesn't exist yet.
312+
parameters:
313+
- in: path
314+
name: sku
315+
schema:
316+
type: string
317+
pattern: ^[^/]+$
318+
required: true
319+
- in: path
320+
name: system_slug
321+
schema:
322+
type: string
323+
pattern: ^[^/]+$
324+
required: true
325+
tags:
326+
- meta
327+
responses:
328+
'200':
329+
content:
330+
application/json:
331+
schema:
332+
$ref: '#/components/schemas/Product'
333+
description: ''
307334
/api/v0/payments/baskets/:
308335
get:
309336
operationId: payments_baskets_list

system_meta/api.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""API functions for system metadata."""
2+
3+
import logging
4+
5+
import requests
6+
from django.conf import settings
7+
8+
from system_meta.models import Product
9+
from unified_ecommerce.utils import parse_readable_id
10+
11+
log = logging.getLogger(__name__)
12+
13+
14+
def get_product_metadata(
15+
platform: str, readable_id: str, *, all_data: bool = False
16+
) -> dict | None:
17+
"""
18+
Get product metadata from the Learn API.
19+
20+
Args:
21+
platform: The platform slug.
22+
readable_id: The readable ID of the product.
23+
all_data: Whether to return all the data returned or the minimal amount
24+
to bootstrap a product.
25+
26+
Returns:
27+
The product metadata from the Learn API.
28+
"""
29+
30+
def _format_output(data: dict, *, all_data: bool) -> dict:
31+
"""Format the Learn API data accordingly."""
32+
33+
if all_data:
34+
return data.get("results", [])[0]
35+
36+
course_data = data.get("results")[0]
37+
image_data = course_data.get("image", {})
38+
prices = course_data.get("prices", [])
39+
prices.sort()
40+
price = prices[-1] if len(prices) else 0
41+
42+
runs = course_data.get("runs", [])
43+
run = next((r for r in runs if r.get("run_id") == readable_id), None)
44+
if run:
45+
run_prices = run.get("prices", [])
46+
run_prices.sort()
47+
run_price = run_prices[-1] if len(run_prices) else 0
48+
49+
return {
50+
"sku": run.get("run_id") if run else course_data.get("readable_id"),
51+
"title": course_data.get("title"),
52+
"description": course_data.get("description"),
53+
"image": {
54+
"image_url": image_data.get("url"),
55+
"alt_text": image_data.get("alt"),
56+
"description": image_data.get("description"),
57+
}
58+
if image_data
59+
else None,
60+
"price": run_price if run and run_price > price else price,
61+
}
62+
63+
try:
64+
split_readable_id, split_run = parse_readable_id(readable_id)
65+
response = requests.get(
66+
f"{settings.MITOL_LEARN_API_URL}learning_resources/",
67+
params={"platform": platform, "readable_id": split_readable_id},
68+
timeout=10,
69+
)
70+
response.raise_for_status()
71+
raw_response = response.json()
72+
73+
if raw_response.get("count", 0) > 0:
74+
course_data = raw_response.get("results")[0]
75+
if split_run and course_data.get("runs"):
76+
test_run = next(
77+
(
78+
r
79+
for r in course_data.get("runs")
80+
if r.get("run_id") == readable_id
81+
),
82+
None,
83+
)
84+
if test_run:
85+
return _format_output(raw_response, all_data=all_data)
86+
87+
return None
88+
89+
return _format_output(raw_response, all_data=all_data)
90+
else:
91+
return None
92+
except requests.RequestException:
93+
log.exception("Failed to get product metadata for %s", readable_id)
94+
return None
95+
96+
97+
def update_product_metadata(product_id: int) -> None:
98+
"""Get product metadata from the Learn API."""
99+
100+
try:
101+
product = Product.objects.get(id=product_id)
102+
fetched_metadata = get_product_metadata(product.system.slug, product.sku)
103+
104+
if not fetched_metadata:
105+
log.warning("No Learn results found for product %s", product)
106+
return
107+
108+
product.image_metadata = (
109+
fetched_metadata.get("image", None) or product.image_metadata
110+
)
111+
112+
product.name = fetched_metadata.get("title", product.name)
113+
product.description = fetched_metadata.get("description", product.description)
114+
product.price = fetched_metadata.get("price", product.price)
115+
116+
product.save()
117+
except requests.RequestException:
118+
log.exception("Failed to update metadata for product %s", product.id)

0 commit comments

Comments
 (0)