Max Halford před 10 roky
revize
ede0ceb2c8

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+*.pyc
+*.db
+*webassests*

+ 107 - 0
README.md

@@ -0,0 +1,107 @@
+# Flask boilerplate
+
+![License](http://img.shields.io/:license-mit-blue.svg)
+
+I didn't really like the said so boilerplate code I found searching the web. I really like Flask and I use for quite a few projects. I decided to make a clean, readable, documented starter project. I didn't include any [makefile](https://www.wikiwand.com/en/Makefile) or [fabric](http://flask.pocoo.org/docs/0.10/patterns/fabric/) as I feel it imposes a choice to the user of this project, I rather keep things simple (even though the word is subject to interpretation).
+
+## Features
+
+- [x] User account sign up, sign in, password reset, all through email confirmation.
+- [x] Form generation.
+- [x] Error handling.
+- [x] HTML macros.
+- [x] HTML layout file.
+- [x] "Functional" file structure.
+- [x] Python 3.x compliant.
+- [] Static file bundling, automatic SCSS to CSS conversion and automatic minifying.
+- [] Websockets (for example for live chatting)
+- [] Virtual environment.
+- [] Easy Heroky deployment.
+- [] Easy Digital Ocean deployment.
+- [] Tests.
+- [] Logging.
+
+If you have any suggestions or want to help, feel free to drop me a line at <maxhalford25@gmail.com> or to create an issue.
+
+## Libraries
+
+### Backend
+
+- [Flask](http://flask.pocoo.org/), obviously.
+- [Flask-Login](https://flask-login.readthedocs.org/en/latest/) for the user accounts.
+- [Flask-SQLAlchemy](https://pythonhosted.org/Flask-SQLAlchemy/) interacting with the database.
+- [Flask-WTF](https://flask-wtf.readthedocs.org/en/latest/) and [WTForms](https://wtforms.readthedocs.org/en/latest/) for the form handling.
+- [Flask-Mail](https://pythonhosted.org/Flask-Mail/) for sending mails.
+- [itsdangerous](http://pythonhosted.org/itsdangerous/) for generating random tokens for the confirmation emails.
+- [Flask-Bcrypt](https://flask-bcrypt.readthedocs.org/en/latest/) for generating secret user passwords.
+
+### Frontend
+
+- [Semantic UI](http://semantic-ui.com/) for the global style. Very similar to [Bootstrap](http://getbootstrap.com/).
+- [Leaflet JS](http://leafletjs.com/) for the map. I only added for the sake of the example.
+
+## Structure
+
+I did what most people recommend for the application's structure. Basically, everything is contained in the ``app/`` folder.
+
+- There you have the classic ``static/`` and ``templates/`` folders. The ``templates/`` folder contains macros, error views and a common layout.
+- I added a ``views/`` folder to separate the user and the website logic, which could be extended to the the admin views.
+- The same goes for the ``forms/`` folder, as the project grows it will be useful to split the WTForms code into separate files.
+- The ``models.py`` script contains the SQLAlchemy code, for the while it only contains the logic for a ``users`` table.
+- The ``toolbox/`` folder is a personal choice, in it I keep all the other code the application will need.
+
+
+## Setup
+
+### Vanilla
+
+- Install the required libraries.
+
+	pip install -r requirements.txt
+
+- Create the database.
+
+	python createdb.py
+
+- Run the application.
+
+	python run.py
+
+- Navigate to ``localhost:5000``
+
+
+### Virtual environment
+
+To do.
+
+
+## Configuration
+
+The goal is to keep most of the application's configuration in a single file called ``config.py``. The one I have included is basic and yet it covers most of the important stuff.
+
+I have included a working Gmail account to confirm user email addresses and reset user passwords, although in production you should't include the file if you push to GitHub because people can see it. The same goes for API keys, you should keep them secret. You can read more about secret configuration files [here](https://exploreflask.com/configuration.html).
+
+Read [this](http://flask.pocoo.org/docs/0.10/config/) for information on the possible configuration options.
+
+
+## Deploy
+
+To do.
+
+
+## Examples
+
+- [Screenshots](screenshots/)
+
+
+## Inspiration
+
+- [The Flask Mega-Tutorial](http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world).
+- [Explore Flask](https://exploreflask.com/index.html).
+
+
+## Other possibilities
+
+- [flask-boilerplate](https://github.com/mjhea0/flask-boilerplate) by [Michael Herman](https://github.com/mjhea0).
+- [Flask-Foundation](https://github.com/JackStouffer/Flask-Foundation) by [Jack Stouffer](https://github.com/JackStouffer).
+- [fbone](https://github.com/imwilsonxu/fbone) by [Wilson Xu](https://github.com/imwilsonxu).

+ 35 - 0
app/__init__.py

@@ -0,0 +1,35 @@
+from flask import Flask
+
+app = Flask(__name__)
+
+# Setup the app with the config.py file
+app.config.from_object('config')
+
+# Setup the database
+from flask.ext.sqlalchemy import SQLAlchemy
+db = SQLAlchemy(app)
+
+# Setup the mail server
+from flask.ext.mail import Mail
+mail = Mail(app)
+
+# Setup the password crypting
+from flask.ext.bcrypt import Bcrypt
+bcrypt = Bcrypt(app)
+
+# Import the views
+from app.views import main, user, error
+app.register_blueprint(user.user)
+
+# Setup the user login process
+from flask.ext.login import LoginManager
+from app.models import User
+
+login_manager = LoginManager()
+login_manager.init_app(app)
+login_manager.login_view = 'user.login'
+
+
+@login_manager.user_loader
+def load_user(email):
+    return User.query.filter(User.email == email).first()

+ 0 - 0
app/forms/__init__.py


+ 78 - 0
app/forms/user.py

@@ -0,0 +1,78 @@
+from flask.ext.wtf import Form
+from wtforms import TextField, PasswordField
+from wtforms.validators import (Required, Length, Email, ValidationError,
+                                EqualTo)
+from app.models import User
+
+
+class Unique(object):
+
+    '''
+    Custom validator to check an object's attribute
+    is unique. For example users should not be able
+    to create an account if the account's email
+    address is already in the database. This class
+    supposes you are using SQLAlchemy to query the
+    database.
+    '''
+
+    def __init__(self, model, field, message):
+        self.model = model
+        self.field = field
+        self.message = message
+
+    def __call__(self, form, field):
+        check = self.model.query.filter(self.field == field.data).first()
+        if check:
+            raise ValidationError(self.message)
+
+
+class Forgot(Form):
+
+    ''' User forgot password form. '''
+
+    email = TextField(validators=[Required(), Email()],
+                      description='Email address')
+
+
+class Reset(Form):
+
+    ''' User reset password form. '''
+
+    password = PasswordField(validators=[
+        Required(), Length(min=6),
+        EqualTo('confirm', message='Passwords must match.')
+    ], description='Password')
+    confirm = PasswordField(description='Confirm password')
+
+
+class Login(Form):
+
+    ''' User login form. '''
+
+    email = TextField(validators=[Required(), Email()],
+                      description='Email address')
+    password = PasswordField(validators=[Required()],
+                             description='Password')
+
+
+class SignUp(Form):
+
+    ''' User sign up form. '''
+
+    name = TextField(validators=[Required(), Length(min=2)],
+                     description='Name')
+    surname = TextField(validators=[Required(), Length(min=2)],
+                        description='Surname')
+    phone = TextField(validators=[Required(), Length(min=6)],
+                      description='Phone number')
+    email = TextField(validators=[Required(), Email(),
+                                  Unique(User, User.email,
+                                         'This email address is ' +
+                                         'already linked to an account.')],
+                      description='Email address')
+    password = PasswordField(validators=[
+        Required(), Length(min=6),
+        EqualTo('confirm', message='Passwords must match.')
+    ], description='Password')
+    confirm = PasswordField(description='Confirm password')

+ 30 - 0
app/models.py

@@ -0,0 +1,30 @@
+from app import db, bcrypt
+from sqlalchemy.ext.hybrid import hybrid_property
+from flask.ext.login import UserMixin
+
+
+class User(db.Model, UserMixin):
+
+    ''' A website user. '''
+
+    __tablename__ = 'users'
+    name = db.Column(db.String)
+    surname = db.Column(db.String)
+    phone = db.Column(db.Integer)
+    email = db.Column(db.String, primary_key=True)
+    confirmation = db.Column(db.Boolean)
+    _password = db.Column(db.String)
+
+    @hybrid_property
+    def password(self):
+        return self._password
+
+    @password.setter
+    def _set_password(self, plaintext):
+        self._password = bcrypt.generate_password_hash(plaintext)
+
+    def check_password(self, plaintext):
+        return bcrypt.check_password_hash(self.password, plaintext)
+
+    def get_id(self):
+        return self.email

+ 51 - 0
app/static/css/custom.css

@@ -0,0 +1,51 @@
+/* Layout */
+
+html,
+body {
+   margin:0;
+   padding:0;
+   height:100%;
+   overflow-x: hidden;
+}
+
+#container {
+   height: 100%;
+   min-height: 100%;
+   position: relative;
+}
+
+#body {
+   margin: 20px;
+   min-height: 100%;
+}
+
+#footer {
+   bottom: 0;
+   width: 100%;
+   height: 50px;   /* Height of the footer */
+   text-align: center;
+}
+
+/* Forms */
+
+.form-user {
+   width: 33% !important;
+   margin: 0 auto !important;
+   margin-top: 10% !important;
+}
+
+.form-error {
+   color: #E50000 !important;
+}
+
+/* Flask flashes */
+
+.flash {
+   margin-right: 20px !important;
+}
+
+/* Errors */
+
+.error-box {
+   margin-top: 8%;
+}

binární
app/static/img/error.png


binární
app/static/img/favicon.ico


+ 15 - 0
app/templates/about.html

@@ -0,0 +1,15 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+{% endblock %}
+
+{% block content %}
+
+	<h1 class="ui header">{{ title }}</h1>
+
+	{% if current_user.is_authenticated %}
+	  	<h2 class="ui header">Hi {{ current_user.name }}!</h2>
+	{% endif %}
+
+{% endblock %}

+ 15 - 0
app/templates/contact.html

@@ -0,0 +1,15 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+{% endblock %}
+
+{% block content %}
+
+	<h1 class="ui header">{{ title }}</h1>
+
+	{% if current_user.is_authenticated %}
+	  	<h2 class="ui header">Hi {{ current_user.name }}!</h2>
+	{% endif %}
+
+{% endblock %}

+ 10 - 0
app/templates/email/confirm.html

@@ -0,0 +1,10 @@
+Please click on the following link to confirm your email address:
+
+<p>
+<a href="{{ confirm_url }}">{{ confirm_url }}</a>
+</p>
+
+<p>
+--<br>
+Flask boilerplate, by <a href="https://github.com/MaxHalford">Max Halford</a>.
+</p>

+ 10 - 0
app/templates/email/reset.html

@@ -0,0 +1,10 @@
+Please click on the following link to reset your password:
+
+<p>
+<a href="{{ reset_url }}">{{ reset_url }}</a>
+</p>
+
+<p>
+--<br>
+Flask boilerplate, by <a href="https://github.com/MaxHalford">Max Halford</a>.
+</p>

+ 14 - 0
app/templates/error.html

@@ -0,0 +1,14 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+{% endblock %}
+
+{% block content %}
+
+    <div class="error-box" align="center">
+        <img class="ui medium image error-image" src="{{ url_for('static', filename='img/error.png') }}">
+        <h1 class="ui header">{{ message }}<h1>
+    </div>
+
+{% endblock %}

+ 51 - 0
app/templates/index.html

@@ -0,0 +1,51 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+    <!-- Leaflet JS -->
+    <script src='https://api.tiles.mapbox.com/mapbox.js/v2.1.9/mapbox.js'></script>
+    <!-- Leaflet CSS -->
+    <link href='https://api.tiles.mapbox.com/mapbox.js/v2.1.9/mapbox.css' rel='stylesheet'/>
+    <!-- Map style -->
+    <style>
+    #map {
+    	height: 600px;
+    }
+    </style>
+{% endblock %}
+
+{% block content %}
+
+	<h1 class="ui header">{{ title }}</h1>
+
+	{% if current_user.is_authenticated %}
+	  	<h2 class="ui header">Hi {{ current_user.name }}!</h2>
+	{% endif %}
+
+    <div id="map"></div>
+    
+    <script type="text/javascript">
+
+        L.mapbox.accessToken = 'pk.eyJ1IjoibGVtYXgiLCJhIjoidnNDV1kzNCJ9.iH26jLhEuimYd6vLOO6v1g';
+        var map = L.mapbox.map('map', 'mapbox.outdoors', {
+            maxZoom: 20,
+            fullscreenControl: true,
+            zoomControl: false
+        })
+        var layers = {
+            "Basique": L.mapbox.tileLayer('mapbox.outdoors').addTo(map),
+            "Lumineuse": L.mapbox.tileLayer('mapbox.light'),
+            "Sombre": L.mapbox.tileLayer('mapbox.dark'),
+            "Comics": L.mapbox.tileLayer('mapbox.comic'),
+            "Crayon": L.mapbox.tileLayer('mapbox.pencil')
+        }
+        L.control.layers(
+            layers,
+            null,
+            {position: 'topleft'}
+        ).addTo(map);
+        map.setView([48.8534100, 2.3488000], 13);
+
+    </script>
+
+{% endblock %}

+ 78 - 0
app/templates/layout.html

@@ -0,0 +1,78 @@
+<!-- HTML macros -->
+{% import "macros.html" as m %}
+<!DOCTYPE html>
+<html lang="en">
+	<div id="container">
+   		<div id="header">
+   			{% block head %}
+				<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no'/>
+				<meta charset="UTF-8">
+				<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
+				<!-- jQuery -->
+				<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
+				<!-- Semantic Main CSS -->
+				<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/1.11.8/semantic.min.css"/>
+				<!-- Semantic Reset CSS -->
+				<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/components/reset.min.css"/>
+				<!-- Semantic Menu CSS -->
+				<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/components/menu.min.css"/>
+				<!-- Main Semantic JS  -->
+			    <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/1.11.8/semantic.min.js"></script>
+				<!-- Custom CSS -->
+				<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
+				<!-- Browser tab name -->
+				<title>{{ title }}</title>
+				<!-- Navbar -->
+				<div class="ui attached inverted menu">
+				  	{{ m.nav_link('index', 'Home') }}
+					{{ m.nav_link('about', 'About') }}
+					{{ m.nav_link('contact', 'Contact') }}
+				 	<div class="right menu">
+				 		<!-- User is logged in -->
+				      	{% if current_user.is_authenticated %}
+				      		<div class="item">
+				        		<a class="ui primary button" href="/user/account">{{ current_user.name }} {{ current_user.surname }}</a>
+				    		</div>
+				    		<div class="item">
+				        		<a class="ui default button" href="/user/signout">Sign out</a>
+				    		</div>
+			          	<!-- User is not logged in -->
+			          	{% else %}
+			          		<div class="item">
+				        		<a class="ui primary button" href="/user/signin">Sign in</a>
+				    		</div>
+				    		<div class="item">
+				        		<a class="ui default button" href="/user/signup">Sign up</a>
+				    		</div>
+						{% endif %}
+			  		</div>
+				</div>
+			{% endblock %}
+   		</div>
+   		<!-- Message flashing -->
+		{% with messages = get_flashed_messages(with_categories=true) %}
+		  	{% if messages %}
+		    	<ul>
+		    		{% for category, message in messages %}
+		      			<li class="ui {{ category }} message flash">{{ message }}</li>
+		    		{% endfor %}
+		    	</ul>
+		  	{% endif %}
+		{% endwith %}
+   		<div id="body" class="body">
+   			{% block content %}{% endblock %}
+   		</div>
+		<div id="footer">
+			<div class="ui divider"></div>
+	    		<div class="ui divided horizontal footer link list">
+			      	<div class="item">
+			        	Max Halford
+			      	</div>
+			      	<div class="item">
+			        	<a href="https://github.com/MaxHalford"><i class="github icon"></i></a>
+			      	</div>
+	    		</div>
+	  		</div>
+		</div>
+	</div>
+</html>

+ 28 - 0
app/templates/macros.html

@@ -0,0 +1,28 @@
+{# myapp/templates/macros.html #}
+
+<!-- Highlight the current tab -->
+{% macro nav_link(endpoint, text) %}
+	{% if request.endpoint is not none %}
+		{% if request.endpoint.endswith(endpoint) %}
+		    <a class="active teal item" href="{{ url_for(endpoint) }}">{{ text }}</a>
+		{% else %}
+		    <a class="item" href="{{ url_for(endpoint) }}">{{ text }}</a>
+		{% endif %}
+	{% else %}
+    	<a class="item" href="{{ url_for(endpoint) }}">{{ text }}</a>
+	{% endif %}
+{% endmacro %}
+
+<!-- Render a WTForm form's field (with it's possible associated errors) from a WTForms form -->
+{% macro render_field(field) %}
+	<div class="field">
+		{{ field(name_=field.name, id=field.name, placeholder=field.description, class_="field") }}
+	</div>
+  	{% if field.errors %}
+    	<ul>
+	    	{% for error in field.errors %}
+	      		<li class="form-error">{{ error }}</li>
+	    	{% endfor %}
+    	</ul>
+  	{% endif %}
+{% endmacro %}

+ 11 - 0
app/templates/user/account.html

@@ -0,0 +1,11 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+{% endblock %}
+
+{% block content %}
+
+    <h1 class="ui header">Hi {{ current_user.name }} {{ current_user.surname }}!</h1>
+
+{% endblock %}

+ 18 - 0
app/templates/user/forgot.html

@@ -0,0 +1,18 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+{% endblock %}
+
+{% block content %}
+
+	<div class="ui form-user center raised very padded text container segment">
+		<form class="ui form" action="/user/forgot" method="POST">
+			<h4 class="ui dividing header">Enter your email address so that we can send you a password reset link</h4>
+			{{ m.render_field(form.email) }}
+		  	{{ form.csrf_token }}
+		  	<button class="ui primary button" type="submit">Confirm</button>
+		</form>
+	</div>
+
+{% endblock %}

+ 24 - 0
app/templates/user/login.html

@@ -0,0 +1,24 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+{% endblock %}
+
+{% block content %}
+
+	<div class="ui form-user center raised very padded text container segment">
+		<form class="ui form" action="/user/signin" method="post">
+			<h4 class="ui dividing header">Login</h4>
+	    	{{ m.render_field(form.email) }}
+	    	{{ m.render_field(form.password) }}
+		  	{{ form.csrf_token }}
+		  	<button class="ui primary button" type="submit">Confirm</button>
+		  	<a href="/user/forgot">
+			  	<div class="ui floated right basic red button">
+	        		I forgot my password
+	    		</div>
+	    	</a>
+		</form>
+	</div>
+
+{% endblock %}

+ 19 - 0
app/templates/user/reset.html

@@ -0,0 +1,19 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+{% endblock %}
+
+{% block content %}
+
+	<div class="ui form-user center raised very padded text container segment">
+		<form class="ui form" action="{{ url_for('user.reset', token=token) }}" method="POST">
+			<h4 class="ui dividing header">Choose a new password</h4>
+			{{ m.render_field(form.password) }}
+	    	{{ m.render_field(form.confirm) }}
+		  	{{ form.csrf_token }}
+		  	<button class="ui primary button" type="submit">Confirm</button>
+		</form>
+	</div>
+
+{% endblock %}

+ 24 - 0
app/templates/user/signin.html

@@ -0,0 +1,24 @@
+{% extends "layout.html" %}
+
+{% block head %}
+    {{ super() }}
+{% endblock %}
+
+{% block content %}
+
+	<div class="ui form-user center raised very padded text container segment">
+		<form class="ui form" action="/user/signin" method="post">
+			<h4 class="ui dividing header">Login</h4>
+	    	{{ m.render_field(form.email) }}
+	    	{{ m.render_field(form.password) }}
+		  	{{ form.csrf_token }}
+		  	<button class="ui primary button" type="submit">Confirm</button>
+		  	<a href="/user/forgot">
+			  	<div class="ui floated right basic red button">
+	        		I forgot my password
+	    		</div>
+	    	</a>
+		</form>
+	</div>
+
+{% endblock %}

+ 19 - 0
app/templates/user/signup.html

@@ -0,0 +1,19 @@
+{% extends "layout.html" %}
+
+{% block content %}
+
+	<div class="ui form-user center raised very padded text container segment">
+		<form class="ui form" action="/user/signup" method="post">
+			<h4 class="ui dividing header">Sign up</h4>
+	    	{{ m.render_field(form.name) }}
+	    	{{ m.render_field(form.surname) }}
+	    	{{ m.render_field(form.phone) }}
+	    	{{ m.render_field(form.email) }}
+	    	{{ m.render_field(form.password) }}
+	    	{{ m.render_field(form.confirm) }}
+		  	{{ form.csrf_token }}
+		  	<button class="ui primary button" type="submit">Submit</button>
+		</form>
+	</div>
+
+{% endblock %}

+ 0 - 0
app/toolbox/__init__.py


+ 14 - 0
app/toolbox/email.py

@@ -0,0 +1,14 @@
+from flask.ext.mail import Message
+from app import app, mail
+
+
+def send(recipient, subject, body):
+    '''
+    Send a mail to a recipient. The body is usually a rendered HTML template.
+    The sender's credentials has been configured in the config.py file.
+    '''
+    sender = app.config['ADMINS'][0]
+    message = Message(subject, sender=sender, recipients=[recipient])
+    message.html = body
+    with app.app_context():
+        mail.send(message)

+ 0 - 0
app/views/__init__.py


+ 22 - 0
app/views/error.py

@@ -0,0 +1,22 @@
+from flask import render_template
+from app import app
+
+
+@app.errorhandler(403)
+def forbidden(e):
+    return render_template('error.html', message='403 forbidden'), 403
+
+
+@app.errorhandler(404)
+def page_not_found(e):
+    return render_template('error.html', message='404 not found'), 404
+
+
+@app.errorhandler(410)
+def gone(e):
+    return render_template('error.html', message='410 gone'), 410
+
+
+@app.errorhandler(500)
+def internal_error(e):
+    return render_template('error.html', message='500 internal error'), 500

+ 18 - 0
app/views/main.py

@@ -0,0 +1,18 @@
+from flask import render_template
+from app import app
+
+
+@app.route('/')
+@app.route('/index')
+def index():
+    return render_template('index.html', title='Home')
+
+
+@app.route('/about')
+def about():
+    return render_template('about.html', title='About')
+
+
+@app.route('/contact')
+def contact():
+    return render_template('contact.html', title='Contact')

+ 150 - 0
app/views/user.py

@@ -0,0 +1,150 @@
+from flask import (Blueprint, render_template, redirect, url_for,
+                   abort, flash)
+from flask.ext.login import login_user, logout_user, login_required
+from itsdangerous import URLSafeTimedSerializer
+from app import app, models, db
+from app.forms import user as user_forms
+from app.toolbox import email
+
+# Serializer for generating random tokens
+ts = URLSafeTimedSerializer(app.config['SECRET_KEY'])
+
+# Create a user blueprint
+user = Blueprint('user', __name__, url_prefix='/user')
+
+
+@user.route('/signup', methods=['GET', 'POST'])
+def signup():
+    form = user_forms.SignUp()
+    if form.validate_on_submit():
+        # Create a user who hasn't validated his email address
+        user = models.User(
+            name=form.name.data,
+            surname=form.surname.data,
+            phone=form.phone.data,
+            email=form.email.data,
+            confirmation=False,
+            password=form.password.data,
+        )
+        # Insert the user in the database
+        db.session.add(user)
+        db.session.commit()
+        # Subject of the confirmation email
+        subject = 'Please confirm your email address.'
+        # Generate a random token
+        token = ts.dumps(user.email, salt='email-confirm-key')
+        # Build a confirm link with token
+        confirmUrl = url_for('user.confirm', token=token, _external=True)
+        # Render an HTML template to send by email
+        html = render_template('email/confirm.html',
+                               confirm_url=confirmUrl)
+        # Send the email to user
+        email.send(user.email, subject, html)
+        # Send back to the home page
+        flash('Check your emails to confirm your email address.', 'positive')
+        return redirect(url_for('index'))
+    return render_template('user/signup.html', form=form, title='Sign up')
+
+
+@user.route('/confirm/<token>', methods=['GET', 'POST'])
+def confirm(token):
+    try:
+        email = ts.loads(token, salt='email-confirm-key', max_age=86400)
+    # The token can either expire or be invalid
+    except:
+        abort(404)
+    # Get the user from the database
+    user = models.User.query.filter_by(email=email).first()
+    # The user has confirmed his or her email address
+    user.confirmation = True
+    # Update the database with the user
+    db.session.commit()
+    # Send to the signin page
+    flash(
+        'Your email address has been confirmed, you can sign in.', 'positive')
+    return redirect(url_for('user.signin'))
+
+
+@user.route('/signin', methods=['GET', 'POST'])
+def signin():
+    form = user_forms.Login()
+    if form.validate_on_submit():
+        user = models.User.query.filter_by(email=form.email.data).first()
+        # Check the user exists
+        if user is not None:
+            # Check the password is correct
+            if user.check_password(form.password.data):
+                login_user(user)
+                # Send back to the home page
+                flash('Succesfully signed in.', 'positive')
+                return redirect(url_for('index'))
+            else:
+                flash('The password you have entered is wrong.', 'negative')
+                return redirect(url_for('user.signin'))
+        else:
+            flash('Unknown email address.', 'negative')
+            return redirect(url_for('user.signin'))
+    return render_template('user/signin.html', form=form, title='Sign in')
+
+
+@user.route('/signout')
+def signout():
+    logout_user()
+    flash('Succesfully signed out.', 'positive')
+    return redirect(url_for('index'))
+
+
+@user.route('/account')
+@login_required
+def account():
+    return render_template('user/account.html', title='Account')
+
+
+@user.route('/forgot', methods=['GET', 'POST'])
+def forgot():
+    form = user_forms.Forgot()
+    if form.validate_on_submit():
+        user = models.User.query.filter_by(email=form.email.data).first()
+        # Check the user exists
+        if user is not None:
+            # Subject of the confirmation email
+            subject = 'Reset your password.'
+            # Generate a random token
+            token = ts.dumps(user.email, salt='password-reset-key')
+            # Build a reset link with token
+            resetUrl = url_for('user.reset', token=token, _external=True)
+            # Render an HTML template to send by email
+            html = render_template('email/reset.html', reset_url=resetUrl)
+            # Send the email to user
+            email.send(user.email, subject, html)
+            # Send back to the home page
+            flash('Check your emails to reset your password.', 'positive')
+            return redirect(url_for('index'))
+        else:
+            flash('Unknown email address.', 'negative')
+            return redirect(url_for('user.forgot'))
+    return render_template('user/forgot.html', form=form)
+
+
+@user.route('/reset/<token>', methods=['GET', 'POST'])
+def reset(token):
+    try:
+        email = ts.loads(token, salt='password-reset-key', max_age=86400)
+    # The token can either expire or be invalid
+    except:
+        abort(404)
+    form = user_forms.Reset()
+    if form.validate_on_submit():
+        user = models.User.query.filter_by(email=email).first()
+        # Check the user exists
+        if user is not None:
+            user.password = form.password.data
+            # Update the database with the user
+            db.session.commit()
+            # Send to the signin page
+            flash('Your password has been reset, you can sign in.', 'positive')
+            return redirect(url_for('user.signin'))
+        else:
+            flash('Unknown email address.', 'negative')
+            return redirect(url_for('user.forgot'))
+    return render_template('user/reset.html', form=form, token=token)

+ 19 - 0
config.py

@@ -0,0 +1,19 @@
+# Name and port of the server
+SERVER_NAME = ['localhost:5000']
+# DEBUG has to be to False in a production enrironment for security reasons
+DEBUG = True
+# Secret key for generating tokens
+SECRET_KEY = 'houdini'
+# Database choice
+SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
+SQLALCHEMY_TRACK_MODIFICATIONS = True
+# Configuration of a Gmail account for sending mails
+MAIL_SERVER = 'smtp.googlemail.com'
+MAIL_PORT = 465
+MAIL_USE_TLS = False
+MAIL_USE_SSL = True
+MAIL_USERNAME = 'flask.boilerplate'
+MAIL_PASSWORD = 'flaskboilerplate123'
+ADMINS = ['flask.boilerplate@gmail.com']
+# Number of times a password is hashed
+BCRYPT_LOG_ROUNDS = 12

+ 2 - 0
createdb.py

@@ -0,0 +1,2 @@
+from app import db
+db.create_all()

+ 8 - 0
requirements.txt

@@ -0,0 +1,8 @@
+flask
+flask-sqlalchemy
+flask-wtf
+wtforms
+flask-login
+flask-mail
+itsdangerous
+flask-bcrypt

+ 2 - 0
run.py

@@ -0,0 +1,2 @@
+from app import app
+app.run()

binární
screenshots/map.png


binární
screenshots/sign_in.png


binární
screenshots/sign_up.png