From 9ad2d421be17d294fc9435a24333d2d8cf7b20ad Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 29 Mar 2019 09:58:05 +0000 Subject: [PATCH 01/63] fleshed out the empty folder structure --- app/__init__.py | 12 ++++++++++++ app/main/__init__.py | 5 +++++ app/main/errors.py | 10 ++++++++++ app/main/views.py | 7 +++++++ app/templates/404.html | 1 + app/templates/500.html | 1 + app/templates/index.html | 1 + config.py | 25 +++++++++++++++++++++++++ flasky.py | 5 +++++ 9 files changed, 67 insertions(+) create mode 100644 app/main/errors.py create mode 100644 app/main/views.py create mode 100644 app/templates/404.html create mode 100644 app/templates/500.html diff --git a/app/__init__.py b/app/__init__.py index e69de29..0740f77 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1,12 @@ +from flask import Flask +from config import config + +def create_app(config_name): + app = Flask(__name__) + app.config.from_object(config[config_name]) + config[config_name].init_app(app) + + from .main import main as main_blueprint + app.register_blueprint(main_blueprint) + + return app \ No newline at end of file diff --git a/app/main/__init__.py b/app/main/__init__.py index e69de29..9ca777c 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +main = Blueprint('main', __name__) + +from . import views, errors \ No newline at end of file diff --git a/app/main/errors.py b/app/main/errors.py new file mode 100644 index 0000000..0a88fd3 --- /dev/null +++ b/app/main/errors.py @@ -0,0 +1,10 @@ +from flask import render_template +from . import main + +@main.app_errorhandler(404) +def page_not_found(e): + return render_template('404.html'), 404 + +@main.app_errorhandler(500) +def internal_server_error(e): + return render_template('500.html'), 500 \ No newline at end of file diff --git a/app/main/views.py b/app/main/views.py new file mode 100644 index 0000000..628b546 --- /dev/null +++ b/app/main/views.py @@ -0,0 +1,7 @@ +from flask import render_template +from . import main + + +@main.route('/', methods=['GET']) +def index(): + return render_template('index.html') \ No newline at end of file diff --git a/app/templates/404.html b/app/templates/404.html new file mode 100644 index 0000000..57db2e9 --- /dev/null +++ b/app/templates/404.html @@ -0,0 +1 @@ +404 \ No newline at end of file diff --git a/app/templates/500.html b/app/templates/500.html new file mode 100644 index 0000000..eb1f494 --- /dev/null +++ b/app/templates/500.html @@ -0,0 +1 @@ +500 \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index e69de29..b2d525b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -0,0 +1 @@ +index \ No newline at end of file diff --git a/config.py b/config.py index e69de29..1883f18 100644 --- a/config.py +++ b/config.py @@ -0,0 +1,25 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' + + @staticmethod + def init_app(app): + pass + +class DevelopmentConfig(Config): + DEBUG = True + +class TestingConfig(Config): + TESTING = True + +class ProductionConfig(Config): + pass + +config = { + 'development': DevelopmentConfig, + 'testing': TestingConfig, + 'production': ProductionConfig, + 'default': DevelopmentConfig +} \ No newline at end of file diff --git a/flasky.py b/flasky.py index e69de29..4fe0027 100644 --- a/flasky.py +++ b/flasky.py @@ -0,0 +1,5 @@ +import os +from app import create_app + + +app = create_app(os.getenv('FLASK_CONFIG') or 'default') \ No newline at end of file From f4db1404b52c98fa5c92f09166ab403b0810f980 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 29 Mar 2019 10:00:48 +0000 Subject: [PATCH 02/63] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c89f7a6..8c8ae81 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # flask-empty-project-shell + +## Now fleshed out with basic roots An empty project shell for structuring new Flask projects From 719c7ce32c8600c67ba483ea9943bc80aafa541d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 10:26:45 +0100 Subject: [PATCH 03/63] Products and orders API - still in progress --- .gitignore | 1 + README.md | 1 + app/__init__.py | 15 ++- app/api/__init__.py | 6 + .../api/authentication.py | 0 app/api/decorators.py | 14 +++ app/api/errors.py | 27 +++++ app/api/orders.py | 33 ++++++ app/api/products.py | 38 +++++++ app/models.py | 61 ++++++++++ config.py | 33 ++++-- manage.py | 20 ++++ migrations/README | 1 + migrations/alembic.ini | 45 ++++++++ migrations/env.py | 95 ++++++++++++++++ migrations/script.py.mako | 24 ++++ tests/test_api.py | 107 ++++++++++++++++++ 17 files changed, 511 insertions(+), 10 deletions(-) create mode 100644 .gitignore create mode 100644 app/api/__init__.py rename migrations/Migration Notes.txt => app/api/authentication.py (100%) create mode 100644 app/api/decorators.py create mode 100644 app/api/errors.py create mode 100644 app/api/orders.py create mode 100644 app/api/products.py create mode 100644 app/models.py create mode 100644 manage.py create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 tests/test_api.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a979ee7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/venv \ No newline at end of file diff --git a/README.md b/README.md index 8c8ae81..4efa0c0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # flask-empty-project-shell ## Now fleshed out with basic roots +## and with api An empty project shell for structuring new Flask projects diff --git a/app/__init__.py b/app/__init__.py index 0740f77..9fb3a77 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,12 +1,21 @@ from flask import Flask from config import config +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + def create_app(config_name): app = Flask(__name__) app.config.from_object(config[config_name]) config[config_name].init_app(app) - + + db.init_app(app) + from .main import main as main_blueprint app.register_blueprint(main_blueprint) - - return app \ No newline at end of file + + from .api import api as api_blueprint + app.register_blueprint(api_blueprint, url_prefix='/api/v1') + + return app diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..93df9f1 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,6 @@ +from flask import Blueprint + +api = Blueprint('api', __name__) + +from . import authentication, errors +from . import products, orders diff --git a/migrations/Migration Notes.txt b/app/api/authentication.py similarity index 100% rename from migrations/Migration Notes.txt rename to app/api/authentication.py diff --git a/app/api/decorators.py b/app/api/decorators.py new file mode 100644 index 0000000..a511d59 --- /dev/null +++ b/app/api/decorators.py @@ -0,0 +1,14 @@ +from functools import wraps +from flask import g +from .errors import forbidden + + +def permission_required(permission): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not g.current_user.can(permission): + return forbidden('Insufficient permissions') + return f(*args, **kwargs) + return decorated_function + return decorator \ No newline at end of file diff --git a/app/api/errors.py b/app/api/errors.py new file mode 100644 index 0000000..13b45a1 --- /dev/null +++ b/app/api/errors.py @@ -0,0 +1,27 @@ +from flask import jsonify +from . import api + +class ValidationError(ValueError): + pass + +def bad_request(message): + response = jsonify({'error': 'bad request', 'message': message}) + response.status_code = 400 + return response + + +def unauthorized(message): + response = jsonify({'error': 'unauthorized', 'message': message}) + response.status_code = 401 + return response + + +def forbidden(message): + response = jsonify({'error': 'forbidden', 'message': message}) + response.status_code = 403 + return response + + +@api.errorhandler(ValidationError) +def validation_error(e): + return bad_request(e.args[0]) diff --git a/app/api/orders.py b/app/api/orders.py new file mode 100644 index 0000000..07d6502 --- /dev/null +++ b/app/api/orders.py @@ -0,0 +1,33 @@ +from . import api +from ..models import Order +from .. import db +from flask import jsonify + +@api.route('/orders/', methods=['GET']) +def get_orders(): + orders = Order.query.all() + return jsonify({ + 'orders': [o.to_json() for o in orders] + }) + +@api.route('/orders/', methods=['DELETE']) +def delete_order(id): + order = Order.query.get_or_404(id) + db.session.delete(order) + db.session.commit() + return jsonify({"success": True}) + +@api.route('/orders/', methods=['PUT']) +def edit_order(id): + order = Order.query.get_or_404(id) + + order.order_id = request.json.get('order_id') + order.name = request.json.get('name') + order.address = request.json.get('address') + order.city = request.json.get('city') + order.state = request.json.get('state') + order.zip = request.json.get('zip') + order.country = request.json.get('country') + + db.session.add(order) + db.session.commit() diff --git a/app/api/products.py b/app/api/products.py new file mode 100644 index 0000000..3fdbe5c --- /dev/null +++ b/app/api/products.py @@ -0,0 +1,38 @@ +from . import api +from ..models import Product +from .. import db +from flask import jsonify, request + +@api.route('/products/', methods=['GET']) +def get_products(): + products = Product.query.all() + return jsonify({ + 'products': [p.to_json() for p in products] + }), 200 + +@api.route('/products/', methods=['POST']) +def new_product(): + product = Product.from_json(request.json) + db.session.add(product) + db.session.commit() + return jsonify(product.to_json()), 201 + +@api.route('/products/', methods=['DELETE']) +def delete_product(id): + product = Product.query.get_or_404(id) + db.session.delete(product) + db.session.commit() + return jsonify({"success": True}), 200 + +@api.route('/products/', methods=['PUT']) +def edit_product(id): + product = Product.query.get_or_404(id) + + product.name = request.json.get('name') + product.category = request.json.get('category') + product.description = request.json.get('description') + product.price = request.json.get('price') + + db.session.add(product) + db.session.commit() + return jsonify(product.to_json()), 200 diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..dbc812b --- /dev/null +++ b/app/models.py @@ -0,0 +1,61 @@ +from app import db +from sqlalchemy import Column + +class Product(db.Model): + __tablename__ = 'products' + + product_id = Column(db.Integer(), primary_key = True) + name = Column(db.String(30)) + category = Column(db.String(50)) + description = Column(db.String(200)) + price = Column(db.Numeric(12, 2)) + + def to_json(self): + json_product = { + 'product_id': self.product_id, + 'name': self.name, + 'category': self.category, + 'description': self.description, + 'price': float(self.price) # Decimal to float + } + return json_product + + @staticmethod + def from_json(json_product): + name = json_product.get('name') + category = json_product.get('category') + description = json_product.get('description') + price = json_product.get('price') + return Product(name=name, category=category, + description=description, price=price) + +class Order(db.Model): + __tablename__ = 'orders' + + order_id = Column(db.Integer(), primary_key = True) + name = Column(db.String(30)) + address = Column(db.String(100)) + city = Column(db.String(50)) + state = Column(db.String(20)) + zip = Column(db.String(7)) + country = Column(db.String(20)) + + def to_json(self): + json_order = { + 'order_id': self.order_id, + 'name': self.name, + 'address': self.address, + 'city': self.city, + 'state': self.state, + 'zip': self.zip, + 'country': self.country + } + return json_order + +class OrderLine(db.Model): + __tablename__ = 'order_lines' + + order_line_id = Column(db.Integer(), primary_key = True) + order_id = Column(db.Integer()) + product_id = Column(db.Integer()) + quantity = Column(db.Integer()) diff --git a/config.py b/config.py index 1883f18..f0ccad1 100644 --- a/config.py +++ b/config.py @@ -3,23 +3,42 @@ class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' - + SQLALCHEMY_TRACK_MODIFICATIONS = False + SSL_REDIRECT = False + @staticmethod def init_app(app): pass - + class DevelopmentConfig(Config): DEBUG = True - + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') + class TestingConfig(Config): + print('Testing config') TESTING = True - + SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') + class ProductionConfig(Config): pass - + +class HerokuConfig(ProductionConfig): + @classmethod + def init_app(cls, app): + ProductionConfig.init_app(app) + + import logging + from logging import StreamHandler + file_handler = StreamHandler() + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + + SSL_REDIRECT = True if os.environ.get('DYNO') else False + config = { 'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig, - 'default': DevelopmentConfig -} \ No newline at end of file + 'default': DevelopmentConfig, + 'heroku': HerokuConfig +} diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..8306063 --- /dev/null +++ b/manage.py @@ -0,0 +1,20 @@ +# Intended use is along the lines of: +# > python manage.py db init +# > python manage.py db migrate +# > python manage.py db upgrade + +import os +from flask_script import Manager # class for handling a set of commands +from flask_migrate import Migrate, MigrateCommand +from app import db, create_app +from app import models + +app = create_app(config_name='testing') +migrate = Migrate(app, db) +manager = Manager(app) + +manager.add_command('db', MigrateCommand) + + +if __name__ == '__main__': + manager.run() diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..169d487 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,95 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option('sqlalchemy.url', + current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..3116f2c --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,107 @@ + +import unittest +import os +import json +from flask import current_app + +import sys +myPath = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, myPath + '/../') + +from app import create_app, db + +class APITestCase(unittest.TestCase): + """This class represents the API test case""" + + def setUp(self): + """Define test variables and initialize app.""" + self.app = create_app(config_name="testing") + self.app_context = self.app.app_context() + self.app_context.push() + self.client = self.app.test_client() + self.bucketlist = { + 'name': 'Oranges', + 'category': 'Food', + 'description': 'Round citrus fruit', + 'price': 5.59 + } + db.create_all() + + + def test_app_exists(self): + self.assertFalse(current_app is None) + + def test_api_creation(self): + """Test API can create a product (POST request)""" + res = self.client.post('/api/v1/products/', + data=json.dumps(self.bucketlist), + content_type='application/json') + self.assertEqual(res.status_code, 201) + self.assertIn('Round citrus fruit', str(res.data)) + + def test_api_can_get_all_products(self): + """Test API can get a product (GET request).""" + res = self.client.post('/api/v1/products/', + data=json.dumps(self.bucketlist), + content_type='application/json') + self.assertEqual(res.status_code, 201) + res = self.client.get('/api/v1/products/') + self.assertEqual(res.status_code, 200) + self.assertIn('Round citrus fruit', str(res.data)) + + + # not implemented + # def test_api_can_get_product_by_id(self): + # """Test API can get a single product by using it's id.""" + # rv = self.client.post('/api/v1/products/', + # data=json.dumps(self.bucketlist), + # content_type='application/json') + # self.assertEqual(rv.status_code, 201) + # result_in_json = json.loads(rv.data.decode('utf-8').replace("'", "\"")) + # print('id',result_in_json['product_id']) + # result = self.client.get( + # '/api/v1/products/{}'.format(result_in_json['product_id'])) + # self.assertEqual(result.status_code, 200) + # self.assertIn('Round citrus fruit', str(result.data)) + + def test_product_can_be_edited(self): + """Test API can edit an existing bucketlist. (PUT request)""" + rv = self.client.post('/api/v1/products/', + data=json.dumps(self.bucketlist), + content_type='application/json') + self.assertEqual(rv.status_code, 201) + rv = self.client.put( + '/api/v1/products/1', + data = json.dumps({ + 'name': 'Oranges', + 'category': 'Food', + 'description': 'Round bright citrus fruit', + 'price': 5.59 + }), content_type='application/json') + self.assertEqual(rv.status_code, 200) + results = self.client.get('/api/v1/products/') + self.assertIn('Round bright', str(results.data)) + + def test_product_deletion(self): + """Test API can delete an existing product. (DELETE request).""" + rv = self.client.post('/api/v1/products/', + data=json.dumps(self.bucketlist), + content_type='application/json') + self.assertEqual(rv.status_code, 201) + res = self.client.delete('/api/v1/products/1') + self.assertEqual(res.status_code, 200) + # Test to see if it exists, should return a 404 + result = self.client.get('/api/v1/products') + self.assertTrue('Round bright' not in str(result.data)) + #self.assertEqual(result.status_code, 404) + + def tearDown(self): + """teardown all initialized variables.""" + with self.app.app_context(): + # drop all tables + db.session.remove() + db.drop_all() + +# Make the tests conveniently executable +if __name__ == "__main__": + unittest.main() From 6e5073ebb0d10a8a7eea74bfd85cbf2a50e990e9 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 10:27:19 +0100 Subject: [PATCH 04/63] Products and orders API - still in progress --- .gitignore | 3 +- migrations/README | 1 - migrations/alembic.ini | 45 ------------------- migrations/env.py | 95 --------------------------------------- migrations/script.py.mako | 24 ---------- 5 files changed, 2 insertions(+), 166 deletions(-) delete mode 100644 migrations/README delete mode 100644 migrations/alembic.ini delete mode 100644 migrations/env.py delete mode 100644 migrations/script.py.mako diff --git a/.gitignore b/.gitignore index a979ee7..86d02c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/venv \ No newline at end of file +/venv +/migrations diff --git a/migrations/README b/migrations/README deleted file mode 100644 index 98e4f9c..0000000 --- a/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini deleted file mode 100644 index f8ed480..0000000 --- a/migrations/alembic.ini +++ /dev/null @@ -1,45 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py deleted file mode 100644 index 169d487..0000000 --- a/migrations/env.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import with_statement - -import logging -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) -logger = logging.getLogger('alembic.env') - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -from flask import current_app -config.set_main_option('sqlalchemy.url', - current_app.config.get('SQLALCHEMY_DATABASE_URI')) -target_metadata = current_app.extensions['migrate'].db.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - # this callback is used to prevent an auto-migration from being generated - # when there are no changes to the schema - # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html - def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): - script = directives[0] - if script.upgrade_ops.is_empty(): - directives[:] = [] - logger.info('No changes in schema detected.') - - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix='sqlalchemy.', - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=target_metadata, - process_revision_directives=process_revision_directives, - **current_app.extensions['migrate'].configure_args - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako deleted file mode 100644 index 2c01563..0000000 --- a/migrations/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} From 9b4db5817a7b5e9f8162badb02456b923f2acb83 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 10:46:09 +0100 Subject: [PATCH 05/63] added requirements.txt --- requirements.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/requirements.txt b/requirements.txt index e69de29..a760747 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,16 @@ +alembic==1.0.8 +Click==7.0 +Flask==1.0.2 +Flask-Migrate==2.4.0 +Flask-Script==2.0.6 +Flask-SQLAlchemy==2.3.2 +itsdangerous==1.1.0 +Jinja2==2.10 +Mako==1.0.8 +MarkupSafe==1.1.1 +psycopg2==2.7.7 +python-dateutil==2.8.0 +python-editor==1.0.4 +six==1.12.0 +SQLAlchemy==1.3.1 +Werkzeug==0.15.1 From b30b7160acd46ff80835582650543d762d6f4933 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 10:53:10 +0100 Subject: [PATCH 06/63] added procfile --- Procfile | 1 + 1 file changed, 1 insertion(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..c65d50e --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: waitress-serve --port=$PORT flasky:app \ No newline at end of file From 3bdd1133dd6603f2b9fab1ced7ab43d6b6c05b12 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 11:18:38 +0100 Subject: [PATCH 07/63] add waitress to requirements --- .gitignore | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 86d02c0..ec47f62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /venv /migrations +.env \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a760747..241343b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ python-dateutil==2.8.0 python-editor==1.0.4 six==1.12.0 SQLAlchemy==1.3.1 +waitress==1.2.1 Werkzeug==0.15.1 From bc97a98233dcc0cab2d5419005e454595fcce843 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 11:39:48 +0100 Subject: [PATCH 08/63] used FLASK_CONFIG for manage.py --- config.py | 2 +- manage.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index f0ccad1..7d1dc92 100644 --- a/config.py +++ b/config.py @@ -20,7 +20,7 @@ class TestingConfig(Config): SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') class ProductionConfig(Config): - pass + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') class HerokuConfig(ProductionConfig): @classmethod diff --git a/manage.py b/manage.py index 8306063..673466a 100644 --- a/manage.py +++ b/manage.py @@ -9,7 +9,7 @@ from app import db, create_app from app import models -app = create_app(config_name='testing') +app = create_app(config_name=os.environ.get('FLASK_CONFIG')) migrate = Migrate(app, db) manager = Manager(app) From 3487029a54be606100164f414f45809f5358ff74 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 11:45:35 +0100 Subject: [PATCH 09/63] edit manage.py getenv used --- manage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage.py b/manage.py index 673466a..1d2ad86 100644 --- a/manage.py +++ b/manage.py @@ -9,7 +9,7 @@ from app import db, create_app from app import models -app = create_app(config_name=os.environ.get('FLASK_CONFIG')) +app = create_app(config_name=os.getenv('FLASK_CONFIG')) migrate = Migrate(app, db) manager = Manager(app) From f7112bd7088c3cd9677dc278203d42f9e4c4f13f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 13:24:21 +0100 Subject: [PATCH 10/63] allow cors at server end of api --- README.md | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4efa0c0..3c37217 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,34 @@ -# flask-empty-project-shell +# Flask API -## Now fleshed out with basic roots -## and with api -An empty project shell for structuring new Flask projects +This is designed as a basic json API to serve **products** and **orders** resources. + +## API resources + +| URL | Method | Description | +| -------------------|-----------|------------------------ | +| /products/ | GET | Get all products | +| /products/ | POST | Create new product | +| /products/ | DELETE | Delete product by id | +| /products/ | PUT | Update product by id | +| /orders/ | GET | Get all orders | +| /orders/ | DELETE | Delete order by id | +| /orders/ | PUT | Update order by id | + +## Heroku deployment + +### Configs + +``` +heroku config:set FLASK_APP=flasky.py +heroku config:set FLASK_CONFIG=heroku +``` +The Heroku Postgres add on will set DATABASE_URL. Make sure to set a SECRET_KEY also. + +### Set up database + +This project uses SQLAlchemy to define the database model. The script `manage.py` uses Flask-Migrate and Flask-Script to allow setup, migration and deployment of the model without losing existing data. +``` +heroku run bash +python manage.py db init +python manage.py db migrate +python manage.py db upgrade \ No newline at end of file From 518c6eddb5913c370eb994f3570c48ab4fe85828 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 14:21:53 +0100 Subject: [PATCH 11/63] used Flask-CORS --- app/__init__.py | 4 ++++ requirements.txt | 1 + 2 files changed, 5 insertions(+) diff --git a/app/__init__.py b/app/__init__.py index 9fb3a77..2c2ad08 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,9 @@ from flask import Flask from config import config from flask_sqlalchemy import SQLAlchemy +from flask_cors import CORS + + db = SQLAlchemy() @@ -11,6 +14,7 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) + CORS(app) from .main import main as main_blueprint app.register_blueprint(main_blueprint) diff --git a/requirements.txt b/requirements.txt index 241343b..34c83eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ alembic==1.0.8 Click==7.0 Flask==1.0.2 +Flask-Cors==3.0.7 Flask-Migrate==2.4.0 Flask-Script==2.0.6 Flask-SQLAlchemy==2.3.2 From 26099919282c8df7f17ef07fbfa63e8306932b9f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 15:00:52 +0100 Subject: [PATCH 12/63] used Flask-CORS --- app/__init__.py | 1 + app/api/products.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 2c2ad08..a373c10 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,6 +15,7 @@ def create_app(config_name): db.init_app(app) CORS(app) + app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint app.register_blueprint(main_blueprint) diff --git a/app/api/products.py b/app/api/products.py index 3fdbe5c..f8a3a85 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -7,7 +7,7 @@ def get_products(): products = Product.query.all() return jsonify({ - 'products': [p.to_json() for p in products] + 'products': {p.to_json() for p in products} }), 200 @api.route('/products/', methods=['POST']) From 889e0849fef8387fddf5cbed22f13b74f8e7fae0 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Apr 2019 15:02:17 +0100 Subject: [PATCH 13/63] used Flask-CORS --- app/api/products.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/products.py b/app/api/products.py index f8a3a85..3fdbe5c 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -7,7 +7,7 @@ def get_products(): products = Product.query.all() return jsonify({ - 'products': {p.to_json() for p in products} + 'products': [p.to_json() for p in products] }), 200 @api.route('/products/', methods=['POST']) From e20a328963b417c95d9ba54a886e05a2059a5e70 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 00:00:02 +0100 Subject: [PATCH 14/63] slight change to products api --- app/api/products.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/api/products.py b/app/api/products.py index 3fdbe5c..4022257 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -6,9 +6,7 @@ @api.route('/products/', methods=['GET']) def get_products(): products = Product.query.all() - return jsonify({ - 'products': [p.to_json() for p in products] - }), 200 + return jsonify([p.to_json() for p in products]), 200 @api.route('/products/', methods=['POST']) def new_product(): From bb69b8470e9ca17bc662c98f212906183d41d0dd Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 11:36:22 +0100 Subject: [PATCH 15/63] authorisation added using JWS --- app/api/authentication.py | 47 +++++++++++++++++++++++++++++++++++++++ app/api/decorators.py | 18 +++++++-------- app/api/products.py | 1 + app/models.py | 43 +++++++++++++++++++++++++++++++++++ requirements.txt | 2 ++ tests/test_api.py | 1 - tests/test_user_model.py | 32 ++++++++++++++++++++++++++ 7 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 tests/test_user_model.py diff --git a/app/api/authentication.py b/app/api/authentication.py index e69de29..74120b8 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -0,0 +1,47 @@ +from flask import g, jsonify +from flask_httpauth import HTTPBasicAuth +from flask_login import current_user +from .errors import unauthorized, forbidden +from . import api +from ..models import User + +auth = HTTPBasicAuth() + +@auth.verify_password +def verify_password(name_or_token, password): + """Return True if login valid; Uses the User method verify_password; + If password is blank, token is assumed""" + if name_or_token == '': + return False + if password == '': + g.current_user = User.verify_auth_token(name_or_token) + g.token_used = True + return g.current_user is not None + user = User.query.filter_by(username=name_or_token).first() + if not user: + return False + g.current_user = user + g.token_used = False + return user.verify_password(password) + +# @auth.error_handler +# def auth_error(): +# return unauthorized('Invalid credentials') + +@auth.error_handler +def auth_error(): + return jsonify({'success': False}) + +@api.before_request +@auth.login_required +def before_request(): + if not g.current_user.is_anonymous and \ + not g.current_user.confirmed: + return forbidden('Unconfirmed account') + +@api.route('/login', methods=['POST']) +def get_token(): + if g.current_user.is_anonymous or g.token_used: + return jsonify({'success': False}) + return jsonify({'token': g.current_user.generate_auth_token( + expiration=3600), 'expiration': 3600, 'success': True}) diff --git a/app/api/decorators.py b/app/api/decorators.py index a511d59..d56462d 100644 --- a/app/api/decorators.py +++ b/app/api/decorators.py @@ -3,12 +3,12 @@ from .errors import forbidden -def permission_required(permission): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if not g.current_user.can(permission): - return forbidden('Insufficient permissions') - return f(*args, **kwargs) - return decorated_function - return decorator \ No newline at end of file +# def permission_required(permission): +# def decorator(f): +# @wraps(f) +# def decorated_function(*args, **kwargs): +# if not g.current_user.can(permission): +# return forbidden('Insufficient permissions') +# return f(*args, **kwargs) +# return decorated_function +# return decorator diff --git a/app/api/products.py b/app/api/products.py index 4022257..1205bac 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -5,6 +5,7 @@ @api.route('/products/', methods=['GET']) def get_products(): + print('get_products') products = Product.query.all() return jsonify([p.to_json() for p in products]), 200 diff --git a/app/models.py b/app/models.py index dbc812b..1700797 100644 --- a/app/models.py +++ b/app/models.py @@ -59,3 +59,46 @@ class OrderLine(db.Model): order_id = Column(db.Integer()) product_id = Column(db.Integer()) quantity = Column(db.Integer()) + +# User model +# + +from werkzeug.security import generate_password_hash, check_password_hash +from itsdangerous import TimedJSONWebSignatureSerializer as Serializer +from flask import current_app +from flask_login import UserMixin + +class User(UserMixin, db.Model): + __tablename__ = 'users' + user_id = Column(db.Integer(), primary_key = True) + username = Column(db.String(50)) + email = Column(db.String(50)) + password_hash = Column(db.String(128)) + confirmed = Column(db.Boolean, default=False) + + @property + def password(self): + """Prevent reading of password setter""" + raise AttributeError('password is not a readable attribute') + + @password.setter + def password(self, password): + self.password_hash = generate_password_hash(password) + + def verify_password(self, password): + return check_password_hash(self.password_hash, password) + + def generate_auth_token(self, expiration): + """Generate signed token that encodes user_id""" + s = Serializer(current_app.config['SECRET_KEY'], + expires_in=expiration) + return s.dumps({'id': self.user_id}).decode('utf-8') + + @staticmethod + def verify_auth_token(token): + s = Serializer(current_app.config['SECRET_KEY']) + try: + data = s.loads(token) + except: + return None + return User.query.get(data['user_id']) diff --git a/requirements.txt b/requirements.txt index 34c83eb..5ee9318 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ alembic==1.0.8 Click==7.0 Flask==1.0.2 Flask-Cors==3.0.7 +Flask-HTTPAuth==3.2.4 +Flask-Login==0.4.1 Flask-Migrate==2.4.0 Flask-Script==2.0.6 Flask-SQLAlchemy==2.3.2 diff --git a/tests/test_api.py b/tests/test_api.py index 3116f2c..5160b3c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,4 +1,3 @@ - import unittest import os import json diff --git a/tests/test_user_model.py b/tests/test_user_model.py new file mode 100644 index 0000000..dda0bf5 --- /dev/null +++ b/tests/test_user_model.py @@ -0,0 +1,32 @@ +import unittest +import os + +import sys +myPath = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, myPath + '/../') + +from app.models import User + +class UserModelTestCase(unittest.TestCase): + def test_password_setter(self): + u = User(password='cat') + self.assertTrue(u.password_hash is not None) + + def test_no_password_getter(self): + u = User(password='cat') + with self.assertRaises(AttributeError): + u.password + + def test_password_verification(self): + u = User(password='cat') + self.assertTrue(u.verify_password('cat')) + self.assertFalse(u.verify_password('dog')) + + def test_password_salts_are_random(self): + u1 = User(password='cat') + u2 = User(password='cat') + self.assertFalse(u1.password_hash == u2.password_hash) + +# Make the tests conveniently executable +if __name__ == "__main__": + unittest.main() From ecf1e1546ce95617a901bfae94cc950023116f32 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 12:02:10 +0100 Subject: [PATCH 16/63] allow access to allow get all products --- app/api/authentication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/api/authentication.py b/app/api/authentication.py index 74120b8..f10b3c8 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -19,7 +19,8 @@ def verify_password(name_or_token, password): return g.current_user is not None user = User.query.filter_by(username=name_or_token).first() if not user: - return False + # allow non user access e.g. get all products + return True g.current_user = user g.token_used = False return user.verify_password(password) From e1f22a73355f1ae9c8622e6ee248fcd2021618f3 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 13:02:16 +0100 Subject: [PATCH 17/63] removed before_request, individual product routes protected --- app/api/authentication.py | 14 +------------- app/api/products.py | 4 ++++ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/api/authentication.py b/app/api/authentication.py index f10b3c8..95f25cb 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -19,27 +19,15 @@ def verify_password(name_or_token, password): return g.current_user is not None user = User.query.filter_by(username=name_or_token).first() if not user: - # allow non user access e.g. get all products - return True + return False g.current_user = user g.token_used = False return user.verify_password(password) -# @auth.error_handler -# def auth_error(): -# return unauthorized('Invalid credentials') - @auth.error_handler def auth_error(): return jsonify({'success': False}) -@api.before_request -@auth.login_required -def before_request(): - if not g.current_user.is_anonymous and \ - not g.current_user.confirmed: - return forbidden('Unconfirmed account') - @api.route('/login', methods=['POST']) def get_token(): if g.current_user.is_anonymous or g.token_used: diff --git a/app/api/products.py b/app/api/products.py index 1205bac..066357a 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -2,6 +2,7 @@ from ..models import Product from .. import db from flask import jsonify, request +from .authentication import auth @api.route('/products/', methods=['GET']) def get_products(): @@ -10,6 +11,7 @@ def get_products(): return jsonify([p.to_json() for p in products]), 200 @api.route('/products/', methods=['POST']) +@auth.login_required def new_product(): product = Product.from_json(request.json) db.session.add(product) @@ -17,6 +19,7 @@ def new_product(): return jsonify(product.to_json()), 201 @api.route('/products/', methods=['DELETE']) +@auth.login_required def delete_product(id): product = Product.query.get_or_404(id) db.session.delete(product) @@ -24,6 +27,7 @@ def delete_product(id): return jsonify({"success": True}), 200 @api.route('/products/', methods=['PUT']) +@auth.login_required def edit_product(id): product = Product.query.get_or_404(id) From 16af2f75e5e957f3e22c540966a2cbdb50d7ca9f Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 13:14:39 +0100 Subject: [PATCH 18/63] corrected login route --- app/api/authentication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/authentication.py b/app/api/authentication.py index 95f25cb..5792497 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -29,6 +29,7 @@ def auth_error(): return jsonify({'success': False}) @api.route('/login', methods=['POST']) +@auth.login_required def get_token(): if g.current_user.is_anonymous or g.token_used: return jsonify({'success': False}) From bebfb14739afa792e99f7a4015b2e51b98b64bbe Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 14:37:49 +0100 Subject: [PATCH 19/63] print to find error --- app/api/authentication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/authentication.py b/app/api/authentication.py index 5792497..881b1eb 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -11,6 +11,7 @@ def verify_password(name_or_token, password): """Return True if login valid; Uses the User method verify_password; If password is blank, token is assumed""" + print(name_or_token, password) if name_or_token == '': return False if password == '': From df08fd9fe22ab5f1cd308d2ab65d02bc1a6df813 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 14:42:20 +0100 Subject: [PATCH 20/63] print to find error --- app/api/authentication.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/api/authentication.py b/app/api/authentication.py index 881b1eb..6ce1da1 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -32,7 +32,14 @@ def auth_error(): @api.route('/login', methods=['POST']) @auth.login_required def get_token(): + print('login route') if g.current_user.is_anonymous or g.token_used: + print('auth failed') + print(g.current_user.is_anonymous) + print(g.token_used) return jsonify({'success': False}) + print('auth succeed') + print(jsonify({'token': g.current_user.generate_auth_token( + expiration=3600), 'expiration': 3600, 'success': True})) return jsonify({'token': g.current_user.generate_auth_token( expiration=3600), 'expiration': 3600, 'success': True}) From 0100f63b210ee2668406ad4225d52311d4dc99d5 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 14:46:29 +0100 Subject: [PATCH 21/63] print to find error --- app/api/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/authentication.py b/app/api/authentication.py index 6ce1da1..981846b 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -11,7 +11,7 @@ def verify_password(name_or_token, password): """Return True if login valid; Uses the User method verify_password; If password is blank, token is assumed""" - print(name_or_token, password) + print('login:', name_or_token, password) if name_or_token == '': return False if password == '': From a56c9597e6e48ff30fd9b73fd8aa009ea271d83f Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 15:21:42 +0100 Subject: [PATCH 22/63] try get instead for login, empty post issues --- app/api/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/authentication.py b/app/api/authentication.py index 981846b..571a163 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -29,7 +29,7 @@ def verify_password(name_or_token, password): def auth_error(): return jsonify({'success': False}) -@api.route('/login', methods=['POST']) +@api.route('/login', methods=['GET']) @auth.login_required def get_token(): print('login route') From 27ec09c35bc190389e2a5424ad2685e53626f037 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 15:33:20 +0100 Subject: [PATCH 23/63] login back to post --- app/api/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/authentication.py b/app/api/authentication.py index 571a163..981846b 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -29,7 +29,7 @@ def verify_password(name_or_token, password): def auth_error(): return jsonify({'success': False}) -@api.route('/login', methods=['GET']) +@api.route('/login', methods=['POST']) @auth.login_required def get_token(): print('login route') From 4a31f4f41fd4a760a5acd304cde76d1cee2e0fca Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 15:54:40 +0100 Subject: [PATCH 24/63] request.data printed --- app/api/authentication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/authentication.py b/app/api/authentication.py index 981846b..049107e 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -12,6 +12,7 @@ def verify_password(name_or_token, password): """Return True if login valid; Uses the User method verify_password; If password is blank, token is assumed""" print('login:', name_or_token, password) + print(request.data) if name_or_token == '': return False if password == '': From 9dc193ad477f96cef5ae4cfa39d6576577c1a989 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Apr 2019 15:57:49 +0100 Subject: [PATCH 25/63] request print removed --- app/api/authentication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/authentication.py b/app/api/authentication.py index 049107e..981846b 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -12,7 +12,6 @@ def verify_password(name_or_token, password): """Return True if login valid; Uses the User method verify_password; If password is blank, token is assumed""" print('login:', name_or_token, password) - print(request.data) if name_or_token == '': return False if password == '': From b389b8d635f35bf270cc6562db12f7c9f4db9164 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 3 Apr 2019 09:52:54 +0100 Subject: [PATCH 26/63] print FLASK_CONFIG --- flasky.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flasky.py b/flasky.py index 4fe0027..f1b7680 100644 --- a/flasky.py +++ b/flasky.py @@ -1,5 +1,5 @@ import os from app import create_app - -app = create_app(os.getenv('FLASK_CONFIG') or 'default') \ No newline at end of file +print('flasky.py', os.getenv('FLASK_CONFIG') or 'default') +app = create_app(os.getenv('FLASK_CONFIG') or 'default') From 5483dccaa994d2e886aa7ad6f150b94323c4fc2f Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 3 Apr 2019 14:10:33 +0100 Subject: [PATCH 27/63] Using Basic Auth instead --- app/api/authentication.py | 2 ++ app/models.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/api/authentication.py b/app/api/authentication.py index 981846b..1f4ef98 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -6,6 +6,8 @@ from ..models import User auth = HTTPBasicAuth() +token_auth = HTTPTokenAuth(scheme='Token') + @auth.verify_password def verify_password(name_or_token, password): diff --git a/app/models.py b/app/models.py index 1700797..306db26 100644 --- a/app/models.py +++ b/app/models.py @@ -101,4 +101,5 @@ def verify_auth_token(token): data = s.loads(token) except: return None - return User.query.get(data['user_id']) + print(data) + return User.query.get(data['id']) From c93b8c73536620f5145f1707a90a089ed41d2f0e Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 3 Apr 2019 14:13:30 +0100 Subject: [PATCH 28/63] Using Basic Auth instead --- app/api/authentication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/authentication.py b/app/api/authentication.py index 1f4ef98..7092c48 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -6,7 +6,6 @@ from ..models import User auth = HTTPBasicAuth() -token_auth = HTTPTokenAuth(scheme='Token') @auth.verify_password From 7aaa5c66850f2b069a472631a359c76294d7100c Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 08:07:20 +0100 Subject: [PATCH 29/63] corrected CORS instantiation --- app/__init__.py | 2 +- app/api/authentication.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index a373c10..742bf14 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,7 +14,7 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app) + CORS(app, resources={r"/*": {"origins": "*"}}) app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint diff --git a/app/api/authentication.py b/app/api/authentication.py index 7092c48..2ad48e1 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -13,6 +13,7 @@ def verify_password(name_or_token, password): """Return True if login valid; Uses the User method verify_password; If password is blank, token is assumed""" print('login:', name_or_token, password) + print('user:', User.verify_auth_token(name_or_token)) if name_or_token == '': return False if password == '': From c7f2f7c1e7f2543b8749024e314dd3085ec90d6e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 08:41:25 +0100 Subject: [PATCH 30/63] corrected CORS instantiation --- app/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 742bf14..44f06ca 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,8 +14,8 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app, resources={r"/*": {"origins": "*"}}) - app.config['CORS_HEADERS'] = 'Content-Type' + CORS(app, resources={r"/api/v1/*": {"origins": "*"}}) + #app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint app.register_blueprint(main_blueprint) From 368a21a5354b1793ad928ac3780ab6369b275da7 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 08:51:16 +0100 Subject: [PATCH 31/63] corrected CORS instantiation --- app/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 44f06ca..09cf6ba 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,8 +14,8 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app, resources={r"/api/v1/*": {"origins": "*"}}) - #app.config['CORS_HEADERS'] = 'Content-Type' + CORS(app, supports_credentials=True) + app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint app.register_blueprint(main_blueprint) From 4f74791d30e71b8e6db0ae9c803c4ae31466d9b4 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:01:43 +0100 Subject: [PATCH 32/63] corrected CORS instantiation --- app/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 09cf6ba..014cd16 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,8 +14,8 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app, supports_credentials=True) - app.config['CORS_HEADERS'] = 'Content-Type' + CORS(app) + # app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint app.register_blueprint(main_blueprint) From ee4f4ee6399a7834eb512717ce515ca3f90932cf Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:31:42 +0100 Subject: [PATCH 33/63] corrected CORS instantiation --- app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 014cd16..0de306d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,7 +14,7 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app) + CORS(app, origins=['http://localhost/', 'https://sportsstoreapi.herokuapp.com/']) # app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint From 28107d3c74c11243fa5d1c4e2ccbbcb72183f4d6 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:34:37 +0100 Subject: [PATCH 34/63] corrected CORS instantiation --- app/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 0de306d..8b19476 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,7 +14,9 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app, origins=['http://localhost/', 'https://sportsstoreapi.herokuapp.com/']) + CORS(app, + origins=['http://localhost/', 'https://sportsstoreapi.herokuapp.com/']. + supports_credentials=True) # app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint From 58d92f81a44d0927739ee6b26bb4258ee85819ef Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:37:10 +0100 Subject: [PATCH 35/63] corrected CORS instantiation --- app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 8b19476..32c817f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,7 +15,7 @@ def create_app(config_name): db.init_app(app) CORS(app, - origins=['http://localhost/', 'https://sportsstoreapi.herokuapp.com/']. + origins=['http://localhost/', 'https://sportsstoreapi.herokuapp.com/'], supports_credentials=True) # app.config['CORS_HEADERS'] = 'Content-Type' From 8a848582ceda89bf7c7a00e51bacec842960e1f1 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:43:21 +0100 Subject: [PATCH 36/63] corrected CORS instantiation --- app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 32c817f..529774c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,7 +15,7 @@ def create_app(config_name): db.init_app(app) CORS(app, - origins=['http://localhost/', 'https://sportsstoreapi.herokuapp.com/'], + origins=['http://localhost/', 'https://sportsstoreapi.herokuapp.com/'], supports_credentials=True) # app.config['CORS_HEADERS'] = 'Content-Type' From 58a002e14e8617130a2a69586217aa5744db4e7f Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:47:25 +0100 Subject: [PATCH 37/63] corrected CORS instantiation --- app/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 529774c..1b7f24c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,7 +15,10 @@ def create_app(config_name): db.init_app(app) CORS(app, - origins=['http://localhost/', 'https://sportsstoreapi.herokuapp.com/'], + origins=[ + 'http://localhost/', + 'http://localhost/admin/auth', + 'https://sportsstoreapi.herokuapp.com/'], supports_credentials=True) # app.config['CORS_HEADERS'] = 'Content-Type' From adb53d293f374229a78b8fef413692477e88f296 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:51:30 +0100 Subject: [PATCH 38/63] corrected CORS instantiation --- app/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/__init__.py b/app/__init__.py index 1b7f24c..dcde105 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -18,6 +18,7 @@ def create_app(config_name): origins=[ 'http://localhost/', 'http://localhost/admin/auth', + 'http://localhost/store', 'https://sportsstoreapi.herokuapp.com/'], supports_credentials=True) # app.config['CORS_HEADERS'] = 'Content-Type' From b9090db8262ff2a62b4dc49f333c0e0088013c40 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:53:21 +0100 Subject: [PATCH 39/63] corrected CORS instantiation --- app/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index dcde105..2e5b0e6 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,13 +14,7 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app, - origins=[ - 'http://localhost/', - 'http://localhost/admin/auth', - 'http://localhost/store', - 'https://sportsstoreapi.herokuapp.com/'], - supports_credentials=True) + CORS(app, supports_credentials=True) # app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint From 4bc68b9c229c09ffdb198458a7560b6744e99439 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 09:59:51 +0100 Subject: [PATCH 40/63] corrected CORS instantiation --- app/__init__.py | 2 +- app/api/products.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 2e5b0e6..014cd16 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,7 +14,7 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app, supports_credentials=True) + CORS(app) # app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint diff --git a/app/api/products.py b/app/api/products.py index 066357a..36f236a 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -3,6 +3,8 @@ from .. import db from flask import jsonify, request from .authentication import auth +from flask_cors import cross_origin + @api.route('/products/', methods=['GET']) def get_products(): @@ -12,6 +14,7 @@ def get_products(): @api.route('/products/', methods=['POST']) @auth.login_required +@cross_origin(allow_headers=['Content-Type'], supports_credentials=True) def new_product(): product = Product.from_json(request.json) db.session.add(product) From 2dcda2380af1f99388c68665b9feb3124338d422 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:04:50 +0100 Subject: [PATCH 41/63] corrected CORS instantiation --- app/__init__.py | 4 ++-- app/api/products.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 014cd16..e914eef 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,7 +1,7 @@ from flask import Flask from config import config from flask_sqlalchemy import SQLAlchemy -from flask_cors import CORS +#from flask_cors import CORS @@ -14,7 +14,7 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app) + #CORS(app) # app.config['CORS_HEADERS'] = 'Content-Type' from .main import main as main_blueprint diff --git a/app/api/products.py b/app/api/products.py index 36f236a..856973f 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -7,6 +7,7 @@ @api.route('/products/', methods=['GET']) +@cross_origin() def get_products(): print('get_products') products = Product.query.all() From 29a666ee604d438fd802880e38fc8c8b8a1a7748 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:07:27 +0100 Subject: [PATCH 42/63] corrected CORS instantiation --- app/api/authentication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/authentication.py b/app/api/authentication.py index 2ad48e1..6079ebb 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -33,6 +33,7 @@ def auth_error(): @api.route('/login', methods=['POST']) @auth.login_required +@cross_origin(supports_credentials=True) def get_token(): print('login route') if g.current_user.is_anonymous or g.token_used: From 57e890649ac2b7ba1a0fb44daea825b04614d0ef Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:09:22 +0100 Subject: [PATCH 43/63] corrected CORS instantiation --- app/api/authentication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/authentication.py b/app/api/authentication.py index 6079ebb..a7f198f 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -1,5 +1,6 @@ from flask import g, jsonify from flask_httpauth import HTTPBasicAuth +from flask_cors import cross_origin from flask_login import current_user from .errors import unauthorized, forbidden from . import api From aa5f1a684e22b930aa8ad04d379b0fc410abe84a Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:13:04 +0100 Subject: [PATCH 44/63] corrected CORS instantiation --- app/api/products.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/products.py b/app/api/products.py index 856973f..ad19d49 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -14,8 +14,8 @@ def get_products(): return jsonify([p.to_json() for p in products]), 200 @api.route('/products/', methods=['POST']) +@cross_origin(allow_headers=['Content-Type', 'Authorization'], supports_credentials=True) @auth.login_required -@cross_origin(allow_headers=['Content-Type'], supports_credentials=True) def new_product(): product = Product.from_json(request.json) db.session.add(product) From 7c8347addbf0a9207fd560ae4232dee86c0e1acf Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:21:04 +0100 Subject: [PATCH 45/63] corrected CORS instantiation --- app/api/products.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/products.py b/app/api/products.py index ad19d49..1ada890 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -31,6 +31,7 @@ def delete_product(id): return jsonify({"success": True}), 200 @api.route('/products/', methods=['PUT']) +@cross_origin(allow_headers=['Content-Type', 'Authorization']) @auth.login_required def edit_product(id): product = Product.query.get_or_404(id) From 45e48320f6586246188793b164910a5467b99505 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:23:58 +0100 Subject: [PATCH 46/63] corrected CORS instantiation --- app/api/products.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/products.py b/app/api/products.py index 1ada890..d4cbc38 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -31,7 +31,7 @@ def delete_product(id): return jsonify({"success": True}), 200 @api.route('/products/', methods=['PUT']) -@cross_origin(allow_headers=['Content-Type', 'Authorization']) +@cross_origin() @auth.login_required def edit_product(id): product = Product.query.get_or_404(id) From ad1f1c84426ea445c7ee7b8d33213be1e50a658b Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:34:45 +0100 Subject: [PATCH 47/63] corrected CORS instantiation --- app/api/products.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/products.py b/app/api/products.py index d4cbc38..16cc9ab 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -14,7 +14,7 @@ def get_products(): return jsonify([p.to_json() for p in products]), 200 @api.route('/products/', methods=['POST']) -@cross_origin(allow_headers=['Content-Type', 'Authorization'], supports_credentials=True) +@cross_origin(headers=['Content-Type', 'Authorization'], supports_credentials=True) @auth.login_required def new_product(): product = Product.from_json(request.json) From 882590eb120b74803cc895f06521cd6e3609c6f7 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:39:54 +0100 Subject: [PATCH 48/63] corrected CORS instantiation --- config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.py b/config.py index 7d1dc92..0b5e890 100644 --- a/config.py +++ b/config.py @@ -33,6 +33,8 @@ def init_app(cls, app): file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) + logging.getLogger('flask_cors').level = logging.DEBUG + SSL_REDIRECT = True if os.environ.get('DYNO') else False config = { From 9a9d60b5b6eddb7a3a01ae077f8fb3fd14b57974 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:46:58 +0100 Subject: [PATCH 49/63] corrected CORS instantiation --- app/__init__.py | 8 +++++--- app/api/authentication.py | 2 -- app/api/products.py | 4 ---- config.py | 2 -- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index e914eef..e3d1909 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,7 +1,7 @@ from flask import Flask from config import config from flask_sqlalchemy import SQLAlchemy -#from flask_cors import CORS +from flask_cors import CORS @@ -14,8 +14,10 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - #CORS(app) - # app.config['CORS_HEADERS'] = 'Content-Type' + CORS(app, CORS(app, + origins="http://localhost/", + allow_headers=["Content-Type", "Authorization", "Access-Control-Allow-Credentials"], + supports_credentials=True) from .main import main as main_blueprint app.register_blueprint(main_blueprint) diff --git a/app/api/authentication.py b/app/api/authentication.py index a7f198f..2ad48e1 100644 --- a/app/api/authentication.py +++ b/app/api/authentication.py @@ -1,6 +1,5 @@ from flask import g, jsonify from flask_httpauth import HTTPBasicAuth -from flask_cors import cross_origin from flask_login import current_user from .errors import unauthorized, forbidden from . import api @@ -34,7 +33,6 @@ def auth_error(): @api.route('/login', methods=['POST']) @auth.login_required -@cross_origin(supports_credentials=True) def get_token(): print('login route') if g.current_user.is_anonymous or g.token_used: diff --git a/app/api/products.py b/app/api/products.py index 16cc9ab..0891e9a 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -3,18 +3,15 @@ from .. import db from flask import jsonify, request from .authentication import auth -from flask_cors import cross_origin @api.route('/products/', methods=['GET']) -@cross_origin() def get_products(): print('get_products') products = Product.query.all() return jsonify([p.to_json() for p in products]), 200 @api.route('/products/', methods=['POST']) -@cross_origin(headers=['Content-Type', 'Authorization'], supports_credentials=True) @auth.login_required def new_product(): product = Product.from_json(request.json) @@ -31,7 +28,6 @@ def delete_product(id): return jsonify({"success": True}), 200 @api.route('/products/', methods=['PUT']) -@cross_origin() @auth.login_required def edit_product(id): product = Product.query.get_or_404(id) diff --git a/config.py b/config.py index 0b5e890..7d1dc92 100644 --- a/config.py +++ b/config.py @@ -33,8 +33,6 @@ def init_app(cls, app): file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) - logging.getLogger('flask_cors').level = logging.DEBUG - SSL_REDIRECT = True if os.environ.get('DYNO') else False config = { From 909b172331676808b960effcdfe92c02a174bd43 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 10:52:50 +0100 Subject: [PATCH 50/63] corrected CORS instantiation --- app/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index e3d1909..dccb364 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,10 +14,9 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app, CORS(app, - origins="http://localhost/", - allow_headers=["Content-Type", "Authorization", "Access-Control-Allow-Credentials"], - supports_credentials=True) + CORS(app, origins="http://localhost/", + allow_headers=["Content-Type", "Authorization", "Access-Control-Allow-Credentials"], + supports_credentials=True) from .main import main as main_blueprint app.register_blueprint(main_blueprint) From e580c9b146a3fab75f16d2a804193645f4c3438e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 11:49:40 +0100 Subject: [PATCH 51/63] corrected CORS instantiation --- app/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index dccb364..fae4e97 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,7 +15,6 @@ def create_app(config_name): db.init_app(app) CORS(app, origins="http://localhost/", - allow_headers=["Content-Type", "Authorization", "Access-Control-Allow-Credentials"], supports_credentials=True) from .main import main as main_blueprint From b12ca965439ab79f0ad6905bd2827da5f50fd53a Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 12:38:40 +0100 Subject: [PATCH 52/63] corrected CORS instantiation --- app/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index fae4e97..2c2ad08 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,8 +14,7 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app, origins="http://localhost/", - supports_credentials=True) + CORS(app) from .main import main as main_blueprint app.register_blueprint(main_blueprint) From 145362cf2d9ab3721990e0d1e494a283387f6034 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 12:52:30 +0100 Subject: [PATCH 53/63] corrected CORS instantiation --- app/api/products.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/products.py b/app/api/products.py index 0891e9a..bfb0708 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -12,7 +12,7 @@ def get_products(): return jsonify([p.to_json() for p in products]), 200 @api.route('/products/', methods=['POST']) -@auth.login_required +# @auth.login_required def new_product(): product = Product.from_json(request.json) db.session.add(product) From d996d2555aeff4844c88714105c5996a6b877b6e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 13:25:22 +0100 Subject: [PATCH 54/63] added CORS logger --- config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.py b/config.py index 7d1dc92..3b80790 100644 --- a/config.py +++ b/config.py @@ -33,6 +33,8 @@ def init_app(cls, app): file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) + logging.getLogger('flask_cors').level = logging.DEBUG + SSL_REDIRECT = True if os.environ.get('DYNO') else False config = { From 889cd48a2121eadfeb4a7c83b53e497fe076a337 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 13:33:45 +0100 Subject: [PATCH 55/63] added CORS logger --- app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 2c2ad08..0aefef3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -14,7 +14,7 @@ def create_app(config_name): config[config_name].init_app(app) db.init_app(app) - CORS(app) + CORS(app, supports_credentials=True) from .main import main as main_blueprint app.register_blueprint(main_blueprint) From a5929e320d4379458cc35b51e90df1b6d1789b58 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Apr 2019 14:22:27 +0100 Subject: [PATCH 56/63] float error correction --- app/api/products.py | 2 +- app/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/products.py b/app/api/products.py index bfb0708..0891e9a 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -12,7 +12,7 @@ def get_products(): return jsonify([p.to_json() for p in products]), 200 @api.route('/products/', methods=['POST']) -# @auth.login_required +@auth.login_required def new_product(): product = Product.from_json(request.json) db.session.add(product) diff --git a/app/models.py b/app/models.py index 306db26..aff37e9 100644 --- a/app/models.py +++ b/app/models.py @@ -16,7 +16,7 @@ def to_json(self): 'name': self.name, 'category': self.category, 'description': self.description, - 'price': float(self.price) # Decimal to float + 'price': float(self.price or 0) # Decimal to float } return json_product From de4919648f4e613a711fd4941bc86e868a80e91a Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 5 Apr 2019 09:14:39 +0100 Subject: [PATCH 57/63] added POST to order api, added from_json to order model --- app/api/orders.py | 9 +++++++++ app/models.py | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/app/api/orders.py b/app/api/orders.py index 07d6502..dec1ea2 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -2,6 +2,7 @@ from ..models import Order from .. import db from flask import jsonify +from .authentication import auth @api.route('/orders/', methods=['GET']) def get_orders(): @@ -10,6 +11,14 @@ def get_orders(): 'orders': [o.to_json() for o in orders] }) +@api.route('/orders/', methods=['POST']) +@auth.login_required +def new_order(): + order = Order.from_json(request.json) + db.session.add(order) + db.session.commit() + return jsonify(order.to_json()), 201 + @api.route('/orders/', methods=['DELETE']) def delete_order(id): order = Order.query.get_or_404(id) diff --git a/app/models.py b/app/models.py index aff37e9..aaed7d2 100644 --- a/app/models.py +++ b/app/models.py @@ -52,6 +52,20 @@ def to_json(self): } return json_order + @staticmethod + def from_json(json_order): + + order_id = json_order.get('order_id') + name = json_order.get('name') + address = json_order.get('address') + city = json_order.get('city') + state = json_order.get('state') + zip = json_order.get('zip') + country = json_order.get('country') + + return Order(order_id=order_id, name=name, address=address, + city=city, state=state, zip=zip, country=country) + class OrderLine(db.Model): __tablename__ = 'order_lines' From 1058dcd5912b99a01c23d223881c045d2c0d8aac Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 5 Apr 2019 09:21:11 +0100 Subject: [PATCH 58/63] removed auth for saving order --- app/api/orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/orders.py b/app/api/orders.py index dec1ea2..80417d3 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -12,7 +12,6 @@ def get_orders(): }) @api.route('/orders/', methods=['POST']) -@auth.login_required def new_order(): order = Order.from_json(request.json) db.session.add(order) From 74201f4c632877ae698a7930c50bc32bece5ba24 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 5 Apr 2019 09:24:12 +0100 Subject: [PATCH 59/63] import request from flask --- app/api/orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/orders.py b/app/api/orders.py index 80417d3..18be672 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -1,7 +1,7 @@ from . import api from ..models import Order from .. import db -from flask import jsonify +from flask import jsonify, request from .authentication import auth @api.route('/orders/', methods=['GET']) From 609df470ea5b64348e925e1b54075dc0b219c39a Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 5 Apr 2019 09:30:51 +0100 Subject: [PATCH 60/63] amend order post api --- app/api/orders.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/api/orders.py b/app/api/orders.py index 18be672..1164b10 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -7,9 +7,7 @@ @api.route('/orders/', methods=['GET']) def get_orders(): orders = Order.query.all() - return jsonify({ - 'orders': [o.to_json() for o in orders] - }) + return jsonify([o.to_json() for o in orders]) @api.route('/orders/', methods=['POST']) def new_order(): From 7432bf52669373c32f079f901e389dbdc57c34f9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 6 Apr 2019 17:58:15 +0100 Subject: [PATCH 61/63] commit before exporing marshmallow --- app/api/orders.py | 55 +++++++++++++++++++++++++++++++++++++++++++++-- app/models.py | 21 ++++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/app/api/orders.py b/app/api/orders.py index 1164b10..b915b25 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -1,18 +1,69 @@ from . import api -from ..models import Order +from ..models import Order, OrderLine, Product from .. import db from flask import jsonify, request from .authentication import auth +import sqlalchemy +from sqlalchemy.sql import func, literal_column, select + +# Helper function: +# Pulls Postgres SQL function json_agg +# See - https://trvrm.github.io/using-sqlalchemy-and-postgres-functions-to-produce-json-tree-structures-from-sql-joins.html +def json_agg(table): + return func.json_agg(literal_column('"'+table.name+'"')) + +def order_details(db): + + OrderProducts = ( + db.session.query( + func.json_agg(func.json_build_object( + 'name', Product.name, + 'category', Product.category, + 'quantity', OrderLine.quantity + ).label('products')), + Order.order_id) + .group_by(Order.order_id) + ).cte('order_products') + + query = ( + db.session.query( + func.json_build_object( + 'order_id', Order.order_id, + 'name', Order.name, + 'address', Order.address, + 'city', Order.city, + 'state', Order.state, + 'zip', Order.zip, + 'country', Order.country, + 'quantity', OrderLine.quantity), + OrderProducts) + .join(OrderLine, OrderLine.order_id == Order.order_id) + .join(OrderProducts) + ) + # Common Table Expressions (CTEs) + results = query.all() + return results + @api.route('/orders/', methods=['GET']) def get_orders(): orders = Order.query.all() - return jsonify([o.to_json() for o in orders]) + # TODO: add cart lines + import pprint + pprint.pprint(order_details(db)) + return jsonify([ + o.to_json() + for o in orders]) @api.route('/orders/', methods=['POST']) def new_order(): order = Order.from_json(request.json) db.session.add(order) + db.session.flush() + order_id = order.order_id + for line in request.json.get('lines'): + order_line = OrderLine.add_line(order_id, line) + db.session.add(order_line) db.session.commit() return jsonify(order.to_json()), 201 diff --git a/app/models.py b/app/models.py index aaed7d2..7ac57cf 100644 --- a/app/models.py +++ b/app/models.py @@ -70,10 +70,27 @@ class OrderLine(db.Model): __tablename__ = 'order_lines' order_line_id = Column(db.Integer(), primary_key = True) - order_id = Column(db.Integer()) - product_id = Column(db.Integer()) + order_id = Column(db.Integer(), db.ForeignKey('orders.order_id')) + product_id = Column(db.Integer(), db.ForeignKey('products.product_id')) quantity = Column(db.Integer()) + @staticmethod + def add_line(order_id, line): + product_id = line.get('product_id') + quantity = line.get('quantity') + + return OrderLine(order_id=order_id, product_id=product_id, + quantity=quantity) + + def to_json(self): + json_order_line = { + 'order_line_id': self.order_line_id, + 'order_id': self.order_id, + 'product_id': self.product_id, + 'quantity': self.quantity + } + return json_order_line + # User model # From 728187d36fbae1129d2d12ab97737aff8e90df40 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 7 Apr 2019 20:58:36 +0100 Subject: [PATCH 62/63] marshmellow --- app/api/orders.py | 53 +++++------------------------------- app/models.py | 68 +++++++---------------------------------------- app/schema.py | 20 ++++++++++++++ app/services.py | 23 ++++++++++++++++ 4 files changed, 58 insertions(+), 106 deletions(-) create mode 100644 app/schema.py create mode 100644 app/services.py diff --git a/app/api/orders.py b/app/api/orders.py index b915b25..67b4bd9 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -3,57 +3,16 @@ from .. import db from flask import jsonify, request from .authentication import auth +from ..services import OrderListService -import sqlalchemy -from sqlalchemy.sql import func, literal_column, select - -# Helper function: -# Pulls Postgres SQL function json_agg -# See - https://trvrm.github.io/using-sqlalchemy-and-postgres-functions-to-produce-json-tree-structures-from-sql-joins.html -def json_agg(table): - return func.json_agg(literal_column('"'+table.name+'"')) - -def order_details(db): - - OrderProducts = ( - db.session.query( - func.json_agg(func.json_build_object( - 'name', Product.name, - 'category', Product.category, - 'quantity', OrderLine.quantity - ).label('products')), - Order.order_id) - .group_by(Order.order_id) - ).cte('order_products') - - query = ( - db.session.query( - func.json_build_object( - 'order_id', Order.order_id, - 'name', Order.name, - 'address', Order.address, - 'city', Order.city, - 'state', Order.state, - 'zip', Order.zip, - 'country', Order.country, - 'quantity', OrderLine.quantity), - OrderProducts) - .join(OrderLine, OrderLine.order_id == Order.order_id) - .join(OrderProducts) - ) - # Common Table Expressions (CTEs) - results = query.all() - return results +from ..schema import OrderSchema, OrderLineSchema @api.route('/orders/', methods=['GET']) def get_orders(): - orders = Order.query.all() - # TODO: add cart lines - import pprint - pprint.pprint(order_details(db)) - return jsonify([ - o.to_json() - for o in orders]) + service = OrderListService({}, db.session) + + print(OrderLineSchema._declared_fields) + return jsonify(service.get()) @api.route('/orders/', methods=['POST']) def new_order(): diff --git a/app/models.py b/app/models.py index 7ac57cf..bd649c6 100644 --- a/app/models.py +++ b/app/models.py @@ -1,5 +1,6 @@ from app import db from sqlalchemy import Column +from sqlalchemy.orm import relationship class Product(db.Model): __tablename__ = 'products' @@ -10,24 +11,8 @@ class Product(db.Model): description = Column(db.String(200)) price = Column(db.Numeric(12, 2)) - def to_json(self): - json_product = { - 'product_id': self.product_id, - 'name': self.name, - 'category': self.category, - 'description': self.description, - 'price': float(self.price or 0) # Decimal to float - } - return json_product - - @staticmethod - def from_json(json_product): - name = json_product.get('name') - category = json_product.get('category') - description = json_product.get('description') - price = json_product.get('price') - return Product(name=name, category=category, - description=description, price=price) + # relationships + products = relationship("OrderLine", back_populates="product") class Order(db.Model): __tablename__ = 'orders' @@ -40,31 +25,8 @@ class Order(db.Model): zip = Column(db.String(7)) country = Column(db.String(20)) - def to_json(self): - json_order = { - 'order_id': self.order_id, - 'name': self.name, - 'address': self.address, - 'city': self.city, - 'state': self.state, - 'zip': self.zip, - 'country': self.country - } - return json_order - - @staticmethod - def from_json(json_order): - - order_id = json_order.get('order_id') - name = json_order.get('name') - address = json_order.get('address') - city = json_order.get('city') - state = json_order.get('state') - zip = json_order.get('zip') - country = json_order.get('country') - - return Order(order_id=order_id, name=name, address=address, - city=city, state=state, zip=zip, country=country) + # relationships + products_sold = relationship("OrderLine", back_populates="order") class OrderLine(db.Model): __tablename__ = 'order_lines' @@ -74,22 +36,10 @@ class OrderLine(db.Model): product_id = Column(db.Integer(), db.ForeignKey('products.product_id')) quantity = Column(db.Integer()) - @staticmethod - def add_line(order_id, line): - product_id = line.get('product_id') - quantity = line.get('quantity') - - return OrderLine(order_id=order_id, product_id=product_id, - quantity=quantity) - - def to_json(self): - json_order_line = { - 'order_line_id': self.order_line_id, - 'order_id': self.order_id, - 'product_id': self.product_id, - 'quantity': self.quantity - } - return json_order_line + # relationships + product = relationship("Product", back_populates="products") + order = relationship("Order", back_populates="products_sold") + # User model # diff --git a/app/schema.py b/app/schema.py new file mode 100644 index 0000000..d55b8fa --- /dev/null +++ b/app/schema.py @@ -0,0 +1,20 @@ +from marshmallow import fields +from marshmallow_sqlalchemy import ModelSchema +from .models import Order, Product, OrderLine + +class OrderSchema(ModelSchema): + products_sold = fields.Nested('OrderLineSchema', many=True) + class Meta: + model = Order + +class ProductSchema(ModelSchema): + price = fields.Float(data_key='price') # Decimal to float + class Meta: + model = Product + + +class OrderLineSchema(ModelSchema): + product = fields.Nested('ProductSchema', exclude=('products',)) + class Meta: + model = OrderLine + fields = ('product', 'quantity') diff --git a/app/services.py b/app/services.py new file mode 100644 index 0000000..3d588c2 --- /dev/null +++ b/app/services.py @@ -0,0 +1,23 @@ +from sqlalchemy.orm import joinedload, selectinload + +from .models import Order, Product, OrderLine +from .schema import OrderSchema, ProductSchema, OrderLineSchema + +class OrderListService: + ''' + This service intended for use exclusively by /api/orders + ''' + def __init__(self, params, _session=None): + # your unit tests can pass in _session=MagicMock() + print('__init__') + self.session = _session or db.session + self.params = params + + def _parents(self): + return ( self.session.query(Order) + .options(selectinload(Order.products_sold)) + .all() ) + + def get(self): + schema = OrderSchema() + return schema.dump(self._parents(), many=True).data From d5e696cfc9ca30046548c4bb38314048a9e657de Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 8 Apr 2019 10:40:46 +0100 Subject: [PATCH 63/63] further updates / exploration --- app/__init__.py | 2 -- app/api/orders.py | 35 ++++++++++++++--------------------- app/models.py | 7 +++++++ app/schema.py | 32 +++++++++++++++++++++++++++----- app/services.py | 11 ++++++++--- 5 files changed, 56 insertions(+), 31 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 0aefef3..a29c5ec 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,10 +4,8 @@ from flask_cors import CORS - db = SQLAlchemy() - def create_app(config_name): app = Flask(__name__) app.config.from_object(config[config_name]) diff --git a/app/api/orders.py b/app/api/orders.py index 67b4bd9..39e5ebc 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -1,3 +1,4 @@ +from marshmallow import ValidationError from . import api from ..models import Order, OrderLine, Product from .. import db @@ -9,22 +10,19 @@ @api.route('/orders/', methods=['GET']) def get_orders(): - service = OrderListService({}, db.session) - - print(OrderLineSchema._declared_fields) + service = OrderListService() return jsonify(service.get()) @api.route('/orders/', methods=['POST']) def new_order(): - order = Order.from_json(request.json) - db.session.add(order) - db.session.flush() - order_id = order.order_id - for line in request.json.get('lines'): - order_line = OrderLine.add_line(order_id, line) - db.session.add(order_line) + schema = OrderSchema() + try: + order = schema.load(request.get_json()) + except ValidationError as err: + print('Validation Error: ', err.messages) + db.session.add(order.data) db.session.commit() - return jsonify(order.to_json()), 201 + return jsonify(schema.dump(order)), 201 @api.route('/orders/', methods=['DELETE']) def delete_order(id): @@ -35,15 +33,10 @@ def delete_order(id): @api.route('/orders/', methods=['PUT']) def edit_order(id): + # get order order = Order.query.get_or_404(id) - - order.order_id = request.json.get('order_id') - order.name = request.json.get('name') - order.address = request.json.get('address') - order.city = request.json.get('city') - order.state = request.json.get('state') - order.zip = request.json.get('zip') - order.country = request.json.get('country') - - db.session.add(order) + # update order and commit + schema = OrderSchema() + schema.load(request.get_json(), instance=order) db.session.commit() + return jsonify(schema.dump(order)) diff --git a/app/models.py b/app/models.py index bd649c6..fd7921d 100644 --- a/app/models.py +++ b/app/models.py @@ -17,6 +17,11 @@ class Product(db.Model): class Order(db.Model): __tablename__ = 'orders' + def __init__(self, products_sold=None, *args, **kwargs): + super(Order, self).__init__(*args, **kwargs) + products_sold = products_sold or [] + for prod in products_sold: + self.products_sold.append(prod) order_id = Column(db.Integer(), primary_key = True) name = Column(db.String(30)) address = Column(db.String(100)) @@ -28,6 +33,8 @@ class Order(db.Model): # relationships products_sold = relationship("OrderLine", back_populates="order") + + class OrderLine(db.Model): __tablename__ = 'order_lines' diff --git a/app/schema.py b/app/schema.py index d55b8fa..88b6ff6 100644 --- a/app/schema.py +++ b/app/schema.py @@ -1,20 +1,42 @@ -from marshmallow import fields +from marshmallow import fields, post_load from marshmallow_sqlalchemy import ModelSchema + +from app import db from .models import Order, Product, OrderLine class OrderSchema(ModelSchema): products_sold = fields.Nested('OrderLineSchema', many=True) - class Meta: + class Meta(ModelSchema.Meta): model = Order + sqla_session = db.session + + @post_load + def make_order(self, data): + if type(data) == Order: + return data + return Order(**data) class ProductSchema(ModelSchema): price = fields.Float(data_key='price') # Decimal to float - class Meta: + class Meta(ModelSchema.Meta): model = Product + sqla_session = db.session + @post_load + def make_product(self, data): + if type(data) == Product: + return data + return Product(**data) class OrderLineSchema(ModelSchema): product = fields.Nested('ProductSchema', exclude=('products',)) - class Meta: + class Meta(ModelSchema.Meta): model = OrderLine - fields = ('product', 'quantity') + sqla_session = db.session + + @post_load + def make_order_line(self, data): + db.session.flush() + if type(data) == OrderLine: + return data + return OrderLine(**data) diff --git a/app/services.py b/app/services.py index 3d588c2..083409b 100644 --- a/app/services.py +++ b/app/services.py @@ -3,15 +3,15 @@ from .models import Order, Product, OrderLine from .schema import OrderSchema, ProductSchema, OrderLineSchema +from app import db + class OrderListService: ''' This service intended for use exclusively by /api/orders ''' - def __init__(self, params, _session=None): + def __init__(self, _session=None): # your unit tests can pass in _session=MagicMock() - print('__init__') self.session = _session or db.session - self.params = params def _parents(self): return ( self.session.query(Order) @@ -19,5 +19,10 @@ def _parents(self): .all() ) def get(self): + # [{"address": "59 Arcubus Avenue", "city": "Sheffield", + # "country": "United Kingdom", "name": "Andrew", "order_id": 17, + # "products_sold": [{ "product": { "category": "Technology", + # "description": "A small computer", "name": "Calculator", + # "price": 15.35, "product_id": 1 }, "quantity": 10 }, ...]}] schema = OrderSchema() return schema.dump(self._parents(), many=True).data