├── roles ├── __init__.py └── models.py ├── file_uploads ├── __init__.py └── models.py ├── ecommerce_api ├── __init__.py ├── factory.py └── views.py ├── migrations ├── README ├── script.py.mako ├── alembic.ini └── versions │ └── 2728d2dc2146_.py ├── shared ├── __init__.py ├── database.py ├── models.py ├── middlewares.py ├── columns.py ├── serializers.py └── security.py ├── routes.py ├── tags ├── __init__.py ├── serializers.py ├── models.py └── views.py ├── github_images ├── postman.png └── db_structure.png ├── orders ├── __init__.py ├── serializers.py ├── models.py └── views.py ├── users ├── __init__.py ├── models.py └── views.py ├── addresses ├── __init__.py ├── serializers.py ├── models.py └── views.py ├── categories ├── __init__.py ├── serializers.py ├── models.py └── views.py ├── comments ├── __init__.py ├── serializers.py ├── models.py └── views.py ├── products ├── __init__.py ├── serializers.py ├── models.py └── views.py ├── reset.bat ├── config.py ├── app.py ├── .gitignore ├── README.md └── seed_database.py /roles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /file_uploads/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ecommerce_api/__init__.py: -------------------------------------------------------------------------------- 1 | import ecommerce_api.views 2 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /shared/__init__.py: -------------------------------------------------------------------------------- 1 | import middlewares 2 | import shared.security 3 | -------------------------------------------------------------------------------- /routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | blueprint = Blueprint('main', __name__) -------------------------------------------------------------------------------- /tags/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import tags.views 3 | 4 | sys.stdout.write('[+] Registering tags routes\n') 5 | -------------------------------------------------------------------------------- /github_images/postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monster0318/FlaskApiEcommerce/HEAD/github_images/postman.png -------------------------------------------------------------------------------- /orders/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import orders.views 4 | 5 | sys.stdout.write('[+] Registering order views\n') 6 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import users.views 4 | 5 | sys.stdout.write('[+] Registering routes for user\n') 6 | -------------------------------------------------------------------------------- /addresses/__init__.py: -------------------------------------------------------------------------------- 1 | import addresses.views 2 | import sys 3 | 4 | sys.stdout.write('[+] Registering addresses routes\n') 5 | -------------------------------------------------------------------------------- /categories/__init__.py: -------------------------------------------------------------------------------- 1 | import categories.views 2 | import sys 3 | 4 | sys.stdout.write('[+] Registering category routes\n') 5 | -------------------------------------------------------------------------------- /github_images/db_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monster0318/FlaskApiEcommerce/HEAD/github_images/db_structure.png -------------------------------------------------------------------------------- /comments/__init__.py: -------------------------------------------------------------------------------- 1 | import comments.views 2 | import sys 3 | 4 | 5 | sys.stdout.write('[+] Registering comment routes\n') 6 | 7 | -------------------------------------------------------------------------------- /tags/serializers.py: -------------------------------------------------------------------------------- 1 | from shared.serializers import PageSerializer 2 | 3 | 4 | class TagListSerializer(PageSerializer): 5 | resource_name = 'tags' 6 | -------------------------------------------------------------------------------- /orders/serializers.py: -------------------------------------------------------------------------------- 1 | from shared.serializers import PageSerializer 2 | 3 | 4 | class OrderListSerializer(PageSerializer): 5 | resource_name = 'orders' 6 | 7 | -------------------------------------------------------------------------------- /categories/serializers.py: -------------------------------------------------------------------------------- 1 | from shared.serializers import PageSerializer 2 | 3 | 4 | class CategoryListSerializer(PageSerializer): 5 | resource_name = 'categories' 6 | -------------------------------------------------------------------------------- /products/__init__.py: -------------------------------------------------------------------------------- 1 | import products.views # this will trigger the execution of that script which in turn register the routes 2 | import sys 3 | 4 | sys.stdout.write('[+] Registering product routes\n') 5 | -------------------------------------------------------------------------------- /reset.bat: -------------------------------------------------------------------------------- 1 | REM delete database /q: quiet mode, do not prompt for confirmation 2 | del /q app.db 3 | REM remove migrations folder 4 | rd /q /s migrations 5 | 6 | flask2 db init && flask2 db migrate && flask2 db upgrade -------------------------------------------------------------------------------- /addresses/serializers.py: -------------------------------------------------------------------------------- 1 | from shared.serializers import PageSerializer 2 | 3 | 4 | class AddressListSerializer(PageSerializer): 5 | resource_name = 'addresses' 6 | 7 | 8 | class CommentDetailsSerializer(): 9 | def __init__(self, comment, include_user=False, include_product=False): 10 | self.data = {'success': True} 11 | self.data.update(comment.get_summary(include_product, include_user)) 12 | -------------------------------------------------------------------------------- /shared/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.sql import ClauseElement 2 | 3 | 4 | def get_or_create(session, model, defaults=None, **kwargs): 5 | instance = session.query(model).filter_by(**kwargs).first() 6 | if instance: 7 | return instance, False 8 | else: 9 | params = dict((k, v) for k, v in kwargs.iteritems() if not isinstance(v, ClauseElement)) 10 | params.update(defaults or {}) 11 | instance = model(**params) 12 | session.add(instance) 13 | return instance, True 14 | -------------------------------------------------------------------------------- /shared/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from flask_sqlalchemy import Model 4 | from sqlalchemy import DateTime, Column, Integer 5 | 6 | 7 | class BaseModel(Model): 8 | id = Column(Integer, primary_key=True) 9 | created_at = Column(DateTime, nullable=False, default=datetime.utcnow) 10 | 11 | def get_or_default(self, ident, default=None): 12 | return self.get(ident) or default 13 | 14 | 15 | class UpdatedAtMixin(object): 16 | updated_at = Column(DateTime, onupdate=datetime.utcnow) 17 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /shared/middlewares.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | from datetime import datetime 4 | 5 | from flask import g 6 | from flask_jwt_extended import current_user 7 | 8 | from ecommerce_api.factory import db, app 9 | 10 | start_time = 0 11 | 12 | 13 | # TODO: not working, what did I wrong ?? 14 | @app.before_request 15 | def before_req(): 16 | global start_time 17 | g.user = current_user 18 | start_time = time.clock() 19 | 20 | 21 | @app.after_request 22 | def after_req(response): 23 | end_time = time.clock() 24 | elapsed = (end_time - start_time) * 1000 25 | sys.stdout.write('Request took %d milliseconds\n' % elapsed) 26 | return response 27 | -------------------------------------------------------------------------------- /shared/columns.py: -------------------------------------------------------------------------------- 1 | from ecommerce_api.factory import db 2 | 3 | 4 | class ColIntEnum(db.TypeDecorator): 5 | """ 6 | Enables passing in a Python enum and storing the enum's *value* in the db. 7 | The default would have stored the enum's *name* (ie the string). 8 | """ 9 | impl = db.Integer 10 | 11 | def __init__(self, enumtype, *args, **kwargs): 12 | super(ColIntEnum, self).__init__(*args, **kwargs) 13 | self._enumtype = enumtype 14 | 15 | def process_bind_param(self, value, dialect): 16 | if isinstance(value, int): 17 | return value 18 | 19 | return value.value 20 | 21 | def process_result_value(self, value, dialect): 22 | return self._enumtype(value) -------------------------------------------------------------------------------- /comments/serializers.py: -------------------------------------------------------------------------------- 1 | from shared.serializers import PageSerializer 2 | 3 | 4 | class CommentListSerializer(PageSerializer): 5 | resource_name = 'comments' 6 | 7 | def __init__(self, comments_or_pagination, **kwargs): 8 | if type(comments_or_pagination) == list: 9 | self.data = [comment.get_summary(**kwargs) for comment in comments_or_pagination] 10 | else: 11 | super(CommentListSerializer, self).__init__(comments_or_pagination, **kwargs) 12 | 13 | 14 | class CommentDetailsSerializer(): 15 | def __init__(self, comment, include_user=False, include_product=False): 16 | self.data = {'success': True} 17 | self.data.update(comment.get_summary(include_product, include_user)) 18 | -------------------------------------------------------------------------------- /ecommerce_api/factory.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | from flask_bcrypt import Bcrypt 5 | from flask_caching import Cache 6 | from flask_cors import CORS 7 | from flask_jwt_extended import JWTManager 8 | from flask_migrate import Migrate 9 | from flask_sqlalchemy import SQLAlchemy 10 | 11 | from config import Config 12 | 13 | basedir = os.path.abspath(os.path.dirname(__file__)) 14 | app = Flask(__name__, root_path=os.getcwd(), static_url_path='/static') 15 | 16 | app.config.from_object(Config) 17 | db = SQLAlchemy(app) # , model_class=BaseModel) 18 | migrate = Migrate(app, db) 19 | cache = Cache() 20 | 21 | # cors with defaults, which means allow all domains, it is fine for the moment 22 | cors = CORS(app) 23 | bcrypt = Bcrypt() 24 | jwt = JWTManager(app) 25 | -------------------------------------------------------------------------------- /ecommerce_api/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import jsonify, send_from_directory 4 | 5 | from ecommerce_api.factory import app 6 | 7 | 8 | 9 | @app.route("/routes") 10 | def site_map(): 11 | links = [] 12 | # for rule in app.url_map.iter_rules(): 13 | for rule in app.url_map._rules: 14 | # Filter out rules we can't navigate to in a browser 15 | # and rules that require parameters 16 | links.append({'ulr': rule.rule, 'view': rule.endpoint}) 17 | return jsonify(links), 200 18 | 19 | 20 | # @app.route('/api/images/') 21 | def send_js(path): 22 | basedir = os.path.join(os.path.realpath(os.getcwd()), 'static', 'bellerin.png') 23 | if os.path.exists(basedir): 24 | return app.send_static_file(basedir) 25 | return send_from_directory('images', path) 26 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class Config(object): 8 | DEBUG = True 9 | 10 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 11 | 'sqlite:///' + os.path.join(basedir, 'app.db') + '?check_same_thread=False' 12 | SQLALCHEMY_TRACK_MODIFICATIONS = False 13 | MAX_CONTENT_LENGTH = 16 * 1024 * 1024 14 | 15 | IMAGES_LOCATION = os.path.join(basedir, 'static', 'images') 16 | # Flask Jwt extended 17 | # for more info either look at the official page or jwt_manager.py file 18 | JWT_SECRET_KEY = os.environ.get('JWT_SECRET', 'JWT_SUPER_SECRET') 19 | JWT_ACCESS_TOKEN_EXPIRES = timedelta(10 ** 6) 20 | JWT_AUTH_USERNAME_KEY = 'username' 21 | JWT_AUTH_HEADER_PREFIX = 'Bearer' 22 | 23 | # CORS 24 | CORS_ORIGIN_WHITELIST = [ 25 | 'http://localhost:4000' 26 | ] 27 | -------------------------------------------------------------------------------- /products/serializers.py: -------------------------------------------------------------------------------- 1 | from comments.serializers import CommentListSerializer 2 | from shared.serializers import PageSerializer 3 | 4 | 5 | class ProductListSerializer(PageSerializer): 6 | resource_name = 'products' 7 | 8 | 9 | class ProductDetailsSerializer(): 10 | def __init__(self, product): 11 | self.data = { 12 | 'success': True, 13 | 'id': product.id, 14 | 'name': product.name, 15 | 'description': product.description, 16 | 'price': product.price, 17 | 'stock': product.stock, 18 | 'slug': product.slug, 19 | 'comments': CommentListSerializer(product.comments.all(), include_user=True).data, 20 | 'tags': [tag.name for tag in product.tags], 21 | 'categories': [category.name for category in product.categories], 22 | 'image_urls': [image.file_path.replace('\\', '/') for image in product.images] 23 | } 24 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /roles/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column 4 | 5 | from ecommerce_api.factory import db 6 | 7 | 8 | class Role(db.Model): 9 | __tablename__ = 'roles' 10 | id = db.Column(db.Integer, primary_key=True) 11 | name = db.Column(db.String(80), unique=True, nullable=False) 12 | description = db.Column(db.String(100), nullable=True) 13 | 14 | 15 | class UserRole(db.Model): 16 | __tablename__ = 'users_roles' 17 | 18 | user_id = db.Column(db.Integer, db.ForeignKey("users.id")) 19 | role_id = db.Column(db.Integer, db.ForeignKey("roles.id")) 20 | 21 | # users = db.relationship("User", foreign_keys=[user_id], backref='roles') 22 | user = db.relationship("User", foreign_keys=[user_id], backref='users_roles') 23 | role = db.relationship("Role", foreign_keys=[role_id], backref='users_roles') 24 | 25 | created_at = Column(db.DateTime, nullable=False, default=datetime.utcnow) 26 | __mapper_args__ = {'primary_key': [user_id, role_id]} 27 | 28 | 29 | users_roles = db.Table( 30 | 'users_roles', 31 | db.Column('user_id', db.Integer, db.ForeignKey('users.id')), 32 | db.Column('role_id', db.Integer, db.ForeignKey('roles.id')), 33 | keep_existing=True 34 | ) 35 | -------------------------------------------------------------------------------- /categories/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from slugify import slugify 4 | from sqlalchemy import event 5 | from ecommerce_api.factory import db 6 | 7 | 8 | class Category(db.Model): 9 | __tablename__ = 'categories' 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | name = db.Column(db.String(255)) 13 | slug = db.Column(db.String(255), index=True, unique=True) 14 | description = db.Column(db.String()) 15 | created_at = db.Column(db.DateTime(), default=datetime.utcnow, index=True) 16 | updated_at = db.Column(db.DateTime()) 17 | 18 | def get_summary(self): 19 | return { 20 | 'id': self.id, 21 | 'name': self.name, 22 | 'description': self.description, 23 | 'image_urls': [image.file_path.replace('\\', '/') for image in self.images] 24 | } 25 | 26 | def __repr__(self): 27 | return self.name 28 | 29 | 30 | @event.listens_for(Category.name, 'set') 31 | def receive_set(target, value, oldvalue, initiator): 32 | target.slug = slugify(unicode(value)) 33 | 34 | 35 | products_categories = \ 36 | db.Table("products_categories", 37 | db.Column("category_id", db.Integer, db.ForeignKey("categories.id")), 38 | db.Column("product_id", db.Integer, db.ForeignKey("products.id"))) 39 | -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from ecommerce_api.factory import db, bcrypt 4 | from roles.models import users_roles 5 | 6 | 7 | class User(db.Model): 8 | __tablename__ = 'users' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | username = db.Column(db.String(64), index=True, unique=True) 12 | email = db.Column(db.String(120), index=True, unique=True) 13 | password = db.Column(db.String(128)) 14 | first_name = db.Column(db.String(300), nullable=False) 15 | last_name = db.Column(db.String(300), nullable=False) 16 | 17 | created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 18 | updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) 19 | 20 | # comments = db.relationship('Comment', foreign_keys='comment.user_id', backref='user', lazy='dynamic') 21 | 22 | roles = db.relationship('Role', secondary=users_roles, backref='users') 23 | 24 | def __repr__(self): 25 | return ''.format(self.username) 26 | 27 | def is_password_valid(self, password): 28 | return bcrypt.check_password_hash(self.password, password) 29 | 30 | def is_admin(self): 31 | return 'ROLE_ADMIN' in [r.name for r in self.roles] 32 | 33 | def is_not_admin(self): 34 | return not self.is_admin() 35 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from addresses.models import Address 2 | from categories.models import Category 3 | from comments.models import Comment 4 | from ecommerce_api.factory import app, db 5 | from file_uploads.models import FileUpload, ProductImage, TagImage, CategoryImage 6 | from orders.models import Order 7 | from products.models import Product 8 | from routes import blueprint 9 | from tags.models import Tag 10 | from users.models import User 11 | 12 | # Extensions, it is not how a well organized project initializes the extensions but hey, it 13 | # is simple and readable anyways. 14 | 15 | 16 | app.register_blueprint(blueprint, url_prefix='/api') 17 | 18 | 19 | # Like the old school Flask-Script for the shell, but using the new Flask CLI which is way better 20 | @app.shell_context_processor 21 | def make_shell_context(): 22 | return dict(app=app, db=db, User=User, address=Address, order=Order, product=Product, 23 | tag=Tag, category=Category, comment=Comment, file_upload=FileUpload, tag_image=TagImage, 24 | category_image=CategoryImage, product_image=ProductImage) 25 | 26 | 27 | ''' 28 | Not used, to seed the database, it is as easy as running the seed_database.py python script 29 | import click 30 | import sys 31 | @app.cli.command() 32 | @click.option('--seed', default=None, help='seed the database') 33 | def seed_db(value): 34 | sys.stdout.write('seed the database') 35 | ''' 36 | 37 | if __name__ == '__main__': 38 | app.run(port=8080) 39 | -------------------------------------------------------------------------------- /comments/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column, Integer, Text, DateTime, ForeignKey 4 | from sqlalchemy.orm import relationship 5 | 6 | from ecommerce_api.factory import db 7 | 8 | 9 | class Comment(db.Model): 10 | __tablename__ = 'comments' 11 | 12 | id = Column(Integer, primary_key=True) 13 | content = Column(Text, nullable=False) 14 | rating = Column(Integer, nullable=True) 15 | user_id = Column(Integer, ForeignKey('users.id'), nullable=False) 16 | # user = relationship('User', backref=db.backref('comments')) 17 | user = relationship('User', backref='comments') 18 | 19 | product_id = Column(Integer, ForeignKey('products.id'), nullable=False) 20 | 21 | # product = relationship('Product', backref=db.backref('comments')) 22 | 23 | created_at = Column(DateTime, nullable=False, default=datetime.utcnow) 24 | updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) 25 | 26 | def get_summary(self, include_product=False, include_user=False): 27 | data = { 28 | 'id': self.id, 29 | 'content': self.content, 30 | 'created_at': self.created_at, 31 | } 32 | 33 | if include_product: 34 | data['product'] = { 35 | 'id': self.product.id, 36 | 'slug': self.product.slug, 37 | 'name': self.product.name 38 | } 39 | 40 | if include_user: 41 | data['user'] = { 42 | 'id': self.user_id, 43 | 'username': self.user.username 44 | } 45 | return data 46 | -------------------------------------------------------------------------------- /addresses/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy.orm import relationship 4 | 5 | from ecommerce_api.factory import db 6 | 7 | 8 | class Address(db.Model): 9 | __tablename__ = 'addresses' 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | first_name = db.Column(db.String, nullable=False) 13 | last_name = db.Column(db.String, nullable=False) 14 | city = db.Column(db.String, nullable=False) 15 | country = db.Column(db.String, nullable=False) 16 | zip_code = db.Column(db.String, nullable=False) 17 | street_address = db.Column(db.String, nullable=False) 18 | phone_number = db.Column(db.String, nullable=True) # nullable because I have not implemented it 19 | 20 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) 21 | user = relationship('User', backref='addresses') 22 | 23 | created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 24 | updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) 25 | 26 | def get_summary(self, include_user=False): 27 | data = { 28 | 'id': self.id, 29 | 'first_name': self.first_name, 30 | 'last_name': self.last_name, 31 | 'address': self.street_address, 32 | 'zip_code': self.zip_code, 33 | 'city': self.city, 34 | 'country': self.country, 35 | 'created_at': self.created_at, 36 | 'updated_at': self.updated_at 37 | } 38 | 39 | if include_user: 40 | data['user'] = {'id': self.user_id, 'username': self.user.username} 41 | 42 | return data 43 | -------------------------------------------------------------------------------- /file_uploads/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy.orm import relationship 4 | 5 | from ecommerce_api.factory import db 6 | 7 | 8 | class FileUpload(db.Model): 9 | __tablename__ = 'file_uploads' 10 | id = db.Column('id', db.Integer, primary_key=True) 11 | type = db.Column('type', db.String(15)) # this will be our discriminator 12 | 13 | file_path = db.Column(db.String, nullable=False) 14 | file_name = db.Column(db.String, nullable=False) 15 | file_size = db.Column(db.Integer, nullable=False) 16 | original_name = db.Column(db.String, nullable=False) 17 | 18 | created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 19 | updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) 20 | __mapper_args__ = { 21 | 'polymorphic_on': type, 22 | 'polymorphic_identity': 'FileUpload' 23 | } 24 | 25 | 26 | class TagImage(FileUpload): 27 | tag_id = db.Column(db.Integer, db.ForeignKey('tags.id'), nullable=True) 28 | tag = relationship('Tag', backref='images') 29 | 30 | __mapper_args__ = { 31 | 'polymorphic_identity': 'TagImage' 32 | } 33 | 34 | 35 | class ProductImage(FileUpload): 36 | product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=True) 37 | product = relationship('Product', backref='images') 38 | 39 | __mapper_args__ = { 40 | 'polymorphic_identity': 'ProductImage' 41 | } 42 | 43 | 44 | class CategoryImage(FileUpload): 45 | category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True) 46 | category = relationship('Category', backref='images') 47 | 48 | __mapper_args__ = { 49 | 'polymorphic_identity': 'CategoryImage' 50 | } 51 | -------------------------------------------------------------------------------- /tags/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from slugify import slugify 4 | from sqlalchemy import event, Column, Integer, ForeignKey, UniqueConstraint 5 | 6 | from ecommerce_api.factory import db 7 | 8 | 9 | class Tag(db.Model): 10 | __tablename__ = 'tags' 11 | id = db.Column(db.Integer, primary_key=True) 12 | name = db.Column(db.String(100)) 13 | slug = db.Column(db.String(), index=True, unique=True) 14 | description = db.Column(db.String()) 15 | created_at = db.Column(db.DateTime(), default=datetime.utcnow, index=True) 16 | updated_at = db.Column(db.DateTime()) 17 | 18 | def __repr__(self): 19 | return self.name 20 | 21 | def get_summary(self): 22 | return { 23 | 'id': self.id, 24 | 'name': self.name, 25 | 'description': self.description, 26 | 'image_urls': [image.file_path.replace('\\', '/') for image in self.images] 27 | } 28 | 29 | 30 | @event.listens_for(Tag.name, 'set') 31 | def receive_set(target, value, oldvalue, initiator): 32 | target.slug = slugify(unicode(value)) 33 | 34 | 35 | class ProductTag(db.Model): 36 | __tablename__ = 'products_tags' 37 | 38 | product_id = Column(Integer, ForeignKey("products.id"), nullable=False) 39 | tag_id = Column(Integer, ForeignKey("tags.id"), nullable=False) 40 | 41 | product = db.relationship("Product", foreign_keys=[product_id], backref='product_tags') 42 | tag = db.relationship("Tag", foreign_keys=[tag_id], backref='product_tags') 43 | 44 | __mapper_args__ = {'primary_key': [product_id, tag_id]} 45 | __table_args__ = (UniqueConstraint('product_id', 'tag_id', name='same_tag_for_same_product'),) 46 | 47 | 48 | products_tags = db.Table( 49 | 'products_tags', 50 | db.Column('product_id', db.Integer, db.ForeignKey('products.id')), 51 | db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')), 52 | keep_existing=True 53 | ) 54 | -------------------------------------------------------------------------------- /addresses/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | from flask_jwt_extended import current_user, jwt_required, get_jwt_identity 3 | from sqlalchemy import desc 4 | 5 | from addresses.models import Address 6 | from addresses.serializers import AddressListSerializer 7 | from ecommerce_api.factory import db 8 | from routes import blueprint 9 | from shared.serializers import get_success_response 10 | 11 | 12 | @blueprint.route('/users/addresses', methods=['GET']) 13 | @jwt_required 14 | def list_addresses(): 15 | page_size = request.args.get('page_size', 5) 16 | page = request.args.get('page', 1) 17 | 18 | user_id = get_jwt_identity() 19 | addresses = Address.query.filter_by(user_id=user_id).order_by(desc(Address.created_at)) \ 20 | .paginate(page=page, per_page=page_size) 21 | return jsonify(AddressListSerializer(addresses, include_user=False).get_data()), 200 22 | 23 | 24 | @blueprint.route('/users/addresses', methods=['POST']) 25 | @jwt_required 26 | def created_address(): 27 | first_name = request.json.get('first_name') 28 | last_name = request.json.get('last_name') 29 | zip_code = request.json.get('zip_code') 30 | phone_number = request.json.get('phone_number') 31 | city = request.json.get('city') 32 | country = request.json.get('country') 33 | street_address = request.json.get('address') 34 | 35 | # Method 1 of retrieving the user_id when using flask-jwt-extended 36 | # claims = get_jwt_claims() 37 | # user_id = claims.get('user_id') 38 | 39 | # Method 2; Method 3 is get_jwt_identity() 40 | user_id = current_user.id 41 | 42 | address = Address(first_name=first_name, last_name=last_name, zip_code=zip_code, phone_number=phone_number, 43 | street_address=street_address, user_id=user_id, city=city, country=country) 44 | 45 | db.session.add(address) 46 | db.session.commit() 47 | 48 | return get_success_response(data=address.get_summary(), messages='Address created successfully') 49 | -------------------------------------------------------------------------------- /products/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from flask_jwt_extended import current_user 4 | from slugify import slugify 5 | from sqlalchemy import event 6 | from sqlalchemy.orm import relationship 7 | 8 | from ecommerce_api.factory import db 9 | from categories.models import products_categories 10 | from tags.models import products_tags 11 | 12 | 13 | class Product(db.Model): 14 | __tablename__ = 'products' 15 | 16 | id = db.Column(db.Integer, primary_key=True) 17 | name = db.Column(db.String(255), nullable=False) 18 | slug = db.Column(db.String, index=True, unique=True) 19 | description = db.Column(db.Text, nullable=False) 20 | 21 | price = db.Column(db.Integer, nullable=False) 22 | stock = db.Column(db.Integer, nullable=False) 23 | 24 | created_at = db.Column(db.DateTime(), default=datetime.utcnow, index=True, nullable=False) 25 | updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) 26 | publish_on = db.Column(db.DateTime, index=True, default=datetime.utcnow) 27 | 28 | tags = relationship('Tag', secondary=products_tags, backref='products') 29 | categories = relationship('Category', secondary=products_categories, backref='products') 30 | 31 | comments = relationship('Comment', backref='product', lazy='dynamic') 32 | 33 | def __repr__(self): 34 | return '' % self.name 35 | 36 | def __str__(self): 37 | return ''.format(self.name) 38 | 39 | def get_summary(self): 40 | return { 41 | 'id': self.id, 42 | 'name': self.name, 43 | 'price': self.price, 44 | 'stock': self.stock, 45 | 'slug': self.slug, 46 | 'comments_count': self.comments.count(), 47 | 'tags': [{'id': t.id, 'name': t.name} for t in self.tags], 48 | 'categories': [{'id': c.id, 'name': c.name} for c in self.categories], 49 | 'image_urls': [i.file_path for i in self.images] 50 | } 51 | 52 | 53 | @event.listens_for(Product.name, 'set') 54 | def receive_set(target, value, oldvalue, initiator): 55 | target.slug = slugify(unicode(value)) 56 | -------------------------------------------------------------------------------- /shared/serializers.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | from flask_sqlalchemy import Pagination 3 | 4 | 5 | class PageSerializer(object): 6 | def __init__(self, pagination_obj, **kwargs): 7 | if type(pagination_obj) != Pagination: 8 | raise EnvironmentError() 9 | self.data = {} 10 | self.items = [resource.get_summary(**kwargs) for resource in pagination_obj.items] 11 | self.data['total_items_count'] = pagination_obj.total 12 | self.data['offset'] = (pagination_obj.page - 1) * pagination_obj.per_page 13 | self.data['requested_page_size'] = pagination_obj.per_page 14 | self.data['current_page_number'] = pagination_obj.page 15 | 16 | self.data['prev_page_number'] = pagination_obj.prev_num or 1 17 | self.data['total_pages_count'] = pagination_obj.pages 18 | 19 | self.data['has_next_page'] = pagination_obj.has_next 20 | self.data['has_prev_page'] = pagination_obj.has_prev 21 | 22 | self.data['next_page_number'] = pagination_obj.next_num or self.data['current_page_number'] 23 | 24 | self.data['next_page_url'] = '%s?page=%d&page_size=%d' % ( 25 | request.path, self.data['next_page_number'], self.data['requested_page_size']) 26 | 27 | self.data['prev_page_url'] = '%s?page=%d&page_size=%d' % ( 28 | request.path, self.data['prev_page_number'], self.data['requested_page_size']) 29 | 30 | def get_data(self): 31 | return { 32 | 'success': True, 33 | 'page_meta': self.data, 34 | self.resource_name: self.items, 35 | } 36 | 37 | 38 | def get_success_response(messages, data=None, status_code=200): 39 | if type(messages) == list: 40 | msgs = messages 41 | elif type(messages) == str: 42 | msgs = [messages] 43 | else: 44 | msgs = [] 45 | 46 | response = { 47 | 'success': True, 48 | 'full_messages': msgs 49 | } 50 | 51 | if data is not None: 52 | response.update(data) 53 | 54 | return jsonify(response), status_code 55 | 56 | 57 | def get_error_response(messages, status_code=500): 58 | if type(messages) == list: 59 | msgs = messages 60 | elif type(messages) == str: 61 | msgs = [messages] 62 | else: 63 | msgs = [] 64 | 65 | return jsonify({ 66 | 'success': False, 67 | 'full_messages': msgs 68 | }), status_code 69 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | from flask_jwt_extended import create_access_token, jwt_optional, get_jwt_identity 3 | 4 | from ecommerce_api.factory import db, bcrypt 5 | from roles.models import Role 6 | from routes import blueprint 7 | from shared.serializers import get_success_response 8 | from users.models import User 9 | 10 | 11 | 12 | @blueprint.route('/users', methods=['POST']) 13 | def register(): 14 | first_name = request.json.get('first_name', None) 15 | last_name = request.json.get('last_name', None) 16 | username = request.json.get('username', None) 17 | password = request.json.get('password', None) 18 | email = request.json.get('email', None) 19 | role = db.session.query(Role).filter_by(name='ROLE_USER').first() 20 | db.session.add( 21 | User(first_name=first_name, last_name=last_name, username=username, 22 | password=bcrypt.generate_password_hash(password).decode('utf-8'), roles=[role], email=email) 23 | ) 24 | db.session.commit() 25 | return get_success_response('User registered successfully') 26 | 27 | 28 | @jwt_optional 29 | def partially_protected(): 30 | # If no JWT is sent in with the request, get_jwt_identity() 31 | # will return None 32 | current_user = get_jwt_identity() 33 | if current_user: 34 | return jsonify(logged_in_as=current_user), 200 35 | else: 36 | return jsonify(loggeed_in_as='anonymous user'), 200 37 | 38 | 39 | @blueprint.route('/users/login', methods=['POST']) 40 | def login(): 41 | if not request.is_json: 42 | return jsonify({"msg": "Missing JSON in request"}), 400 43 | 44 | username = request.json.get('username', None) 45 | password = request.json.get('password', None) 46 | if username is None: 47 | return jsonify({"msg": "You must supply a username"}), 400 48 | if password is None: 49 | return jsonify({"msg": "Missing password parameter"}), 400 50 | 51 | user = User.query.filter_by(username=username).first() 52 | 53 | if user is None or not user.is_password_valid(str(password)): 54 | return jsonify({"msg": ""}), 401 55 | 56 | # Identity can be any data that is json serializable 57 | access_token = create_access_token(identity=user) 58 | 59 | return jsonify({ 60 | 'success': True, 61 | 'user': { 62 | 'username': user.username, 'id': user.id, 63 | 'roles': [role.name for role in user.roles], 64 | 'token': access_token} 65 | }), 200 66 | -------------------------------------------------------------------------------- /tags/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import request, jsonify 4 | from flask_jwt_extended import jwt_required, current_user 5 | from sqlalchemy import desc 6 | from werkzeug.utils import secure_filename 7 | 8 | from ecommerce_api.factory import db, app 9 | from file_uploads.models import TagImage 10 | from routes import blueprint 11 | from shared.serializers import get_success_response, get_error_response 12 | from tags.models import Tag 13 | from tags.serializers import TagListSerializer 14 | 15 | 16 | @blueprint.route('/tags', methods=['GET']) 17 | def list_tags(): 18 | page_size = request.args.get('page_size', 5) 19 | page = request.args.get('page', 1) 20 | tags = Tag.query.order_by(desc(Tag.created_at)).paginate(page=page, per_page=page_size) 21 | return jsonify(TagListSerializer(tags).get_data()), 200 22 | 23 | 24 | def validate_file_upload(filename): 25 | return '.' in filename and \ 26 | filename.rsplit('.', 1)[1].lower() in ['png', 'jpeg', 'jpg'] 27 | 28 | 29 | @blueprint.route('/tags', methods=['POST']) 30 | @jwt_required 31 | def create_tag(): 32 | if current_user.is_not_admin(): 33 | return jsonify(get_error_response('Permission denied, you must be admin', status_code=401)) 34 | 35 | name = request.form.get('name') 36 | description = request.form.get('description') 37 | 38 | tag = Tag(name=name, description=description) 39 | 40 | if 'images[]' in request.files: 41 | for image in request.files.getlist('images[]'): 42 | if image and validate_file_upload(image.filename): 43 | filename = secure_filename(image.filename) 44 | dir_path = app.config['IMAGES_LOCATION'] 45 | dir_path = os.path.join((os.path.join(dir_path, 'tags'))) 46 | 47 | if not os.path.exists(dir_path): 48 | os.makedirs(dir_path) 49 | 50 | file_path = os.path.join(dir_path, filename) 51 | image.save(file_path) 52 | 53 | file_path = file_path.replace(app.config['IMAGES_LOCATION'].rsplit(os.sep, 2)[0], '') 54 | if image.content_length == 0: 55 | file_size = image.content_length 56 | else: 57 | file_size = os.stat(file_path).st_size 58 | 59 | ti = TagImage(file_path=file_path, file_name=filename, original_name=image.filename, 60 | file_size=file_size) 61 | tag.images.append(ti) 62 | 63 | db.session.add(tag) 64 | db.session.commit() 65 | 66 | return get_success_response(data=tag.get_summary(), messages='Tag created successfully') 67 | -------------------------------------------------------------------------------- /shared/security.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from ecommerce_api.factory import jwt 4 | from users.models import User 5 | 6 | 7 | # Using the user_claims_loader, we can specify a method that will be 8 | # called when creating access tokens, and add these claims to the said 9 | # token. This method is passed the identity of who the token is being 10 | # created for, and must return data that is json serializable 11 | # @jwt.user_claims_loader 12 | def add_claims_to_access_token(identity): 13 | return { 14 | 'user_id': identity.id, 15 | 'username': identity.username, 16 | 'roles': [role.name for role in identity.roles] 17 | } 18 | 19 | 20 | def no_jwt_for_protected_endpoint(error_message): 21 | return jsonify({'success': False, 'full_messages': [error_message]}) 22 | 23 | 24 | # This is called on request, when the user accesses a restricted endpoint with a jwt token 25 | # we have to check if the user_id provided indeed maps to an existing user in the db 26 | # how do you know if user_id is indeed user_id and not username or email? well, it is the value 27 | # that you provided in identity_loader(), that value was issued to the user, now you get it back, we issued the id 28 | # then we have the id back :) 29 | def user_loader(user_id): 30 | # the user will be now available in current_user 31 | return User.query.get(user_id) 32 | 33 | 34 | # This is called when a jwt token is gonna be created, 35 | # you have to pass a json serializable object that will be used as the id of the user(identity) 36 | def identity_loader(user): 37 | return user.id 38 | 39 | 40 | def token_revoked(): 41 | return jsonify({'success': False, 'full_messages': ['Revoked token']}) 42 | 43 | 44 | def invalid_token_loader(error_message): 45 | return jsonify({'success': False, 'full_messages': [error_message]}) 46 | 47 | 48 | # anything this function returns will be available as current_user 49 | # it is called when the request is trying to reach a protected endpoint 50 | jwt.user_loader_callback_loader(user_loader) 51 | 52 | jwt.user_identity_loader(identity_loader) 53 | jwt.user_claims_loader(add_claims_to_access_token) 54 | 55 | 56 | # jwt.unauthorized_loader(no_jwt_for_protected_endpoint) 57 | # jwt.revoked_token_loader(token_revoked) 58 | 59 | 60 | @jwt.expired_token_loader 61 | def valid_but_expired_token(expired_token): 62 | token_type = expired_token['type'] 63 | return jsonify({ 64 | 'success': False, 65 | 'full_messages': [ 66 | 'Token expired' 67 | ] 68 | }), 401 69 | 70 | 71 | def validate_file_upload(filename): 72 | return '.' in filename and \ 73 | filename.rsplit('.', 1)[1].lower() in ['png', 'jpeg', 'jpg'] -------------------------------------------------------------------------------- /categories/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import request, jsonify 4 | from flask_jwt_extended import jwt_required, current_user 5 | from sqlalchemy import desc 6 | from werkzeug.utils import secure_filename 7 | 8 | from categories.models import Category 9 | from categories.serializers import CategoryListSerializer 10 | from ecommerce_api.factory import db, app 11 | from file_uploads.models import CategoryImage 12 | from routes import blueprint 13 | from shared.serializers import get_success_response, get_error_response 14 | 15 | 16 | @blueprint.route('/categories', methods=['GET']) 17 | def list_categories(): 18 | page_size = request.args.get('page_size', 5) 19 | page = request.args.get('page', 1) 20 | categories = Category.query.order_by(desc(Category.created_at)).paginate(page=page, per_page=page_size) 21 | return jsonify(CategoryListSerializer(categories).get_data()), 200 22 | 23 | 24 | def validate_file_upload(filename): 25 | return '.' in filename and \ 26 | filename.rsplit('.', 1)[1].lower() in ['png', 'jpeg', 'jpg'] 27 | 28 | 29 | @blueprint.route('/categories', methods=['POST']) 30 | @jwt_required 31 | def create_category(): 32 | if current_user.is_not_admin(): 33 | return jsonify(get_error_response('Permission denied, you must be admin', status_code=401)) 34 | 35 | name = request.form.get('name') 36 | description = request.form.get('description') 37 | 38 | category = Category(name=name, description=description) 39 | 40 | if 'images[]' in request.files: 41 | for image in request.files.getlist('images[]'): 42 | if image and validate_file_upload(image.filename): 43 | filename = secure_filename(image.filename) 44 | dir_path = app.config['IMAGES_LOCATION'] 45 | dir_path = os.path.join((os.path.join(dir_path, 'categories'))) 46 | 47 | if not os.path.exists(dir_path): 48 | os.makedirs(dir_path) 49 | 50 | file_path = os.path.join(dir_path, filename) 51 | image.save(file_path) 52 | 53 | file_path = file_path.replace(app.config['IMAGES_LOCATION'].rsplit(os.sep, 2)[0], '') 54 | if image.content_length == 0: 55 | file_size = image.content_length 56 | else: 57 | file_size = os.stat(file_path).st_size 58 | 59 | ci = CategoryImage(file_path=file_path, file_name=filename, original_name=image.filename, 60 | file_size=file_size) 61 | category.images.append(ci) 62 | 63 | db.session.add(category) 64 | db.session.commit() 65 | 66 | return get_success_response(data=category.get_summary(), messages='Category created successfully') 67 | -------------------------------------------------------------------------------- /orders/models.py: -------------------------------------------------------------------------------- 1 | ''' 2 | import enum 3 | from shared.columns import ColIntEnum 4 | 5 | # https://www.michaelcho.me/product/using-python-enums-in-sqlalchemy-models 6 | class OrderStatus(enum.IntEnum): 7 | processed = 1 8 | image = 2 9 | audio = 3 10 | reply = 4 11 | unknown = 5 12 | ''' 13 | from datetime import datetime 14 | 15 | from sqlalchemy.orm import relationship 16 | 17 | from ecommerce_api.factory import db 18 | 19 | ORDER_STATUS = ['processed', 'delivered', 'in transit', 'shipped'] 20 | 21 | 22 | class Order(db.Model): 23 | __tablename__ = 'orders' 24 | 25 | id = db.Column(db.Integer, primary_key=True) 26 | # order_status = db.Column(ColIntEnum(OrderStatus), default=OrderStatus.text) 27 | order_status = db.Column(db.Integer) 28 | tracking_number = db.Column(db.String) 29 | 30 | address_id = db.Column(db.Integer, db.ForeignKey('addresses.id'), nullable=False) 31 | address = relationship('Address', backref='orders', lazy=False) 32 | 33 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) 34 | user = relationship('User', backref='orders') 35 | 36 | created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 37 | 38 | def get_summary(self, include_order_items=False): 39 | dto = { 40 | 'id': self.id, 41 | 'order_status': ORDER_STATUS[self.order_status], 42 | 'tracking_number': self.tracking_number, 43 | # Please notice how we retrieve the count, through len(), instead of count() 44 | # as we did in Product.get_summary() for comments, why? we declared the association in different places 45 | 'address': self.address.get_summary() 46 | } 47 | 48 | if include_order_items: 49 | dto['order_items'] = [] 50 | for oi in self.order_items: 51 | dto['order_items'].append(oi.get_summary()) 52 | else: 53 | dto['order_items_count'] = len(self.order_items) 54 | 55 | return dto 56 | 57 | 58 | class OrderItem(db.Model): 59 | __tablename__ = 'order_items' 60 | 61 | id = db.Column(db.Integer, primary_key=True) 62 | name = db.Column(db.String, index=True, nullable=False) 63 | slug = db.Column(db.String) 64 | price = db.Column(db.Integer, index=True, nullable=False) 65 | quantity = db.Column(db.Integer, index=True, nullable=False) 66 | 67 | order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False) 68 | order = relationship('Order', backref='order_items') 69 | 70 | product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) 71 | product = relationship('Product', backref='order_items') 72 | 73 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) 74 | user = relationship('User', backref='products_bought') 75 | 76 | created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 77 | updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) 78 | 79 | def get_summary(self): 80 | return { 81 | 'name': self.name, 'slug': self.slug, 82 | 'product_id': self.product_id, 83 | 'price': self.price, 'quantity': self.quantity 84 | } 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app.db 2 | static/ 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/usage.statistics.xml 10 | .idea/**/dictionaries 11 | .idea/**/shelf 12 | 13 | # Generated files 14 | .idea/**/contentModel.xml 15 | 16 | # Sensitive or high-churn files 17 | .idea/**/dataSources/ 18 | .idea/**/dataSources.ids 19 | .idea/**/dataSources.local.xml 20 | .idea/**/sqlDataSources.xml 21 | .idea/**/dynamic.xml 22 | .idea/**/uiDesigner.xml 23 | .idea/**/dbnavigator.xml 24 | 25 | # Gradle 26 | .idea/**/gradle.xml 27 | .idea/**/libraries 28 | 29 | # Gradle and Maven with auto-import 30 | # When using Gradle or Maven with auto-import, you should exclude module files, 31 | # since they will be recreated, and may cause churn. Uncomment if using 32 | # auto-import. 33 | # .idea/modules.xml 34 | # .idea/*.iml 35 | # .idea/modules 36 | 37 | # CMake 38 | cmake-build-*/ 39 | 40 | # Mongo Explorer plugin 41 | .idea/**/mongoSettings.xml 42 | 43 | # File-based project format 44 | *.iws 45 | 46 | # IntelliJ 47 | out/ 48 | 49 | # mpeltonen/sbt-idea plugin 50 | .idea_modules/ 51 | 52 | # JIRA plugin 53 | atlassian-ide-plugin.xml 54 | 55 | # Cursive Clojure plugin 56 | .idea/replstate.xml 57 | 58 | # Crashlytics plugin (for Android Studio and IntelliJ) 59 | com_crashlytics_export_strings.xml 60 | crashlytics.properties 61 | crashlytics-build.properties 62 | fabric.properties 63 | 64 | # Editor-based Rest Client 65 | .idea/httpRequests 66 | 67 | # Android studio 3.1+ serialized cache file 68 | .idea/caches/build_file_checksums.ser 69 | 70 | .DS_Store 71 | .env 72 | .flaskenv 73 | *.pyc 74 | *.pyo 75 | env/ 76 | env* 77 | dist/ 78 | build/ 79 | *.egg 80 | *.egg-info/ 81 | _mailinglist 82 | .tox/ 83 | .cache/ 84 | .pytest_cache/ 85 | .idea/ 86 | docs/_build/ 87 | 88 | # Coverage reports 89 | htmlcov/ 90 | .coverage 91 | .coverage.* 92 | *,cover 93 | 94 | 95 | # Byte-compiled / optimized / DLL files 96 | __pycache__/ 97 | *.py[cod] 98 | *$py.class 99 | 100 | # C extensions 101 | *.so 102 | 103 | # Distribution / packaging 104 | .Python 105 | env/ 106 | build/ 107 | develop-eggs/ 108 | dist/ 109 | downloads/ 110 | eggs/ 111 | .eggs/ 112 | lib/ 113 | lib64/ 114 | parts/ 115 | sdist/ 116 | var/ 117 | *.egg-info/ 118 | .installed.cfg 119 | *.egg 120 | 121 | # PyInstaller 122 | # Usually these files are written by a python script from a template 123 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 124 | *.manifest 125 | *.spec 126 | 127 | # Installer logs 128 | pip-log.txt 129 | pip-delete-this-directory.txt 130 | 131 | # Unit test / coverage reports 132 | htmlcov/ 133 | .tox/ 134 | .coverage 135 | .coverage.* 136 | .cache 137 | nosetests.xml 138 | coverage.xml 139 | *,cover 140 | .hypothesis/ 141 | 142 | # Translations 143 | *.mo 144 | *.pot 145 | 146 | # Django stuff: 147 | *.log 148 | local_settings.py 149 | 150 | # Flask stuff: 151 | instance/ 152 | .webassets-cache 153 | 154 | # Scrapy stuff: 155 | .scrapy 156 | 157 | # Sphinx documentation 158 | docs/_build/ 159 | 160 | # PyBuilder 161 | target/ 162 | 163 | # IPython Notebook 164 | .ipynb_checkpoints 165 | 166 | # pyenv 167 | .python-version 168 | 169 | # celery beat schedule file 170 | celerybeat-schedule 171 | 172 | # dotenv 173 | .env 174 | 175 | # virtualenv 176 | venv/ 177 | ENV/ 178 | 179 | # Spyder project settings 180 | .spyderproject 181 | 182 | # Rope project settings 183 | .ropeproject -------------------------------------------------------------------------------- /comments/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | from flask_jwt_extended import jwt_required, get_jwt_claims, current_user 3 | from sqlalchemy import desc 4 | 5 | from ecommerce_api.factory import db 6 | 7 | from products.models import Product 8 | from comments.models import Comment 9 | from comments.serializers import CommentListSerializer, CommentDetailsSerializer 10 | from routes import blueprint 11 | from shared.serializers import get_success_response, get_error_response 12 | 13 | 14 | @blueprint.route('/products//comments', methods=['GET']) 15 | def list_comments(product_slug): 16 | page_size = request.args.get('page_size', 5) 17 | page = request.args.get('page', 1) 18 | product_id = Product.query.filter_by(slug=product_slug).with_entities('id').first()[0] 19 | comments = Comment.query.filter_by(product_id=product_id).order_by(desc(Comment.created_at)).paginate(page=page, 20 | per_page=page_size) 21 | return jsonify(CommentListSerializer(comments, include_user=True).get_data()), 200 22 | 23 | 24 | @blueprint.route('/comments/', methods=['GET']) 25 | def show_comment(comment_id): 26 | comment = Comment.query.get(comment_id) 27 | return jsonify(CommentDetailsSerializer(comment).data), 200 28 | 29 | 30 | @blueprint.route('/products//comments', methods=['POST']) 31 | @jwt_required 32 | def create_comment(product_slug): 33 | content = request.json.get('content') 34 | 35 | # claims = get_jwt_claims() 36 | # user_id = claims.get('user_id') 37 | # user_id = get_jwt_identity() 38 | # user = current_user 39 | 40 | claims = get_jwt_claims() 41 | user_id = claims.get('user_id') 42 | product_id = db.session.query(Product.id).filter_by(slug=product_slug).first()[0] 43 | comment = Comment(content=content, user_id=user_id, product_id=product_id) 44 | 45 | db.session.add(comment) 46 | db.session.commit() 47 | 48 | return get_success_response(data=CommentDetailsSerializer(comment).data, messages='Comment created successfully') 49 | 50 | 51 | @blueprint.route('/comments/', methods=['PUT']) 52 | @jwt_required 53 | def update_comment(comment_id): 54 | # comment = Comment.query.get_or_404(comment_id) 55 | comment = Comment.query.get(comment_id) 56 | if comment is None: 57 | return get_error_response(messages='not found', status_code=404) 58 | 59 | if current_user.is_admin() or comment.user_id == current_user.id: 60 | content = request.json.get('content') 61 | rating = request.json.get('rating') 62 | 63 | if content: 64 | comment.content = content 65 | if rating: 66 | comment.rating = rating 67 | 68 | db.session.commit() 69 | return get_success_response(data=CommentDetailsSerializer(comment).data, 70 | messages='Comment updated successfully') 71 | else: 72 | return get_error_response('Permission denied, you can not update this comment', status_code=401) 73 | 74 | 75 | @blueprint.route('/comments/', methods=['DELETE']) 76 | @jwt_required 77 | def destroy_comment(comment_id): 78 | comment = Comment.query.get(comment_id) 79 | if comment is None: 80 | return get_error_response('Comment not found', status_code=404) 81 | 82 | if current_user.is_admin() or comment.user_id == current_user.id: 83 | db.session.delete(comment) 84 | db.session.commit() 85 | return get_success_response('Comment deleted successfully') 86 | else: 87 | return get_error_response('Permission denied, you can not delete this comment', status_code=401) 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | This is one of my E-commerce API app implementations. It is written in Python with Flask as the main dependency. 3 | This is not a finished project by any means, but it has a valid enough shape to be git cloned and studied if you are interested in this topic. 4 | If you are interested in this project take a look at my other server API implementations I have made with: 5 | 6 | - [Node Js + Sequelize](https://github.com/melardev/ApiEcomSequelizeExpress) 7 | - [Node Js + Bookshelf](https://github.com/melardev/ApiEcomBookshelfExpress) 8 | - [Node Js + Mongoose](https://github.com/melardev/ApiEcomMongooseExpress) 9 | - [Python Django](https://github.com/melardev/DjangoRestShopApy) 10 | - [Java EE Spring Boot and Hibernate](https://github.com/melardev/SBootApiEcomMVCHibernate) 11 | - [Ruby on Rails](https://github.com/melardev/RailsApiEcommerce) 12 | - [AspNet Core](https://github.com/melardev/ApiAspCoreEcommerce) 13 | - [Laravel](https://github.com/melardev/ApiEcommerceLaravel) 14 | 15 | The next projects to come will be: 16 | - Elixir with phoenix and Ecto 17 | - AspNet MVC 6 18 | - Java EE with Jax RS with jersey 19 | - Java EE with Apache Struts 2 20 | - Spring Boot with Kotlin 21 | - Go with Gorilla and Gorm 22 | - Go with Beego 23 | - Laravel with Fractal and Api Resources 24 | - Flask with other Rest Api frameworks such as apisec, flask restful 25 | 26 | ## WARNING 27 | If you debug the seed_database.py file then be warned you may run into exceptions related to thread safety, in that 28 | case just rerun the script and try to not place the breakpoint where creating the model takes place 29 | # Getting started 30 | 1. Git clone the project 31 | 1. Setup Flask-Migrate 32 | `flask db init && flask db migrate && flask db upgrade` 33 | 1. Seed the database with 34 | 1. Seed the database 35 | `python2 seed_database.py` 36 | 37 | While you play with the project, if you want to reset the database(delete the app.db, regenerate the migrations and migrate) 38 | you can execute reset.bat, it relies on flask2 executable(you may want to change it to flask instead); Flask2 it is 39 | just a custom bat file that I have that points to flask.exe from my Python2 Installation folder 40 | 41 | # Useful commands 42 | - Setup the migrations folder to hold migration files 43 | `flask2 db init` 44 | - Generate migration files 45 | `flask2 db migrate` 46 | - Make the actual migration and create tables on database 47 | `flask2 db upgrade` 48 | 49 | 50 | # Features 51 | - Authentication / Authorization 52 | - JWT middleware for authentication 53 | - Multi file upload 54 | - Database seed 55 | - Paging 56 | - CRUD operations on products, comments, tags, categories, orders 57 | ![Fetching products page](./github_images/postman.png) 58 | - Orders, guest users may place an order 59 | ![Database diagram](./github_images/db_structure.png) 60 | 61 | # What you will learn 62 | - Flask 63 | - Jwt authentication with flask 64 | - Controllers 65 | - Middlewares 66 | - JWT Authentication 67 | - Role based authorization 68 | - Flask SQLAlchemy ORM 69 | - associations: ManyToMany, OneToMany, ManyToOne 70 | - Select specific columns 71 | - Eager loading 72 | - Count related association 73 | 74 | - seed data 75 | - misc 76 | - project structure 77 | 78 | # Understanding the project 79 | In project apps you will most of the times see that there is the launcher file, and the creation of the app and db is done 80 | elsewhere, why? to avoid circular dependencies. The app flask instance along with the db are heavily import from 81 | all over the places in our project, those variables are better placed in a separate file, I will explain this aspect 82 | in a youtube video if you did not get it. 83 | 84 | # TODO 85 | - Security, validations, fix vulnerabilities 86 | - File upload validation 87 | - unit testing 88 | - Create annotation to require authenticated user to be admin 89 | - Considering changing the project structure to a flask-ish way, with extensions clearly separated in a separate file 90 | even though the way it is implemented right now is not that bad 91 | - I do not think it is a good idea to put the get_dto inside the model itself, that should be placed in the corresponding 92 | serializer class to clearly separate the concerns, this is easy to refactor 93 | 94 | # Resources 95 | - [](https://flask-jwt-extended.readthedocs.io/en/latest/api.html#flask_jwt_extended.JWTManager.user_identity_loader -------------------------------------------------------------------------------- /orders/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | from flask_jwt_extended import jwt_required, get_jwt_claims, current_user, jwt_optional 3 | from sqlalchemy import desc 4 | 5 | from addresses.models import Address 6 | from comments.models import Comment 7 | from ecommerce_api.factory import db 8 | from orders.models import Order, OrderItem 9 | from orders.serializers import OrderListSerializer 10 | from products.models import Product 11 | from routes import blueprint 12 | from shared.serializers import get_success_response, get_error_response 13 | 14 | 15 | @blueprint.route('/orders', methods=['GET']) 16 | @jwt_required 17 | def my_orders(): 18 | page_size = request.args.get('page_size', 5) 19 | page = request.args.get('page', 1) 20 | 21 | claims = get_jwt_claims() 22 | user_id = claims.get('user_id') 23 | 24 | orders = Order.query.filter_by(user_id=user_id).order_by(desc(Order.created_at)) \ 25 | .paginate(page=page, 26 | per_page=page_size) 27 | return jsonify(OrderListSerializer(orders, include_user=True).get_data()), 200 28 | 29 | 30 | @blueprint.route('/orders/', methods=['GET']) 31 | @jwt_required 32 | def order_details(order_id): 33 | order = Order.query.get(order_id) 34 | user = current_user 35 | if order.user_id is user.id or user.is_admin(): 36 | return jsonify(order.get_summary(include_order_items=True)), 200 37 | else: 38 | return get_error_response('Access denied, this does not belong to you', status_code=401) 39 | 40 | 41 | @blueprint.route('/orders', methods=['POST']) 42 | @jwt_optional 43 | def create_order(): 44 | user = current_user 45 | # You can not check is user is not None because user is LocalProxy even when no authenticated 46 | # to check if the user is authenticated we may do hasattr 47 | user_id = user.id if hasattr(user, 'id') else None 48 | 49 | address_id = request.json.get('address_id', None) 50 | 51 | if address_id is not None: 52 | # reusing address, the user has to be authenticated and owning that address 53 | address = Address.query.filter_by(id=address_id, user_id=user_id).first() 54 | if address is None: 55 | return get_error_response('Permission Denied, you can not use this address', 401) 56 | else: 57 | first_name = request.json.get('first_name', None) 58 | last_name = request.json.get('last_name', None) 59 | zip_code = request.json.get('zip_code', None) 60 | street_address = request.json.get('address', None) 61 | country = request.json.get('address', None) 62 | city = request.json.get('address', None) 63 | 64 | if user_id is not None: 65 | if first_name is None: 66 | first_name = user.first_name 67 | 68 | if last_name is None: 69 | last_name = user.last_name 70 | 71 | address = Address(first_name=first_name, last_name=last_name, city=city, country=country, 72 | street_address=street_address, zip_code=zip_code, ) 73 | if hasattr(user, 'id'): 74 | address.user_id = user.id 75 | 76 | db.session.add(address) 77 | db.session.flush() # we would need the address.id so let's save the address to the db to have the id 78 | 79 | import faker 80 | fake = faker.Faker() 81 | order = Order(order_status=0, tracking_number=fake.uuid4(), address_id=address.id) 82 | 83 | cart_items = request.json.get('cart_items') 84 | product_ids = [ci['id'] for ci in cart_items] 85 | products = db.session.query(Product).filter(Product.id.in_(product_ids)).all() 86 | if len(products) != len(cart_items): 87 | return get_error_response('Error, make sure all products you want to order are still available') 88 | 89 | for index, product in enumerate(products): 90 | order.order_items.append(OrderItem(price=product.price, 91 | quantity=cart_items[index]['quantity'], product=product, 92 | name=product.name, 93 | slug=product.slug, 94 | user_id=user_id)) 95 | 96 | db.session.add(order) 97 | db.session.commit() 98 | return get_success_response('Order created successfully', data=order.get_summary(include_order_items=True), 99 | status_code=200) 100 | -------------------------------------------------------------------------------- /products/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from flask import Blueprint, request, jsonify 5 | from flask_jwt_extended import jwt_required, current_user 6 | from sqlalchemy import desc 7 | from werkzeug.utils import secure_filename 8 | 9 | from categories.models import Category 10 | from ecommerce_api.factory import db, app 11 | from file_uploads.models import ProductImage 12 | from products.models import Product 13 | from products.serializers import ProductListSerializer, ProductDetailsSerializer 14 | from routes import blueprint 15 | from shared.database import get_or_create 16 | from shared.security import validate_file_upload 17 | from shared.serializers import get_error_response, get_success_response 18 | from tags.models import Tag 19 | 20 | product_blueprint = Blueprint('product', __name__) 21 | 22 | 23 | @blueprint.route('/products', methods=['GET']) 24 | def list_products(): 25 | page = request.args.get('page', 1) 26 | page_size = request.args.get('page', 5) 27 | # products = Product.query.order_by(desc(Product.publish_on)).offset((page - 1) * page_size).limit(page_size).all() 28 | products = Product.query.order_by(desc(Product.publish_on)).paginate(page=1, per_page=5) 29 | return jsonify(ProductListSerializer(products).get_data()), 200 30 | 31 | 32 | @blueprint.route('/products/', methods=['GET']) 33 | def show(product_slug): 34 | product = Product.query.filter_by(slug=product_slug).first() 35 | # product = Product.query.filter_by(slug=product_slug).first_or_404() 36 | return jsonify(ProductDetailsSerializer(product).data), 200 37 | 38 | 39 | @blueprint.route('/products/by_id/', methods=['GET']) 40 | def by_id(product_id): 41 | product = Product.query.get(product_id) 42 | # product = Product.query.filter_by(slug=product_slug).first_or_404() 43 | return jsonify(ProductDetailsSerializer(product).data), 200 44 | 45 | 46 | @blueprint.route('/products', methods=['POST']) 47 | @jwt_required 48 | def create(): 49 | if current_user.is_not_admin(): 50 | return jsonify(get_error_response('Permission denied, you must be admin', status_code=401)) 51 | 52 | product_name = request.form.get('name') 53 | description = request.form.get('description') 54 | price = request.form.get('price') 55 | stock = request.form.get('stock') 56 | tags = [] 57 | categories = [] 58 | 59 | for header_key in list(request.form.keys()): 60 | if 'tags[' in header_key: 61 | name = header_key[header_key.find("[") + 1:header_key.find("]")] 62 | description = request.form[header_key] 63 | tags.append(get_or_create(db.session, Tag, {'description': description}, name=name)[0]) 64 | 65 | if header_key.startswith('categories['): 66 | result = re.search('\[(.*?)\]', header_key) 67 | if len(result.groups()) == 1: 68 | name = result.group(1) 69 | description = request.form[header_key] 70 | categories.append( 71 | get_or_create(db.session, Category, {'description': description}, 72 | name=name)[0]) 73 | 74 | product = Product(name=product_name, description=description, price=price, stock=stock, 75 | tags=tags, categories=categories) 76 | 77 | if 'images[]' in request.files: 78 | for image in request.files.getlist('images[]'): 79 | if image and validate_file_upload(image.filename): 80 | filename = secure_filename(image.filename) 81 | dir_path = app.config['IMAGES_LOCATION'] 82 | dir_path = os.path.join((os.path.join(dir_path, 'products'))) 83 | 84 | if not os.path.exists(dir_path): 85 | os.makedirs(dir_path) 86 | 87 | file_path = os.path.join(dir_path, filename) 88 | image.save(file_path) 89 | 90 | file_path = file_path.replace(app.config['IMAGES_LOCATION'].rsplit(os.sep, 2)[0], '') 91 | if image.content_length == 0: 92 | file_size = image.content_length 93 | else: 94 | file_size = os.stat(file_path).st_size 95 | 96 | product_image = ProductImage(file_path=file_path, file_name=filename, original_name=image.filename, 97 | file_size=file_size) 98 | product.images.append(product_image) 99 | 100 | db.session.add(product) 101 | db.session.commit() 102 | 103 | response = {'full_messages': ['Product created successfully']} 104 | response.update(ProductDetailsSerializer(product).data) 105 | return jsonify(response) 106 | 107 | 108 | @blueprint.route('/products/', methods=['PUT']) 109 | @jwt_required 110 | def update(product_slug): 111 | name = request.json.get('name') 112 | description = request.json.get('description') 113 | stock = request.json.get('stock') 114 | price = request.json.get('price') 115 | 116 | if not (name and description and price and stock and price): 117 | return jsonify(get_error_response('You must provide a name, description, stock and price')) 118 | 119 | product = Product.query.filter_by(slug=product_slug).first() 120 | if product is None: 121 | return get_error_response(messages='not found', status_code=404) 122 | 123 | product.name = name 124 | product.description = description 125 | product.price = price 126 | product.body = stock 127 | 128 | tags_input = request.json.get('tags') 129 | categories_input = request.json.get('categories') 130 | tags = [] 131 | categories = [] 132 | if categories_input: 133 | for category in categories_input: 134 | categories.append( 135 | get_or_create(db.session, Category, {'description': category.get('description', None)}, 136 | name=category['name'])[0]) 137 | 138 | if tags_input: 139 | for tag in tags_input: 140 | tags.append(get_or_create(db.session, Tag, {'description': tag.get('description')}, name=tag['name'])[0]) 141 | 142 | product.tags = tags 143 | product.categories = categories 144 | db.session.commit() 145 | response = {'full_messages': ['Product updated successfully']} 146 | response.update(ProductDetailsSerializer(product).data) 147 | return jsonify(response) 148 | 149 | 150 | @blueprint.route('/products/', methods=['DELETE']) 151 | @jwt_required 152 | def destroy(product_slug): 153 | product = Product.query.filter_by(slug=product_slug).first() 154 | db.session.delete(product) 155 | db.session.commit() 156 | return get_success_response('Product deleted successfully') 157 | 158 | 159 | @blueprint.route('/products/by_id/', methods=['DELETE']) 160 | @jwt_required 161 | def destroy_by_id(product_id): 162 | product = Product.query.get(product_id).first() 163 | db.session.delete(product) 164 | db.session.commit() 165 | return get_success_response('Product deleted successfully') 166 | -------------------------------------------------------------------------------- /migrations/versions/2728d2dc2146_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2728d2dc2146 4 | Revises: 5 | Create Date: 2019-02-17 17:29:11.553000 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '2728d2dc2146' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('categories', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=255), nullable=True), 24 | sa.Column('slug', sa.String(length=255), nullable=True), 25 | sa.Column('description', sa.String(), nullable=True), 26 | sa.Column('created_at', sa.DateTime(), nullable=True), 27 | sa.Column('updated_at', sa.DateTime(), nullable=True), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | op.create_index(op.f('ix_categories_created_at'), 'categories', ['created_at'], unique=False) 31 | op.create_index(op.f('ix_categories_slug'), 'categories', ['slug'], unique=True) 32 | op.create_table('products', 33 | sa.Column('id', sa.Integer(), nullable=False), 34 | sa.Column('name', sa.String(length=255), nullable=False), 35 | sa.Column('slug', sa.String(), nullable=True), 36 | sa.Column('description', sa.Text(), nullable=False), 37 | sa.Column('price', sa.Integer(), nullable=False), 38 | sa.Column('stock', sa.Integer(), nullable=False), 39 | sa.Column('created_at', sa.DateTime(), nullable=False), 40 | sa.Column('updated_at', sa.DateTime(), nullable=False), 41 | sa.Column('publish_on', sa.DateTime(), nullable=True), 42 | sa.PrimaryKeyConstraint('id') 43 | ) 44 | op.create_index(op.f('ix_products_created_at'), 'products', ['created_at'], unique=False) 45 | op.create_index(op.f('ix_products_publish_on'), 'products', ['publish_on'], unique=False) 46 | op.create_index(op.f('ix_products_slug'), 'products', ['slug'], unique=True) 47 | op.create_table('roles', 48 | sa.Column('id', sa.Integer(), nullable=False), 49 | sa.Column('name', sa.String(length=80), nullable=False), 50 | sa.Column('description', sa.String(length=100), nullable=True), 51 | sa.PrimaryKeyConstraint('id'), 52 | sa.UniqueConstraint('name') 53 | ) 54 | op.create_table('tags', 55 | sa.Column('id', sa.Integer(), nullable=False), 56 | sa.Column('name', sa.String(length=100), nullable=True), 57 | sa.Column('slug', sa.String(), nullable=True), 58 | sa.Column('description', sa.String(), nullable=True), 59 | sa.Column('created_at', sa.DateTime(), nullable=True), 60 | sa.Column('updated_at', sa.DateTime(), nullable=True), 61 | sa.PrimaryKeyConstraint('id') 62 | ) 63 | op.create_index(op.f('ix_tags_created_at'), 'tags', ['created_at'], unique=False) 64 | op.create_index(op.f('ix_tags_slug'), 'tags', ['slug'], unique=True) 65 | op.create_table('users', 66 | sa.Column('id', sa.Integer(), nullable=False), 67 | sa.Column('username', sa.String(length=64), nullable=True), 68 | sa.Column('email', sa.String(length=120), nullable=True), 69 | sa.Column('password', sa.String(length=128), nullable=True), 70 | sa.Column('first_name', sa.String(length=300), nullable=False), 71 | sa.Column('last_name', sa.String(length=300), nullable=False), 72 | sa.Column('created_at', sa.DateTime(), nullable=False), 73 | sa.Column('updated_at', sa.DateTime(), nullable=False), 74 | sa.PrimaryKeyConstraint('id') 75 | ) 76 | op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) 77 | op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True) 78 | op.create_table('addresses', 79 | sa.Column('id', sa.Integer(), nullable=False), 80 | sa.Column('first_name', sa.String(), nullable=False), 81 | sa.Column('last_name', sa.String(), nullable=False), 82 | sa.Column('city', sa.String(), nullable=False), 83 | sa.Column('country', sa.String(), nullable=False), 84 | sa.Column('zip_code', sa.String(), nullable=False), 85 | sa.Column('street_address', sa.String(), nullable=False), 86 | sa.Column('phone_number', sa.String(), nullable=True), 87 | sa.Column('user_id', sa.Integer(), nullable=True), 88 | sa.Column('created_at', sa.DateTime(), nullable=False), 89 | sa.Column('updated_at', sa.DateTime(), nullable=False), 90 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 91 | sa.PrimaryKeyConstraint('id') 92 | ) 93 | op.create_table('comments', 94 | sa.Column('id', sa.Integer(), nullable=False), 95 | sa.Column('content', sa.Text(), nullable=False), 96 | sa.Column('rating', sa.Integer(), nullable=True), 97 | sa.Column('user_id', sa.Integer(), nullable=False), 98 | sa.Column('product_id', sa.Integer(), nullable=False), 99 | sa.Column('created_at', sa.DateTime(), nullable=False), 100 | sa.Column('updated_at', sa.DateTime(), nullable=False), 101 | sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), 102 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 103 | sa.PrimaryKeyConstraint('id') 104 | ) 105 | op.create_table('file_uploads', 106 | sa.Column('id', sa.Integer(), nullable=False), 107 | sa.Column('type', sa.String(length=15), nullable=True), 108 | sa.Column('file_path', sa.String(), nullable=False), 109 | sa.Column('file_name', sa.String(), nullable=False), 110 | sa.Column('file_size', sa.Integer(), nullable=False), 111 | sa.Column('original_name', sa.String(), nullable=False), 112 | sa.Column('created_at', sa.DateTime(), nullable=False), 113 | sa.Column('updated_at', sa.DateTime(), nullable=False), 114 | sa.Column('tag_id', sa.Integer(), nullable=True), 115 | sa.Column('product_id', sa.Integer(), nullable=True), 116 | sa.Column('category_id', sa.Integer(), nullable=True), 117 | sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ), 118 | sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), 119 | sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ), 120 | sa.PrimaryKeyConstraint('id') 121 | ) 122 | op.create_table('products_categories', 123 | sa.Column('category_id', sa.Integer(), nullable=True), 124 | sa.Column('product_id', sa.Integer(), nullable=True), 125 | sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ), 126 | sa.ForeignKeyConstraint(['product_id'], ['products.id'], ) 127 | ) 128 | op.create_table('products_tags', 129 | sa.Column('product_id', sa.Integer(), nullable=False), 130 | sa.Column('tag_id', sa.Integer(), nullable=False), 131 | sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), 132 | sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ), 133 | sa.UniqueConstraint('product_id', 'tag_id', name='same_tag_for_same_product') 134 | ) 135 | op.create_table('users_roles', 136 | sa.Column('user_id', sa.Integer(), nullable=True), 137 | sa.Column('role_id', sa.Integer(), nullable=True), 138 | sa.Column('created_at', sa.DateTime(), nullable=False), 139 | sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), 140 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ) 141 | ) 142 | op.create_table('orders', 143 | sa.Column('id', sa.Integer(), nullable=False), 144 | sa.Column('order_status', sa.Integer(), nullable=True), 145 | sa.Column('tracking_number', sa.String(), nullable=True), 146 | sa.Column('address_id', sa.Integer(), nullable=False), 147 | sa.Column('user_id', sa.Integer(), nullable=True), 148 | sa.Column('created_at', sa.DateTime(), nullable=False), 149 | sa.ForeignKeyConstraint(['address_id'], ['addresses.id'], ), 150 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 151 | sa.PrimaryKeyConstraint('id') 152 | ) 153 | op.create_table('order_items', 154 | sa.Column('id', sa.Integer(), nullable=False), 155 | sa.Column('name', sa.String(), nullable=False), 156 | sa.Column('slug', sa.String(), nullable=True), 157 | sa.Column('price', sa.Integer(), nullable=False), 158 | sa.Column('quantity', sa.Integer(), nullable=False), 159 | sa.Column('order_id', sa.Integer(), nullable=False), 160 | sa.Column('product_id', sa.Integer(), nullable=False), 161 | sa.Column('user_id', sa.Integer(), nullable=True), 162 | sa.Column('created_at', sa.DateTime(), nullable=False), 163 | sa.Column('updated_at', sa.DateTime(), nullable=True), 164 | sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ), 165 | sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), 166 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 167 | sa.PrimaryKeyConstraint('id') 168 | ) 169 | op.create_index(op.f('ix_order_items_name'), 'order_items', ['name'], unique=False) 170 | op.create_index(op.f('ix_order_items_price'), 'order_items', ['price'], unique=False) 171 | op.create_index(op.f('ix_order_items_quantity'), 'order_items', ['quantity'], unique=False) 172 | # ### end Alembic commands ### 173 | 174 | 175 | def downgrade(): 176 | # ### commands auto generated by Alembic - please adjust! ### 177 | op.drop_index(op.f('ix_order_items_quantity'), table_name='order_items') 178 | op.drop_index(op.f('ix_order_items_price'), table_name='order_items') 179 | op.drop_index(op.f('ix_order_items_name'), table_name='order_items') 180 | op.drop_table('order_items') 181 | op.drop_table('orders') 182 | op.drop_table('users_roles') 183 | op.drop_table('products_tags') 184 | op.drop_table('products_categories') 185 | op.drop_table('file_uploads') 186 | op.drop_table('comments') 187 | op.drop_table('addresses') 188 | op.drop_index(op.f('ix_users_username'), table_name='users') 189 | op.drop_index(op.f('ix_users_email'), table_name='users') 190 | op.drop_table('users') 191 | op.drop_index(op.f('ix_tags_slug'), table_name='tags') 192 | op.drop_index(op.f('ix_tags_created_at'), table_name='tags') 193 | op.drop_table('tags') 194 | op.drop_table('roles') 195 | op.drop_index(op.f('ix_products_slug'), table_name='products') 196 | op.drop_index(op.f('ix_products_publish_on'), table_name='products') 197 | op.drop_index(op.f('ix_products_created_at'), table_name='products') 198 | op.drop_table('products') 199 | op.drop_index(op.f('ix_categories_slug'), table_name='categories') 200 | op.drop_index(op.f('ix_categories_created_at'), table_name='categories') 201 | op.drop_table('categories') 202 | # ### end Alembic commands ### 203 | -------------------------------------------------------------------------------- /seed_database.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | import sys 4 | 5 | import faker 6 | from sqlalchemy.orm import load_only 7 | from sqlalchemy.sql import ClauseElement 8 | from sqlalchemy.sql.expression import func 9 | 10 | from addresses.models import Address 11 | 12 | from categories.models import Category 13 | from comments.models import Comment 14 | from ecommerce_api.factory import db, bcrypt 15 | from file_uploads.models import ProductImage, TagImage, CategoryImage 16 | from orders.models import Order, OrderItem 17 | from products.models import Product 18 | from roles.models import Role, UserRole 19 | from tags.models import Tag 20 | from users.models import User 21 | 22 | fake = faker.Faker() 23 | tags = [] 24 | categories = [] 25 | 26 | 27 | def get_or_create(session, model, defaults=None, **kwargs): 28 | instance = session.query(model).filter_by(**kwargs).first() 29 | if instance: 30 | return instance, False 31 | else: 32 | params = dict((k, v) for k, v in kwargs.iteritems() if not isinstance(v, ClauseElement)) 33 | params.update(defaults or {}) 34 | instance = model(**params) 35 | session.add(instance) 36 | return instance, True 37 | 38 | 39 | def encrypt_password(password): 40 | return bcrypt.generate_password_hash(password).decode('utf-8') 41 | 42 | 43 | def generate_image(model): 44 | # pattern = "".join([random.choice(['?', '#']) for i in range(0, 10)]) + '.png' 45 | filename_pattern = "".join(fake.random_choices(elements=('?', '#'), 46 | length=fake.random_int(min=16, max=32))) + '.png' 47 | # file_name=fake.md5(raw_output=False) + '.png' 48 | return model(file_name="".join(fake.random_letters(length=16)) + '.png', 49 | file_path=fake.image_url(width=None, height=None), 50 | file_size=fake.random_int(min=1000, max=15000), 51 | original_name=fake.bothify(text=filename_pattern)) 52 | 53 | 54 | def seed_admin(): 55 | role, created = get_or_create(db.session, Role, 56 | defaults={'description': 'for admin only'}, 57 | name='ROLE_ADMIN') 58 | ''' 59 | # count admin 60 | admin_count = User.query.filter(User.roles.any(id=role.id)).count() 61 | 62 | # 4 ways of retrieving the admin users 63 | admin_users = User.query.filter(User.users_roles.any(role_id=role.id)).all() 64 | admin_users = User.query.filter(User.roles.any(id=role.id)).all() 65 | admin_users = User.query.filter(User.roles.any(name='ROLE_ADMIN')).all() 66 | admin_users = User.query.join(User.roles).filter_by(name=role.name).all() 67 | ''' 68 | 69 | user, created = get_or_create(db.session, User, defaults={'first_name': 'adminFN', 70 | 'last_name': 'adminFN', 71 | 'email': 'admin@flaskblogapi.app', 72 | 'password': bcrypt.generate_password_hash('password')}, 73 | **{'username': 'admin'}) 74 | 75 | db.session.commit() 76 | 77 | if len(user.roles) == 0: 78 | # user.users_roles.append(UserRole(user_id=user.id, role_id=role.id)) 79 | user.roles.append(role) 80 | db.session.commit() 81 | 82 | 83 | def seed_authors(): 84 | role, created = get_or_create(db.session, Role, defaults={'description': 'for authors only'}, 85 | name='ROLE_AUTHOR') 86 | 87 | authors_count = db.session.query(User.id).filter(User.roles.any(id=role.id)).count() 88 | authors_count = User.query.filter(User.roles.any(id=role.id)).count() 89 | authors_to_seed = 5 90 | authors_to_seed -= authors_count 91 | 92 | for i in range(0, authors_to_seed): 93 | profile = fake.profile(fields='username,mail,name') 94 | username = profile['username'] 95 | first_name = profile['name'].split()[0] 96 | last_name = profile['name'].split()[1] 97 | email = profile['mail'] 98 | password = bcrypt.generate_password_hash('password') 99 | user = User(username=username, first_name=first_name, last_name=last_name, email=email, 100 | password=password, roles=[role]) 101 | db.session.add(user) 102 | db.session.commit() 103 | # db.session.add(UserRole(user_id=user.id, role_id=role.id)) 104 | 105 | db.session.commit() 106 | 107 | 108 | def seed_users(): 109 | role, created = get_or_create(db.session, Role, 110 | defaults={'description': 'for standard users'}, 111 | name='ROLE_USER') 112 | db.session.commit() 113 | non_standard_user_ids = db.session.query(User.id) \ 114 | .filter(~User.roles.any(id=role.id)).all() 115 | 116 | all_users_count = db.session.query(func.count(User.id)).all()[0][0] 117 | all_users_count = db.session.query(User.id).count() 118 | 119 | # User.query.filter(User.roles.any(UserRole.role_id.in_([1,2]))).count() 120 | standard_users_count = db.session.query(User).filter(User.roles.any(UserRole.role_id.in_([role.id]))).count() 121 | standard_users_count = db.session.query(User.id).filter( 122 | User.roles.any(id=role.id)).count() 123 | 124 | users_to_seed = 23 125 | users_to_seed -= standard_users_count 126 | sys.stdout.write('[+] Seeding %d users\n' % users_to_seed) 127 | 128 | for i in range(0, users_to_seed): 129 | profile = fake.profile(fields='username,mail,name') 130 | username = profile['username'] 131 | # fake.first_name() fake.first_name_male() fake.first_name_female(), same for last_name() 132 | first_name = profile['name'].split()[0] 133 | last_name = profile['name'].split()[1] 134 | email = profile['mail'] 135 | password = bcrypt.generate_password_hash('password') 136 | user = User(username=username, first_name=first_name, last_name=last_name, email=email, 137 | password=password) 138 | user.roles.append(role) 139 | db.session.add(user) 140 | db.session.commit() 141 | 142 | db.session.commit() 143 | 144 | 145 | def seed_tags(): 146 | sys.stdout.write('[+] Seeding tags\n') 147 | pairs = [] 148 | 149 | tag, created = get_or_create(db.session, Tag, defaults={'description': 'shoes for everyone'}, name='Shoes') 150 | tags.append(tag) 151 | pairs.append((tag, created)) 152 | 153 | tag, created = get_or_create(db.session, Tag, defaults={'description': 'jeans for everyone'}, name='Jeans') 154 | tags.append(tag) 155 | pairs.append((tag, created)) 156 | 157 | tag, created = get_or_create(db.session, Tag, defaults={'description': 'jackets for everyone'}, name='Jackets') 158 | tags.append(tag) 159 | pairs.append((tag, created)) 160 | 161 | tag, created = get_or_create(db.session, Tag, defaults={'description': 'shorts for everyone'}, name='Shorts') 162 | tags.append(tag) 163 | pairs.append((tag, created)) 164 | 165 | for pair in pairs: 166 | if pair[1]: # if created 167 | for i in range(0, random.randint(1, 2)): 168 | pi = generate_image(TagImage) 169 | pair[0].images.append(pi) 170 | 171 | db.session.add_all(tags) 172 | db.session.commit() 173 | 174 | 175 | def seed_categories(): 176 | sys.stdout.write('[+] Seeding categories\n') 177 | pairs = [] 178 | category, created = get_or_create(db.session, Category, 179 | defaults={'description': 'clothes for men'}, 180 | name='Men') 181 | categories.append(category) 182 | pairs.append((category, created)) 183 | 184 | category, created = get_or_create(db.session, Category, 185 | defaults={'description': 'clothes for women'}, name='Women') 186 | categories.append(category) 187 | pairs.append((category, created)) 188 | 189 | category, created = get_or_create(db.session, Category, 190 | defaults={'description': 'clothes for kids'}, name='Kids') 191 | categories.append(category) 192 | pairs.append((category, created)) 193 | 194 | category, created = get_or_create(db.session, Category, 195 | defaults={'description': 'clothes for teenagers'}, name='Teenagers') 196 | categories.append(category) 197 | pairs.append((category, created)) 198 | 199 | for pair in pairs: 200 | if pair[1]: # if created 201 | for i in range(0, random.randint(1, 2)): 202 | category_image = generate_image(CategoryImage) 203 | pair[0].images.append(category_image) 204 | 205 | db.session.add_all(categories) 206 | db.session.commit() 207 | 208 | 209 | def seed_products(): 210 | products_count = db.session.query(func.count(Product.id)).all()[0][0] 211 | products_to_seed = 23 212 | sys.stdout.write('[+] Seeding %d products\n' % products_to_seed) 213 | 214 | # tag_ids = [tag[0] for tag in db.session.query(Tag.id).all()] 215 | # category_ids = [category[0] for category in db.session.query(Category.id).all()] 216 | 217 | for i in range(products_count, products_to_seed): 218 | name = fake.sentence() 219 | description = fake.text() 220 | 221 | start_date = datetime.date(year=2017, month=1, day=1) 222 | random_date = fake.date_between(start_date=start_date, end_date='+4y') 223 | publish_on = random_date 224 | product = Product(name=name, description=description, price=fake.random_int(min=50, max=2500), 225 | stock=fake.random_int(min=5, max=1000), publish_on=publish_on) 226 | 227 | # product.tags.append(db.session.query(Tag).order_by(func.random()).first()) 228 | 229 | tags_for_product = [] 230 | categories_for_product = [] 231 | 232 | for i in range(0, random.randint(1, 2)): 233 | tag_to_add = random.choice(tags) 234 | if tag_to_add.id not in tags_for_product: 235 | product.tags.append(tag_to_add) 236 | tags_for_product.append(tag_to_add.id) 237 | 238 | for i in range(0, random.randint(1, 2)): 239 | category_to_add = random.choice(categories) 240 | if category_to_add.id not in categories_for_product: 241 | product.categories.append(category_to_add) 242 | categories_for_product.append(category_to_add.id) 243 | 244 | for i in range(0, random.randint(1, 2)): 245 | product_image = generate_image(ProductImage) 246 | product.images.append(product_image) 247 | 248 | db.session.add(product) 249 | db.session.commit() 250 | 251 | 252 | def seed_comments(): 253 | comments_count = db.session.query(func.count(Comment.id)).scalar() 254 | comments_to_seed = 31 255 | comments_to_seed -= comments_count 256 | sys.stdout.write('[+] Seeding %d comments\n' % comments_to_seed) 257 | comments = [] 258 | 259 | user_ids = [user[0] for user in User.query.with_entities(User.id).all()] 260 | product_ids = [product[0] for product in Product.query.with_entities(Product.id)] 261 | # equivalent: 262 | # user_ids = [user[0] for user in db.session.query(User.id).all()] 263 | # product_ids = [product[0] for product in db.session.query(Product.id).all()] 264 | 265 | for i in range(comments_count, comments_to_seed): 266 | user_id = random.choice(user_ids) 267 | product_id = random.choice(product_ids) 268 | rating = fake.random_int(min=1, max=5) if fake.boolean(chance_of_getting_true=50) else None 269 | comments.append(Comment(product_id=product_id, 270 | user_id=user_id, rating=rating, 271 | content=fake.paragraph(nb_sentences=4, variable_nb_sentences=True, ext_word_list=None))) 272 | 273 | db.session.add_all(comments) 274 | db.session.commit() 275 | 276 | 277 | def seed_addresses(): 278 | addresses_count = db.session.query(func.count(Address.id)).scalar() 279 | addresses_to_seed = 30 280 | user_ids = [user[0] for user in db.session.query(User.id).all()] 281 | 282 | for i in range(addresses_count, addresses_to_seed): 283 | user_id = random.choice(user_ids) if fake.boolean(chance_of_getting_true=80) else None 284 | 285 | first_name = fake.first_name() 286 | last_name = fake.last_name() 287 | zip_code = fake.zipcode_plus4() # postcode(), postalcode(), zipcode(), postalcode_plus4 288 | street_address = fake.address() 289 | phone_number = fake.phone_number() 290 | city = fake.city() 291 | country = fake.country() 292 | db.session.add(Address(user_id=user_id, first_name=first_name, last_name=last_name, zip_code=zip_code, 293 | street_address=street_address, phone_number=phone_number, city=city, country=country)) 294 | 295 | db.session.commit() 296 | 297 | 298 | def seed_orders(): 299 | orders_count = db.session.query(func.count(Order.id)).scalar() 300 | orders_to_seed = 31 301 | addresses = db.session.query(Address).options(load_only('id', 'user_id')).all() 302 | products = db.session.query(Product).options(load_only('id', 'name', 'slug', 'price')).all() 303 | 304 | for i in range(orders_count, orders_to_seed): 305 | address = random.choice(addresses) 306 | tracking_number = fake.uuid4() 307 | order_status = fake.random_int(min=0, max=2) 308 | user_id = address.user_id 309 | order = Order(tracking_number=tracking_number, order_status=order_status, address_id=address.id, 310 | user_id=user_id) 311 | 312 | db.session.add(order) 313 | 314 | ''' 315 | this is to save the order now, so I can have the id to be used in order items 316 | or the other way is to comment flush(), order_id=order.id, and session.add(oi). 317 | Instead use order.order_items.append(oi); See below. Both ways lead to the same result 318 | ''' 319 | 320 | db.session.flush() 321 | 322 | for i in range(fake.random_int(min=1, max=6)): 323 | product = random.choice(products) 324 | oi = OrderItem(name=product.name, slug=product.slug, price=product.price, 325 | order_id=order.id, 326 | product_id=product.id, user_id=user_id, quantity=fake.random_int(min=1, max=5)) 327 | db.session.add(oi) 328 | 329 | # order.order_items.append(oi) 330 | 331 | db.session.commit() 332 | 333 | 334 | if __name__ == '__main__': 335 | seed_admin() 336 | seed_users() 337 | seed_tags() 338 | seed_categories() 339 | seed_products() 340 | seed_comments() 341 | seed_addresses() 342 | seed_orders() 343 | --------------------------------------------------------------------------------