Skip to content

Commit

Permalink
Pull more product data from the Learn API (#193)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkachel authored Jan 13, 2025
1 parent 62279cc commit 1f732ca
Show file tree
Hide file tree
Showing 14 changed files with 523 additions and 45 deletions.
142 changes: 142 additions & 0 deletions frontends/api/src/generated/v0/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,58 @@ export const MetaApiAxiosParamCreator = function (
options: localVarRequestOptions,
}
},
/**
* Pre-loads the product metadata for a given SKU, even if the SKU doesn\'t exist yet.
* @param {string} sku
* @param {string} system_slug
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
metaProductPreloadRetrieve: async (
sku: string,
system_slug: string,
options: RawAxiosRequestConfig = {},
): Promise<RequestArgs> => {
// verify required parameter 'sku' is not null or undefined
assertParamExists("metaProductPreloadRetrieve", "sku", sku)
// verify required parameter 'system_slug' is not null or undefined
assertParamExists(
"metaProductPreloadRetrieve",
"system_slug",
system_slug,
)
const localVarPath = `/api/v0/meta/product/preload/{system_slug}/{sku}/`
.replace(`{${"sku"}}`, encodeURIComponent(String(sku)))
.replace(`{${"system_slug"}}`, encodeURIComponent(String(system_slug)))
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
let baseOptions
if (configuration) {
baseOptions = configuration.baseOptions
}

const localVarRequestOptions = {
method: "GET",
...baseOptions,
...options,
}
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any

setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
}

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
}
},
/**
* Viewset for Product model.
* @param {number} id A unique integer value identifying this product.
Expand Down Expand Up @@ -3185,6 +3237,37 @@ export const MetaApiFp = function (configuration?: Configuration) {
configuration,
)(axios, operationBasePath || basePath)
},
/**
* Pre-loads the product metadata for a given SKU, even if the SKU doesn\'t exist yet.
* @param {string} sku
* @param {string} system_slug
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async metaProductPreloadRetrieve(
sku: string,
system_slug: string,
options?: RawAxiosRequestConfig,
): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Product>
> {
const localVarAxiosArgs =
await localVarAxiosParamCreator.metaProductPreloadRetrieve(
sku,
system_slug,
options,
)
const index = configuration?.serverIndex ?? 0
const operationBasePath =
operationServerMap["MetaApi.metaProductPreloadRetrieve"]?.[index]?.url
return (axios, basePath) =>
createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration,
)(axios, operationBasePath || basePath)
},
/**
* Viewset for Product model.
* @param {number} id A unique integer value identifying this product.
Expand Down Expand Up @@ -3420,6 +3503,24 @@ export const MetaApiFactory = function (
)
.then((request) => request(axios, basePath))
},
/**
* Pre-loads the product metadata for a given SKU, even if the SKU doesn\'t exist yet.
* @param {MetaApiMetaProductPreloadRetrieveRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
metaProductPreloadRetrieve(
requestParameters: MetaApiMetaProductPreloadRetrieveRequest,
options?: RawAxiosRequestConfig,
): AxiosPromise<Product> {
return localVarFp
.metaProductPreloadRetrieve(
requestParameters.sku,
requestParameters.system_slug,
options,
)
.then((request) => request(axios, basePath))
},
/**
* Viewset for Product model.
* @param {MetaApiMetaProductRetrieveRequest} requestParameters Request parameters.
Expand Down Expand Up @@ -3644,6 +3745,27 @@ export interface MetaApiMetaProductPartialUpdateRequest {
readonly PatchedProductRequest?: PatchedProductRequest
}

/**
* Request parameters for metaProductPreloadRetrieve operation in MetaApi.
* @export
* @interface MetaApiMetaProductPreloadRetrieveRequest
*/
export interface MetaApiMetaProductPreloadRetrieveRequest {
/**
*
* @type {string}
* @memberof MetaApiMetaProductPreloadRetrieve
*/
readonly sku: string

/**
*
* @type {string}
* @memberof MetaApiMetaProductPreloadRetrieve
*/
readonly system_slug: string
}

/**
* Request parameters for metaProductRetrieve operation in MetaApi.
* @export
Expand Down Expand Up @@ -3871,6 +3993,26 @@ export class MetaApi extends BaseAPI {
.then((request) => request(this.axios, this.basePath))
}

/**
* Pre-loads the product metadata for a given SKU, even if the SKU doesn\'t exist yet.
* @param {MetaApiMetaProductPreloadRetrieveRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof MetaApi
*/
public metaProductPreloadRetrieve(
requestParameters: MetaApiMetaProductPreloadRetrieveRequest,
options?: RawAxiosRequestConfig,
) {
return MetaApiFp(this.configuration)
.metaProductPreloadRetrieve(
requestParameters.sku,
requestParameters.system_slug,
options,
)
.then((request) => request(this.axios, this.basePath))
}

/**
* Viewset for Product model.
* @param {MetaApiMetaProductRetrieveRequest} requestParameters Request parameters.
Expand Down
27 changes: 27 additions & 0 deletions openapi/specs/v0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,33 @@ paths:
responses:
'204':
description: No response body
/api/v0/meta/product/preload/{system_slug}/{sku}/:
get:
operationId: meta_product_preload_retrieve
description: Pre-loads the product metadata for a given SKU, even if the SKU
doesn't exist yet.
parameters:
- in: path
name: sku
schema:
type: string
pattern: ^[^/]+$
required: true
- in: path
name: system_slug
schema:
type: string
pattern: ^[^/]+$
required: true
tags:
- meta
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
description: ''
/api/v0/payments/baskets/:
get:
operationId: payments_baskets_list
Expand Down
118 changes: 118 additions & 0 deletions system_meta/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""API functions for system metadata."""

import logging

import requests
from django.conf import settings

from system_meta.models import Product
from unified_ecommerce.utils import parse_readable_id

log = logging.getLogger(__name__)


def get_product_metadata(
platform: str, readable_id: str, *, all_data: bool = False
) -> dict | None:
"""
Get product metadata from the Learn API.
Args:
platform: The platform slug.
readable_id: The readable ID of the product.
all_data: Whether to return all the data returned or the minimal amount
to bootstrap a product.
Returns:
The product metadata from the Learn API.
"""

def _format_output(data: dict, *, all_data: bool) -> dict:
"""Format the Learn API data accordingly."""

if all_data:
return data.get("results", [])[0]

course_data = data.get("results")[0]
image_data = course_data.get("image", {})
prices = course_data.get("prices", [])
prices.sort()
price = prices[-1] if len(prices) else 0

runs = course_data.get("runs", [])
run = next((r for r in runs if r.get("run_id") == readable_id), None)
if run:
run_prices = run.get("prices", [])
run_prices.sort()
run_price = run_prices[-1] if len(run_prices) else 0

return {
"sku": run.get("run_id") if run else course_data.get("readable_id"),
"title": course_data.get("title"),
"description": course_data.get("description"),
"image": {
"image_url": image_data.get("url"),
"alt_text": image_data.get("alt"),
"description": image_data.get("description"),
}
if image_data
else None,
"price": run_price if run and run_price > price else price,
}

try:
split_readable_id, split_run = parse_readable_id(readable_id)
response = requests.get(
f"{settings.MITOL_LEARN_API_URL}learning_resources/",
params={"platform": platform, "readable_id": split_readable_id},
timeout=10,
)
response.raise_for_status()
raw_response = response.json()

if raw_response.get("count", 0) > 0:
course_data = raw_response.get("results")[0]
if split_run and course_data.get("runs"):
test_run = next(
(
r
for r in course_data.get("runs")
if r.get("run_id") == readable_id
),
None,
)
if test_run:
return _format_output(raw_response, all_data=all_data)

return None

return _format_output(raw_response, all_data=all_data)
else:
return None
except requests.RequestException:
log.exception("Failed to get product metadata for %s", readable_id)
return None


def update_product_metadata(product_id: int) -> None:
"""Get product metadata from the Learn API."""

try:
product = Product.objects.get(id=product_id)
fetched_metadata = get_product_metadata(product.system.slug, product.sku)

if not fetched_metadata:
log.warning("No Learn results found for product %s", product)
return

product.image_metadata = (
fetched_metadata.get("image", None) or product.image_metadata
)

product.name = fetched_metadata.get("title", product.name)
product.description = fetched_metadata.get("description", product.description)
product.price = fetched_metadata.get("price", product.price)

product.save()
except requests.RequestException:
log.exception("Failed to update metadata for product %s", product.id)
Loading

0 comments on commit 1f732ca

Please sign in to comment.