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

Pull more product data from the Learn API #193

Merged
merged 5 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
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
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
Loading