Flask+ Angularjs 實例: 建立博客

 

 

  • 容許任何用戶註冊
  • 容許註冊的用戶登陸
  • 容許登陸的用戶建立博客
  • 容許在首頁展現博客
  • 容許登陸的用戶退

 

後端 

 

  • Flask-RESTful - Flask 的 RESTful 擴展
  • Flask-SQLAlchemy - Flask 的 SQLAlchemy 擴展
  • Flask-Bcrypt - Flask 的 一個爲你的應用提供 bcrypt 哈希的工具擴展
  • Flask-HTTPAuth - 一個爲 Flask 路由提供 Basic and Digest HTTP authentication 的擴展
  • Flask-WTF - http://docs.jinkan.org/docs/flask-wtf/
  • WTForms-Alchemy - 一個 WTForms 擴展,能很簡單的基於表單建立模型的工具集
  • marshmallow - 是一個 ORM/ODM/ 的框架,用於轉換複雜的數據類型,http://marshmallow.readthedocs.org/en/latest/quickstart.html

requirements.txt:javascript

Flask==0.10.1 Flask-Bcrypt==0.6.0 Flask-HTTPAuth==2.2.1 Flask-RESTful==0.2.12 Flask-SQLAlchemy==1.0 Flask-WTF==0.10.0 Jinja2==2.7.3 MarkupSafe==0.23 SQLAlchemy==0.9.7 SQLAlchemy-Utils==0.26.9 WTForms==2.0.1 WTForms-Alchemy==0.12.8 WTForms-Components==0.9.5 Werkzeug==0.9.6 aniso8601==0.83 decorator==3.4.0 infinity==1.3 intervals==0.3.1 itsdangerous==0.24 marshmallow==0.7.0 py-bcrypt==0.4 pytz==2014.4 six==1.7.3 validators==0.6.0 wsgiref==0.1.2

 

應用的初始化設置

config.py:css

 

DEBUG = True
WTF_CSRF_ENABLED = False

 server.py:html

basedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../')

app = Flask(__name__)
app.config.from_object('app.config')

# flask-sqlalchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'app.sqlite')
db = SQLAlchemy(app)

# flask-restful
api = restful.Api(app)

# flask-bcrypt
flask_bcrypt = Bcrypt(app)

# flask-httpauth
auth = HTTPBasicAuth()

@app.after_request
def after_request(response):
    response.headers.add('Access-Control-Allow-Origin', '*')
    response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
    response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
    return response

import views

在這個文件中,咱們初始化了 Flask,加載了從一個配置文件中加載了配置文件的變量,建立了 flask-sqlalchemy,flask-restful 對象等等。。。並且咱們也在 after_request 函數中加了一些響應頭,它容許跨域資源共享(CORS),這將容許咱們的託管服務器(REST API)和客戶端(AngularJS app)在不一樣的域以及不一樣的子域(例如:api.johnsblog.com 和 johnsblog.com)。在開發期間,這將容許咱們把它們運行在不一樣的端口(例如:localhost:8000 和 localhost:5000)前端

 

Models

models.py:java

 

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), unique=True, nullable=False, info={'validators': Email()})
    password = db.Column(db.String(80), nullable=False)
    posts = db.relationship('Post', backref='user', lazy='dynamic')

    def __init__(self, email, password):
        self.email = email
        self.password = flask_bcrypt.generate_password_hash(password)

    def __repr__(self):
        return '<User %r>' % self.email

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(120), nullable=False)
    body = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    created_at = db.Column(db.DateTime, default=db.func.now())

    def __init__(self, title, body):
        self.title = title
        self.body = body
        self.user_id = g.user.id

    def __repr__(self):
        return '<Post %r>' % self.title

在以上的代碼中,咱們定義了一個用戶和文章的模型,用戶模型有一個 id 做爲它的主鍵,被定義成 integer 類型,email 和password 屬性被定義成 strings。經過 posts 屬性它也和 POST 模型有關聯。POST 模型也有一個 id 做爲它的主鍵,並也被定義成 integer,title 和 body 屬性被定義成 strings。它也有一個 user_id 屬性被定義成 integer 並做爲 User 模型的 id 屬性的外鍵。它還有一個 created_at 屬性被定義成 DateTime。node

 

 

 

 

Forms

如今咱們已經完成了模型定義部分,讓咱們定義 forms,咱們將使用 forms 校驗咱們的用戶輸入。爲了 form 校驗,咱們將使用是一個名稱爲 WTForms 的 Flask 擴展,而且咱們將使用 WTForms-Alchemy 擴展它 來更快更簡單的定義咱們的 forms。在 blog/server/app 目錄下建立一個新文件並命名爲 forms.py,而後拷貝和粘貼如下代碼到文件中:nginx

 

from flask.ext.wtf import Form

from wtforms_alchemy import model_form_factory
from wtforms import StringField
from wtforms.validators import DataRequired

from app.server import db
from models import User, Post

BaseModelForm = model_form_factory(Form)

class ModelForm(BaseModelForm):
    @classmethod
    def get_session(self):
        return db.session

class UserCreateForm(ModelForm):
    class Meta:
        model = User

class SessionCreateForm(Form):
    email = StringField('name', validators=[DataRequired()])
    password = StringField('password', validators=[DataRequired()])

class PostCreateForm(ModelForm):
    class Meta:
        model = Post

 

Serializers:

 

爲了在咱們的 responses 中把咱們的 model 實例渲染成 JSON,咱們首先須要把它們轉換成原生的 Python 數據類型, Flask-RESTful 可使用 fields 模塊 和 marshal_with() 裝飾器(更多的詳細信息請移步 - http://flask-restful.readthedocs.org/en/latest/quickstart.html#data-formatting)。當我開始構建 REST API 的時候我不知道 Flask-RESTful 支持這個,所以我以 Marshmallow 完成 http://marshmallow.readthedocs.org/en/latest/ 。angularjs

 

serializers.py:sql

from marshmallow import Serializer, fields

class UserSerializer(Serializer):
    class Meta:
        fields = ("id", "email")

class PostSerializer(Serializer):
    user = fields.Nested(UserSerializer)

    class Meta:
        fields = ("id", "title", "body", "user", "created_at")

Views

views.py:數據庫

 

from flask import g
from flask.ext import restful

from server import api, db, flask_bcrypt, auth
from models import User, Post
from forms import UserCreateForm, SessionCreateForm, PostCreateForm
from serializers import UserSerializer, PostSerializer

@auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if not user:
        return False
    g.user = user
    return flask_bcrypt.check_password_hash(user.password, password)

class UserView(restful.Resource):
    def post(self):
        form = UserCreateForm()
        if not form.validate_on_submit():
            return form.errors, 422

        user = User(form.email.data, form.password.data)
        db.session.add(user)
        db.session.commit()
        return UserSerializer(user).data

class SessionView(restful.Resource):
    def post(self):
        form = SessionCreateForm()
        if not form.validate_on_submit():
            return form.errors, 422

        user = User.query.filter_by(email=form.email.data).first()
        if user and flask_bcrypt.check_password_hash(user.password, form.password.data):
            return UserSerializer(user).data, 201
        return '', 401

class PostListView(restful.Resource):
    def get(self):
        posts = Post.query.all()
        return PostSerializer(posts, many=True).data

    @auth.login_required
    def post(self):
        form = PostCreateForm()
        if not form.validate_on_submit():
            return form.errors, 422
        post = Post(form.title.data, form.body.data)
        db.session.add(post)
        db.session.commit()
        return PostSerializer(post).data, 201

class PostView(restful.Resource):
    def get(self, id):
        posts = Post.query.filter_by(id=id).first()
        return PostSerializer(posts).data

api.add_resource(UserView, '/api/v1/users')
api.add_resource(SessionView, '/api/v1/sessions')
api.add_resource(PostListView, '/api/v1/posts')
api.add_resource(PostView, '/api/v1/posts/<int:id>')

上面的 verify_password 函數被 auth.verify_password 裝飾,並將被 Flask-HTTPAuth 使用來鑑定用戶。它基本上經過 email 來獲取用戶,以及經過校驗給出的密碼是否與數據庫中存儲的密碼匹配。

UserView 類將處理用戶的註冊請求,SessionView 類將處理用戶的登陸請求,PostListView 將處理獲取文章列表和建立文章的請求。最後,PostView將處理獲取單篇文章的請求。在文件的底部,咱們簡單的設置了 API 的資源路由。

建立數據庫

 

db_create.py :

 

from app.server import db

db.create_all()

 

運行 REST API 服務

 run.py:

from app.server import app

app.run()

 

測試驗證:

註冊一個用戶:

curl --dump-header - -H "Content-Type: application/json" -X POST -d '{"email": "johndoe@gmail.com","password": "admin"}' http://localhost:5000/api/v1/users

登錄一個用戶

curl --dump-header - -H "Content-Type: application/json" -X POST -d '{"email": "johndoe@gmail.com","password": "admin"}' http://localhost:5000/api/v1/sessions

建立一篇文章:



(說明: 爲了建立一篇文章,你須要發送一個 POST 請求給 localhost:5000/api/v1/posts。須要的屬性是 title 和 body。由於建立文章的時候要求用戶是已經登錄的,注意你須要發送一個包含 base64 編碼的用戶資格的 Authorization header ,它是經過冒號(":")分離的。)curl --dump-header - -H "Content-Type: application/json" -H "Authorization: Basic am9obmRvZUBnbWFpbC5jb206YWRtaW4=" -X POST -d '{"title": "Example post","body": "Lorem ipsum"}' http://localhost:5000/api/v1/posts

獲取文章

curl -i http://localhost:5000/api/v1/posts

 

 

前端:

bower.json

{
  "name": "client",
  "version": "0.0.0",
  "authors": [
    "John Kevin Basco <basco.johnkevin@gmail.com>"
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ],
  "dependencies": {
    "angular-route": "~1.2.21",
    "bootstrap": "~3.2.0",
    "restangular": "~1.4.0",
    "angularjs": "~1.2.21",
    "angular-local-storage": "~0.0.7"
  }
}

 

index.html:

<!DOCTYPE html>
<html ng-app="Blog" ng-controller="ApplicationCtrl">
    <head>
        <meta charset="utf-8" />
        <title>Blog</title>

        <link rel="stylesheet" type="text/css" href="./bower_components/bootstrap/dist/css/bootstrap.css">
        <link rel="stylesheet" type="text/css" href="./css/theme.css">
        <link rel="stylesheet" type="text/css" href="./css/styles.css">
    </head>
    <body>

        <div class="blog-masthead">
            <div class="container">
                <nav class="blog-nav">
                    <a class="blog-nav-item" href="#" ng-class="{active: isActive('/')}">Home</a>
                    <a class="blog-nav-item" href="#/sessions/create" ng-hide="isLoggedIn" ng-class="{active: isActive('/sessions/create')}">Login</a>
                    <a class="blog-nav-item" href="#/sessions/destroy" ng-show="isLoggedIn">Logout</a>
                    <a class="blog-nav-item" href="#/users/create" ng-class="{active: isActive('/users/create')}">Register</a>
                    <a class="blog-nav-item" href="#/posts/create" ng-show="isLoggedIn" ng-class="{active: isActive('/posts/create')}">Create a post</a>
                </nav>
            </div>
        </div>

        <div class="container main-view">

            <div ng-view></div>

        </div><!-- /.container -->

        <div class="blog-footer">
            <p>Blog built by <a href="https://twitter.com/johnkevinmbasco">@johnkevinmbasco</a> using Flask and AngularJS.</p>
            <p>
                <a href="#">Back to top</a>
            </p>
        </div>

        <script type="text/javascript" src="./bower_components/angularjs/angular.js"></script>
        <script type="text/javascript" src="./bower_components/angular-route/angular-route.js"></script>
        <script type="text/javascript" src="./bower_components/lodash/dist/lodash.js"></script>
        <script type="text/javascript" src="./bower_components/restangular/dist/restangular.js"></script>
        <script type="text/javascript" src="./bower_components/angular-local-storage/angular-local-storage.js"></script>
        <script type="text/javascript" src="./js/main.js"></script>
        <script type="text/javascript" src="./js/controllers/HomeDetailCtrl.js"></script>
        <script type="text/javascript" src="./js/controllers/ApplicationCtrl.js"></script>
        <script type="text/javascript" src="./js/controllers/SessionCreateCtrl.js"></script>
        <script type="text/javascript" src="./js/controllers/SessionDestroyCtrl.js"></script>
        <script type="text/javascript" src="./js/controllers/UserCreateCtrl.js"></script>
        <script type="text/javascript" src="./js/controllers/PostCreateCtrl.js"></script>
        <script type="text/javascript" src="./js/factories/Session.js"></script>
        <script type="text/javascript" src="./js/factories/User.js"></script>
        <script type="text/javascript" src="./js/factories/Post.js"></script>
        <script type="text/javascript" src="./js/services/AuthService.js"></script>
        <script type="text/javascript" src="./js/directives/match.js"></script>

    </body>
</html>

.bowerrc:

 

{
  "directory" : "bower_components"
}

 angularjs腳本文件清單:

 

 

 

main.js:

window.Blog = angular.module('Blog', ['ngRoute', 'restangular', 'LocalStorageModule'])

.run(function($location, Restangular, AuthService) {
    Restangular.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
        if (AuthService.isAuthenticated()) {
            headers['Authorization'] = 'Basic ' + AuthService.getToken();
        }
        return {
            headers: headers
        };
    });

    Restangular.setErrorInterceptor(function(response, deferred, responseHandler) {
        if (response.config.bypassErrorInterceptor) {
            return true;
        } else {
            switch (response.status) {
                case 401:
                    AuthService.logout();
                    $location.path('/sessions/create');
                    break;
                default:
                    throw new Error('No handler for status code ' + response.status);
            }
            return false;
        }
    });
})

.config(function($routeProvider, RestangularProvider) {

    RestangularProvider.setBaseUrl('http://localhost:5000/api/v1');

    var partialsDir = '../partials';

    var redirectIfAuthenticated = function(route) {
        return function($location, $q, AuthService) {

            var deferred = $q.defer();

            if (AuthService.isAuthenticated()) {
                deferred.reject()
                $location.path(route);
            } else {
                deferred.resolve()
            }

            return deferred.promise;
        }
    }

    var redirectIfNotAuthenticated = function(route) {
        return function($location, $q, AuthService) {

            var deferred = $q.defer();

            if (! AuthService.isAuthenticated()) {
                deferred.reject()
                $location.path(route);
            } else {
                deferred.resolve()
            }

            return deferred.promise;
        }
    }

    $routeProvider
        .when('/', {
            controller: 'HomeDetailCtrl',
            templateUrl: partialsDir + '/home/detail.html'
        })
        .when('/sessions/create', {
            controller: 'SessionCreateCtrl',
            templateUrl: partialsDir + '/session/create.html',
            resolve: {
                redirectIfAuthenticated: redirectIfAuthenticated('/posts/create')
            }
        })
        .when('/sessions/destroy', {
            controller: 'SessionDestroyCtrl',
            templateUrl: partialsDir + '/session/destroy.html'
        })
        .when('/users/create', {
            controller: 'UserCreateCtrl',
            templateUrl: partialsDir + '/user/create.html'
        })
        .when('/posts/create', {
            controller: 'PostCreateCtrl',
            templateUrl: partialsDir + '/post/create.html',
            resolve: {
                redirectIfNotAuthenticated: redirectIfNotAuthenticated('/sessions/create')
            }
        });
})

全部代碼下載地址: 

  這裏

相關文章
相關標籤/搜索