diff --git a/features/products.feature b/features/products.feature index d01dd6d..3f522e4 100644 --- a/features/products.feature +++ b/features/products.feature @@ -71,7 +71,7 @@ Scenario: Delete a Product And I press the "Delete" button Then I should see the message "Product has been Deleted!" When I press the "Retrieve" button - Then I should see the message "404 Not Found: Product with id" + Then I should see the message "not found" Scenario: Update a Product When I visit the "Home Page" diff --git a/features/steps/product_steps.py b/features/steps/product_steps.py index c6c7bf4..a1a3d30 100644 --- a/features/steps/product_steps.py +++ b/features/steps/product_steps.py @@ -12,7 +12,7 @@ def step_impl(context): """Delete all Products and load new ones""" # List all of the products and delete them one by one - rest_endpoint = f"{context.base_url}/products" + rest_endpoint = f"{context.base_url}/api/products" context.resp = requests.get(rest_endpoint) assert context.resp.status_code == HTTP_200_OK for products in context.resp.json(): diff --git a/k8s/requirements.txt b/k8s/requirements.txt index be2b0e3..42ed5f8 100644 --- a/k8s/requirements.txt +++ b/k8s/requirements.txt @@ -4,6 +4,9 @@ SQLAlchemy==2.0.0 # Runtime dependencies Flask==2.3.2 +flask-restx==1.1.0 +cloudant==2.15.0 +retry2==0.9.5 Flask-SQLAlchemy==3.0.2 # psycopg2==2.9.5 psycopg2-binary==2.9.5 diff --git a/requirements.txt b/requirements.txt index 26a0cfb..be9a5ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,9 @@ SQLAlchemy==2.0.0 # Runtime dependencies Flask==2.3.2 +flask-restx==1.1.0 +cloudant==2.15.0 +retry2==0.9.5 Flask-SQLAlchemy==3.0.2 psycopg2==2.9.5 # psycopg2-binary==2.9.5 diff --git a/service/__init__.py b/service/__init__.py index 5ebc789..add868e 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -6,12 +6,26 @@ """ import sys from flask import Flask +from flask_restx import Api from service import config from service.common import log_handlers # Create Flask application app = Flask(__name__) app.config.from_object(config) +app.config["ERROR_404_HELP"] = False + +api = Api( + app, + version="1.0.0", + title="Product Demo REST API Service", + description="This is a sample Product server.", + default="products", + default_label="Product operations", + doc="/apidocs", # default also could use doc='/apidocs/' + prefix="/api", +) + # Dependencies require we import the routes AFTER the Flask app is created # pylint: disable=wrong-import-position, wrong-import-order, cyclic-import diff --git a/service/common/error_handlers.py b/service/common/error_handlers.py index 5a7c6cc..a715c53 100644 --- a/service/common/error_handlers.py +++ b/service/common/error_handlers.py @@ -45,72 +45,72 @@ def bad_request(error): ) -@app.errorhandler(status.HTTP_404_NOT_FOUND) -def not_found(error): - """Handles resources not found with 404_NOT_FOUND""" - message = str(error) - app.logger.warning(message) - return ( - jsonify(status=status.HTTP_404_NOT_FOUND, error="Not Found", message=message), - status.HTTP_404_NOT_FOUND, - ) +# @app.errorhandler(status.HTTP_404_NOT_FOUND) +# def not_found(error): +# """Handles resources not found with 404_NOT_FOUND""" +# message = str(error) +# app.logger.warning(message) +# return ( +# jsonify(status=status.HTTP_404_NOT_FOUND, error="Not Found", message=message), +# status.HTTP_404_NOT_FOUND, +# ) -@app.errorhandler(status.HTTP_405_METHOD_NOT_ALLOWED) -def method_not_supported(error): - """Handles unsupported HTTP methods with 405_METHOD_NOT_SUPPORTED""" - message = str(error) - app.logger.warning(message) - return ( - jsonify( - status=status.HTTP_405_METHOD_NOT_ALLOWED, - error="Method not Allowed", - message=message, - ), - status.HTTP_405_METHOD_NOT_ALLOWED, - ) +# @app.errorhandler(status.HTTP_405_METHOD_NOT_ALLOWED) +# def method_not_supported(error): +# """Handles unsupported HTTP methods with 405_METHOD_NOT_SUPPORTED""" +# message = str(error) +# app.logger.warning(message) +# return ( +# jsonify( +# status=status.HTTP_405_METHOD_NOT_ALLOWED, +# error="Method not Allowed", +# message=message, +# ), +# status.HTTP_405_METHOD_NOT_ALLOWED, +# ) -@app.errorhandler(status.HTTP_409_CONFLICT) -def resource_conflict(error): - """Handles resource conflicts with HTTP_409_CONFLICT""" - message = str(error) - app.logger.warning(message) - return ( - jsonify( - status=status.HTTP_409_CONFLICT, - error="Conflict", - message=message, - ), - status.HTTP_409_CONFLICT, - ) +# @app.errorhandler(status.HTTP_409_CONFLICT) +# def resource_conflict(error): +# """Handles resource conflicts with HTTP_409_CONFLICT""" +# message = str(error) +# app.logger.warning(message) +# return ( +# jsonify( +# status=status.HTTP_409_CONFLICT, +# error="Conflict", +# message=message, +# ), +# status.HTTP_409_CONFLICT, +# ) -@app.errorhandler(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) -def mediatype_not_supported(error): - """Handles unsupported media requests with 415_UNSUPPORTED_MEDIA_TYPE""" - message = str(error) - app.logger.warning(message) - return ( - jsonify( - status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - error="Unsupported media type", - message=message, - ), - status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - ) +# @app.errorhandler(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) +# def mediatype_not_supported(error): +# """Handles unsupported media requests with 415_UNSUPPORTED_MEDIA_TYPE""" +# message = str(error) +# app.logger.warning(message) +# return ( +# jsonify( +# status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, +# error="Unsupported media type", +# message=message, +# ), +# status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, +# ) -@app.errorhandler(status.HTTP_500_INTERNAL_SERVER_ERROR) -def internal_server_error(error): - """Handles unexpected server error with 500_SERVER_ERROR""" - message = str(error) - app.logger.error(message) - return ( - jsonify( - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - error="Internal Server Error", - message=message, - ), - status.HTTP_500_INTERNAL_SERVER_ERROR, - ) +# @app.errorhandler(status.HTTP_500_INTERNAL_SERVER_ERROR) +# def internal_server_error(error): +# """Handles unexpected server error with 500_SERVER_ERROR""" +# message = str(error) +# app.logger.error(message) +# return ( +# jsonify( +# status=status.HTTP_500_INTERNAL_SERVER_ERROR, +# error="Internal Server Error", +# message=message, +# ), +# status.HTTP_500_INTERNAL_SERVER_ERROR, +# ) diff --git a/service/routes.py b/service/routes.py index a721746..32b063b 100644 --- a/service/routes.py +++ b/service/routes.py @@ -4,12 +4,12 @@ Describe what your service does here """ -from flask import jsonify, request, abort, url_for +from flask_restx import Resource, fields, reqparse, inputs from service.common import status # HTTP Status Codes from service.models import Product, Category # Import Flask application -from . import app +from . import app, api ############################################################ @@ -66,214 +66,481 @@ def index(): return app.send_static_file("index.html") +# Define the model so that the docs reflect what can be sent +create_model = api.model( + "Product", + { + "name": fields.String(required=True, description="The name of the Product"), + "description": fields.String( + required=True, + description="The description of Product", + ), + "price": fields.Float( + required=True, + description="The price of Product", + ), + "available": fields.Boolean( + required=True, description="Is the Product available for purchase?" + ), + "image_url": fields.String( + required=True, + description="The image url of Product", + ), + # pylint: disable=protected-access + "category": fields.String( + enum=Category._member_names_, + description="he category of Product (e.g., ELECTRONICS, FOOD, etc.)" + ), + }, +) + +product_model = api.inherit( + "ProductModel", + create_model, + { + "id": fields.String( + readOnly=True, description="The unique id assigned internally by service" + ), + }, +) + +# query string arguments +product_args = reqparse.RequestParser() +product_args.add_argument( + "name", type=str, location="args", required=False, help="List Products by name" +) +product_args.add_argument( + "category", type=str, location="args", required=False, help="List Products by category" +) +product_args.add_argument( + "available", + type=inputs.boolean, + location="args", + required=False, + help="List Products by availability", +) ###################################################################### # R E S T A P I E N D P O I N T S ###################################################################### ###################################################################### -# LIST ALL PRODUCTS +# PATH: /products/{id} ###################################################################### -@app.route("/products", methods=["GET"]) -def list_products(): - """Returns all of the Products""" - app.logger.info("Request for product list") - category = request.args.get("category") - name = request.args.get("name") - available = request.args.get("available") - products = Product.all() - - if category: - products_category = Product.find_by_category(category) - products = [product for product in products if product in products_category] - if name: - products_name = Product.find_by_name(name) - products = [product for product in products if product in products_name] - if available: - products_available = Product.find_by_availability(available) - products = [product for product in products if product in products_available] - - results = [product.serialize() for product in products] - app.logger.info("Returning %d products", len(results)) - return jsonify(results), status.HTTP_200_OK - - -###################################################################### -# ADD A NEW PRODUCT -###################################################################### -@app.route("/products", methods=["POST"]) -def create_products(): +@api.route("/products/") +@api.param("product_id", "The Product identifier") +class ProductResource(Resource): """ - Creates a Product - This endpoint will create a Product based the data in the body that is posted + ProductResource class + + Allows the manipulation of a single Product + GET /product{id} - Returns a Product with the id + PUT /product{id} - Update a Product with the id + DELETE /product{id} - Deletes a Product with the id """ - app.logger.info("Request to create a product") - check_content_type("application/json") - product = Product() - product.deserialize(request.get_json()) - product.create() - message = product.serialize() - location_url = url_for("read_products", product_id=product.id, _external=True) - app.logger.info("Product with ID [%s] created.", product.id) - return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url} - - # try: - # product.deserialize(request.get_json()) - # product.create() - # message = product.serialize() - # location_url = url_for("read_products", product_id=product.id, _external=True) - # app.logger.info("Product with ID [%s] created.", product.id) - # return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url} - # except sqlalchemy.exc.PendingRollbackError as rollback_error: - # # Rollback the session in case of error - # db.session.rollback() - # print("rollback") - # app.logger.error("Error creating product: %s", str(rollback_error)) - # return jsonify({"error": "Error creating product"}), status.HTTP_400_BAD_REQUEST + + # ------------------------------------------------------------------ + # RETRIEVE A PRODUCT + # ------------------------------------------------------------------ + @api.doc("get_products") + @api.response(404, "Product not found") + @api.marshal_with(product_model) + def get(self, product_id): + """ + Retrieve a single Product + + This endpoint will return a Product based on it's id + """ + app.logger.info("Request to Retrieve a product with id [%s]", product_id) + product = Product.find(product_id) + if not product: + abort(status.HTTP_404_NOT_FOUND, f"Product with id '{product_id}' was not found.") + return product.serialize(), status.HTTP_200_OK + + # ------------------------------------------------------------------ + # UPDATE AN EXISTING PRODUCT + # ------------------------------------------------------------------ + @api.doc("update_products") + @api.response(404, "Product not found") + @api.response(400, "The posted Product data was not valid") + @api.expect(product_model) + @api.marshal_with(product_model) + def put(self, product_id): + """ + Update a Product + + This endpoint will update a Product based the body that is posted + """ + app.logger.info("Request to Update a product with id [%s]", product_id) + product = Product.find(product_id) + if not product: + abort(status.HTTP_404_NOT_FOUND, f"Product with id '{product_id}' was not found.") + app.logger.debug("Payload = %s", api.payload) + data = api.payload + product.deserialize(data) + product.id = product_id + product.update() + return product.serialize(), status.HTTP_200_OK + + # ------------------------------------------------------------------ + # DELETE A PRODUCT + # ------------------------------------------------------------------ + @api.doc("delete_products") + @api.response(204, "Product deleted") + def delete(self, product_id): + """ + Delete a Product + + This endpoint will delete a Product based the id specified in the path + """ + app.logger.info("Request to Delete a product with id [%s]", product_id) + product = Product.find(product_id) + if product: + product.delete() + app.logger.info("Product with id [%s] was deleted", product_id) + + return "", status.HTTP_204_NO_CONTENT ###################################################################### -# ADD MULTIPLE NEW PRODUCT +# PATH: /products ###################################################################### -@app.route("/products/collect", methods=["POST"]) -def create_collect_products(): - """ - Creates multiple Products - This endpoint will create multiple Products based the data in the body that is posted - """ - app.logger.info("Request to create multiple products") - check_content_type("application/json") - products_data = request.get_json() - products = Product.create_multiple_products(products_data) - message = [] - for product in products: - app.logger.info("Product with ID [%s] created.", product.id) - message.append(product.serialize()) - return jsonify(message), status.HTTP_201_CREATED +@api.route("/products", strict_slashes=False) +class ProductCollection(Resource): + """Handles all interactions with collections of Products""" + + # ------------------------------------------------------------------ + # LIST ALL PRODUCTS + # ------------------------------------------------------------------ + @api.doc("list_products") + @api.expect(product_args) + @api.marshal_list_with(product_model) + def get(self): + """Returns all of the Products""" + app.logger.info("Request to list Products...") + products = [] + args = product_args.parse_args() + if args["category"]: + app.logger.info("Filtering by category: %s", args["category"]) + products = Product.find_by_category(args["category"]) + elif args["name"]: + app.logger.info("Filtering by name: %s", args["name"]) + products = Product.find_by_name(args["name"]) + elif args["available"] is not None: + app.logger.info("Filtering by availability: %s", args["available"]) + products = Product.find_by_availability(args["available"]) + else: + app.logger.info("Returning unfiltered list.") + products = Product.all() + + results = [product.serialize() for product in products] + app.logger.info("[%s] Products returned", len(results)) + return results, status.HTTP_200_OK + + # ------------------------------------------------------------------ + # ADD A NEW PRODUCT + # ------------------------------------------------------------------ + @api.doc("create_products") + @api.response(400, "The posted data was not valid") + @api.expect(create_model) + @api.marshal_with(product_model, code=201) + def post(self): + """ + Creates a Product + This endpoint will create a Product based the data in the body that is posted + """ + app.logger.info("Request to Create a Product") + product = Product() + app.logger.debug("Payload = %s", api.payload) + product.deserialize(api.payload) + product.create() + app.logger.info("Product with new id [%s] created!", product.id) + location_url = api.url_for(ProductResource, product_id=product.id, _external=True) + return product.serialize(), status.HTTP_201_CREATED, {"Location": location_url} ###################################################################### -# UPDATE A PRODUCT +# PATH: /products/{id}/change_availability ###################################################################### -@app.route("/products/", methods=["PUT"]) -def update_product(product_id): - """ - Update a Product - This endpoint will update a existing Product based the data in the body that is posted - or return 404 there is no product with id provided in payload - """ - - app.logger.info("Request to update a product") - check_content_type("application/json") - - product: Product = Product.find(product_id) - if not product: - app.logger.info("Invalid product id: %s", product_id) - abort( - status.HTTP_404_NOT_FOUND, f"There is no exist product with id {product_id}" +@api.route("/products//change_availability") +@api.param("product_id", "The Product identifier") +class ChangeAvailResource(Resource): + """Change availability actions on a Product""" + + @api.doc("change_availability") + @api.response(404, "Product not found") + def put(self, product_id): + """ + Change Product Availability + + This endpoint will change the availability of a Product based on the id specified in the path. + """ + app.logger.info( + "Request to change availability for product with id: %s", product_id ) - product.deserialize(request.get_json()) - product.update() - message = product.serialize() + product = Product.find(product_id) + if not product: + abort( + status.HTTP_404_NOT_FOUND, + f"Product with id '{product_id}' was not found.", + ) + product.change_availability() + message = {"message": f"Product availability changed to {product.available}"} + message = {**message, **product.serialize()} - return jsonify(message), status.HTTP_200_OK - - -###################################################################### -# DELETE A PRODUCT -###################################################################### -@app.route("/products/", methods=["DELETE"]) -def delete_products(product_id): - """ - Delete a Product - This endpoint will delete a Product based the id specified in the path - """ - app.logger.info("Request to delete product with id: %s", product_id) - product = Product.find(product_id) - if product: - product.delete() + app.logger.info("Product availability changed for ID [%s].", product_id) - app.logger.info("Product with ID [%s] delete complete.", product_id) - return "", status.HTTP_204_NO_CONTENT + return message, status.HTTP_200_OK ###################################################################### -# READ A PRODUCT +# PATH: /products/collect ###################################################################### -@app.route("/products/", methods=["GET"]) -def read_products(product_id): +@api.route("/products/collect") +class CollectionResource(Resource): """ - Read a Product - This endpoint will Read a Product for detail based the id specified in the path + Creates multiple Products """ - app.logger.info("Request to read product with id: %s", product_id) - product = Product.find(product_id) - if not product: - abort( - status.HTTP_404_NOT_FOUND, f"Product with id '{product_id}' was not found." - ) - - app.logger.info("Returning product with ID [%s].", product_id) - return jsonify(product.serialize()), status.HTTP_200_OK + @api.doc("create_muiltiple_products") + @api.response(400, "The posted data was not valid") + # @api.expect(create_model) + @api.marshal_list_with(product_model, code=201) + def post(self): + """ + Creates multiple Products + This endpoint will create multiple Products based the data in the body that is posted + """ + app.logger.info("Request to create multiple products") + app.logger.debug("Payload = %s", api.payload) + products = Product.create_multiple_products(api.payload) + message = [] + for product in products: + app.logger.info("Product with ID [%s] created.", product.id) + message.append(product.serialize()) + return message, status.HTTP_201_CREATED ###################################################################### -# ACTION TO CHANGE A PRODUCT'S AVAILABILITY +# PATH: /categories ###################################################################### -@app.route("/products//change_availability", methods=["PUT"]) -def change_product_availability(product_id): +@api.route("/categories", strict_slashes=False) +class Categories(Resource): """ - Change Product Availability - This endpoint will change the availability of a Product based on the id specified in the path. + Get Product categories """ - app.logger.info( - "Request to change availability for product with id: %s", product_id - ) - product = Product.find(product_id) - if not product: - abort( - status.HTTP_404_NOT_FOUND, - f"Product with id '{product_id}' was not found.", - ) - - product.change_availability() - message = {"message": f"Product availability changed to {product.available}"} - message = {**message, **product.serialize()} - - app.logger.info("Product availability changed for ID [%s].", product_id) - - return jsonify(message), status.HTTP_200_OK - - -###################################################################### -# get product categories -###################################################################### -@app.route("/categories", methods=["GET"]) -def get_categories(): - """Endpoint to get product categories""" - categories = [category.name for category in Category] - return jsonify(categories) + @api.doc("get_categories") + def get(self): + """Endpoint to get product categories""" + categories = [category.name for category in Category] + return categories + +# ###################################################################### +# # LIST ALL PRODUCTS +# ###################################################################### +# @app.route("/products", methods=["GET"]) +# def list_products(): +# """Returns all of the Products""" +# app.logger.info("Request for product list") +# category = request.args.get("category") +# name = request.args.get("name") +# available = request.args.get("available") +# products = Product.all() + +# if category: +# products_category = Product.find_by_category(category) +# products = [product for product in products if product in products_category] +# if name: +# products_name = Product.find_by_name(name) +# products = [product for product in products if product in products_name] +# if available: +# products_available = Product.find_by_availability(available) +# products = [product for product in products if product in products_available] + +# results = [product.serialize() for product in products] +# app.logger.info("Returning %d products", len(results)) +# return jsonify(results), status.HTTP_200_OK + + +# ###################################################################### +# # ADD A NEW PRODUCT +# ###################################################################### +# @app.route("/products", methods=["POST"]) +# def create_products(): +# """ +# Creates a Product +# This endpoint will create a Product based the data in the body that is posted +# """ +# app.logger.info("Request to create a product") +# check_content_type("application/json") +# product = Product() +# product.deserialize(request.get_json()) +# product.create() +# message = product.serialize() +# location_url = url_for("read_products", product_id=product.id, _external=True) +# app.logger.info("Product with ID [%s] created.", product.id) +# return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url} + +# # try: +# # product.deserialize(request.get_json()) +# # product.create() +# # message = product.serialize() +# # location_url = url_for("read_products", product_id=product.id, _external=True) +# # app.logger.info("Product with ID [%s] created.", product.id) +# # return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url} +# # except sqlalchemy.exc.PendingRollbackError as rollback_error: +# # # Rollback the session in case of error +# # db.session.rollback() +# # print("rollback") +# # app.logger.error("Error creating product: %s", str(rollback_error)) +# # return jsonify({"error": "Error creating product"}), status.HTTP_400_BAD_REQUEST + + +# ###################################################################### +# # ADD MULTIPLE NEW PRODUCT +# ###################################################################### +# @app.route("/products/collect", methods=["POST"]) +# def create_collect_products(): +# """ +# Creates multiple Products +# This endpoint will create multiple Products based the data in the body that is posted +# """ +# app.logger.info("Request to create multiple products") +# check_content_type("application/json") +# products_data = request.get_json() +# products = Product.create_multiple_products(products_data) +# message = [] +# for product in products: +# app.logger.info("Product with ID [%s] created.", product.id) +# message.append(product.serialize()) +# return jsonify(message), status.HTTP_201_CREATED + + +# ###################################################################### +# # UPDATE A PRODUCT +# ###################################################################### +# @app.route("/products/", methods=["PUT"]) +# def update_product(product_id): +# """ +# Update a Product +# This endpoint will update a existing Product based the data in the body that is posted +# or return 404 there is no product with id provided in payload +# """ + +# app.logger.info("Request to update a product") +# check_content_type("application/json") + +# product: Product = Product.find(product_id) +# if not product: +# app.logger.info("Invalid product id: %s", product_id) +# abort( +# status.HTTP_404_NOT_FOUND, f"There is no exist product with id {product_id}" +# ) +# product.deserialize(request.get_json()) +# product.update() +# message = product.serialize() + +# return jsonify(message), status.HTTP_200_OK + + +# ###################################################################### +# # DELETE A PRODUCT +# ###################################################################### +# @app.route("/products/", methods=["DELETE"]) +# def delete_products(product_id): +# """ +# Delete a Product +# This endpoint will delete a Product based the id specified in the path +# """ +# app.logger.info("Request to delete product with id: %s", product_id) +# product = Product.find(product_id) +# if product: +# product.delete() + +# app.logger.info("Product with ID [%s] delete complete.", product_id) +# return "", status.HTTP_204_NO_CONTENT + + +# ###################################################################### +# # READ A PRODUCT +# ###################################################################### +# @app.route("/products/", methods=["GET"]) +# def read_products(product_id): +# """ +# Read a Product +# This endpoint will Read a Product for detail based the id specified in the path +# """ +# app.logger.info("Request to read product with id: %s", product_id) +# product = Product.find(product_id) +# if not product: +# abort( +# status.HTTP_404_NOT_FOUND, f"Product with id '{product_id}' was not found." +# ) + +# app.logger.info("Returning product with ID [%s].", product_id) +# return jsonify(product.serialize()), status.HTTP_200_OK + + +# ###################################################################### +# # ACTION TO CHANGE A PRODUCT'S AVAILABILITY +# ###################################################################### +# @app.route("/products//change_availability", methods=["PUT"]) +# def change_product_availability(product_id): +# """ +# Change Product Availability +# This endpoint will change the availability of a Product based on the id specified in the path. +# """ +# app.logger.info( +# "Request to change availability for product with id: %s", product_id +# ) +# product = Product.find(product_id) +# if not product: +# abort( +# status.HTTP_404_NOT_FOUND, +# f"Product with id '{product_id}' was not found.", +# ) + +# product.change_availability() +# message = {"message": f"Product availability changed to {product.available}"} +# message = {**message, **product.serialize()} + +# app.logger.info("Product availability changed for ID [%s].", product_id) + +# return jsonify(message), status.HTTP_200_OK + + +# ###################################################################### +# # get product categories +# ###################################################################### +# @app.route("/categories", methods=["GET"]) +# def get_categories(): +# """Endpoint to get product categories""" +# categories = [category.name for category in Category] +# return jsonify(categories) ###################################################################### # U T I L I T Y F U N C T I O N S ###################################################################### - -def check_content_type(content_type): - """Checks that the media type is correct""" - if "Content-Type" not in request.headers: - app.logger.error("No Content-Type specified.") - abort( - status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - f"Content-Type must be {content_type}", - ) - - if request.headers["Content-Type"] == content_type: - return - - app.logger.error("Invalid Content-Type: %s", request.headers["Content-Type"]) - abort( - status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - f"Content-Type must be {content_type}", - ) +def abort(error_code: int, message: str): + """Logs errors before aborting""" + app.logger.error(message) + api.abort(error_code, message) + +# def check_content_type(content_type): +# """Checks that the media type is correct""" +# if "Content-Type" not in request.headers: +# app.logger.error("No Content-Type specified.") +# abort( +# status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, +# f"Content-Type must be {content_type}", +# ) + +# if request.headers["Content-Type"] == content_type: +# return + +# app.logger.error("Invalid Content-Type: %s", request.headers["Content-Type"]) +# abort( +# status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, +# f"Content-Type must be {content_type}", +# ) diff --git a/service/static/js/rest_api.js b/service/static/js/rest_api.js index d4d9f7a..79cf334 100644 --- a/service/static/js/rest_api.js +++ b/service/static/js/rest_api.js @@ -67,7 +67,7 @@ $(function () { let ajax = $.ajax({ type: "POST", - url: "/products", + url: "/api/products", contentType: "application/json", data: JSON.stringify(data), }); @@ -110,7 +110,7 @@ $(function () { let ajax = $.ajax({ type: "PUT", - url: `/products/${product_id}`, + url: `/api/products/${product_id}`, contentType: "application/json", data: JSON.stringify(data) }) @@ -138,7 +138,7 @@ $(function () { let ajax = $.ajax({ type: "GET", - url: `/products/${product_id}`, + url: `/api/products/${product_id}`, contentType: "application/json", data: '' }) @@ -168,7 +168,7 @@ $(function () { let ajax = $.ajax({ type: "DELETE", - url: `/products/${product_id}`, + url: `/api/products/${product_id}`, contentType: "application/json", data: '', }) @@ -227,7 +227,7 @@ $(function () { let ajax = $.ajax({ type: "GET", - url: `/products?${queryString}`, + url: `/api/products?${queryString}`, contentType: "application/json", data: '' }) @@ -284,7 +284,7 @@ $(function () { let ajax = $.ajax({ type: "PUT", - url: `/products/${product_id}/change_availability`, + url: `/api/products/${product_id}/change_availability`, contentType: "application/json", data: '', }) @@ -307,7 +307,7 @@ $(function () { function loadCategories() { $.ajax({ - url: '/categories', + url: '/api/categories', type: 'GET', dataType: 'json', success: function(categories) { diff --git a/tests/test_routes.py b/tests/test_routes.py index ad0b1f8..be0ce9a 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -19,8 +19,9 @@ "DATABASE_URI", "postgresql://postgres:postgres@localhost:5432/testdb" ) -BASE_URL = "/products" -COLLECT_URL = "/products/collect" +BASE_URL = "/api/products" +COLLECT_URL = "/api/products/collect" +CONTENT_TYPE_JSON = "application/json" ###################################################################### @@ -279,7 +280,7 @@ def test_change_product_availability_not_found(self): def test_get_categories(self): """It should return all product categories.""" - response = self.client.get("/categories") + response = self.client.get("/api/categories") self.assertEqual(response.status_code, 200) data = response.get_json() @@ -328,20 +329,10 @@ def test_create_product_bad_category(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_update_missing_product(self): - """It should not update a Product""" - - # create a product - test_product_original = ProductFactory() - logging.debug("Test Product: %s", test_product_original.serialize()) - response = self.client.post(BASE_URL, json=test_product_original.serialize()) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - new_product = response.get_json() - - # update - test_product_new = ProductFactory() - test_product_new.id = new_product["id"] + 1 - logging.debug("Test Product: %s", test_product_new.serialize()) - response = self.client.put( - f"{BASE_URL}/{test_product_new.id}", json=test_product_new.serialize() + """It should not update a Product that doesn't exist""" + resp = self.client.put( + f"{BASE_URL}/2147483647", + json={}, + content_type=CONTENT_TYPE_JSON, ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)