From 6f02a2c30152f7b11128e47dfe0db19c2d93647b Mon Sep 17 00:00:00 2001 From: James Kachel Date: Wed, 8 Jan 2025 11:58:29 -0600 Subject: [PATCH] Adding pre-load API; added data pull API; fixed tests; regen OpenAPI spec; remove a test endpoint - Pre-load API: a separate API that can be used to load and build a product for a given system and SKU - Created a function to load the data from Learn for a given system and SKU (but just load it) - Fixed some test issues - Removed the traefik_test_request endpoint, since we're not doing Traefik at all now - Added some settings for throttling --- frontends/api/src/generated/v0/api.ts | 142 ++ openapi/specs/v0.yaml | 1822 +++++++++++++------------ system_meta/api.py | 36 +- system_meta/api_test.py | 3 +- system_meta/private_urls.py | 6 - system_meta/urls.py | 6 + system_meta/views.py | 88 +- unified_ecommerce/settings.py | 1 + 8 files changed, 1188 insertions(+), 916 deletions(-) diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 2134d9b8..30281ed3 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -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 => { + // 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. @@ -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 + > { + 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. @@ -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 { + return localVarFp + .metaProductPreloadRetrieve( + requestParameters.sku, + requestParameters.system_slug, + options, + ) + .then((request) => request(axios, basePath)) + }, /** * Viewset for Product model. * @param {MetaApiMetaProductRetrieveRequest} requestParameters Request parameters. @@ -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 @@ -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. diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index 0038d24c..772ee8c5 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -9,527 +9,558 @@ paths: operationId: meta_integrated_system_list description: Viewset for IntegratedSystem model. parameters: - - name: limit - required: false - in: query - description: Number of results to return per page. - schema: - type: integer - - name: offset - required: false - in: query - description: The initial index from which to return the results. - schema: - type: integer + - name: limit + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: offset + required: false + in: query + description: The initial index from which to return the results. + schema: + type: integer tags: - - meta + - meta responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/PaginatedIntegratedSystemList' - description: '' + $ref: "#/components/schemas/PaginatedIntegratedSystemList" + description: "" post: operationId: meta_integrated_system_create description: Viewset for IntegratedSystem model. tags: - - meta + - meta requestBody: content: application/json: schema: - $ref: '#/components/schemas/IntegratedSystemRequest' + $ref: "#/components/schemas/IntegratedSystemRequest" application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/IntegratedSystemRequest' + $ref: "#/components/schemas/IntegratedSystemRequest" multipart/form-data: schema: - $ref: '#/components/schemas/IntegratedSystemRequest' + $ref: "#/components/schemas/IntegratedSystemRequest" required: true responses: - '201': + "201": content: application/json: schema: - $ref: '#/components/schemas/IntegratedSystem' - description: '' + $ref: "#/components/schemas/IntegratedSystem" + description: "" /api/v0/meta/integrated_system/{id}/: get: operationId: meta_integrated_system_retrieve description: Viewset for IntegratedSystem model. parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this integrated system. - required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this integrated system. + required: true tags: - - meta + - meta responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/IntegratedSystem' - description: '' + $ref: "#/components/schemas/IntegratedSystem" + description: "" put: operationId: meta_integrated_system_update description: Viewset for IntegratedSystem model. parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this integrated system. - required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this integrated system. + required: true tags: - - meta + - meta requestBody: content: application/json: schema: - $ref: '#/components/schemas/IntegratedSystemRequest' + $ref: "#/components/schemas/IntegratedSystemRequest" application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/IntegratedSystemRequest' + $ref: "#/components/schemas/IntegratedSystemRequest" multipart/form-data: schema: - $ref: '#/components/schemas/IntegratedSystemRequest' + $ref: "#/components/schemas/IntegratedSystemRequest" required: true responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/IntegratedSystem' - description: '' + $ref: "#/components/schemas/IntegratedSystem" + description: "" patch: operationId: meta_integrated_system_partial_update description: Viewset for IntegratedSystem model. parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this integrated system. - required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this integrated system. + required: true tags: - - meta + - meta requestBody: content: application/json: schema: - $ref: '#/components/schemas/PatchedIntegratedSystemRequest' + $ref: "#/components/schemas/PatchedIntegratedSystemRequest" application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/PatchedIntegratedSystemRequest' + $ref: "#/components/schemas/PatchedIntegratedSystemRequest" multipart/form-data: schema: - $ref: '#/components/schemas/PatchedIntegratedSystemRequest' + $ref: "#/components/schemas/PatchedIntegratedSystemRequest" responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/IntegratedSystem' - description: '' + $ref: "#/components/schemas/IntegratedSystem" + description: "" delete: operationId: meta_integrated_system_destroy description: Viewset for IntegratedSystem model. parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this integrated system. - required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this integrated system. + required: true tags: - - meta + - meta responses: - '204': + "204": description: No response body /api/v0/meta/product/: get: operationId: meta_product_list description: Viewset for Product model. parameters: - - name: limit - required: false - in: query - description: Number of results to return per page. - schema: - type: integer - - in: query - name: name - schema: - type: string - - name: offset - required: false - in: query - description: The initial index from which to return the results. - schema: - type: integer - - in: query - name: system__slug - schema: - type: string + - name: limit + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - in: query + name: name + schema: + type: string + - name: offset + required: false + in: query + description: The initial index from which to return the results. + schema: + type: integer + - in: query + name: system__slug + schema: + type: string tags: - - meta + - meta responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/PaginatedProductList' - description: '' + $ref: "#/components/schemas/PaginatedProductList" + description: "" post: operationId: meta_product_create description: Viewset for Product model. tags: - - meta + - meta requestBody: content: application/json: schema: - $ref: '#/components/schemas/ProductRequest' + $ref: "#/components/schemas/ProductRequest" application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/ProductRequest' + $ref: "#/components/schemas/ProductRequest" multipart/form-data: schema: - $ref: '#/components/schemas/ProductRequest' + $ref: "#/components/schemas/ProductRequest" required: true responses: - '201': + "201": content: application/json: schema: - $ref: '#/components/schemas/Product' - description: '' + $ref: "#/components/schemas/Product" + description: "" /api/v0/meta/product/{id}/: get: operationId: meta_product_retrieve description: Viewset for Product model. parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this product. - required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this product. + required: true tags: - - meta + - meta responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/Product' - description: '' + $ref: "#/components/schemas/Product" + description: "" put: operationId: meta_product_update description: Viewset for Product model. parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this product. - required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this product. + required: true tags: - - meta + - meta requestBody: content: application/json: schema: - $ref: '#/components/schemas/ProductRequest' + $ref: "#/components/schemas/ProductRequest" application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/ProductRequest' + $ref: "#/components/schemas/ProductRequest" multipart/form-data: schema: - $ref: '#/components/schemas/ProductRequest' + $ref: "#/components/schemas/ProductRequest" required: true responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/Product' - description: '' + $ref: "#/components/schemas/Product" + description: "" patch: operationId: meta_product_partial_update description: Viewset for Product model. parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this product. - required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this product. + required: true tags: - - meta + - meta requestBody: content: application/json: schema: - $ref: '#/components/schemas/PatchedProductRequest' + $ref: "#/components/schemas/PatchedProductRequest" application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/PatchedProductRequest' + $ref: "#/components/schemas/PatchedProductRequest" multipart/form-data: schema: - $ref: '#/components/schemas/PatchedProductRequest' + $ref: "#/components/schemas/PatchedProductRequest" responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/Product' - description: '' + $ref: "#/components/schemas/Product" + description: "" delete: operationId: meta_product_destroy description: Viewset for Product model. parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this product. - required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this product. + required: true tags: - - meta + - meta responses: - '204': + "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 description: Retrives the current user's baskets, one per system. parameters: - - in: query - name: integrated_system - schema: - type: integer - - name: limit - required: false - in: query - description: Number of results to return per page. - schema: - type: integer - - name: offset - required: false - in: query - description: The initial index from which to return the results. - schema: - type: integer + - in: query + name: integrated_system + schema: + type: integer + - name: limit + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: offset + required: false + in: query + description: The initial index from which to return the results. + schema: + type: integer tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/PaginatedBasketWithProductList' - description: '' + $ref: "#/components/schemas/PaginatedBasketWithProductList" + description: "" /api/v0/payments/baskets/{id}/: get: operationId: payments_baskets_retrieve description: Retrieve a basket for the current user. parameters: - - in: path - name: id - schema: - type: integer - required: true + - in: path + name: id + schema: + type: integer + required: true tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/BasketWithProduct' - description: '' + $ref: "#/components/schemas/BasketWithProduct" + description: "" /api/v0/payments/baskets/add_discount/{system_slug}/: post: operationId: payments_baskets_add_discount_create - description: Creates or updates a basket for the current user, adding the discount + description: + Creates or updates a basket for the current user, adding the discount if valid. parameters: - - in: query - name: discount_code - schema: - type: string - required: true - - in: path - name: system_slug - schema: - type: string - required: true + - in: query + name: discount_code + schema: + type: string + required: true + - in: path + name: system_slug + schema: + type: string + required: true tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/BasketWithProduct' - description: '' + $ref: "#/components/schemas/BasketWithProduct" + description: "" /api/v0/payments/baskets/clear/{system_slug}/: delete: operationId: payments_baskets_clear_destroy description: Clears the basket for the current user. parameters: - - in: path - name: system_slug - schema: - type: string - required: true + - in: path + name: system_slug + schema: + type: string + required: true tags: - - payments + - payments responses: - '204': + "204": description: No response body /api/v0/payments/baskets/create_from_product/{system_slug}/{sku}/: post: operationId: payments_baskets_create_from_product_create - description: Creates or updates a basket for the current user, adding the selected + description: + Creates or updates a basket for the current user, adding the selected product. parameters: - - in: path - name: sku - schema: - type: string - required: true - - in: path - name: system_slug - schema: - type: string - required: true + - in: path + name: sku + schema: + type: string + required: true + - in: path + name: system_slug + schema: + type: string + required: true tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/BasketWithProduct' - description: '' + $ref: "#/components/schemas/BasketWithProduct" + description: "" /api/v0/payments/baskets/for_system/{system_slug}/: get: operationId: payments_baskets_for_system_retrieve description: Returns or creates a basket for the current user and system. parameters: - - in: path - name: system_slug - schema: - type: string - required: true + - in: path + name: system_slug + schema: + type: string + required: true tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/BasketWithProduct' - description: '' + $ref: "#/components/schemas/BasketWithProduct" + description: "" /api/v0/payments/checkout/{system_slug}/: post: operationId: payments_checkout_create - description: Generates and returns the form payload for the current basket for + description: + Generates and returns the form payload for the current basket for the specified system, which can be used to start the checkout process. parameters: - - in: path - name: system_slug - schema: - type: string - required: true + - in: path + name: system_slug + schema: + type: string + required: true tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/CyberSourceCheckout' - description: '' + $ref: "#/components/schemas/CyberSourceCheckout" + description: "" /api/v0/payments/discounts/: post: operationId: payments_discounts_create description: Create a discount. tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/Discount' - description: '' + $ref: "#/components/schemas/Discount" + description: "" /api/v0/payments/orders/history/: get: operationId: payments_orders_history_list description: Retrives the current user's completed orders. parameters: - - name: limit - required: false - in: query - description: Number of results to return per page. - schema: - type: integer - - name: offset - required: false - in: query - description: The initial index from which to return the results. - schema: - type: integer + - name: limit + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: offset + required: false + in: query + description: The initial index from which to return the results. + schema: + type: integer tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/PaginatedOrderHistoryList' - description: '' + $ref: "#/components/schemas/PaginatedOrderHistoryList" + description: "" /api/v0/payments/orders/history/{id}/: get: operationId: payments_orders_history_retrieve description: Retrieve a completed order for the current user. parameters: - - in: path - name: id - schema: - type: integer - required: true + - in: path + name: id + schema: + type: integer + required: true tags: - - payments + - payments responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/OrderHistory' - description: '' + $ref: "#/components/schemas/OrderHistory" + description: "" /api/v0/users/me/: get: operationId: users_me_retrieve description: User retrieve and update viewsets for the current user tags: - - users + - users security: - - {} + - {} responses: - '200': + "200": content: application/json: schema: - $ref: '#/components/schemas/User' - description: '' + $ref: "#/components/schemas/User" + description: "" components: schemas: BasketItemWithProduct: @@ -537,7 +568,7 @@ components: description: Basket item model serializer with product information properties: product: - $ref: '#/components/schemas/Product' + $ref: "#/components/schemas/Product" id: type: integer readOnly: true @@ -561,14 +592,14 @@ components: minimum: 0 discount_applied: allOf: - - $ref: '#/components/schemas/SimpleDiscount' + - $ref: "#/components/schemas/SimpleDiscount" readOnly: true required: - - discount_applied - - discounted_price - - id - - price - - product + - discount_applied + - discounted_price + - id + - price + - product BasketWithProduct: type: object description: Basket model serializer with items and products @@ -579,11 +610,11 @@ components: user: type: integer integrated_system: - $ref: '#/components/schemas/IntegratedSystem' + $ref: "#/components/schemas/IntegratedSystem" basket_items: type: array items: - $ref: '#/components/schemas/BasketItemWithProduct' + $ref: "#/components/schemas/BasketItemWithProduct" subtotal: type: number format: double @@ -595,21 +626,21 @@ components: description: Get the tax for the basket readOnly: true tax_rate: - $ref: '#/components/schemas/TaxRate' + $ref: "#/components/schemas/TaxRate" total_price: type: number format: double description: Get the total price for the basket readOnly: true required: - - basket_items - - id - - integrated_system - - subtotal - - tax - - tax_rate - - total_price - - user + - basket_items + - id + - integrated_system + - subtotal + - tax + - tax_rate + - total_price + - user Company: type: object description: Serializer for companies. @@ -621,259 +652,259 @@ components: type: string maxLength: 255 required: - - id - - name + - id + - name CountryCodeEnum: enum: - - AF - - AX - - AL - - DZ - - AS - - AD - - AO - - AI - - AQ - - AG - - AR - - AM - - AW - - AU - - AT - - AZ - - BS - - BH - - BD - - BB - - BY - - BE - - BZ - - BJ - - BM - - BT - - BO - - BQ - - BA - - BW - - BV - - BR - - IO - - BN - - BG - - BF - - BI - - CV - - KH - - CM - - CA - - KY - - CF - - TD - - CL - - CN - - CX - - CC - - CO - - KM - - CG - - CD - - CK - - CR - - CI - - HR - - CU - - CW - - CY - - CZ - - DK - - DJ - - DM - - DO - - EC - - EG - - SV - - GQ - - ER - - EE - - SZ - - ET - - FK - - FO - - FJ - - FI - - FR - - GF - - PF - - TF - - GA - - GM - - GE - - DE - - GH - - GI - - GR - - GL - - GD - - GP - - GU - - GT - - GG - - GN - - GW - - GY - - HT - - HM - - VA - - HN - - HK - - HU - - IS - - IN - - ID - - IR - - IQ - - IE - - IM - - IL - - IT - - JM - - JP - - JE - - JO - - KZ - - KE - - KI - - KW - - KG - - LA - - LV - - LB - - LS - - LR - - LY - - LI - - LT - - LU - - MO - - MG - - MW - - MY - - MV - - ML - - MT - - MH - - MQ - - MR - - MU - - YT - - MX - - FM - - MD - - MC - - MN - - ME - - MS - - MA - - MZ - - MM - - NA - - NR - - NP - - NL - - NC - - NZ - - NI - - NE - - NG - - NU - - NF - - KP - - MK - - MP - - 'NO' - - OM - - PK - - PW - - PS - - PA - - PG - - PY - - PE - - PH - - PN - - PL - - PT - - PR - - QA - - RE - - RO - - RU - - RW - - BL - - SH - - KN - - LC - - MF - - PM - - VC - - WS - - SM - - ST - - SA - - SN - - RS - - SC - - SL - - SG - - SX - - SK - - SI - - SB - - SO - - ZA - - GS - - KR - - SS - - ES - - LK - - SD - - SR - - SJ - - SE - - CH - - SY - - TW - - TJ - - TZ - - TH - - TL - - TG - - TK - - TO - - TT - - TN - - TR - - TM - - TC - - TV - - UG - - UA - - AE - - GB - - UM - - US - - UY - - UZ - - VU - - VE - - VN - - VG - - VI - - WF - - EH - - YE - - ZM - - ZW + - AF + - AX + - AL + - DZ + - AS + - AD + - AO + - AI + - AQ + - AG + - AR + - AM + - AW + - AU + - AT + - AZ + - BS + - BH + - BD + - BB + - BY + - BE + - BZ + - BJ + - BM + - BT + - BO + - BQ + - BA + - BW + - BV + - BR + - IO + - BN + - BG + - BF + - BI + - CV + - KH + - CM + - CA + - KY + - CF + - TD + - CL + - CN + - CX + - CC + - CO + - KM + - CG + - CD + - CK + - CR + - CI + - HR + - CU + - CW + - CY + - CZ + - DK + - DJ + - DM + - DO + - EC + - EG + - SV + - GQ + - ER + - EE + - SZ + - ET + - FK + - FO + - FJ + - FI + - FR + - GF + - PF + - TF + - GA + - GM + - GE + - DE + - GH + - GI + - GR + - GL + - GD + - GP + - GU + - GT + - GG + - GN + - GW + - GY + - HT + - HM + - VA + - HN + - HK + - HU + - IS + - IN + - ID + - IR + - IQ + - IE + - IM + - IL + - IT + - JM + - JP + - JE + - JO + - KZ + - KE + - KI + - KW + - KG + - LA + - LV + - LB + - LS + - LR + - LY + - LI + - LT + - LU + - MO + - MG + - MW + - MY + - MV + - ML + - MT + - MH + - MQ + - MR + - MU + - YT + - MX + - FM + - MD + - MC + - MN + - ME + - MS + - MA + - MZ + - MM + - NA + - NR + - NP + - NL + - NC + - NZ + - NI + - NE + - NG + - NU + - NF + - KP + - MK + - MP + - "NO" + - OM + - PK + - PW + - PS + - PA + - PG + - PY + - PE + - PH + - PN + - PL + - PT + - PR + - QA + - RE + - RO + - RU + - RW + - BL + - SH + - KN + - LC + - MF + - PM + - VC + - WS + - SM + - ST + - SA + - SN + - RS + - SC + - SL + - SG + - SX + - SK + - SI + - SB + - SO + - ZA + - GS + - KR + - SS + - ES + - LK + - SD + - SR + - SJ + - SE + - CH + - SY + - TW + - TJ + - TZ + - TH + - TL + - TG + - TK + - TO + - TT + - TN + - TR + - TM + - TC + - TV + - UG + - UA + - AE + - GB + - UM + - US + - UY + - UZ + - VU + - VE + - VN + - VG + - VI + - WF + - EH + - YE + - ZM + - ZW type: string description: |- * `AF` - Afghanistan @@ -1126,258 +1157,259 @@ components: * `ZM` - Zambia * `ZW` - Zimbabwe x-enum-descriptions: - - Afghanistan - - Åland Islands - - Albania - - Algeria - - American Samoa - - Andorra - - Angola - - Anguilla - - Antarctica - - Antigua and Barbuda - - Argentina - - Armenia - - Aruba - - Australia - - Austria - - Azerbaijan - - Bahamas - - Bahrain - - Bangladesh - - Barbados - - Belarus - - Belgium - - Belize - - Benin - - Bermuda - - Bhutan - - Bolivia - - Bonaire, Sint Eustatius and Saba - - Bosnia and Herzegovina - - Botswana - - Bouvet Island - - Brazil - - British Indian Ocean Territory - - Brunei - - Bulgaria - - Burkina Faso - - Burundi - - Cabo Verde - - Cambodia - - Cameroon - - Canada - - Cayman Islands - - Central African Republic - - Chad - - Chile - - China - - Christmas Island - - Cocos (Keeling) Islands - - Colombia - - Comoros - - Congo - - Congo (the Democratic Republic of the) - - Cook Islands - - Costa Rica - - Côte d'Ivoire - - Croatia - - Cuba - - Curaçao - - Cyprus - - Czechia - - Denmark - - Djibouti - - Dominica - - Dominican Republic - - Ecuador - - Egypt - - El Salvador - - Equatorial Guinea - - Eritrea - - Estonia - - Eswatini - - Ethiopia - - Falkland Islands (Malvinas) - - Faroe Islands - - Fiji - - Finland - - France - - French Guiana - - French Polynesia - - French Southern Territories - - Gabon - - Gambia - - Georgia - - Germany - - Ghana - - Gibraltar - - Greece - - Greenland - - Grenada - - Guadeloupe - - Guam - - Guatemala - - Guernsey - - Guinea - - Guinea-Bissau - - Guyana - - Haiti - - Heard Island and McDonald Islands - - Holy See - - Honduras - - Hong Kong - - Hungary - - Iceland - - India - - Indonesia - - Iran - - Iraq - - Ireland - - Isle of Man - - Israel - - Italy - - Jamaica - - Japan - - Jersey - - Jordan - - Kazakhstan - - Kenya - - Kiribati - - Kuwait - - Kyrgyzstan - - Laos - - Latvia - - Lebanon - - Lesotho - - Liberia - - Libya - - Liechtenstein - - Lithuania - - Luxembourg - - Macao - - Madagascar - - Malawi - - Malaysia - - Maldives - - Mali - - Malta - - Marshall Islands - - Martinique - - Mauritania - - Mauritius - - Mayotte - - Mexico - - Micronesia - - Moldova - - Monaco - - Mongolia - - Montenegro - - Montserrat - - Morocco - - Mozambique - - Myanmar - - Namibia - - Nauru - - Nepal - - Netherlands - - New Caledonia - - New Zealand - - Nicaragua - - Niger - - Nigeria - - Niue - - Norfolk Island - - North Korea - - North Macedonia - - Northern Mariana Islands - - Norway - - Oman - - Pakistan - - Palau - - Palestine, State of - - Panama - - Papua New Guinea - - Paraguay - - Peru - - Philippines - - Pitcairn - - Poland - - Portugal - - Puerto Rico - - Qatar - - Réunion - - Romania - - Russia - - Rwanda - - Saint Barthélemy - - Saint Helena, Ascension and Tristan da Cunha - - Saint Kitts and Nevis - - Saint Lucia - - Saint Martin (French part) - - Saint Pierre and Miquelon - - Saint Vincent and the Grenadines - - Samoa - - San Marino - - Sao Tome and Principe - - Saudi Arabia - - Senegal - - Serbia - - Seychelles - - Sierra Leone - - Singapore - - Sint Maarten (Dutch part) - - Slovakia - - Slovenia - - Solomon Islands - - Somalia - - South Africa - - South Georgia and the South Sandwich Islands - - South Korea - - South Sudan - - Spain - - Sri Lanka - - Sudan - - Suriname - - Svalbard and Jan Mayen - - Sweden - - Switzerland - - Syria - - Taiwan - - Tajikistan - - Tanzania - - Thailand - - Timor-Leste - - Togo - - Tokelau - - Tonga - - Trinidad and Tobago - - Tunisia - - Türkiye - - Turkmenistan - - Turks and Caicos Islands - - Tuvalu - - Uganda - - Ukraine - - United Arab Emirates - - United Kingdom - - United States Minor Outlying Islands - - United States of America - - Uruguay - - Uzbekistan - - Vanuatu - - Venezuela - - Vietnam - - Virgin Islands (British) - - Virgin Islands (U.S.) - - Wallis and Futuna - - Western Sahara - - Yemen - - Zambia - - Zimbabwe + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua and Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bonaire, Sint Eustatius and Saba + - Bosnia and Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei + - Bulgaria + - Burkina Faso + - Burundi + - Cabo Verde + - Cambodia + - Cameroon + - Canada + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo (the Democratic Republic of the) + - Cook Islands + - Costa Rica + - Côte d'Ivoire + - Croatia + - Cuba + - Curaçao + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Eswatini + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-Bissau + - Guyana + - Haiti + - Heard Island and McDonald Islands + - Holy See + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran + - Iraq + - Ireland + - Isle of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Kuwait + - Kyrgyzstan + - Laos + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia + - Moldova + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - North Korea + - North Macedonia + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestine, State of + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Réunion + - Romania + - Russia + - Rwanda + - Saint Barthélemy + - Saint Helena, Ascension and Tristan da Cunha + - Saint Kitts and Nevis + - Saint Lucia + - Saint Martin (French part) + - Saint Pierre and Miquelon + - Saint Vincent and the Grenadines + - Samoa + - San Marino + - Sao Tome and Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Sint Maarten (Dutch part) + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia and the South Sandwich Islands + - South Korea + - South Sudan + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard and Jan Mayen + - Sweden + - Switzerland + - Syria + - Taiwan + - Tajikistan + - Tanzania + - Thailand + - Timor-Leste + - Togo + - Tokelau + - Tonga + - Trinidad and Tobago + - Tunisia + - Türkiye + - Turkmenistan + - Turks and Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States Minor Outlying Islands + - United States of America + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Vietnam + - Virgin Islands (British) + - Virgin Islands (U.S.) + - Wallis and Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe CyberSourceCheckout: type: object - description: Really basic serializer for the payload that we need to send to + description: + Really basic serializer for the payload that we need to send to CyberSource. properties: payload: @@ -1388,9 +1420,9 @@ components: method: type: string required: - - method - - payload - - url + - method + - payload + - url Discount: type: object description: Serializer for discounts. @@ -1408,8 +1440,8 @@ components: payment_type: nullable: true oneOf: - - $ref: '#/components/schemas/PaymentTypeEnum' - - $ref: '#/components/schemas/NullEnum' + - $ref: "#/components/schemas/PaymentTypeEnum" + - $ref: "#/components/schemas/NullEnum" max_redemptions: type: integer maximum: 2147483647 @@ -1419,46 +1451,48 @@ components: type: string format: date-time nullable: true - description: If set, this discount code will not be redeemable before this + description: + If set, this discount code will not be redeemable before this date. expiration_date: type: string format: date-time nullable: true - description: If set, this discount code will not be redeemable after this + description: + If set, this discount code will not be redeemable after this date. integrated_system: - $ref: '#/components/schemas/IntegratedSystem' + $ref: "#/components/schemas/IntegratedSystem" product: - $ref: '#/components/schemas/Product' + $ref: "#/components/schemas/Product" assigned_users: type: array items: - $ref: '#/components/schemas/User' + $ref: "#/components/schemas/User" company: - $ref: '#/components/schemas/Company' + $ref: "#/components/schemas/Company" required: - - amount - - assigned_users - - company - - discount_code - - id - - integrated_system - - product + - amount + - assigned_users + - company + - discount_code + - id + - integrated_system + - product DiscountTypeEnum: enum: - - percent-off - - dollars-off - - fixed-price + - percent-off + - dollars-off + - fixed-price type: string description: |- * `percent-off` - percent-off * `dollars-off` - dollars-off * `fixed-price` - fixed-price x-enum-descriptions: - - percent-off - - dollars-off - - fixed-price + - percent-off + - dollars-off + - fixed-price IntegratedSystem: type: object description: Serializer for IntegratedSystem model. @@ -1476,8 +1510,8 @@ components: description: type: string required: - - id - - name + - id + - name IntegratedSystemRequest: type: object description: Serializer for IntegratedSystem model. @@ -1493,7 +1527,7 @@ components: description: type: string required: - - name + - name Line: type: object description: Serializes a line item for an order. @@ -1518,17 +1552,17 @@ components: format: decimal pattern: ^-?\d{0,7}(?:\.\d{0,2})?$ product: - $ref: '#/components/schemas/Product' + $ref: "#/components/schemas/Product" required: - - id - - item_description - - product - - quantity - - total_price - - unit_price + - id + - item_description + - product + - quantity + - total_price + - unit_price NullEnum: enum: - - null + - null OrderHistory: type: object description: Serializer for order history. @@ -1537,7 +1571,7 @@ components: type: integer readOnly: true state: - $ref: '#/components/schemas/StateEnum' + $ref: "#/components/schemas/StateEnum" reference_number: type: string maxLength: 255 @@ -1550,7 +1584,7 @@ components: lines: type: array items: - $ref: '#/components/schemas/Line' + $ref: "#/components/schemas/Line" created_on: type: string format: date-time @@ -1560,17 +1594,17 @@ components: format: date-time readOnly: true required: - - created_on - - id - - lines - - purchaser - - total_price_paid - - updated_on + - created_on + - id + - lines + - purchaser + - total_price_paid + - updated_on PaginatedBasketWithProductList: type: object required: - - count - - results + - count + - results properties: count: type: integer @@ -1588,12 +1622,12 @@ components: results: type: array items: - $ref: '#/components/schemas/BasketWithProduct' + $ref: "#/components/schemas/BasketWithProduct" PaginatedIntegratedSystemList: type: object required: - - count - - results + - count + - results properties: count: type: integer @@ -1611,12 +1645,12 @@ components: results: type: array items: - $ref: '#/components/schemas/IntegratedSystem' + $ref: "#/components/schemas/IntegratedSystem" PaginatedOrderHistoryList: type: object required: - - count - - results + - count + - results properties: count: type: integer @@ -1634,12 +1668,12 @@ components: results: type: array items: - $ref: '#/components/schemas/OrderHistory' + $ref: "#/components/schemas/OrderHistory" PaginatedProductList: type: object required: - - count - - results + - count + - results properties: count: type: integer @@ -1657,7 +1691,7 @@ components: results: type: array items: - $ref: '#/components/schemas/Product' + $ref: "#/components/schemas/Product" PatchedIntegratedSystemRequest: type: object description: Serializer for IntegratedSystem model. @@ -1703,18 +1737,19 @@ components: description: Price (decimal to two places) image_metadata: nullable: true - description: Image metadata including URL, alt text, and description (in + description: + Image metadata including URL, alt text, and description (in JSON). PaymentTypeEnum: enum: - - marketing - - sales - - financial-assistance - - customer-support - - staff - - legacy - - credit_card - - purchase_order + - marketing + - sales + - financial-assistance + - customer-support + - staff + - legacy + - credit_card + - purchase_order type: string description: |- * `marketing` - marketing @@ -1726,14 +1761,14 @@ components: * `credit_card` - credit_card * `purchase_order` - purchase_order x-enum-descriptions: - - marketing - - sales - - financial-assistance - - customer-support - - staff - - legacy - - credit_card - - purchase_order + - marketing + - sales + - financial-assistance + - customer-support + - staff + - legacy + - credit_card + - purchase_order Product: type: object description: Serializer for Product model. @@ -1768,16 +1803,17 @@ components: readOnly: true image_metadata: nullable: true - description: Image metadata including URL, alt text, and description (in + description: + Image metadata including URL, alt text, and description (in JSON). required: - - deleted_by_cascade - - description - - id - - name - - price - - sku - - system + - deleted_by_cascade + - description + - id + - name + - price + - sku + - system ProductRequest: type: object description: Serializer for Product model. @@ -1809,14 +1845,15 @@ components: description: Price (decimal to two places) image_metadata: nullable: true - description: Image metadata including URL, alt text, and description (in + description: + Image metadata including URL, alt text, and description (in JSON). required: - - description - - name - - price - - sku - - system + - description + - name + - price + - sku + - system SimpleDiscount: type: object description: Simpler serializer for discounts. @@ -1832,7 +1869,7 @@ components: format: decimal pattern: ^-?\d{0,18}(?:\.\d{0,2})?$ discount_type: - $ref: '#/components/schemas/DiscountTypeEnum' + $ref: "#/components/schemas/DiscountTypeEnum" formatted_discount_amount: type: string description: |- @@ -1841,20 +1878,20 @@ components: This quantizes percent discounts to whole numbers. This is probably fine. readOnly: true required: - - amount - - discount_code - - discount_type - - formatted_discount_amount - - id + - amount + - discount_code + - discount_type + - formatted_discount_amount + - id StateEnum: enum: - - pending - - fulfilled - - canceled - - refunded - - declined - - errored - - review + - pending + - fulfilled + - canceled + - refunded + - declined + - errored + - review type: string description: |- * `pending` - Pending @@ -1865,13 +1902,13 @@ components: * `errored` - Errored * `review` - Review x-enum-descriptions: - - Pending - - Fulfilled - - Canceled - - Refunded - - Declined - - Errored - - Review + - Pending + - Fulfilled + - Canceled + - Refunded + - Declined + - Errored + - Review TaxRate: type: object description: TaxRate model serializer @@ -1880,7 +1917,7 @@ components: type: integer readOnly: true country_code: - $ref: '#/components/schemas/CountryCodeEnum' + $ref: "#/components/schemas/CountryCodeEnum" tax_rate: type: string format: decimal @@ -1889,8 +1926,8 @@ components: type: string maxLength: 100 required: - - country_code - - id + - country_code + - id User: type: object description: Serializer for User model. @@ -1900,7 +1937,8 @@ components: readOnly: true username: type: string - description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + description: + Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. pattern: ^[\w.@+-]+$ maxLength: 150 @@ -1916,5 +1954,5 @@ components: type: string maxLength: 150 required: - - id - - username + - id + - username diff --git a/system_meta/api.py b/system_meta/api.py index 968f3b8b..0821db54 100644 --- a/system_meta/api.py +++ b/system_meta/api.py @@ -11,6 +11,40 @@ log = logging.getLogger(__name__) +def get_product_metadata(platform: str, readable_id: str) -> None: + """Get product metadata from the Learn API.""" + + 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() + + if response.json().get("count", 0) > 0: + course_data = response.json().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 response.json() + + return {"count": 0} + + return response.json() + except requests.RequestException: + log.exception("Failed to get product metadata for %s", readable_id) + return {"count": 0} + + def update_product_metadata(product_id: int) -> None: """Get product metadata from the Learn API.""" @@ -52,7 +86,7 @@ def update_product_metadata(product_id: int) -> None: runs = course_data.get("runs") if runs: - run = next((r for r in runs if r.get("readable_id") == product.sku), None) + run = next((r for r in runs if r.get("run_id") == product.sku), None) if run: product_prices = run.get("prices", []) product_prices.sort() diff --git a/system_meta/api_test.py b/system_meta/api_test.py index 8b157836..fef88ae6 100644 --- a/system_meta/api_test.py +++ b/system_meta/api_test.py @@ -29,9 +29,8 @@ def test_retrieve_product_metadata(mocker, valid_run): "readable_id": "course-v1:MITx+12.345x", "runs": [ { - "run_id": "123", + "run_id": "course-v1:MITx+12.345x+2T2099", "prices": [150, 250], - "readable_id": "course-v1:MITx+12.345x+2T2099", } ] if valid_run diff --git a/system_meta/private_urls.py b/system_meta/private_urls.py index 2e240c9a..0fc01f03 100644 --- a/system_meta/private_urls.py +++ b/system_meta/private_urls.py @@ -5,7 +5,6 @@ from system_meta.views import ( apisix_test_request, authed_traefik_test_request, - traefik_test_request, ) urlpatterns = [ @@ -14,11 +13,6 @@ apisix_test_request, name="apisix_test_request", ), - re_path( - r"^traefik_test_request/$", - traefik_test_request, - name="traefik_test_request", - ), re_path( r"^authed_traefik_test_request/$", authed_traefik_test_request, diff --git a/system_meta/urls.py b/system_meta/urls.py index dac3f399..7524318f 100644 --- a/system_meta/urls.py +++ b/system_meta/urls.py @@ -7,6 +7,7 @@ from system_meta.views import ( IntegratedSystemViewSet, ProductViewSet, + preload_sku, ) v0_router = routers.DefaultRouter() @@ -18,7 +19,12 @@ ) v0_router.register(r"^meta/product", ProductViewSet, basename="meta_products_api") +app_name = "v0" urlpatterns = [ path("admin/", admin.site.urls), + re_path( + r"^api/v0/meta/product/preload/(?P[^/]+)/(?P[^/]+)/$", + preload_sku, + ), re_path("^api/v0/", include((v0_router.urls, "v0"))), ] diff --git a/system_meta/views.py b/system_meta/views.py index 1d4940ca..5d162775 100644 --- a/system_meta/views.py +++ b/system_meta/views.py @@ -3,18 +3,22 @@ import logging from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.authentication import SessionAuthentication from rest_framework.decorators import ( api_view, authentication_classes, permission_classes, + throttle_classes, ) from rest_framework.permissions import ( IsAuthenticated, ) from rest_framework.response import Response +from rest_framework.throttling import AnonRateThrottle +from system_meta.api import get_product_metadata, update_product_metadata from system_meta.models import IntegratedSystem, Product from system_meta.serializers import ( AdminIntegratedSystemSerializer, @@ -59,6 +63,75 @@ class ProductViewSet(AuthVariegatedModelViewSet): ] +@extend_schema( + description=( + "Pre-loads the product metadata for a given SKU, even if the " + "SKU doesn't exist yet." + ), + methods=["GET"], + request=None, + responses=ProductSerializer, +) +@api_view(["GET"]) +@permission_classes([]) +@authentication_classes([SessionAuthentication]) +@throttle_classes( + [ + AnonRateThrottle, + ] +) +def preload_sku(request, system_slug, sku): # noqa: ARG001 + """ + Preload the SKU for the product. + + If we have this product, then we just return the product info (serialized + like you'd have gotten otherwise). If we don't, we'll create a skeleton + product, make the call to update it from Learn, and then return the new + product. + + Integrated systems should be able to use this to ensure the resource they're + displaying to the user has a corresponding product in UE before the user + tries to buy it. (This of course requires that the resource has also been + loaded into Learn too.) + + This is throttled for unauthenticated users to prevent abuse - this is + pretty likely to spawn a request to the Learn API, so we don't want to kill + it by accident. + """ + + if not IntegratedSystem.objects.filter(slug=system_slug).exists(): + return Response( + {"error": "System not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + existing_product = Product.objects.filter(sku=sku, system__slug=system_slug) + + if existing_product.exists(): + return Response(ProductSerializer(existing_product.first()).data) + + product_metadata = get_product_metadata(system_slug, sku) + if product_metadata.get("count", 0) == 0: + return Response( + {"error": "Resource not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + product = Product.objects.create( + name=sku, + sku=sku, + description=sku, + price=0, + system=IntegratedSystem.objects.get(slug=system_slug), + ) + product.save() + + update_product_metadata(product.id) + product.refresh_from_db() + + return Response(ProductSerializer(product).data) + + @api_view(["GET"]) @permission_classes([]) @authentication_classes([SessionAuthentication]) @@ -78,21 +151,6 @@ def apisix_test_request(request): return Response(response, status=status.HTTP_200_OK) -@api_view(["GET"]) -@permission_classes([]) -@authentication_classes([SessionAuthentication]) -def traefik_test_request(request): - """Test API request so we can see how the Traefik integration works.""" - - response = { - "name": "Response ok!", - "user": request.user.username if request.user is not None else None, - "metas": [f"{key}: {value}" for key, value in request.META.items()], - } - - return Response(response, status=status.HTTP_200_OK) - - @api_view(["GET"]) @permission_classes([IsAuthenticated]) @authentication_classes([SessionAuthentication]) diff --git a/unified_ecommerce/settings.py b/unified_ecommerce/settings.py index 40bb0914..26db1866 100644 --- a/unified_ecommerce/settings.py +++ b/unified_ecommerce/settings.py @@ -451,6 +451,7 @@ "ALLOWED_VERSIONS": [ "v0", ], + "DEFAULT_THROTTLE_RATES": {"anon": "100/hour", "user": "1000/day"}, } USE_X_FORWARDED_PORT = get_bool("USE_X_FORWARDED_PORT", False) # noqa: FBT003