├── .gitignore ├── client ├── .bowerrc ├── bower.json ├── css │ ├── styles.css │ └── theme.css ├── index.html └── js │ ├── controllers │ ├── ApplicationCtrl.js │ ├── HomeDetailCtrl.js │ ├── PostCreateCtrl.js │ ├── SessionCreateCtrl.js │ ├── SessionDestroyCtrl.js │ └── UserCreateCtrl.js │ ├── directives │ └── match.js │ ├── factories │ ├── Post.js │ ├── Session.js │ └── User.js │ ├── main.js │ └── services │ └── AuthService.js └── server ├── app ├── __init__.py ├── config.py.template ├── forms.py ├── models.py ├── serializers.py ├── server.py └── views.py ├── db_create.py ├── requirements.txt └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Installer logs 4 | pip-log.txt 5 | 6 | # OSX 7 | .DS_Store 8 | 9 | /client/bower_components/ 10 | 11 | server/app.sqlite 12 | server/app/config.py -------------------------------------------------------------------------------- /client/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "bower_components" 3 | } -------------------------------------------------------------------------------- /client/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "John Kevin Basco " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "angular-route": "~1.2.21", 17 | "bootstrap": "~3.2.0", 18 | "restangular": "~1.4.0", 19 | "angularjs": "~1.2.21", 20 | "angular-local-storage": "~0.0.7" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/css/styles.css: -------------------------------------------------------------------------------- 1 | .main-view { 2 | margin-top: 20px; 3 | margin-bottom: 20px; 4 | } -------------------------------------------------------------------------------- /client/css/theme.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | body { 6 | font-family: Georgia, "Times New Roman", Times, serif; 7 | color: #555; 8 | } 9 | 10 | h1, .h1, 11 | h2, .h2, 12 | h3, .h3, 13 | h4, .h4, 14 | h5, .h5, 15 | h6, .h6 { 16 | margin-top: 0; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | font-weight: normal; 19 | color: #333; 20 | } 21 | 22 | 23 | /* 24 | * Override Bootstrap's default container. 25 | */ 26 | 27 | @media (min-width: 1200px) { 28 | .container { 29 | width: 970px; 30 | } 31 | } 32 | 33 | 34 | /* 35 | * Masthead for nav 36 | */ 37 | 38 | .blog-masthead { 39 | background-color: #428bca; 40 | -webkit-box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); 41 | box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); 42 | } 43 | 44 | /* Nav links */ 45 | .blog-nav-item { 46 | position: relative; 47 | display: inline-block; 48 | padding: 10px; 49 | font-weight: 500; 50 | color: #cdddeb; 51 | } 52 | .blog-nav-item:hover, 53 | .blog-nav-item:focus { 54 | color: #fff; 55 | text-decoration: none; 56 | } 57 | 58 | /* Active state gets a caret at the bottom */ 59 | .blog-nav .active { 60 | color: #fff; 61 | } 62 | .blog-nav .active:after { 63 | position: absolute; 64 | bottom: 0; 65 | left: 50%; 66 | width: 0; 67 | height: 0; 68 | margin-left: -5px; 69 | vertical-align: middle; 70 | content: " "; 71 | border-right: 5px solid transparent; 72 | border-bottom: 5px solid; 73 | border-left: 5px solid transparent; 74 | } 75 | 76 | 77 | /* 78 | * Blog name and description 79 | */ 80 | 81 | .blog-header { 82 | padding-top: 20px; 83 | padding-bottom: 20px; 84 | } 85 | .blog-title { 86 | margin-top: 30px; 87 | margin-bottom: 0; 88 | font-size: 60px; 89 | font-weight: normal; 90 | } 91 | .blog-description { 92 | font-size: 20px; 93 | color: #999; 94 | } 95 | 96 | 97 | /* 98 | * Main column and sidebar layout 99 | */ 100 | 101 | .blog-main { 102 | font-size: 18px; 103 | line-height: 1.5; 104 | } 105 | 106 | /* Sidebar modules for boxing content */ 107 | .sidebar-module { 108 | padding: 15px; 109 | margin: 0 -15px 15px; 110 | } 111 | .sidebar-module-inset { 112 | padding: 15px; 113 | background-color: #f5f5f5; 114 | border-radius: 4px; 115 | } 116 | .sidebar-module-inset p:last-child, 117 | .sidebar-module-inset ul:last-child, 118 | .sidebar-module-inset ol:last-child { 119 | margin-bottom: 0; 120 | } 121 | 122 | 123 | 124 | /* Pagination */ 125 | .pager { 126 | margin-bottom: 60px; 127 | text-align: left; 128 | } 129 | .pager > li > a { 130 | width: 140px; 131 | padding: 10px 20px; 132 | text-align: center; 133 | border-radius: 30px; 134 | } 135 | 136 | 137 | /* 138 | * Blog posts 139 | */ 140 | 141 | .blog-post { 142 | margin-bottom: 60px; 143 | } 144 | .blog-post-title { 145 | margin-bottom: 5px; 146 | font-size: 40px; 147 | } 148 | .blog-post-meta { 149 | margin-bottom: 20px; 150 | color: #999; 151 | } 152 | 153 | 154 | /* 155 | * Footer 156 | */ 157 | 158 | .blog-footer { 159 | padding: 40px 0; 160 | color: #999; 161 | text-align: center; 162 | background-color: #f9f9f9; 163 | border-top: 1px solid #e5e5e5; 164 | } 165 | .blog-footer p:last-child { 166 | margin-bottom: 0; 167 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blog 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /client/js/controllers/ApplicationCtrl.js: -------------------------------------------------------------------------------- 1 | Blog.controller('ApplicationCtrl', function($scope, $location, Post, AuthService) { 2 | $scope.$on('$routeChangeStart', function (event, next) { 3 | if (AuthService.isAuthenticated()) { 4 | $scope.isLoggedIn = true; 5 | } else { 6 | $scope.isLoggedIn = false; 7 | } 8 | }); 9 | 10 | $scope.isActive = function(path) { 11 | if ($location.path().substr(0, path.length) === path) { 12 | if (path === "/" && $location.path() === "/") { 13 | return true; 14 | } else if (path === "/") { 15 | return false; 16 | } else { 17 | return true; 18 | } 19 | } else { 20 | return false; 21 | } 22 | }; 23 | }) -------------------------------------------------------------------------------- /client/js/controllers/HomeDetailCtrl.js: -------------------------------------------------------------------------------- 1 | Blog.controller('HomeDetailCtrl', function($scope, Post) { 2 | Post.get().then(function(posts) { 3 | $scope.posts = posts; 4 | }); 5 | }) -------------------------------------------------------------------------------- /client/js/controllers/PostCreateCtrl.js: -------------------------------------------------------------------------------- 1 | Blog.controller('PostCreateCtrl', function($scope, $location, Post) { 2 | $scope.submit = function(isValid, post) { 3 | $scope.submitted = true; 4 | $scope.postCreateForm.$setDirty(); 5 | 6 | if (!isValid) { 7 | return; 8 | } 9 | 10 | Post.create(post).then(function(response) { 11 | $location.path('/'); 12 | }); 13 | }; 14 | }) -------------------------------------------------------------------------------- /client/js/controllers/SessionCreateCtrl.js: -------------------------------------------------------------------------------- 1 | Blog.controller('SessionCreateCtrl', function($scope, $location, Session, AuthService) { 2 | $scope.submit = function(isValid, credentials) { 3 | $scope.submitted = true; 4 | $scope.authenticationForm.$setDirty(); 5 | 6 | if (!isValid) { 7 | return; 8 | } 9 | 10 | AuthService.login(credentials).then(function(user) { 11 | $location.path('/posts/create') 12 | }, function(response) { 13 | $scope.failedLoginAttempt = true; 14 | }); 15 | }; 16 | }) -------------------------------------------------------------------------------- /client/js/controllers/SessionDestroyCtrl.js: -------------------------------------------------------------------------------- 1 | Blog.controller('SessionDestroyCtrl', function($scope, $location, AuthService) { 2 | AuthService.logout(); 3 | $location.path('/'); 4 | }) -------------------------------------------------------------------------------- /client/js/controllers/UserCreateCtrl.js: -------------------------------------------------------------------------------- 1 | Blog.controller('UserCreateCtrl', function($scope, User) { 2 | 3 | var defaultForm = { 4 | email: '', 5 | password: '', 6 | passwordConfirmation: '' 7 | }; 8 | 9 | $scope.user = angular.copy(defaultForm); 10 | 11 | $scope.submit = function(isValid, user) { 12 | $scope.submitted = true; 13 | $scope.accountCreated = false; 14 | 15 | $scope.userCreateForm.$setDirty(); 16 | 17 | if (!isValid) { 18 | return; 19 | } 20 | 21 | User.create(user).then(function(response) { 22 | $scope.accountCreated = true; 23 | 24 | // reset form 25 | $scope.submitted = false; 26 | $scope.user = angular.copy(defaultForm); 27 | $scope.userCreateForm.$setPristine(); 28 | }); 29 | }; 30 | }) -------------------------------------------------------------------------------- /client/js/directives/match.js: -------------------------------------------------------------------------------- 1 | Blog.directive('match', function () { 2 | return { 3 | require: 'ngModel', 4 | restrict: 'A', 5 | scope: { 6 | match: '=' 7 | }, 8 | link: function(scope, elem, attrs, ctrl) { 9 | scope.$watch(function() { 10 | return (ctrl.$pristine && angular.isUndefined(ctrl.$modelValue)) || scope.match === ctrl.$modelValue; 11 | }, function(currentValue) { 12 | ctrl.$setValidity('match', currentValue); 13 | }); 14 | } 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /client/js/factories/Post.js: -------------------------------------------------------------------------------- 1 | Blog.factory('Post', function(Restangular) { 2 | var Post; 3 | Post = { 4 | get: function() { 5 | return Restangular 6 | .one('posts') 7 | .getList(); 8 | }, 9 | create: function(data) { 10 | return Restangular 11 | .one('posts') 12 | .customPOST(data); 13 | } 14 | }; 15 | return Post; 16 | }) -------------------------------------------------------------------------------- /client/js/factories/Session.js: -------------------------------------------------------------------------------- 1 | Blog.factory('Session', function(Restangular) { 2 | var Session; 3 | Session = { 4 | create: function(data, bypassErrorInterceptor) { 5 | return Restangular 6 | .one('sessions') 7 | .withHttpConfig({bypassErrorInterceptor: bypassErrorInterceptor}) 8 | .customPOST(data); 9 | } 10 | }; 11 | return Session; 12 | }) -------------------------------------------------------------------------------- /client/js/factories/User.js: -------------------------------------------------------------------------------- 1 | Blog.factory('User', function(Restangular) { 2 | var User; 3 | User = { 4 | create: function(user) { 5 | return Restangular 6 | .one('users') 7 | .customPOST(user); 8 | } 9 | }; 10 | return User; 11 | }) -------------------------------------------------------------------------------- /client/js/main.js: -------------------------------------------------------------------------------- 1 | window.Blog = angular.module('Blog', ['ngRoute', 'restangular', 'LocalStorageModule']) 2 | 3 | .run(function($location, Restangular, AuthService) { 4 | Restangular.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) { 5 | if (AuthService.isAuthenticated()) { 6 | headers['Authorization'] = 'Basic ' + AuthService.getToken(); 7 | } 8 | return { 9 | headers: headers 10 | }; 11 | }); 12 | 13 | Restangular.setErrorInterceptor(function(response, deferred, responseHandler) { 14 | if (response.config.bypassErrorInterceptor) { 15 | return true; 16 | } else { 17 | switch (response.status) { 18 | case 401: 19 | AuthService.logout(); 20 | $location.path('/sessions/create'); 21 | break; 22 | default: 23 | throw new Error('No handler for status code ' + response.status); 24 | } 25 | return false; 26 | } 27 | }); 28 | }) 29 | 30 | .config(function($routeProvider, RestangularProvider) { 31 | 32 | RestangularProvider.setBaseUrl('http://localhost:5000/api/v1'); 33 | 34 | var partialsDir = '../partials'; 35 | 36 | var redirectIfAuthenticated = function(route) { 37 | return function($location, $q, AuthService) { 38 | 39 | var deferred = $q.defer(); 40 | 41 | if (AuthService.isAuthenticated()) { 42 | deferred.reject() 43 | $location.path(route); 44 | } else { 45 | deferred.resolve() 46 | } 47 | 48 | return deferred.promise; 49 | } 50 | } 51 | 52 | var redirectIfNotAuthenticated = function(route) { 53 | return function($location, $q, AuthService) { 54 | 55 | var deferred = $q.defer(); 56 | 57 | if (! AuthService.isAuthenticated()) { 58 | deferred.reject() 59 | $location.path(route); 60 | } else { 61 | deferred.resolve() 62 | } 63 | 64 | return deferred.promise; 65 | } 66 | } 67 | 68 | $routeProvider 69 | .when('/', { 70 | controller: 'HomeDetailCtrl', 71 | templateUrl: partialsDir + '/home/detail.html' 72 | }) 73 | .when('/sessions/create', { 74 | controller: 'SessionCreateCtrl', 75 | templateUrl: partialsDir + '/session/create.html', 76 | resolve: { 77 | redirectIfAuthenticated: redirectIfAuthenticated('/posts/create') 78 | } 79 | }) 80 | .when('/sessions/destroy', { 81 | controller: 'SessionDestroyCtrl', 82 | templateUrl: partialsDir + '/session/destroy.html' 83 | }) 84 | .when('/users/create', { 85 | controller: 'UserCreateCtrl', 86 | templateUrl: partialsDir + '/user/create.html' 87 | }) 88 | .when('/posts/create', { 89 | controller: 'PostCreateCtrl', 90 | templateUrl: partialsDir + '/post/create.html', 91 | resolve: { 92 | redirectIfNotAuthenticated: redirectIfNotAuthenticated('/sessions/create') 93 | } 94 | }); 95 | }) -------------------------------------------------------------------------------- /client/js/services/AuthService.js: -------------------------------------------------------------------------------- 1 | Blog.service('AuthService', AuthService = function($q, localStorageService, Session) { 2 | 3 | this.login = function(credentials) { 4 | var me = this; 5 | deferred = $q.defer() 6 | Session.create(credentials, true).then(function(user) { 7 | me.setToken(credentials); 8 | return deferred.resolve(user); 9 | }, function(response) { 10 | if (response.status == 401) { 11 | return deferred.reject(false); 12 | } 13 | throw new Error('No handler for status code ' + response.status); 14 | }); 15 | return deferred.promise 16 | }; 17 | 18 | this.logout = function() { 19 | localStorageService.clearAll(); 20 | }; 21 | 22 | this.isAuthenticated = function() { 23 | var token = this.getToken(); 24 | if (token) { 25 | return true 26 | } 27 | return false; 28 | }; 29 | 30 | this.setToken = function(credentials) { 31 | localStorageService.set('token', btoa(credentials.email + ':' + credentials.password)); 32 | }; 33 | 34 | this.getToken = function() { 35 | return localStorageService.get('token'); 36 | }; 37 | 38 | return this; 39 | }); 40 | -------------------------------------------------------------------------------- /server/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/building-a-blog-using-flask-and-angularjs/b34f9b32159b88874eae34f3132ac91492824b92/server/app/__init__.py -------------------------------------------------------------------------------- /server/app/config.py.template: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | WTF_CSRF_ENABLED = False -------------------------------------------------------------------------------- /server/app/forms.py: -------------------------------------------------------------------------------- 1 | from flask.ext.wtf import Form 2 | 3 | from wtforms_alchemy import model_form_factory 4 | from wtforms import StringField 5 | from wtforms.validators import DataRequired 6 | 7 | from app.server import db 8 | from models import User, Post 9 | 10 | BaseModelForm = model_form_factory(Form) 11 | 12 | class ModelForm(BaseModelForm): 13 | @classmethod 14 | def get_session(self): 15 | return db.session 16 | 17 | class UserCreateForm(ModelForm): 18 | class Meta: 19 | model = User 20 | 21 | class SessionCreateForm(Form): 22 | email = StringField('name', validators=[DataRequired()]) 23 | password = StringField('password', validators=[DataRequired()]) 24 | 25 | class PostCreateForm(ModelForm): 26 | class Meta: 27 | model = Post -------------------------------------------------------------------------------- /server/app/models.py: -------------------------------------------------------------------------------- 1 | from flask import g 2 | 3 | from wtforms.validators import Email 4 | 5 | from server import db, flask_bcrypt 6 | 7 | class User(db.Model): 8 | id = db.Column(db.Integer, primary_key=True) 9 | email = db.Column(db.String(120), unique=True, nullable=False, info={'validators': Email()}) 10 | password = db.Column(db.String(80), nullable=False) 11 | posts = db.relationship('Post', backref='user', lazy='dynamic') 12 | 13 | def __init__(self, email, password): 14 | self.email = email 15 | self.password = flask_bcrypt.generate_password_hash(password) 16 | 17 | def __repr__(self): 18 | return '' % self.email 19 | 20 | class Post(db.Model): 21 | id = db.Column(db.Integer, primary_key=True) 22 | title = db.Column(db.String(120), nullable=False) 23 | body = db.Column(db.Text, nullable=False) 24 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 25 | created_at = db.Column(db.DateTime, default=db.func.now()) 26 | 27 | def __init__(self, title, body): 28 | self.title = title 29 | self.body = body 30 | self.user_id = g.user.id 31 | 32 | def __repr__(self): 33 | return '' % self.title -------------------------------------------------------------------------------- /server/app/serializers.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Serializer, fields 2 | 3 | class UserSerializer(Serializer): 4 | class Meta: 5 | fields = ("id", "email") 6 | 7 | class PostSerializer(Serializer): 8 | user = fields.Nested(UserSerializer) 9 | 10 | class Meta: 11 | fields = ("id", "title", "body", "user", "created_at") -------------------------------------------------------------------------------- /server/app/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | from flask.ext import restful 5 | from flask.ext.restful import reqparse, Api 6 | from flask.ext.sqlalchemy import SQLAlchemy 7 | from flask.ext.bcrypt import Bcrypt 8 | from flask.ext.httpauth import HTTPBasicAuth 9 | 10 | basedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../') 11 | 12 | app = Flask(__name__) 13 | app.config.from_object('app.config') 14 | 15 | # flask-sqlalchemy 16 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'app.sqlite') 17 | db = SQLAlchemy(app) 18 | 19 | # flask-restful 20 | api = restful.Api(app) 21 | 22 | # flask-bcrypt 23 | flask_bcrypt = Bcrypt(app) 24 | 25 | # flask-httpauth 26 | auth = HTTPBasicAuth() 27 | 28 | @app.after_request 29 | def after_request(response): 30 | response.headers.add('Access-Control-Allow-Origin', '*') 31 | response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization') 32 | response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE') 33 | return response 34 | 35 | import views 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /server/app/views.py: -------------------------------------------------------------------------------- 1 | from flask import g 2 | from flask.ext import restful 3 | 4 | from server import api, db, flask_bcrypt, auth 5 | from models import User, Post 6 | from forms import UserCreateForm, SessionCreateForm, PostCreateForm 7 | from serializers import UserSerializer, PostSerializer 8 | 9 | @auth.verify_password 10 | def verify_password(email, password): 11 | user = User.query.filter_by(email=email).first() 12 | if not user: 13 | return False 14 | g.user = user 15 | return flask_bcrypt.check_password_hash(user.password, password) 16 | 17 | class UserView(restful.Resource): 18 | def post(self): 19 | form = UserCreateForm() 20 | if not form.validate_on_submit(): 21 | return form.errors, 422 22 | 23 | user = User(form.email.data, form.password.data) 24 | db.session.add(user) 25 | db.session.commit() 26 | return UserSerializer(user).data 27 | 28 | class SessionView(restful.Resource): 29 | def post(self): 30 | form = SessionCreateForm() 31 | if not form.validate_on_submit(): 32 | return form.errors, 422 33 | 34 | user = User.query.filter_by(email=form.email.data).first() 35 | if user and flask_bcrypt.check_password_hash(user.password, form.password.data): 36 | return UserSerializer(user).data, 201 37 | return '', 401 38 | 39 | class PostListView(restful.Resource): 40 | def get(self): 41 | posts = Post.query.all() 42 | return PostSerializer(posts, many=True).data 43 | 44 | @auth.login_required 45 | def post(self): 46 | form = PostCreateForm() 47 | if not form.validate_on_submit(): 48 | return form.errors, 422 49 | post = Post(form.title.data, form.body.data) 50 | db.session.add(post) 51 | db.session.commit() 52 | return PostSerializer(post).data, 201 53 | 54 | class PostView(restful.Resource): 55 | def get(self, id): 56 | posts = Post.query.filter_by(id=id).first() 57 | return PostSerializer(posts).data 58 | 59 | api.add_resource(UserView, '/api/v1/users') 60 | api.add_resource(SessionView, '/api/v1/sessions') 61 | api.add_resource(PostListView, '/api/v1/posts') 62 | api.add_resource(PostView, '/api/v1/posts/') 63 | -------------------------------------------------------------------------------- /server/db_create.py: -------------------------------------------------------------------------------- 1 | from app.server import db 2 | 3 | db.create_all() -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Bcrypt==0.6.0 3 | Flask-HTTPAuth==2.2.1 4 | Flask-RESTful==0.2.12 5 | Flask-SQLAlchemy==1.0 6 | Flask-WTF==0.10.0 7 | Jinja2==2.7.3 8 | MarkupSafe==0.23 9 | SQLAlchemy==0.9.7 10 | SQLAlchemy-Utils==0.26.9 11 | WTForms==2.0.1 12 | WTForms-Alchemy==0.12.8 13 | WTForms-Components==0.9.5 14 | Werkzeug==0.9.6 15 | aniso8601==0.83 16 | decorator==3.4.0 17 | infinity==1.3 18 | intervals==0.3.1 19 | itsdangerous==0.24 20 | marshmallow==0.7.0 21 | py-bcrypt==0.4 22 | pytz==2014.4 23 | six==1.7.3 24 | validators==0.6.0 25 | wsgiref==0.1.2 26 | -------------------------------------------------------------------------------- /server/run.py: -------------------------------------------------------------------------------- 1 | from app.server import app 2 | 3 | app.run() --------------------------------------------------------------------------------