├── .gitignore ├── .travis.yml ├── __init__.py ├── app ├── __init__.py ├── admin.py ├── assets.py ├── food │ ├── __init__.py │ ├── forms.py │ ├── routes.py │ ├── templates │ │ └── food │ │ │ ├── profile.html │ │ │ └── upload.html │ ├── utils.py │ └── views.py ├── models.py ├── restaurant │ ├── __init__.py │ ├── forms.py │ ├── routes.py │ ├── templates │ │ └── restaurant │ │ │ ├── profile.html │ │ │ └── upload.html │ ├── utils.py │ └── views.py ├── routes.py ├── static │ ├── css │ │ ├── .gitkeep │ │ └── sticky-footer.css │ ├── img │ │ ├── food │ │ │ └── .gitkeep │ │ ├── restaurant │ │ │ └── .gitkeep │ │ └── user │ │ │ └── .gitkeep │ └── js │ │ └── .gitkeep ├── templates │ ├── error │ │ ├── 403.html │ │ ├── 404.html │ │ └── 500.html │ ├── index.html │ ├── layout │ │ ├── base.html │ │ └── partials │ │ │ ├── footer.html │ │ │ └── header.html │ ├── macros │ │ └── _render_field.html │ └── security │ │ ├── login.html │ │ └── register.html └── user │ ├── __init__.py │ ├── forms.py │ ├── oauth.py │ ├── routes.py │ ├── templates │ └── user │ │ ├── profile.html │ │ └── upload.html │ ├── utils.py │ └── views.py ├── config.py ├── manage.py ├── readme.md ├── requirements.txt ├── run.py └── tests ├── __init__.py ├── test_back_end.py └── test_front_end.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | share/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | .venv/ 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | ### VirtualEnv template 95 | # Virtualenv 96 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 97 | .Python 98 | [Bb]in 99 | [Ii]nclude 100 | [Ll]ib 101 | [Ll]ib64 102 | [Ll]ocal 103 | 104 | [Ss]cripts 105 | pyvenv.cfg 106 | .venv 107 | pip-selfcheck.json 108 | 109 | app/static/img/food/* 110 | !app/static/img/food/.gitkeep 111 | app/static/img/restaurant/* 112 | !app/static/img/restaurant/.gitkeep 113 | app/static/img/user/* 114 | !app/static/img/user/.gitkeep 115 | 116 | 117 | !app/static/css/.gitkeep 118 | !app/static/js/.gitkeep 119 | 120 | .idea/ 121 | .vs 122 | env/ 123 | /app/static/vendor/ 124 | /app/static/js/libs.js 125 | /app/static/css/min.css 126 | /migrations/ 127 | /app.db 128 | /testing.db 129 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: required 3 | addons: 4 | chrome: stable 5 | python: 6 | - "2.7" 7 | - "3.4" 8 | - "3.5" 9 | - "3.6" 10 | cache: pip 11 | install: 12 | - pip install -r requirements.txt 13 | before_script: 14 | - sudo apt-get install xvfb libxi6 libgconf-2-4 15 | - wget http://chromedriver.storage.googleapis.com/2.44/chromedriver_linux64.zip 16 | - unzip chromedriver_linux64.zip 17 | - sudo cp chromedriver /bin/ 18 | - sudo mv -f chromedriver /usr/local/share/chromedriver 19 | - sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver 20 | - sudo ln -s /usr/local/share/chromedriver /usr/bin/chromedriver 21 | - sudo chmod a+x /usr/local/bin/chromedriver 22 | script: 23 | - whereis google-chrome-stable 24 | - whereis chromedriver 25 | - python -m unittest discover tests/ -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/__init__.py -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | from flask import Flask, render_template, current_app 4 | from flask_assets import Environment 5 | from flask_wtf import CSRFProtect 6 | from flask_security import Security, SQLAlchemyUserDatastore, utils 7 | from flask_via import Via 8 | from flask_uploads import configure_uploads 9 | 10 | from sqlalchemy_utils import database_exists, create_database 11 | from sqlalchemy import create_engine 12 | 13 | from .assets import create_assets 14 | from .models import db, FinalUser, Role 15 | from .user.forms import SecurityRegisterForm 16 | from .admin import create_security_admin 17 | 18 | from config import app_config 19 | 20 | import os.path 21 | 22 | 23 | user_datastore = SQLAlchemyUserDatastore(db, FinalUser, Role) 24 | 25 | 26 | def create_app(config_name): 27 | global user_datastore 28 | app = Flask(__name__) 29 | 30 | app.config.from_object(app_config[config_name]) 31 | 32 | csrf = CSRFProtect() 33 | csrf.init_app(app) 34 | 35 | assets = Environment(app) 36 | create_assets(assets) 37 | 38 | via = Via() 39 | via.init_app(app) 40 | 41 | # Code for desmostration the flask upload in several models - - - - 42 | 43 | from .user import user_photo 44 | from .restaurant import restaurant_photo 45 | from .food import food_photo 46 | 47 | configure_uploads(app, (restaurant_photo, food_photo, user_photo)) 48 | 49 | engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI']) 50 | if not database_exists(engine.url): 51 | create_database(engine.url) 52 | 53 | security = Security(app, user_datastore, register_form=SecurityRegisterForm) 54 | 55 | create_security_admin(app=app, path=os.path.join(os.path.dirname(__file__))) 56 | 57 | with app.app_context(): 58 | db.init_app(app) 59 | db.create_all() 60 | user_datastore.find_or_create_role(name='admin', description='Administrator') 61 | db.session.commit() 62 | user_datastore.find_or_create_role(name='end-user', description='End user') 63 | db.session.commit() 64 | 65 | @app.route('/', methods=['GET']) 66 | @app.route('/home', methods=['GET']) 67 | def index(): 68 | return render_template('index.html') 69 | 70 | @app.errorhandler(403) 71 | def forbidden(error): 72 | return render_template('error/403.html', title='Forbidden'), 403 73 | 74 | @app.errorhandler(404) 75 | def page_not_found(error): 76 | return render_template('error/404.html', title='Page Not Found'), 404 77 | 78 | @app.errorhandler(500) 79 | def internal_server_error(error): 80 | db.session.rollback() 81 | return render_template('error/500.html', title='Server Error'), 500 82 | 83 | return app -------------------------------------------------------------------------------- /app/admin.py: -------------------------------------------------------------------------------- 1 | from flask_admin import Admin 2 | from flask_admin.contrib.sqla import ModelView 3 | from flask_admin.contrib.fileadmin import FileAdmin 4 | from flask_security import current_user 5 | from .models import * 6 | 7 | # https://github.com/sasaporta/flask-security-admin-example/blob/master/main.py 8 | 9 | # Customized User model for SQL-Admin 10 | class UserAdmin(ModelView): 11 | 12 | # Prevent administration of Users unless the currently logged-in user has the "admin" role 13 | def is_accessible(self): 14 | return current_user.has_role('admin') 15 | 16 | class MyFileAdmin(FileAdmin): 17 | # Prevent administration of Roles unless the currently logged-in user has the "admin" role 18 | def is_accessible(self): 19 | return current_user.has_role('admin') 20 | 21 | class _Admin(Admin, UserAdmin): 22 | def add_model_view(self, model): 23 | self.add_view(UserAdmin(model, db.session)) 24 | 25 | def add_model_views(self, models): 26 | for model in models: 27 | self.add_model_view(model) 28 | 29 | def create_security_admin(app, path): 30 | admin = _Admin(app, name='Flask MVC Template', template_mode='bootstrap3') 31 | admin.add_model_views([FinalUser, Role, FinalUserImage]) 32 | admin.add_view(MyFileAdmin(path, '/static/', name='Static Files')) -------------------------------------------------------------------------------- /app/assets.py: -------------------------------------------------------------------------------- 1 | from flask_assets import Bundle 2 | 3 | def create_assets(assets): 4 | # js = Bundle( 5 | # 'vendor/jquery/dist/jquery.min.js', 6 | # output='js/libs.js' 7 | # ) 8 | # assets.register('JS_FRAMEWORS', js) 9 | 10 | css = Bundle( 11 | 'css/sticky-footer.css', 12 | output='css/min.css' 13 | ) 14 | assets.register('CSS_FRAMEWORKS', css) -------------------------------------------------------------------------------- /app/food/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_uploads import UploadSet, IMAGES 2 | 3 | food_photo = UploadSet('food', IMAGES) -------------------------------------------------------------------------------- /app/food/forms.py: -------------------------------------------------------------------------------- 1 | from wtforms import * 2 | from flask_wtf.file import FileField, FileAllowed, FileRequired 3 | from . import food_photo 4 | 5 | # Form for demo of flask-upload 6 | 7 | class FoodImageForm(Form): 8 | food_photo = FileField('', validators=[FileRequired(), FileAllowed(food_photo, 'Images only!')]) 9 | submit = SubmitField('Submit') -------------------------------------------------------------------------------- /app/food/routes.py: -------------------------------------------------------------------------------- 1 | from flask_via.routers.default import Pluggable 2 | from .views import * 3 | 4 | routes = [ 5 | Pluggable('/food/', ProfileView, 'profile'), 6 | Pluggable('/food/upload', FoodUploadView, 'upload') 7 | ] -------------------------------------------------------------------------------- /app/food/templates/food/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | 3 | 4 | {% block content %} 5 |

Food Profile

6 | {% endblock %} -------------------------------------------------------------------------------- /app/food/templates/food/upload.html: -------------------------------------------------------------------------------- 1 | {% block register %} 2 |
3 |

Upload in Food!

4 |
5 | 6 | {{ form.food_photo }} 7 | {{ form.submit }} 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /app/food/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/app/food/utils.py -------------------------------------------------------------------------------- /app/food/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, url_for, redirect, render_template, current_app 2 | from flask.views import MethodView 3 | from ..models import FinalUserImage, db 4 | from .forms import FoodImageForm 5 | from flask_security import current_user 6 | from . import food_photo 7 | # from .. import app 8 | 9 | class ProfileView(MethodView): 10 | def get(self): 11 | return render_template('food/profile.html') 12 | 13 | class FoodUploadView(MethodView): 14 | def get(self): 15 | return render_template('food/upload.html', form=FoodImageForm()) 16 | 17 | def post(self): 18 | if 'food_photo' in request.files: 19 | filename = food_photo.save(request.files['food_photo']) 20 | image = FinalUserImage(user_id=current_user.id, 21 | image_filename=filename, 22 | image_url=current_app.config['UPLOADED_FOOD_DEST'][11:] + "/" + filename) 23 | db.session.add(image) 24 | db.session.commit() 25 | return redirect(url_for('food.profile')) 26 | return redirect(url_for('food.profile')) -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from sqlalchemy import func 3 | from flask_security import UserMixin, RoleMixin 4 | import datetime 5 | 6 | # https://pythonhosted.org/Flask-Security/quickstart.html 7 | # python manage.py db upgrade && python manage.py db revision --autogenerate 8 | 9 | 10 | db = SQLAlchemy() 11 | 12 | 13 | class BaseModel(db.Model): 14 | __abstract__ = True 15 | id = db.Column(db.Integer, primary_key=True) 16 | date_created = db.Column(db.DATETIME, default=func.current_timestamp()) 17 | date_modified = db.Column(db.DATETIME, default=func.current_timestamp(), onupdate=func.current_timestamp()) 18 | 19 | 20 | roles_users = db.Table('roles_users', 21 | db.Column('final_user_id', db.Integer(), db.ForeignKey('final_user.id')), 22 | db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) 23 | 24 | 25 | class Role(db.Model, RoleMixin): 26 | id = db.Column(db.Integer(), primary_key=True) 27 | name = db.Column(db.String(80), unique=True) 28 | description = db.Column(db.String(255)) 29 | 30 | 31 | class FinalUser(db.Model, UserMixin): 32 | id = db.Column(db.Integer, primary_key=True) 33 | social_id = db.Column(db.String(64), nullable=True, unique=True) 34 | email = db.Column(db.String(255), unique=True) 35 | username = db.Column(db.String(80), unique=True) 36 | password = db.Column(db.String(255)) 37 | create_date = db.Column(db.DateTime, default=datetime.datetime.now) 38 | last_login_at = db.Column(db.DateTime()) 39 | current_login_at = db.Column(db.DateTime()) 40 | last_login_ip = db.Column(db.String(45)) 41 | current_login_ip = db.Column(db.String(45)) 42 | login_count = db.Column(db.Integer) 43 | active = db.Column(db.Boolean()) 44 | confirmed_at = db.Column(db.DateTime()) 45 | roles = db.relationship('Role', secondary=roles_users, 46 | backref=db.backref('users', lazy='dynamic')) 47 | 48 | 49 | # Code for desmostration the flask upload 50 | 51 | 52 | class FinalUserImage(BaseModel): 53 | __tablename__= 'final_user_image' 54 | user_id = db.Column(db.Integer, db.ForeignKey('final_user.id')) 55 | image_filename = db.Column(db.String, default=None, nullable=True) 56 | image_url = db.Column(db.String, default=None, nullable=True) -------------------------------------------------------------------------------- /app/restaurant/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_uploads import UploadSet, IMAGES 2 | 3 | restaurant_photo = UploadSet('restaurant', IMAGES) 4 | -------------------------------------------------------------------------------- /app/restaurant/forms.py: -------------------------------------------------------------------------------- 1 | from wtforms import * 2 | from flask_wtf.file import FileField, FileAllowed, FileRequired 3 | from . import restaurant_photo 4 | 5 | # Form for demo of flask-upload 6 | 7 | class RestaurantImageForm(Form): 8 | restaurant_photo = FileField('', validators=[FileRequired(), FileAllowed(restaurant_photo, 'Images only!')]) 9 | submit = SubmitField('Submit') -------------------------------------------------------------------------------- /app/restaurant/routes.py: -------------------------------------------------------------------------------- 1 | from flask_via.routers.default import Pluggable 2 | from .views import * 3 | 4 | routes = [ 5 | Pluggable('/restaurant/', ProfileView, 'profile'), 6 | Pluggable('/restaurant/upload', RestaurantUploadView, 'upload') 7 | ] -------------------------------------------------------------------------------- /app/restaurant/templates/restaurant/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | 3 | 4 | {% block content %} 5 |

Restaurant Profile

6 | {% endblock %} -------------------------------------------------------------------------------- /app/restaurant/templates/restaurant/upload.html: -------------------------------------------------------------------------------- 1 | {% block register %} 2 |
3 |

Upload in Restaurant!

4 |
5 | 6 | {{ form.restaurant_photo }} 7 | {{ form.submit }} 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /app/restaurant/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/app/restaurant/utils.py -------------------------------------------------------------------------------- /app/restaurant/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, url_for, redirect, render_template, current_app 2 | from flask.views import MethodView 3 | from ..models import FinalUserImage, db 4 | from .forms import RestaurantImageForm 5 | from flask_security import current_user 6 | from . import restaurant_photo 7 | # from .. import app 8 | 9 | class ProfileView(MethodView): 10 | def get(self): 11 | return render_template('restaurant/profile.html') 12 | 13 | class RestaurantUploadView(MethodView): 14 | def get(self): 15 | return render_template('restaurant/upload.html', form=RestaurantImageForm()) 16 | 17 | def post(self): 18 | if 'restaurant_photo' in request.files: 19 | filename = restaurant_photo.save(request.files['restaurant_photo']) 20 | image = FinalUserImage(user_id=current_user.id, 21 | image_filename=filename, 22 | image_url=current_app.config['UPLOADED_RESTAURANT_DEST'][11:] + "/" + filename) 23 | db.session.add(image) 24 | db.session.commit() 25 | return redirect(url_for('restaurant.profile')) 26 | return redirect(url_for('restaurant.profile')) -------------------------------------------------------------------------------- /app/routes.py: -------------------------------------------------------------------------------- 1 | from flask_via.routers.default import Blueprint 2 | 3 | routes = [ 4 | Blueprint('user', 'app.user', template_folder="templates"), 5 | Blueprint('restaurant', 'app.restaurant', template_folder="templates"), 6 | Blueprint('food', 'app.food', template_folder="templates"), 7 | ] -------------------------------------------------------------------------------- /app/static/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/app/static/css/.gitkeep -------------------------------------------------------------------------------- /app/static/css/sticky-footer.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles 2 | -------------------------------------------------- */ 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | body { 8 | /* Margin bottom by footer height */ 9 | margin-bottom: 60px; 10 | } 11 | .footer { 12 | position: absolute; 13 | bottom: 0; 14 | width: 100%; 15 | /* Set the fixed height of the footer here */ 16 | height: 60px; 17 | background-color: #f5f5f5; 18 | } 19 | 20 | 21 | /* Custom page CSS 22 | -------------------------------------------------- */ 23 | /* Not required for template or sticky footer method. */ 24 | 25 | .container { 26 | width: auto; 27 | max-width: 680px; 28 | padding: 0 15px; 29 | } 30 | .container .text-muted { 31 | margin: 20px 0; 32 | } 33 | -------------------------------------------------------------------------------- /app/static/img/food/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/app/static/img/food/.gitkeep -------------------------------------------------------------------------------- /app/static/img/restaurant/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/app/static/img/restaurant/.gitkeep -------------------------------------------------------------------------------- /app/static/img/user/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/app/static/img/user/.gitkeep -------------------------------------------------------------------------------- /app/static/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/app/static/js/.gitkeep -------------------------------------------------------------------------------- /app/templates/error/403.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
8 |

403 Error {{ title }}

9 |

You do not have sufficient permissions to access this page.

10 |
11 | 12 | Home 13 | 14 |
15 |
16 |
17 |
18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/error/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
8 |

404 Error | {{ title }}

9 |

The page you're looking for doesn't exist..

10 |
11 | 12 | Home 13 | 14 |
15 |
16 |
17 |
18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/error/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
8 |

500 Error | {{ title }}

9 |

The server encountered an internal error. That's all we know.

10 |
11 | 12 | Home 13 | 14 |
15 |
16 |
17 |
18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 | 9 |

Flask-MVC Template It is a template with "batteries included" created for the fast development of applications in the microframework flask

10 | {% if current_user.is_authenticated %} 11 |

Hi, {{ current_user.username }}!

12 |

Logout

13 | {% else %} 14 |

I don't know you!

15 |

Login with Facebook

16 |

Login with Twitter

17 |

Login with Google

18 | {% endif %} 19 |
20 | 21 | 22 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/layout/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Try Flask MVC 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% assets "CSS_FRAMEWORKS" %} 23 | 24 | 25 | 26 | {% endassets %} 27 | 28 | 34 | 35 | 36 | {% include 'layout/partials/header.html' %} 37 | {% block header %} 38 | {% endblock %} 39 | 40 | {% block content %} 41 | {% endblock %} 42 | 43 | {% include 'layout/partials/footer.html' %} 44 | {% block footer %} 45 | {% endblock %} 46 | 47 | {# assets "JS_FRAMEWORS" #} 48 | 49 | {##} 50 | 51 | {# endassets #} 52 | 53 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/templates/layout/partials/footer.html: -------------------------------------------------------------------------------- 1 | {% block footer %} 2 | 9 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/layout/partials/header.html: -------------------------------------------------------------------------------- 1 | {% block header %} 2 | 3 | 30 | 31 | {% endblock%} -------------------------------------------------------------------------------- /app/templates/macros/_render_field.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 | {{ field.label}} 3 |
{{ field(**kwargs)|safe }} 4 | {% if field.errors %} 5 |
6 | {% for error in field.errors %} 7 |
8 |
{{ error }}
9 |
10 | 11 | {% endfor %} 12 |
13 | {% endif %} 14 | {% endmacro %} -------------------------------------------------------------------------------- /app/templates/security/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | {% from "security/_macros.html" import render_field_with_errors, render_field %} 3 | 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

No te has registrado?

11 | 12 |
13 |

Oauth Login

14 | {% if current_user.is_authenticated %} 15 |

Hi, {{ current_user.username }}!

16 |

Logout

17 | {% else %} 18 |

I don't know you!

19 |

Login with Facebook

20 |

Login with Twitter

21 |

Login with Google

22 | {% endif %} 23 |
24 |
25 |
26 | {{ login_user_form.hidden_tag() }} 27 | 28 | Login 29 |
30 | {{ render_field_with_errors(login_user_form.email, id="email", class_="form-control") }} 31 |
32 | 33 |
34 | {{ render_field_with_errors(login_user_form.password, id="password", class_="form-control") }} 35 |
36 | 37 |
38 | 41 |
42 | {{ login_user_form.next }} 43 |
44 | {{ login_user_form.submit(class_="btn btn-default btn-submit" ) }} 45 |
46 |
47 |
48 |
49 | 50 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/security/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | {% from "security/_macros.html" import render_field_with_errors, render_field %} 3 | 4 | {% block content %} 5 |
6 | 7 |
8 |

Ya tienes cuenta?

9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | Registrate 17 | 18 | {{ register_user_form.hidden_tag() }} 19 |
20 | {{ render_field_with_errors(register_user_form.email, class_="form-control", id="email") }} 21 |
22 |
23 | {{ render_field_with_errors(register_user_form.password, class_="form-control", id="password") }} 24 |
25 |
26 | {% if register_user_form.password_confirm %} 27 | {{ render_field_with_errors(register_user_form.password_confirm, class_="form-control", id="confirm_password") }} 28 | {% endif %} 29 |
30 |
31 | {{ render_field_with_errors(register_user_form.username, class_="form-control", id="username") }} 32 |
33 |
34 | {{ register_user_form.submit(class_="btn btn-default btn-submit")}} 35 |
36 |
37 |
38 |
39 |
40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /app/user/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_uploads import UploadSet, IMAGES 2 | 3 | user_photo = UploadSet('user', IMAGES) 4 | -------------------------------------------------------------------------------- /app/user/forms.py: -------------------------------------------------------------------------------- 1 | from wtforms import * 2 | from flask_security.forms import RegisterForm 3 | from flask_wtf.file import FileField, FileAllowed, FileRequired 4 | from . import user_photo 5 | 6 | 7 | class SecurityRegisterForm(RegisterForm): 8 | username = StringField('Username', [ 9 | validators.Regexp('^\w+$', message="Regex: Username must contain only letters numbers or underscore"), 10 | validators.DataRequired(message='El campo esta vacio.'), 11 | validators.length(min=5, message='Min 5 letter, Try Again')]) 12 | 13 | # Form for demo of flask-upload 14 | 15 | class UserImageForm(Form): 16 | profile_photo = FileField('', validators=[FileRequired(), FileAllowed(user_photo, 'Images only!')]) 17 | submit = SubmitField('Submit') -------------------------------------------------------------------------------- /app/user/oauth.py: -------------------------------------------------------------------------------- 1 | from rauth import OAuth1Service, OAuth2Service 2 | from flask import url_for, request, redirect, session, current_app 3 | # from .. import app 4 | import json 5 | 6 | 7 | class OAuthSignIn(object): 8 | providers = None 9 | 10 | def __init__(self, provider_name): 11 | self.provider_name = provider_name 12 | credentials = current_app.config['OAUTH_CREDENTIALS'][provider_name] 13 | self.consumer_id = credentials['id'] 14 | self.consumer_secret = credentials['secret'] 15 | 16 | def authorize(self): 17 | pass 18 | 19 | def callback(self): 20 | pass 21 | 22 | def get_callback_url(self): 23 | return url_for('user.oauth_callback', provider=self.provider_name, 24 | _external=True) 25 | 26 | @classmethod 27 | def get_provider(self, provider_name): 28 | if self.providers is None: 29 | self.providers = {} 30 | for provider_class in self.__subclasses__(): 31 | provider = provider_class() 32 | self.providers[provider.provider_name] = provider 33 | return self.providers[provider_name] 34 | 35 | 36 | class GoogleSignIn(OAuthSignIn): 37 | def __init__(self): 38 | super(GoogleSignIn, self).__init__('google') 39 | self.service = OAuth2Service( 40 | name='google', 41 | client_id=self.consumer_id, 42 | client_secret=self.consumer_secret, 43 | base_url='https://www.googleapis.com/oauth2/v1/', 44 | access_token_url='https://accounts.google.com/o/oauth2/token', 45 | authorize_url='https://accounts.google.com/o/oauth2/auth' 46 | ) 47 | 48 | def authorize(self): 49 | return redirect(self.service.get_authorize_url( 50 | scope='https://www.googleapis.com/auth/userinfo.email', 51 | response_type='code', 52 | access_type ='offline', 53 | redirect_uri=self.get_callback_url()), 54 | ) 55 | 56 | def callback(self): 57 | if 'code' not in request.args : 58 | return None, None, None, None 59 | code = request.args['code'] 60 | print('code -> ', code) 61 | 62 | payload = { 63 | 'grant_type': 'authorization_code', 64 | 'code': code, 65 | 'scope':'https://www.googleapis.com/auth/userinfo.email', 66 | 'redirect_uri':self.get_callback_url() 67 | } 68 | access_token = self.service.get_access_token(decoder=json.loads, data=payload) 69 | 70 | print('access_token ->', access_token) 71 | 72 | oauth_session = self.service.get_session(access_token) 73 | me = oauth_session.get('userinfo').json() 74 | print(me) 75 | social_id = 'google$' + me.get('id') 76 | username = me.get('email').split('@')[0] 77 | # picture = me.get('picture') 78 | email = me.get('email') 79 | return social_id, username, email 80 | 81 | 82 | class FacebookSignIn(OAuthSignIn): 83 | def __init__(self): 84 | super(FacebookSignIn, self).__init__('facebook') 85 | self.service = OAuth2Service( 86 | name='facebook', 87 | client_id=self.consumer_id, 88 | client_secret=self.consumer_secret, 89 | authorize_url='https://graph.facebook.com/oauth/authorize', 90 | access_token_url='https://graph.facebook.com/oauth/access_token', 91 | base_url='https://graph.facebook.com/' 92 | ) 93 | 94 | def authorize(self): 95 | return redirect(self.service.get_authorize_url( 96 | scope='email', 97 | response_type='code', 98 | redirect_uri=self.get_callback_url()) 99 | ) 100 | 101 | def callback(self): 102 | if 'code' not in request.args: 103 | return None, None, None 104 | oauth_session = self.service.get_auth_session( 105 | data={'code': request.args['code'], 106 | 'grant_type': 'authorization_code', 107 | 'redirect_uri': self.get_callback_url()} 108 | ,decoder=json.loads) 109 | me = oauth_session.get('me?fields=id,email').json() 110 | return ( 111 | 'facebook$' + me['id'], 112 | me.get('email').split('@')[0], # Facebook does not provide 113 | # username, so the email's user 114 | # is used instead 115 | me.get('email') 116 | ) 117 | 118 | 119 | class TwitterSignIn(OAuthSignIn): 120 | def __init__(self): 121 | super(TwitterSignIn, self).__init__('twitter') 122 | self.service = OAuth1Service( 123 | name='twitter', 124 | consumer_key=self.consumer_id, 125 | consumer_secret=self.consumer_secret, 126 | request_token_url='https://api.twitter.com/oauth/request_token', 127 | authorize_url='https://api.twitter.com/oauth/authorize', 128 | access_token_url='https://api.twitter.com/oauth/access_token', 129 | base_url='https://api.twitter.com/1.1/' 130 | ) 131 | 132 | def authorize(self): 133 | request_token = self.service.get_request_token( 134 | params={'oauth_callback': self.get_callback_url()} 135 | ) 136 | session['request_token'] = request_token 137 | return redirect(self.service.get_authorize_url(request_token[0])) 138 | 139 | def callback(self): 140 | request_token = session.pop('request_token') 141 | if 'oauth_verifier' not in request.args: 142 | return None, None, None 143 | oauth_session = self.service.get_auth_session( 144 | request_token[0], 145 | request_token[1], 146 | data={'oauth_verifier': request.args['oauth_verifier']}) 147 | me = oauth_session.get('account/verify_credentials.json').json() 148 | social_id = 'twitter$' + str(me.get('id')) 149 | username = me.get('screen_name') 150 | return social_id, username, None # Twitter does not provide email 151 | -------------------------------------------------------------------------------- /app/user/routes.py: -------------------------------------------------------------------------------- 1 | from flask_via.routers.default import Pluggable 2 | from .views import * 3 | 4 | routes = [ 5 | Pluggable('/user/', ProfileView, 'profile'), 6 | Pluggable('/user/upload', UserUploadView, 'upload'), 7 | 8 | Pluggable('/authorize/', OauthAuthorize, 'oauth_authorize'), 9 | Pluggable('/callback/', OauthCallback, 'oauth_callback') 10 | 11 | ] -------------------------------------------------------------------------------- /app/user/templates/user/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.html' %} 2 | 3 | 4 | {% block content %} 5 |

User Profile

6 | {% endblock %} -------------------------------------------------------------------------------- /app/user/templates/user/upload.html: -------------------------------------------------------------------------------- 1 | {% block register %} 2 |
3 |

Upload in user!

4 |
5 | 6 | {{ form.profile_photo }} 7 | {{ form.submit }} 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /app/user/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/app/user/utils.py -------------------------------------------------------------------------------- /app/user/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, url_for, redirect, render_template, flash, current_app 2 | from flask.views import MethodView 3 | from flask_security.utils import login_user 4 | from flask_security import current_user 5 | from flask_security.datastore import SQLAlchemyUserDatastore 6 | 7 | from .forms import UserImageForm 8 | from .oauth import OAuthSignIn 9 | 10 | from ..models import FinalUser, FinalUserImage, Role, db 11 | from . import user_photo 12 | # from .. import app 13 | 14 | user_datastore = SQLAlchemyUserDatastore(db, FinalUser, Role) 15 | 16 | class ProfileView(MethodView): 17 | def get(self): 18 | return render_template('user/profile.html') 19 | 20 | class UserUploadView(MethodView): 21 | def get(self): 22 | return render_template('user/upload.html', form=UserImageForm()) 23 | 24 | def post(self): 25 | if 'profile_photo' in request.files: 26 | filename = user_photo.save(request.files['profile_photo']) 27 | image = FinalUserImage(user_id=current_user.id, 28 | image_filename=filename, 29 | image_url=current_app.config['UPLOADED_USER_DEST'][11:] + "/" + filename) 30 | db.session.add(image) 31 | db.session.commit() 32 | return redirect(url_for('user.profile')) 33 | return redirect(url_for('user.profile')) 34 | 35 | 36 | class OauthAuthorize(MethodView): 37 | def get(self, provider): 38 | if not current_user.is_anonymous: 39 | return redirect(url_for('index')) 40 | oauth = OAuthSignIn.get_provider(provider) 41 | return oauth.authorize() 42 | 43 | class OauthCallback(MethodView): 44 | def get(self, provider): 45 | if not current_user.is_anonymous: 46 | return redirect(url_for('index')) 47 | oauth = OAuthSignIn.get_provider(provider) 48 | social_id, username, email = oauth.callback() 49 | if social_id is None: 50 | flash('Authentication failed.') 51 | return redirect(url_for('index')) 52 | user = db.session.query(FinalUser).filter_by(email=email).first() 53 | if user is None: 54 | user_datastore.create_user(social_id=social_id, username=username, email=email) 55 | db.session.commit() 56 | user_datastore.add_role_to_user(email, 'user') 57 | db.session.commit() 58 | login_user(user) 59 | print(current_user) 60 | return redirect(url_for('index')) -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random, string 3 | 4 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | class DevelopmentConfig(object): 7 | # Flask 8 | 9 | SECRET_KEY = 'SECRET_KEY' 10 | TEMPLATES_AUTO_RELOAD = True 11 | DEBUG = True 12 | SEND_FILE_MAX_AGE_DEFAULT = 0 13 | 14 | #Flask-Assets 15 | 16 | ASSETS_DEBUG = False 17 | 18 | # Flask-Via 19 | 20 | VIA_ROUTES_MODULE = "app.routes" 21 | 22 | #Flask-Security 23 | 24 | SECURITY_REGISTERABLE = True 25 | SECURITY_TRACKABLE = True 26 | SECURITY_SEND_REGISTER_EMAIL = False 27 | SECURITY_LOGIN_URL = '/login/' 28 | SECURITY_LOGOUT_URL = '/logout/' 29 | SECURITY_REGISTER_URL = '/register/' 30 | SECURITY_POST_LOGIN_VIEW = "/" 31 | SECURITY_POST_LOGOUT_VIEW = "/" 32 | SECURITY_POST_REGISTER_VIEW = "/" 33 | SECURITY_LOGIN_USER_TEMPLATE = 'security/login.html' 34 | SECURITY_REGISTER_USER_TEMPLATE = 'security/register.html' 35 | 36 | #Flask-SQLAlchemy 37 | 38 | SQLALCHEMY_DATABASE_URI = 'sqlite:///{}'.format(os.path.join(BASE_DIR, 'app.db')) 39 | SQLALCHEMY_TRACK_MODIFICATIONS = True 40 | 41 | #Flask-Script 42 | 43 | APP_FOLDER = "app/" 44 | 45 | #Flask-Uploads 46 | 47 | UPLOADED_RESTAURANT_DEST = APP_FOLDER + "static/img/restaurant" 48 | UPLOADED_RESTAURANT_URL = 'http://0.0.0.0:8000/restaurant/upload' 49 | UPLOADED_FOOD_DEST = APP_FOLDER + "static/img/food" 50 | UPLOADED_FOOD_URL = 'http://0.0.0.0:8000/food/upload' 51 | UPLOADED_USER_DEST = APP_FOLDER + "static/img/user" 52 | UPLOADED_USER_URL = 'http://0.0.0.0:8000/user/upload' 53 | 54 | #OAUTH LOGIN 55 | 56 | OAUTH_CREDENTIALS = { 57 | 'facebook': { 58 | 'id': '638111216387395', 59 | 'secret': 'c374a53decb75c2043ccd0e4a0eb8c28' 60 | }, 61 | 'twitter': { 62 | 'id': 't6nK168ytZ7w7Rj4uJD3bXi5L', 63 | 'secret': 'L537M7QT810Qe0zMCB1od3bKe6ljx2nyDkxxF49gaHtSJrmHA1' 64 | }, 65 | 'google': { 66 | 'id': '1080912678595-adm52eo5f78jru65923qia22itfasa7d.apps.googleusercontent.com', 67 | 'secret': '1vq9zxw2rMiBtUVeLlAlNOVw' 68 | } 69 | } 70 | 71 | class TestingConfig(DevelopmentConfig): 72 | TESTING = True 73 | SQLALCHEMY_DATABASE_URI = 'sqlite:///{}'.format(os.path.join(BASE_DIR, 'testing.db')) 74 | 75 | app_config = { 76 | 'development': DevelopmentConfig, 77 | 'testing': TestingConfig 78 | } -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from colorama import Fore, init 2 | 3 | from flask import current_app 4 | from flask_script import Manager, prompt 5 | from flask_migrate import Migrate, MigrateCommand 6 | from app import create_app, db, FinalUser, user_datastore 7 | from config import DevelopmentConfig 8 | from flask_security import utils 9 | 10 | import re 11 | import os 12 | 13 | app = create_app('development') 14 | 15 | migrate = Migrate(app, db) 16 | manager = Manager(app) 17 | manager.add_command('db', MigrateCommand) 18 | 19 | @manager.command 20 | def createapp(): 21 | path = prompt(Fore.BLUE + "Write the name of the blueprint").lower() 22 | try: 23 | int(path) 24 | print (Fore.RED + "Name no valid") 25 | return 26 | except ValueError: 27 | pass 28 | folder = current_app.config['APP_FOLDER'] + path 29 | register_blueprint_str = "Blueprint('{0}', 'app.{0}', template_folder='templates')".format(path.lower()) 30 | if not os.path.exists(folder): 31 | # Scaffold new blueprint 32 | os.makedirs(folder) 33 | python_files = ["forms", "routes", "utils", "views", "__init__.py"] 34 | for i, file in enumerate(python_files): 35 | with open(os.path.join(folder, file + ".py"), 'w') as temp_file: 36 | if i != 4: 37 | if file is "routes": 38 | temp_file.write("from flask_via.routers.default import Pluggable\nfrom .views import *\nroutes=[]") 39 | if file is "forms": 40 | temp_file.write("from wtforms import *\n") 41 | if file is "views": 42 | temp_file.write("from flask import jsonify, request, " 43 | "url_for, redirect, current_app, render_template, flash, make_response\n" 44 | "from flask.views import MethodView") 45 | else: 46 | os.makedirs(folder + "/template/" + path) 47 | 48 | # Register blueprint in app/route.py 49 | route_path = os.path.join(current_app.config['APP_FOLDER'], "routes.py") 50 | with open(route_path, "r") as old_routes: 51 | data = old_routes.readlines() 52 | data[-2] = data[-2] + " " + register_blueprint_str + ',\n' 53 | os.remove(os.path.join(current_app.config['APP_FOLDER'], "routes.py")) 54 | 55 | with open(route_path, 'w') as new_routes: 56 | new_routes.writelines(data) 57 | else: 58 | print (Fore.RED + "This path exist") 59 | 60 | @manager.command 61 | def createadmin(): 62 | username = prompt(Fore.BLUE + "Username") 63 | query_username = db.session.query(FinalUser).filter_by(username=username).first() 64 | email = prompt(Fore.BLUE + "Write Email") 65 | if re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', email) == None: 66 | print(Fore.RED + "Invalid email format") 67 | return 68 | query_email = db.session.query(FinalUser).filter_by(email=email).first() 69 | if query_username is None and query_email is None: 70 | password = prompt(Fore.BLUE + "Write password") 71 | repeat_password = prompt(Fore.BLUE + "Repeat password") 72 | if password == repeat_password: 73 | encrypted_password = utils.encrypt_password(password) 74 | user_datastore.create_user(username=username, 75 | password=encrypted_password, 76 | email=email) 77 | db.session.commit() 78 | user_datastore.add_role_to_user(email, 'admin') 79 | db.session.commit() 80 | print(Fore.GREEN + "Admin created") 81 | else: 82 | print(Fore.RED + "The password does not match") 83 | return 84 | else: 85 | print(Fore.RED + "The username or email are in use") 86 | return 87 | 88 | 89 | if __name__ == '__main__': 90 | manager.run() 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Flask-MVC Template 2 | ------------------ 3 | [![Build Status](https://travis-ci.org/CharlyJazz/Flask-MVC-Template.svg?branch=master)](https://travis-ci.org/CharlyJazz/Flask-MVC-Template) 4 | 5 | Flask-MVC Template It is a template with "batteries included" created for the fast development of applications in the microframework flask 6 | 7 | Feature: 8 | -------- 9 | [Flask-Via](http://flask-via.soon.build/en/latest/): 10 | For create routes like a [Django Rest Framework](http://www.django-rest-framework.org) style using Blueprints! 11 | 12 | [Flask-Security](https://pythonhosted.org/Flask-Security/): 13 | To easily have login, logout, recovery password and to keep administrator views restricted. 14 | 15 | This template has a sub folder in templates / security in which are the custom templates for flask-security. Already configured. 16 | 17 | [Flask-Admin](https://flask-admin.readthedocs.io/en/latest/): 18 | A cool admin interface customizable for your models and assets recources. 19 | 20 | **Add yours models in the file admin.py** 21 | 22 | [Flask-Upload](http://flask.pocoo.org/docs/0.12/patterns/fileuploads/): 23 | This template brings an example of how to use flask-upload in different blueprints and how to save the url of file in the database 24 | 25 | **Delete restaurant and food folders and rewrite app / __ init__.py for delete the pretty example** 26 | 27 | [Flask-Script](https://flask-script.readthedocs.io/en/latest/): 28 | Awesome commands for your projects, including the [Flask-Migrate](https://flask-migrate.readthedocs.io/en/latest/) commands: 29 | - `createadmin`: Create admin user 30 | - `createapp`: Scaffold new blueprint folder and register in the file app/routes.py 31 | 32 | [Rauth](https://rauth.readthedocs.io/en/latest/) 33 | Social Login with facebook, google and twitter 34 | 35 | [Flask-Testing](https://pythonhosted.org/Flask-Testing/) 36 | Simple test unit with [Faker](https://github.com/joke2k/faker) for generate forget data and unittest 37 | And Selenium webdriver for front end testing 38 | - `python -m unittest discover -p `: Test the specific file 39 | 40 | TODO: 41 | ----- 42 | 43 | * [x] Flask-Script 44 | * [x] Admin command 45 | * [x] Create app command 46 | * [x] Flask-Migrate 47 | * [x] Flask-Uploads 48 | * [x] Create one instance of this in each blueprint 49 | * [x] Oauth 50 | * [x] Facebook 51 | * [x] Twitter 52 | * [x] Google 53 | * [x] Testing with Flask-Testing 54 | * [x] Faker for generate forged data 55 | * [x] Front end test with Selenium webdriver 56 | * [x] Back end test 57 | * [x] Create command to create an admin 58 | * [x] Factory App 59 | * [x] HTTP Templates for error handling 60 | * [x] Create easy way for Unit Test 61 | 62 | ## :warning: Be carefull 63 | 64 | Before use this for your projects keep in mind that this project is quite old, and it is preferable to create one with more modern tools and libraries. This project is useful if you want to read the source code and learn a little about the use of Flask on monolithic architectures. 65 | 66 | ## New version. 67 | 68 | Since a lot of extensions of flask are outdated and bugged. We going write features with native flask code, add more features and make the project more clean and easy to use and scale. 69 | 70 | [Issue to track progress](https://github.com/CharlyJazz/Flask-MVC-Template/issues/18) 71 | 72 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.3.0 2 | appdirs==1.4.3 3 | astroid==2.3.2 4 | autopep8==1.4.4 5 | blinker==1.4 6 | certifi==2017.4.17 7 | chardet==3.0.3 8 | click==6.7 9 | colorama==0.4.1 10 | Faker==0.7.12 11 | Flask==0.10.1 12 | Flask-Admin==1.5.0 13 | Flask-Assets==0.12 14 | Flask-Login==0.3.2 15 | Flask-Mail==0.9.1 16 | Flask-Migrate==2.5.2 17 | Flask-Principal==0.4.0 18 | Flask-Script==2.0.6 19 | Flask-Security==1.7.5 20 | Flask-SQLAlchemy==2.2 21 | Flask-Testing==0.6.2 22 | Flask-Uploads==0.2.1 23 | Flask-Via==2015.1.1 24 | Flask-WTF==0.14.2 25 | idna==2.5 26 | ipaddress==1.0.18 27 | isort==4.3.21 28 | itsdangerous==0.24 29 | Jinja2==2.9.6 30 | lazy-object-proxy==1.4.3 31 | Mako==1.1.0 32 | MarkupSafe==1.0 33 | mccabe==0.6.1 34 | packaging==16.8 35 | passlib==1.7.1 36 | pycodestyle==2.5.0 37 | pylint==2.4.3 38 | pyparsing==2.2.0 39 | python-dateutil==2.6.0 40 | python-editor==1.0.4 41 | rauth==0.7.3 42 | requests==2.16.5 43 | selenium==3.4.2 44 | six==1.10.0 45 | SQLAlchemy==1.1.10 46 | SQLAlchemy-Utils==0.32.14 47 | urllib3==1.21.1 48 | webassets==0.12.1 49 | Werkzeug==0.12.2 50 | wrapt==1.11.2 51 | WTForms==2.1 52 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from app import create_app 4 | 5 | app = create_app('development') 6 | 7 | if __name__ == '__main__': 8 | app.run(host='0.0.0.0', port=8000, debug=True) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlyJazz/Flask-MVC-Template/097a6651498a661cda279f413f7f19974a6e5905/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_back_end.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from flask_testing import TestCase 4 | 5 | from faker import Factory 6 | from faker.providers.misc import Provider 7 | 8 | from app import create_app, user_datastore 9 | from app.models import * 10 | 11 | fake = Factory.create() 12 | 13 | 14 | class TestBase(TestCase): 15 | 16 | def create_app(self): 17 | config_name = 'testing' 18 | app = create_app(config_name) 19 | return app 20 | 21 | def setUp(self): 22 | email_admin = fake.email() 23 | db.session.commit() 24 | db.drop_all() 25 | db.create_all() 26 | 27 | user_datastore.create_user(email = fake.email(), username=fake.name(), password=Provider.password()) 28 | user_datastore.create_user(email=email_admin, username=fake.name(), password=Provider.password()) 29 | user_datastore.find_or_create_role(name='admin', description='Administrator') 30 | user_datastore.find_or_create_role(name='end-user', description='End user') 31 | user_datastore.add_role_to_user(email_admin, 'admin') 32 | db.session.commit() 33 | 34 | def tearDown(self): 35 | db.session.remove() 36 | db.drop_all() 37 | 38 | 39 | class TestUserRoles(TestBase): 40 | 41 | def test_count_user(self): 42 | self.assertEqual(FinalUser.query.count(), 2) 43 | 44 | def test_find_admin_role(self): 45 | self.assertIsNotNone(user_datastore.find_role('admin')) 46 | 47 | def test_find_end_user_role(self): 48 | self.assertIsNotNone(user_datastore.find_role('end-user')) 49 | 50 | 51 | if __name__ == '__main__': 52 | unittest.main() -------------------------------------------------------------------------------- /tests/test_front_end.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | try: 3 | # For Python 3.0 and later 4 | from urllib.request import urlopen 5 | except ImportError: 6 | # Fall back to Python 2's urllib2 7 | from urllib2 import urlopen 8 | import time 9 | 10 | from faker import Factory 11 | from faker.providers.misc import Provider 12 | 13 | from flask import url_for 14 | from flask_testing import LiveServerTestCase 15 | 16 | from selenium import webdriver 17 | from selenium.common.exceptions import NoSuchElementException 18 | 19 | from app import create_app, user_datastore 20 | from app.models import * 21 | 22 | fake = Factory.create() 23 | 24 | test_admin_email, test_admin_username, test_admin_password = fake.email(), fake.name(), Provider.password(), 25 | test_user_final_email, test_user_final_username, test_user_final_password = fake.email(), fake.name(), Provider.password() 26 | 27 | class CreateObjects(object): 28 | 29 | def login_admin_user(self): 30 | login_link = self.get_server_url() + url_for('security.login') 31 | self.driver.get(login_link) 32 | self.driver.find_element_by_id("email").send_keys(test_admin_email) 33 | self.driver.find_element_by_id("password").send_keys(test_admin_password) 34 | 35 | def login_final_user(self): 36 | login_link = self.get_server_url() + url_for('security.login') 37 | self.driver.get(login_link) 38 | self.driver.find_element_by_id("email").send_keys(test_user_final_email) 39 | self.driver.find_element_by_id("password").send_keys(test_user_final_password) 40 | 41 | 42 | class TestBase(LiveServerTestCase): 43 | 44 | def create_app(self): 45 | config_name = 'testing' 46 | app = create_app(config_name) 47 | app.config.update( 48 | LIVESERVER_PORT=8000 49 | ) 50 | return app 51 | 52 | def setUp(self): 53 | from selenium.webdriver.chrome.options import Options 54 | chrome_options = Options() 55 | chrome_options.add_argument('--no-sandbox') 56 | chrome_options.add_argument('--no-default-browser-check') 57 | chrome_options.add_argument('--no-first-run') 58 | chrome_options.add_argument('--disable-default-apps') 59 | chrome_options.add_argument('--remote-debugging-port=9222') 60 | chrome_options.add_argument('--headless') 61 | chrome_options.add_argument('--disable-gpu') 62 | 63 | """Setup the test driver and create test users""" 64 | self.driver = webdriver.Chrome(chrome_options=chrome_options) 65 | self.driver.get(self.get_server_url()) 66 | 67 | email_admin = test_admin_email 68 | 69 | db.session.commit() 70 | db.drop_all() 71 | db.create_all() 72 | 73 | user_datastore.create_user(email=test_admin_email, username=test_admin_username, password=test_admin_password) 74 | user_datastore.create_user(email=test_user_final_email, username=test_user_final_username, password=test_user_final_password) 75 | user_datastore.find_or_create_role(name='admin', description='Administrator') 76 | user_datastore.find_or_create_role(name='end-user', description='End user') 77 | user_datastore.add_role_to_user(email_admin, 'admin') 78 | db.session.commit() 79 | 80 | def tearDown(self): 81 | self.driver.quit() 82 | 83 | 84 | class TestRegistration(TestBase): 85 | 86 | def test_registration(self): 87 | # Click register menu link 88 | self.driver.find_element_by_link_text("Register").click() 89 | time.sleep(1) 90 | _password = Provider.password() 91 | _username = fake.profile()["username"] 92 | # Fill in registration form 93 | self.driver.find_element_by_id("email").send_keys(fake.email()) 94 | self.driver.find_element_by_id("username").send_keys(_username) 95 | self.driver.find_element_by_id("password").send_keys(_password) 96 | self.driver.find_element_by_id("confirm_password").send_keys(_password) 97 | self.driver.find_element_by_css_selector('.btn-submit').click() 98 | time.sleep(1) 99 | 100 | welcome_message = self.driver.find_element_by_id("welcome-message").text 101 | assert "Hi, {0}!".format(_username) in welcome_message 102 | 103 | 104 | class TestLogin(TestBase, CreateObjects): 105 | 106 | def test_login_final_user(self): 107 | self.driver.find_element_by_link_text("Login").click() 108 | self.login_final_user() 109 | self.driver.find_element_by_css_selector('.btn-submit').click() 110 | 111 | time.sleep(1) 112 | 113 | welcome_message = self.driver.find_element_by_id("welcome-message").text 114 | assert "Hi, {0}!".format(test_user_final_username) in welcome_message 115 | 116 | def test_login_admin_user(self): 117 | self.driver.find_element_by_link_text("Login").click() 118 | self.login_admin_user() 119 | self.driver.find_element_by_css_selector('.btn-submit').click() 120 | 121 | time.sleep(1) 122 | 123 | welcome_message = self.driver.find_element_by_id("welcome-message").text 124 | assert "Hi, {0}!".format(test_admin_username) in welcome_message 125 | 126 | class TestAdminSecurity(TestBase, CreateObjects): 127 | def check_exists_by_xpath(self, xpath): 128 | try: 129 | self.driver.find_element_by_xpath(xpath) 130 | except NoSuchElementException: 131 | return False 132 | return True 133 | 134 | def test_final_user_access_admin(self): 135 | self.login_final_user() 136 | self.driver.find_element_by_css_selector('.btn-submit').click() 137 | 138 | time.sleep(1) 139 | 140 | self.driver.get(self.get_server_url() + url_for('admin.index')) 141 | self.assertEqual(self.check_exists_by_xpath('//a[text()="Final User"]'), False) 142 | 143 | def test_admin_user_access_admin(self): 144 | self.login_admin_user() 145 | self.driver.find_element_by_css_selector('.btn-submit').click() 146 | 147 | time.sleep(1) 148 | 149 | self.driver.get(self.get_server_url() + url_for('admin.index')) 150 | time.sleep(1) 151 | self.assertEqual(self.check_exists_by_xpath('//a[text()="Final User"]'), True) 152 | 153 | 154 | if __name__ == '__main__': 155 | unittest.main() 156 | --------------------------------------------------------------------------------