├── .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 |
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 |
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 |
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 |
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 |
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 |
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 | [](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 |
--------------------------------------------------------------------------------
|