瀏覽代碼

More stuff!

Max Halford 10 年之前
父節點
當前提交
55524e4861

+ 21 - 11
README.md

@@ -6,20 +6,21 @@ I didn't really like the Flask starter projects I found searching the web. I rea
 
 ## Features
 
-- [x] User account sign up, sign in, password reset, all through email confirmation.
+- [x] User account sign up, sign in, password reset, all through asynchronous email confirmation.
 - [x] Form generation.
 - [x] Error handling.
 - [x] HTML macros.
 - [x] HTML layout file.
 - [x] "Functional" file structure.
 - [x] Python 3.x compliant.
+- [x] Asynchronous AJAX calls.
 - [ ] Application factory.
-- [ ] Flask admin.
+- [x] Online administration.
 - [ ] 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.
+- [x] Virtual environment example.
+- [ ] Heroky deployment example.
+- [x] Digital Ocean deployment example.
 - [ ] Tests.
 - [ ] Logging.
 
@@ -36,6 +37,7 @@ If you have any suggestions or want to help, feel free to drop me a line at <max
 - [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.
+- [Flask-Admin](https://flask-admin.readthedocs.org/en/latest/) for building an administration interface.
 
 ### Frontend
 
@@ -74,7 +76,20 @@ I did what most people recommend for the application's structure. Basically, eve
 
 ### Virtual environment
 
-To do.
+```
+pip install virtualenv
+virtualenv venv
+venv/bin/activate (venv\scripts\activate on Windows)
+pip install -r requirements.txt
+python createdb.py
+python run.py
+```
+
+
+## Deploy
+
+- Heroku
+- [Digital Ocean](deployment/Digital-Ocean.md)
 
 
 ## Configuration
@@ -86,11 +101,6 @@ I have included a working Gmail account to confirm user email addresses and rese
 Read [this](http://flask.pocoo.org/docs/0.10/config/) for information on the possible configuration options.
 
 
-## Deploy
-
-To do.
-
-
 ## Examples
 
 - [Screenshots](screenshots/)

+ 28 - 1
app/__init__.py

@@ -19,7 +19,7 @@ bcrypt = Bcrypt(app)
 
 # Import the views
 from app.views import main, user, error
-app.register_blueprint(user.user)
+app.register_blueprint(user.userbp)
 
 # Setup the user login process
 from flask.ext.login import LoginManager
@@ -33,3 +33,30 @@ login_manager.login_view = 'user.login'
 @login_manager.user_loader
 def load_user(email):
     return User.query.filter(User.email == email).first()
+
+# Setup the admin interface
+from flask import request, Response
+from werkzeug.exceptions import HTTPException
+from flask_admin import Admin
+from flask.ext.admin.contrib.sqla import ModelView
+from flask.ext.login import LoginManager
+from flask.ext.admin.contrib.fileadmin import FileAdmin
+import os.path as op
+
+admin = Admin(app, name='Admin', template_mode='bootstrap3')
+
+class ModelView(ModelView):
+
+    def is_accessible(self):
+        auth = request.authorization or request.environ.get('REMOTE_USER')  # workaround for Apache
+        if not auth or (auth.username, auth.password) != app.config['ADMIN_CREDENTIALS']:
+            raise HTTPException('', Response('You have to an administrator.', 401,
+                {'WWW-Authenticate': 'Basic realm="Login Required"'}
+            ))
+        return True
+
+# Users
+admin.add_view(ModelView(User, db.session))
+# Static files
+path = op.join(op.dirname(__file__), 'static')
+admin.add_view(FileAdmin(path, '/static/', name='Static'))

+ 1 - 0
app/models.py

@@ -28,3 +28,4 @@ class User(db.Model, UserMixin):
 
     def get_id(self):
         return self.email
+

+ 0 - 15
app/templates/about.html

@@ -1,15 +0,0 @@
-{% 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 %}

+ 5 - 0
app/templates/admin/index.html

@@ -0,0 +1,5 @@
+{% extends 'admin/master.html' %}
+
+{% block body %}
+  	Welcome admin.
+{% endblock %}

+ 0 - 36
app/templates/index.html

@@ -2,16 +2,6 @@
 
 {% 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 %}
@@ -22,30 +12,4 @@
 	  	<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 %}

+ 1 - 1
app/templates/layout.html

@@ -25,7 +25,7 @@
 				<!-- Navbar -->
 				<div class="ui attached inverted menu">
 				  	{{ m.nav_link('index', 'Home') }}
-					{{ m.nav_link('about', 'About') }}
+				  	{{ m.nav_link('map', 'Map') }}
 					{{ m.nav_link('contact', 'Contact') }}
 				 	<div class="right menu">
 				 		<!-- User is logged in -->

+ 80 - 0
app/templates/map.html

@@ -0,0 +1,80 @@
+{% 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 = {
+            "Baisc": L.mapbox.tileLayer('mapbox.outdoors').addTo(map),
+            "Light": L.mapbox.tileLayer('mapbox.light'),
+            "Dark": L.mapbox.tileLayer('mapbox.dark'),
+            "Comics": L.mapbox.tileLayer('mapbox.comic'),
+            "Pencil": L.mapbox.tileLayer('mapbox.pencil')
+        }
+        L.control.layers(
+            layers,
+            null,
+            {position: 'topleft'}
+        ).addTo(map);
+        map.setView({{[48.8534100, 2.3488000]}}, 13);
+
+        markers = [];
+
+        function remove_points(points){
+            for (i in points){
+                map.removeLayer(points[i])
+            }
+        }
+
+        function refresh_points(){
+            $.ajax({
+                type: "POST",
+                async: true,
+                url: "/map/refresh"
+            }).done(function(response){
+                remove_points(markers);
+                points = response.points;
+                for (i in points){
+                    marker = L.marker(points[i]);
+                    markers.push(marker);
+                    marker.addTo(map);
+                }
+            });
+        }
+
+        refresh_points();
+        window.setInterval(function(){
+            refresh_points();
+        }, 1000);
+
+    </script>
+
+{% endblock %}

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

@@ -1,24 +0,0 @@
-{% 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 %}

+ 1 - 1
app/templates/user/reset.html

@@ -7,7 +7,7 @@
 {% 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">
+		<form class="ui form" action="{{ url_for('userbp.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) }}

+ 9 - 1
app/toolbox/email.py

@@ -1,3 +1,4 @@
+from threading import Thread
 from flask.ext.mail import Message
 from app import app, mail
 
@@ -10,5 +11,12 @@ def send(recipient, subject, body):
     sender = app.config['ADMINS'][0]
     message = Message(subject, sender=sender, recipients=[recipient])
     message.html = body
+    # Create a new thread
+    thr = Thread(target=send_async, args=[app, message])
+    thr.start()
+
+
+def send_async(app, message):
+    ''' Send the mail asynchronously. '''
     with app.app_context():
-        mail.send(message)
+        mail.send(message)

+ 13 - 4
app/views/main.py

@@ -1,5 +1,6 @@
-from flask import render_template
+from flask import render_template, jsonify
 from app import app
+import random
 
 
 @app.route('/')
@@ -8,9 +9,17 @@ def index():
     return render_template('index.html', title='Home')
 
 
-@app.route('/about')
-def about():
-    return render_template('about.html', title='About')
+@app.route('/map')
+def map():
+    return render_template('map.html', title='Map')
+
+
+@app.route('/map/refresh', methods=['POST'])
+def map_refresh():
+    points = [(random.uniform(48.8434100, 48.8634100),
+               random.uniform(2.3388000, 2.3588000))
+              for _ in range(random.randint(2, 9))]
+    return jsonify({'points': points})
 
 
 @app.route('/contact')

+ 16 - 16
app/views/user.py

@@ -10,10 +10,10 @@ from app.toolbox import email
 ts = URLSafeTimedSerializer(app.config['SECRET_KEY'])
 
 # Create a user blueprint
-user = Blueprint('user', __name__, url_prefix='/user')
+userbp = Blueprint('userbp', __name__, url_prefix='/user')
 
 
-@user.route('/signup', methods=['GET', 'POST'])
+@userbp.route('/signup', methods=['GET', 'POST'])
 def signup():
     form = user_forms.SignUp()
     if form.validate_on_submit():
@@ -34,7 +34,7 @@ def signup():
         # 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)
+        confirmUrl = url_for('userbp.confirm', token=token, _external=True)
         # Render an HTML template to send by email
         html = render_template('email/confirm.html',
                                confirm_url=confirmUrl)
@@ -46,7 +46,7 @@ def signup():
     return render_template('user/signup.html', form=form, title='Sign up')
 
 
-@user.route('/confirm/<token>', methods=['GET', 'POST'])
+@userbp.route('/confirm/<token>', methods=['GET', 'POST'])
 def confirm(token):
     try:
         email = ts.loads(token, salt='email-confirm-key', max_age=86400)
@@ -62,10 +62,10 @@ def confirm(token):
     # Send to the signin page
     flash(
         'Your email address has been confirmed, you can sign in.', 'positive')
-    return redirect(url_for('user.signin'))
+    return redirect(url_for('userbp.signin'))
 
 
-@user.route('/signin', methods=['GET', 'POST'])
+@userbp.route('/signin', methods=['GET', 'POST'])
 def signin():
     form = user_forms.Login()
     if form.validate_on_submit():
@@ -80,27 +80,27 @@ def signin():
                 return redirect(url_for('index'))
             else:
                 flash('The password you have entered is wrong.', 'negative')
-                return redirect(url_for('user.signin'))
+                return redirect(url_for('userbp.signin'))
         else:
             flash('Unknown email address.', 'negative')
-            return redirect(url_for('user.signin'))
+            return redirect(url_for('userbp.signin'))
     return render_template('user/signin.html', form=form, title='Sign in')
 
 
-@user.route('/signout')
+@userbp.route('/signout')
 def signout():
     logout_user()
     flash('Succesfully signed out.', 'positive')
     return redirect(url_for('index'))
 
 
-@user.route('/account')
+@userbp.route('/account')
 @login_required
 def account():
     return render_template('user/account.html', title='Account')
 
 
-@user.route('/forgot', methods=['GET', 'POST'])
+@userbp.route('/forgot', methods=['GET', 'POST'])
 def forgot():
     form = user_forms.Forgot()
     if form.validate_on_submit():
@@ -112,7 +112,7 @@ def forgot():
             # 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)
+            resetUrl = url_for('userbp.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
@@ -122,11 +122,11 @@ def forgot():
             return redirect(url_for('index'))
         else:
             flash('Unknown email address.', 'negative')
-            return redirect(url_for('user.forgot'))
+            return redirect(url_for('userbp.forgot'))
     return render_template('user/forgot.html', form=form)
 
 
-@user.route('/reset/<token>', methods=['GET', 'POST'])
+@userbp.route('/reset/<token>', methods=['GET', 'POST'])
 def reset(token):
     try:
         email = ts.loads(token, salt='password-reset-key', max_age=86400)
@@ -143,8 +143,8 @@ def reset(token):
             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'))
+            return redirect(url_for('userbp.signin'))
         else:
             flash('Unknown email address.', 'negative')
-            return redirect(url_for('user.forgot'))
+            return redirect(url_for('userbp.forgot'))
     return render_template('user/reset.html', form=form, token=token)

+ 2 - 0
config.py

@@ -2,6 +2,8 @@
 DEBUG = True
 # Secret key for generating tokens
 SECRET_KEY = 'houdini'
+# Admin credentials
+ADMIN_CREDENTIALS = ('admin', 'pa$$word')
 # Database choice
 SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
 SQLALCHEMY_TRACK_MODIFICATIONS = True

+ 1 - 0
createdb.py

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

+ 58 - 0
deployment/Digital-Ocean.md

@@ -0,0 +1,58 @@
+# Digital Ocean deployment
+
+The following script sets everything up so that the application can be deployed on a [Digital Ocean](https://www.digitalocean.com/) droplet with Ubuntu 14.04 and the Apache web server. Of course you should go through it and modify the parts that are unique to your application, I've marked them with ``<>``.
+
+```sh
+# Login and change password
+ssh root@<IP>
+
+# Add a new user "MAX" and give him a password
+adduser <MAX>
+# Give "MAX" sudo rights
+gpasswd -a <MAX> sudo
+
+# Change "PermitRootLogin yes" to "PermitRootLogin no"
+nano /etc/ssh/sshd_config
+
+# Restart SSH
+service ssh restart
+
+# Switch to user "MAX"
+sudo su MAX
+
+# Configure the timezone
+sudo dpkg-reconfigure tzdata
+# Configure NTP Synchronization
+sudo apt-get update
+sudo apt-get install ntp
+# Create a Swap File
+sudo fallocate -l 4G /swapfile
+sudo chmod 600 /swapfile
+sudo mkswap /swapfile
+sudo swapon /swapfile
+sudo sh -c 'echo "/swapfile none swap sw 0 0" >> /etc/fstab'
+
+# Setup Python and Apache
+sudo apt-get update
+sudo apt-get install apache2
+sudo apt-get install python3-pip python3-dev libapache2-mod-wsgi-py3
+
+# Clone the repository containing the code
+cd /var/www
+sudo apt-get install git
+sudo git clone https://github.com/MaxHalford/Flask-Boilerplate
+sudo chmod -R 777 Flask-Boilerplate
+cd Flask-Boilerplate
+
+# Install the necessary Python libraries (takes some time)
+sudo pip3 install -r setup/requirements.txt
+
+# Configure and enable a virtual host
+sudo cp deployment/Flask-Boilerplate.conf /etc/apache2/sites-available/
+sudo a2ensite Flask-Boilerplate
+sudo service apache2 reload
+sudo service apache2 restart
+
+# Reboot the server and you should be done!
+sudo reboot
+```

+ 17 - 0
deployment/Flask-Boilerplate.conf

@@ -0,0 +1,17 @@
+<VirtualHost *:80>
+		ServerName openbikes.co
+		ServerAdmin maxhalford25@gmail.com
+		WSGIScriptAlias / /var/www/OpenBikes/openbikes.wsgi
+		<Directory /var/www/OpenBikes/>
+			Order allow,deny
+			Allow from all
+		</Directory>
+		Alias /static /var/www/OpenBikes/static
+		<Directory /var/www/OpenBikes/static/>
+			Order allow,deny
+			Allow from all
+		</Directory>
+		ErrorLog ${APACHE_LOG_DIR}/error.log
+		LogLevel warn
+		CustomLog ${APACHE_LOG_DIR}/access.log combined
+</VirtualHost>

+ 0 - 0
deployment/Heroku.md


+ 1 - 0
requirements.txt

@@ -6,3 +6,4 @@ flask-login
 flask-mail
 itsdangerous
 flask-bcrypt
+flask-admin

二進制
screenshots/admin_auth.png


二進制
screenshots/admin_panel.png