├── app ├── templates │ ├── base_templates │ │ ├── page_base.html │ │ ├── flask_user_base.html │ │ ├── form_macros.html │ │ └── base.html │ ├── flask_user │ │ ├── member_base.html │ │ ├── public_base.html │ │ └── register.html │ ├── pages │ │ └── home_page.html │ ├── users │ │ └── user_profile_page.html │ └── _readme.txt ├── users │ ├── __init__.py │ ├── forms.py │ ├── views.py │ └── models.py ├── __init__.py ├── config │ ├── __init__.py │ ├── settings.py │ └── local_settings_example.py ├── pages │ ├── __init__.py │ └── views.py ├── startup │ ├── __init__.py │ └── init_app.py ├── static │ └── app │ │ └── app.css ├── app_and_db.py └── _readme.txt ├── tests ├── __init__.py ├── .coveragerc ├── _readme.txt ├── test_simple_urls.py └── conftest.py ├── requirements.txt ├── runserver.py ├── .gitignore ├── fabfile.py └── README.rst /app/templates/base_templates/page_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base_templates/base.html" %} 2 | -------------------------------------------------------------------------------- /app/templates/flask_user/member_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_templates/flask_user_base.html' %} 2 | 3 | -------------------------------------------------------------------------------- /app/templates/flask_user/public_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_templates/flask_user_base.html' %} 2 | 3 | -------------------------------------------------------------------------------- /app/users/__init__.py: -------------------------------------------------------------------------------- 1 | # The __init__.py files make Python's import statement work inside subdirectories. 2 | # Most __init__.py files are empty: they just must be preset. -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # special Python file to turn a subdirectory into a Python 'package' 2 | # Python packages can be accessed using the Python 'import' statement 3 | 4 | # Intentionally left empty -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # special Python file to turn a subdirectory into a Python 'package' 2 | # Python packages can be accessed using the Python 'import' statement 3 | 4 | # Intentionally left empty -------------------------------------------------------------------------------- /app/config/__init__.py: -------------------------------------------------------------------------------- 1 | # special Python file to turn a subdirectory into a Python 'package' 2 | # Python packages can be accessed using the Python 'import' statement 3 | 4 | # Intentionally left empty -------------------------------------------------------------------------------- /app/pages/__init__.py: -------------------------------------------------------------------------------- 1 | # special Python file to turn a subdirectory into a Python 'package' 2 | # Python packages can be accessed using the Python 'import' statement 3 | 4 | # Intentionally left empty -------------------------------------------------------------------------------- /app/startup/__init__.py: -------------------------------------------------------------------------------- 1 | # special Python file to turn a subdirectory into a Python 'package' 2 | # Python packages can be accessed using the Python 'import' statement 3 | 4 | # Intentionally left empty -------------------------------------------------------------------------------- /tests/.coveragerc: -------------------------------------------------------------------------------- 1 | # Configuration for the test coverage tool (py.test --cov) 2 | # 3 | # Copyright 2014 SolidBuilds.com. All rights reserved 4 | # 5 | # Authors: Ling Thio 6 | 7 | [run] 8 | omit = app/config/local_settings_example.py 9 | 10 | [report] 11 | show_missing = True 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is used by pip to install required python packages 2 | # Usage: pip install -r requirements.txt 3 | 4 | # Flask Framework 5 | Flask==0.10.1 6 | Flask-SQLAlchemy==2.0 7 | 8 | # Flask Packages 9 | Flask-User==0.5.4 10 | Flask-WTF==0.10.2 11 | 12 | # Development tools 13 | pytest==2.6.3 14 | 15 | -------------------------------------------------------------------------------- /app/static/app/app.css: -------------------------------------------------------------------------------- 1 | /**** element styles ****/ 2 | hr { border-color: #cccccc; margin: } 3 | 4 | /**** header, main and footer divs ****/ 5 | 6 | /**** class-based style modifiers ****/ 7 | 8 | .no-margins { margin: 0px; } 9 | 10 | .with-margins { margin: 10px; } 11 | 12 | .col-centered { float: none; margin: 0 auto; } 13 | -------------------------------------------------------------------------------- /app/templates/base_templates/flask_user_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base_templates/base.html" %} 2 | 3 | {% block main %} 4 |
5 |
6 |
7 | {% block content %}{% endblock %} 8 |
9 |
10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /runserver.py: -------------------------------------------------------------------------------- 1 | #!/Users/lingthio/envs/glamdring/bin/python 2 | 3 | # The 'runserver.py' file is used to run a Flask application 4 | # using the development WSGI web server provided by Flask. 5 | # Run 'python runserver.py' and point your web browser to http://localhost:5000/ 6 | 7 | 8 | from app.app_and_db import app, db 9 | from app.startup.init_app import init_app 10 | 11 | init_app(app, db) 12 | app.run(port=5000, debug=True) 13 | -------------------------------------------------------------------------------- /app/pages/views.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 SolidBuilds.com. All rights reserved 2 | # 3 | # Authors: Ling Thio 4 | 5 | 6 | from flask import redirect, render_template, render_template_string 7 | from flask import request, url_for 8 | from flask_user import current_user 9 | 10 | from app.app_and_db import app 11 | 12 | 13 | # 14 | # Home page 15 | # 16 | @app.route('/') 17 | def home_page(): 18 | return render_template('pages/home_page.html') 19 | 20 | -------------------------------------------------------------------------------- /app/templates/pages/home_page.html: -------------------------------------------------------------------------------- 1 | {% extends "base_templates/page_base.html" %} {# base_templates/page_base.html extends base_templates/base.html #} 2 | 3 | {% block main %} 4 |

Home page

5 | 6 | {% if current_user.is_authenticated() %} 7 |

Edit Profile

8 |

Sign out

9 | {% else %} 10 |

Sign in

11 | {% endif %} 12 | 13 | {% endblock %} -------------------------------------------------------------------------------- /app/app_and_db.py: -------------------------------------------------------------------------------- 1 | # This file declares the Flask Singletons 'app' and 'db' 2 | # 'app' and 'db' are defined in a separate file to avoid circular imports 3 | # Usage: from app.app_and_db import app, db 4 | # 5 | # Copyright 2014 SolidBuilds.com. All rights reserved 6 | # 7 | # Authors: Ling Thio 8 | 9 | from flask import Flask 10 | from flask_sqlalchemy import SQLAlchemy 11 | 12 | # This is the WSGI compliant web application object 13 | app = Flask(__name__) 14 | 15 | # This is the SQLAlchemy ORM object 16 | db = SQLAlchemy(app) 17 | -------------------------------------------------------------------------------- /app/config/settings.py: -------------------------------------------------------------------------------- 1 | # Application settings are stored in two files: 2 | # - local_settings.py contain settings that are environment-specific or security related 3 | # This file is NOT checked into the code repository. 4 | # See also local_settings_example.py (which IS part of the code repository). 5 | # - settings.py contain settings that are the same across different environments 6 | # This file IS checked into the code repository. 7 | 8 | import os 9 | 10 | # Get application base dir. 11 | _basedir = os.path.abspath(os.path.dirname(__file__)) 12 | 13 | # Flask settings 14 | CSRF_ENABLED = True 15 | 16 | # Application settings 17 | APP_NAME = "AppName" 18 | APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error" -------------------------------------------------------------------------------- /app/templates/users/user_profile_page.html: -------------------------------------------------------------------------------- 1 | {% extends "base_templates/page_base.html" %} {# base_templates/page_base.html extends base_templates/base.html #} 2 | 3 | {% block main %} 4 |

User Profile

5 | 6 |

Change username

7 |

Change password

8 | 9 | {% from "base_templates/form_macros.html" import render_field, render_submit_field %} 10 |
11 |
12 |
13 | {{ form.hidden_tag() }} 14 | 15 | {{ render_field(form.first_name, tabindex=240) }} 16 | 17 | {{ render_field(form.last_name, tabindex=250) }} 18 | 19 | {{ render_submit_field(form.submit, tabindex=280) }} 20 |
21 |
22 |
23 | 24 | {% endblock %} -------------------------------------------------------------------------------- /tests/_readme.txt: -------------------------------------------------------------------------------- 1 | # This directory contains all the unit tests for this Flask application. 2 | # pytest is used to run these tests (pytest is more 'pythonic' than unittest). 3 | 4 | File Description 5 | ------------- ----------- 6 | .coverage # Configuration file for the test coverage tool (fab test_cov) 7 | conftest.py # Contains module level test fixtures for pytest 8 | # pytest requires this file to be 'conftest.py' 9 | test_*.py # Unit test files 10 | # pytest requires these files to start with the name 'test' 11 | 12 | 13 | Install 14 | ------- 15 | 16 | pytest is installed using 17 | pip install pytest 18 | The command line tool is named 'py.test' (with a dot) 19 | 20 | 21 | Usage 22 | ----- 23 | 24 | Tests are run from the root directory: 25 | py.test -s tests/ 26 | 27 | Code coverage is performed from the root directory: 28 | py.test -s --cov app --cov-config tests/.coveragerc --cov-report term-missing tests/') -------------------------------------------------------------------------------- /app/users/forms.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 SolidBuilds.com. All rights reserved 2 | # 3 | # Authors: Ling Thio 4 | 5 | from flask_user.forms import RegisterForm 6 | from flask_wtf import Form 7 | from wtforms import StringField, SubmitField, validators 8 | 9 | 10 | # Define the User registration form 11 | # It augments the Flask-User RegisterForm with additional fields 12 | class MyRegisterForm(RegisterForm): 13 | first_name = StringField('First name', validators=[ 14 | validators.DataRequired('First name is required')]) 15 | last_name = StringField('Last name', validators=[ 16 | validators.DataRequired('Last name is required')]) 17 | 18 | 19 | # Define the User profile form 20 | class UserProfileForm(Form): 21 | first_name = StringField('First name', validators=[ 22 | validators.DataRequired('First name is required')]) 23 | last_name = StringField('Last name', validators=[ 24 | validators.DataRequired('Last name is required')]) 25 | submit = SubmitField('Save') 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # General Python settings (from https://github.com/github/gitignore) 3 | # 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | 61 | # 62 | # Application specific settings 63 | # 64 | .idea/ 65 | *.sqlite 66 | app/config/local_settings.py -------------------------------------------------------------------------------- /app/config/local_settings_example.py: -------------------------------------------------------------------------------- 1 | # Application settings are stored in two files: 2 | # - local_settings.py contain settings that are environment-specific or security related 3 | # This file is NOT checked into the code repository. 4 | # See also local_settings_example.py (which IS part of the code repository). 5 | # - settings.py contain settings that are the same across different environments 6 | # This file IS checked into the code repository. 7 | 8 | # Flask settings 9 | SECRET_KEY = '\xb9\x8d\xb5\xc2\xc4Q\xe7\x8ej\xe0\x05\xf3\xa3kp\x99l\xe7\xf2i\x00\xb1-\xcd' # Generated with: import os; os.urandom(24) 10 | 11 | # Flask-Mail settings for smtp.webfaction.com 12 | MAIL_USERNAME = 'YOURNAME@gmail.com' 13 | MAIL_PASSWORD = 'YOURPASSWORD' 14 | MAIL_DEFAULT_SENDER = 'NOREPLY ' 15 | 16 | MAIL_SERVER = 'smtp.gmail.com' 17 | MAIL_PORT = 587 18 | MAIL_USE_SSL = False 19 | MAIL_USE_TLS = True 20 | 21 | # SQLAlchemy settings 22 | SQLALCHEMY_DATABASE_URI = 'sqlite:///app.sqlite' # SQLite DB 23 | 24 | # Admins to receive System Error emails 25 | ADMINS = [ 26 | '"YOUR NAME" ', 27 | ] 28 | -------------------------------------------------------------------------------- /app/users/views.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 SolidBuilds.com. All rights reserved 2 | # 3 | # Authors: Ling Thio 4 | 5 | 6 | from flask import redirect, render_template, render_template_string 7 | from flask import request, url_for 8 | from flask_user import current_user, login_required 9 | 10 | from app.app_and_db import app, db 11 | from app.users.forms import UserProfileForm 12 | 13 | # 14 | # User Profile form 15 | # 16 | @app.route('/user/profile', methods=['GET', 'POST']) 17 | @login_required 18 | def user_profile_page(): 19 | # Initialize form 20 | user_profile = current_user.user_profile 21 | form = UserProfileForm(request.form, user_profile) 22 | 23 | # Process valid POST 24 | if request.method=='POST' and form.validate(): 25 | 26 | # Copy form fields to user_profile fields 27 | form.populate_obj(user_profile) 28 | 29 | # Save user_profile 30 | db.session.commit() 31 | 32 | # Redirect to home page 33 | return redirect(url_for('home_page')) 34 | 35 | # Process GET or invalid POST 36 | return render_template('users/user_profile_page.html', 37 | form=form) 38 | 39 | -------------------------------------------------------------------------------- /app/_readme.txt: -------------------------------------------------------------------------------- 1 | This is the root directory of a Flask application. 2 | 3 | It contains the following Flask-specific subdirectories: 4 | - static # This subdirectory will be mapped to the "/static/" URL 5 | - templates # Jinja2 HTML template files 6 | 7 | It contains the following special App subdirectories: 8 | - config # Application setting files 9 | - startup # Application startup code 10 | 11 | The remaining subdirectories organizes the code into functional modules: 12 | - pages # Web pages (without a CMS for now) 13 | - users # User and Role related code 14 | 15 | Each functional models organizes code in the following way: 16 | - models.py # Database/Object models 17 | - views.py # Model-View functions that typically: 18 | # - Loads objects from the database 19 | # - Prepares a template data context 20 | # - Renders the context using a Jinja2 template file 21 | 22 | It contains the following Python-specific files: 23 | - __init__.py # special Python file to turn a subdirectory into a Python 'package' 24 | # Python packages can be accessed using the Python 'import' statement 25 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | # The 'fabfile.py' is used by Fabric and must reside in the application root directory. 2 | 3 | from __future__ import with_statement 4 | from fabric.api import * 5 | from fabric.contrib.console import confirm 6 | from contextlib import contextmanager 7 | 8 | @task 9 | def runserver(): 10 | """ 11 | Start the web application using a development WSGI webserver provided by Flask 12 | """ 13 | local('python runserver.py') 14 | 15 | 16 | @task 17 | def test_cov(): 18 | """ 19 | Run the automated test suite using py.test 20 | """ 21 | local('py.test --tb=short -s --cov app --cov-config tests/.coveragerc --cov-report term-missing tests/') 22 | 23 | 24 | @task 25 | def test(): 26 | """ 27 | Run the automated test suite using py.test 28 | """ 29 | local('py.test --tb=short -s tests/') 30 | 31 | 32 | @task 33 | def update_env(): 34 | """ 35 | Install required Python packages using pip and requirements.txt 36 | """ 37 | local('if [ ! -f app/config/local_settings.py ]; then cp app/config/local_settings_example.py app/config/local_settings.py; fi') 38 | local('pip install -r requirements.txt') 39 | 40 | 41 | @contextmanager 42 | def virtualenv(venv_name): 43 | with prefix('source ~/.virtualenvs/'+venv_name+'/bin/activate'): 44 | yield 45 | -------------------------------------------------------------------------------- /app/templates/_readme.txt: -------------------------------------------------------------------------------- 1 | # A flask Application routes URLs to model-view functions (in app/XYZ/views.py). 2 | # 3 | # A model-view function typically performs the following steps: 4 | # - Retrieves objects from the database 5 | # - Prepares a template context as a dictionary of name/value pairs 6 | # - Calls render_template('filename.html') 7 | # 8 | # The render_template('filename.html', context) function 9 | # uses the Jinja2 template engine to: 10 | # - Retrieve the HTML template file app/templates/filename.html 11 | # - Replace "{{ name }}" instances with the value of the context['name'] dictionary 12 | # - Return an HTML string wrapped in an HTTP envelope 13 | # 14 | # Jinja2 allows template files to extend base template files. 15 | # E.g. home_page.html extends page_base.html which extends base.html. 16 | # allowing page generic html to be defined in the base file and page specific html in the extended file. 17 | 18 | Flask and Flask-User related subdirectories 19 | - base_templates # Base templates that other templates extend from 20 | - flask_user # Flask-User template files. These must be in app/templates/flask_user/ 21 | 22 | Application template files, organized by module. 23 | Roughly follows the app/ directory structure. 24 | - pages # Web page templates 25 | - users # User form templates 26 | -------------------------------------------------------------------------------- /tests/test_simple_urls.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 SolidBuilds.com. All rights reserved 2 | # 3 | # Authors: Ling Thio 4 | 5 | from __future__ import print_function # Use print() instead of print 6 | from flask import url_for 7 | 8 | def test_simple_urls(app): 9 | """ 10 | Visit 'simple' URLs known to the Flask application. 11 | Simple URLs are URLs without default-less parameters. 12 | """ 13 | # Retrieve all URLs known to the Flask application 14 | print('') 15 | for rule in app.url_map.iter_rules(): 16 | # Calculate number of default-less parameters 17 | params = len(rule.arguments) if rule.arguments else 0 18 | params_with_default = len(rule.defaults) if rule.defaults else 0 19 | params_without_default = params - params_with_default 20 | 21 | # Skip routes with default-less parameters 22 | if params_without_default>0: continue 23 | 24 | # Skip routes without a GET method 25 | if 'GET' not in rule.methods: continue 26 | 27 | # Retrieve a browser client simulator from the Flask app 28 | client = app.test_client() 29 | 30 | # Simulate visiting the simple URL 31 | url = url_for(rule.endpoint) 32 | print('Visiting URL ' + url) 33 | client.get(url, follow_redirects=True) 34 | 35 | return -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # This file contains pytest 'fixtures'. 2 | # If a test functions specifies the name of a fixture function as a parameter, 3 | # the fixture function is called and its result is passed to the test function. 4 | # 5 | # Copyright 2014 SolidBuilds.com. All rights reserved 6 | # 7 | # Authors: Ling Thio 8 | 9 | import pytest 10 | from app.app_and_db import app as flask_app, db as sqlalchemy_db 11 | from app.startup.init_app import init_app 12 | 13 | @pytest.fixture(scope='module') 14 | def app(): 15 | """ 16 | Initializes and returns a Flask application object 17 | """ 18 | # Initialize the Flask-App with test-specific settings 19 | test_config_settings = dict( 20 | TESTING=True, # Propagate exceptions 21 | LOGIN_DISABLED=False, # Enable @register_required 22 | MAIL_SUPPRESS_SEND=True, # Disable Flask-Mail send 23 | SERVER_NAME='localhost', # Enable url_for() without request context 24 | SQLALCHEMY_DATABASE_URI='sqlite:///:memory:', # In-memory SQLite DB 25 | WTF_CSRF_ENABLED=False, # Disable CSRF form validation 26 | ) 27 | init_app(flask_app, sqlalchemy_db, test_config_settings) 28 | 29 | # Setup an application context (since the tests run outside of the webserver context) 30 | flask_app.app_context().push() 31 | 32 | return flask_app 33 | 34 | @pytest.fixture(scope='module') 35 | def db(): 36 | """ 37 | Initializes and returns a SQLAlchemy DB object 38 | """ 39 | return sqlalchemy_db 40 | -------------------------------------------------------------------------------- /app/templates/base_templates/form_macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%} 2 |
3 | {% if field.type != 'HiddenField' and label_visible %} 4 | {% if not label %}{% set label=field.label.text %}{% endif %} 5 | 6 | {% endif %} 7 | {{ field(class_='form-control', **kwargs) }} 8 | {% if field.errors %} 9 | {% for e in field.errors %} 10 |

{{ e }}

11 | {% endfor %} 12 | {% endif %} 13 |
14 | {%- endmacro %} 15 | 16 | {% macro render_checkbox_field(field, label=None) -%} 17 | {% if not label %}{% set label=field.label.text %}{% endif %} 18 |
19 | 22 |
23 | {%- endmacro %} 24 | 25 | {% macro render_radio_field(field) -%} 26 | {% for value, label, _ in field.iter_choices() %} 27 |
28 | 31 |
32 | {% endfor %} 33 | {%- endmacro %} 34 | 35 | {% macro render_submit_field(field, label=None, tabindex=None) -%} 36 | {% if not label %}{% set label=field.label.text %}{% endif %} 37 | {##} 38 | 41 | {%- endmacro %} 42 | -------------------------------------------------------------------------------- /app/templates/flask_user/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/public_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Register{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | 10 | {# Username or Email #} 11 | {% set field = form.username if user_manager.enable_username else form.email %} 12 |
13 | {# Label on left, "Already registered? Sign in." on right #} 14 |
15 |
16 | 17 |
18 |
19 | {% if user_manager.enable_register %} 20 | 21 | {%trans%}Already registered? Sign in.{%endtrans%} 22 | {% endif %} 23 |
24 |
25 | {{ field(class_='form-control', tabindex=210) }} 26 | {% if field.errors %} 27 | {% for e in field.errors %} 28 |

{{ e }}

29 | {% endfor %} 30 | {% endif %} 31 |
32 | 33 | {% if user_manager.enable_email and user_manager.enable_username %} 34 | {{ render_field(form.email, tabindex=220) }} 35 | {% endif %} 36 | 37 | {{ render_field(form.first_name, tabindex=240) }} 38 | 39 | {{ render_field(form.last_name, tabindex=250) }} 40 | 41 | {{ render_field(form.password, tabindex=260) }} 42 | 43 | {% if user_manager.enable_retype_password %} 44 | {{ render_field(form.retype_password, tabindex=270) }} 45 | {% endif %} 46 | 47 | {{ render_submit_field(form.submit, tabindex=280) }} 48 |
49 | 50 | {% endblock %} -------------------------------------------------------------------------------- /app/users/models.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 SolidBuilds.com. All rights reserved 2 | # 3 | # Authors: Ling Thio 4 | 5 | from flask_user import UserMixin 6 | from app.app_and_db import db 7 | 8 | # Define the User model. Make sure to add the flask_user.UserMixin !! 9 | class User(db.Model, UserMixin): 10 | id = db.Column(db.Integer, primary_key=True) 11 | user_profile_id = db.Column(db.Integer(), db.ForeignKey('user_profile.id', ondelete='CASCADE')) 12 | 13 | # Flask-User fields 14 | active = db.Column(db.Boolean(), nullable=False, server_default='0') 15 | username = db.Column(db.String(50), nullable=False, unique=True) 16 | email = db.Column(db.String(255), nullable=False, unique=True) 17 | confirmed_at = db.Column(db.DateTime()) 18 | password = db.Column(db.String(255), nullable=False, server_default='') 19 | reset_password_token = db.Column(db.String(100), nullable=False, server_default='') 20 | 21 | # Relationships 22 | user_profile = db.relationship('UserProfile', uselist=False, backref="user") 23 | 24 | 25 | # Define the UserProfile model 26 | # The User model contains login-related fields 27 | # The UserProfile model contains additional User fields 28 | class UserProfile(db.Model): 29 | id = db.Column(db.Integer, primary_key=True) 30 | first_name = db.Column(db.String(50), nullable=False, server_default='') 31 | last_name = db.Column(db.String(50), nullable=False, server_default='') 32 | 33 | # def full_name(self): 34 | # """ Return 'first_name last_name' """ 35 | # # Handle records with an empty first_name or an empty last_name 36 | # name = self.first_name 37 | # name += ' ' if self.first_name and self.last_name else '' 38 | # name += self.last_name 39 | # return name 40 | 41 | 42 | # Define the Role model 43 | class Role(db.Model): 44 | id = db.Column(db.Integer(), primary_key=True) 45 | name = db.Column(db.String(50), unique=True) 46 | description = db.Column(db.String(255)) 47 | 48 | 49 | # Define the UserRoles association model 50 | class UserRoles(db.Model): 51 | id = db.Column(db.Integer(), primary_key=True) 52 | user_id = db.Column(db.Integer(), db.ForeignKey('user.id', ondelete='CASCADE')) 53 | role_id = db.Column(db.Integer(), db.ForeignKey('role.id', ondelete='CASCADE')) 54 | 55 | # Relationships 56 | user = db.relationship('User', backref='roles') 57 | role = db.relationship('Role', backref='users') 58 | 59 | -------------------------------------------------------------------------------- /app/templates/base_templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | App Title 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | {% if current_user.is_authenticated() %} 27 | {{ current_user.user_profile.first_name }} 28 |   |   29 | Sign out 30 | {% else %} 31 | Sign in 32 | {% endif %} 33 |
34 |
35 |
36 | 37 |
38 | {# One-time system messages called Flash messages #} 39 | {% block flash_messages %} 40 | {%- with messages = get_flashed_messages(with_categories=true) -%} 41 | {% if messages %} 42 | {% for category, message in messages %} 43 | {% if category=='error' %} 44 | {% set category='danger' %} 45 | {% endif %} 46 |
{{ message|safe }}
47 | {% endfor %} 48 | {% endif %} 49 | {%- endwith %} 50 | {% endblock %} 51 | 52 | {% block main %}{% endblock %} 53 |
54 | 55 |
56 |
57 | 61 | 62 | -------------------------------------------------------------------------------- /app/startup/init_app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 SolidBuilds.com. All rights reserved 2 | # 3 | # Authors: Ling Thio 4 | 5 | import logging 6 | from logging.handlers import SMTPHandler 7 | from flask_mail import Mail 8 | from flask_user import UserManager, SQLAlchemyAdapter 9 | 10 | def init_app(app, db, extra_config_settings={}): 11 | """ 12 | Initialize Flask applicaton 13 | """ 14 | 15 | # Initialize app config settings 16 | # - settings.py is checked into Git. 17 | # - local_settings.py is different for each deployment 18 | # - extra_config_settings{} is specified by the automated test suite 19 | app.config.from_object('app.config.settings') # Read config from 'app/settings.py' file 20 | app.config.from_object('app.config.local_settings') # Overwrite with 'app/local_settings.py' file 21 | app.config.update(extra_config_settings) # Overwrite with 'extra_config_settings' parameter 22 | if app.testing: 23 | app.config['WTF_CSRF_ENABLED'] = False # Disable CSRF checks while testing 24 | 25 | # Setup Flask-Mail 26 | mail = Mail(app) 27 | 28 | # Setup an error-logger to send emails to app.config.ADMINS 29 | init_error_logger_with_email_handler(app) 30 | 31 | # Setup Flask-User to handle user account related forms 32 | from app.users.models import User, UserProfile 33 | from app.users.forms import MyRegisterForm 34 | db_adapter = SQLAlchemyAdapter(db, User, # Select database adapter 35 | UserProfileClass=UserProfile) # with a custom UserProfile model 36 | user_manager = UserManager(db_adapter, app, # Init Flask-User and bind to app 37 | register_form=MyRegisterForm) # using a custom register form with UserProfile fields 38 | 39 | # Load all models.py files to register db.Models with SQLAlchemy 40 | from app.users import models 41 | 42 | # Load all views.py files to register @app.routes() with Flask 43 | from app.pages import views 44 | from app.users import views 45 | 46 | # Automatically create all DB tables in app/app.sqlite file 47 | db.create_all() 48 | 49 | return app 50 | 51 | 52 | def init_error_logger_with_email_handler(app): 53 | """ 54 | Initialize a logger to send emails on error-level messages. 55 | Unhandled exceptions will now send an email message to app.config.ADMINS. 56 | """ 57 | if app.debug: return # Do not send error emails while developing 58 | 59 | # Retrieve email settings from app.config 60 | host = app.config['MAIL_SERVER'] 61 | port = app.config['MAIL_PORT'] 62 | from_addr = app.config['MAIL_DEFAULT_SENDER'] 63 | username = app.config['MAIL_USERNAME'] 64 | password = app.config['MAIL_PASSWORD'] 65 | secure = () if app.config.get('MAIL_USE_TLS') else None 66 | 67 | # Retrieve app settings from app.config 68 | to_addr_list = app.config['ADMINS'] 69 | subject = app.config.get('APP_SYSTEM_ERROR_SUBJECT_LINE', 'System Error') 70 | 71 | # Setup an SMTP mail handler for error-level messages 72 | mail_handler = SMTPHandler( 73 | mailhost=(host, port), # Mail host and port 74 | fromaddr=from_addr, # From address 75 | toaddrs=to_addr_list, # To address 76 | subject=subject, # Subject line 77 | credentials=(username, password), # Credentials 78 | secure=secure, 79 | ) 80 | mail_handler.setLevel(logging.ERROR) 81 | app.logger.addHandler(mail_handler) 82 | 83 | # Log errors using: app.logger.error('Some error message') 84 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-User starter app 2 | ======== 3 | 4 | This code base serves as a great starting point to write your next Flask application. 5 | 6 | 7 | **Development Feature set** 8 | 9 | * Modular and well-document directory structure with _readme.txt files. 10 | * Well-named source code files with lots of helpful comments. 11 | * Separate ``settings.py`` for general settings and ``local_settings.py`` for environment-specific settings. 12 | * Bootstrap3 base template that can easily be changed. 13 | * Complete with a skeleton for an automated test suite. 14 | 15 | It's a great starting point for new Flask applications -- whether you plan to use Flask-User or not. 16 | 17 | 18 | **Application Feature set** 19 | 20 | * Home page 21 | * User Profile page 22 | * User account management (Register, Confirm, Forgot password, 23 | Login, Change username, Change password, Logout) using Flask-User 24 | * SMTPHandler for error-level log messages -- sends emails on unhandled exceptions 25 | 26 | 27 | **Dependencies** 28 | 29 | * Flask 30 | * Flask-SQLAlchemy 31 | * Flask-User 32 | 33 | 34 | Installation 35 | ~~~~~~~~ 36 | 37 | **Install git** 38 | 39 | See http://git-scm.com/book/en/Getting-Started-Installing-Git 40 | 41 | **Clone this github repository** 42 | 43 | Open a command line shell and type: 44 | 45 | :: 46 | 47 | mkdir -p ~/dev 48 | cd ~/dev 49 | git clone https://github.com/lingthio/Flask-User-starter-app.git app 50 | cd ~/dev/app 51 | 52 | **Install virtualenvwrapper** 53 | 54 | Install it with:: 55 | 56 | sudo pip install virtualenvwrapper 57 | 58 | Configure it with:: 59 | 60 | export WORKON_HOME=$HOME/.virtualenvs 61 | export PROJECT_HOME=$HOME/dev 62 | source /usr/local/bin/virtualenvwrapper.sh 63 | 64 | You may want to place the above in your .bashrc or .profile or .bash_profile file 65 | 66 | See http://virtualenvwrapper.readthedocs.org/en/latest/install.html 67 | 68 | **Create a new virtualenv** 69 | 70 | Find a Python 2.7 executable using ``python --version`` and ``which python``. 71 | 72 | Run this command once: 73 | 74 | :: 75 | 76 | mkvirtualenv -p /full/path/to/python2.7 app 77 | workon app 78 | 79 | where the result of ``which python`` can be used instead of ``/full/path/to/python2.7``, 80 | and where ``app`` is the name of the new virtualenv. 81 | 82 | **Install Fabric** 83 | 84 | Fabric is a build and deployment tool that uses the Python language for its scripts. 85 | Though the product name is 'Fabric', the command line tool is 'fab'. 86 | 87 | :: 88 | 89 | workon app 90 | pip install python-dev 91 | pip install python-setuptools 92 | pip install fabric 93 | 94 | See also: http://www.fabfile.org/installing.html 95 | 96 | **Install required Python packages** 97 | 98 | :: 99 | 100 | workon app 101 | cd ~/dev/app 102 | fab update_env 103 | 104 | **Update configuration settings** 105 | 106 | Before we can use this application, we will have to configure the SMTP account that it will be using to send emails. 107 | 108 | Make sure that app/config/local_settings.py exists, or create it from a copy of local_settings_example.py file 109 | 110 | Edit app/config/local_settings.py and configure the following settings: 111 | 112 | * MAIL_USERNAME 113 | * MAIL_PASSWORD 114 | * MAIL_DEFAULT_SENDER 115 | * ADMINS 116 | 117 | 118 | Automated tests and code coverage 119 | ------ 120 | The tests are in the tests/ directory. 121 | 122 | pytest is used to run the automated tests. 123 | 124 | pytest is also used to run the code coverage assessment. 125 | 126 | :: 127 | 128 | workon app 129 | cd ~/dev/app 130 | fab test 131 | fab test_cov 132 | 133 | 134 | Running the app 135 | ~~~~~~~~ 136 | 137 | **Start the development webserver** 138 | 139 | Flask comes with a convenient WSGI web application server for development environments. 140 | 141 | :: 142 | 143 | workon app 144 | cd ~/dev/app 145 | fab runserver 146 | 147 | Point your web browser to http://localhost:5000/ 148 | 149 | 150 | Creating a user account 151 | ~~~~~~~ 152 | * Make sure that app/config/local_settings.py has the appropriate ``MAIL_*`` settings. 153 | * Point your web browser to http://localhost:5000/ 154 | * Click on 'Sign in' and then 'Register' and register a new user account. 155 | * Confirm your email address 156 | 157 | 158 | Acknowledgements 159 | ~~~~~~~~ 160 | This project used `Flask-User-starter-app `_ as a starting point. 161 | --------------------------------------------------------------------------------