zoukankan      html  css  js  c++  java
  • 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:

    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:

    DEBUG = True
    WTF_CSRF_ENABLED = False

     server.py:

    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:

    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。

    Forms

    现在我们已经完成了模型定义部分,让我们定义 forms,我们将使用 forms 校验我们的用户输入。为了 form 校验,我们将使用是一个名称为 WTForms 的 Flask 扩展,并且我们将使用 WTForms-Alchemy 扩展它 来更快更简单的定义我们的 forms。在 blog/server/app 目录下创建一个新文件并命名为 forms.py,然后拷贝和粘贴以下代码到文件中:

    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/ 。

    serializers.py:

    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

    创建一篇文章:

    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

    (说明: 为了创建一篇文章,你需要发送一个 POST 请求给 localhost:5000/api/v1/posts。需要的属性是 title 和 body。因为创建文章的时候要求用户是已经登陆的,注意你需要发送一个包含 base64 编码的用户资格的 Authorization header ,它是通过冒号(":")分离的。)

    获取文章

    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')
                }
            });
    })

    所有代码下载地址: 

      这里

     

  • 相关阅读:
    Python中的try...except...finally
    JavaScript 实现双向队列并用此来测试一个单词是否为回文
    js 触发 change 事件
    MySQL8 重置改root密码及开放远程访问
    根据数组下标在MongoDB中修改数组元素
    pymongo CursorNotFound错误
    mongodb根据子项中的指标查找最小或最大值
    正则文本过滤时的一些注意事项
    github page更新后不生效
    Xpath同时选取不同属性的元素
  • 原文地址:https://www.cnblogs.com/bayueman/p/6628653.html
Copyright © 2011-2022 走看看