diff --git a/pytest_bdd_example/auth/__init__.py b/pytest_bdd_example/auth/__init__.py index c48f1d0..a02bbd9 100644 --- a/pytest_bdd_example/auth/__init__.py +++ b/pytest_bdd_example/auth/__init__.py @@ -1,7 +1,6 @@ -import views - from .decorators import public_endpoint from .blueprint import auth +from .security import user_datastore -__all__ = ['auth', 'public_endpoint', 'views'] +__all__ = ['auth', 'public_endpoint', 'user_datastore'] diff --git a/pytest_bdd_example/auth/blueprint.py b/pytest_bdd_example/auth/blueprint.py index 4bd4c43..48e41db 100644 --- a/pytest_bdd_example/auth/blueprint.py +++ b/pytest_bdd_example/auth/blueprint.py @@ -1,11 +1,21 @@ from flask import Blueprint +from flask.ext.security import Security from .manager import login_manager, check_valid_login +from .security import user_datastore +from .forms import LoginForm auth = Blueprint('auth', __name__, template_folder='../') @auth.record_once def on_registered(state): + state.app.config['SECURITY_LOGIN_USER_TEMPLATE'] = 'auth/login.html' + Security( + state.app, + user_datastore, + login_form=LoginForm, + ) + login_manager.init_app(state.app) state.app.before_request(check_valid_login) diff --git a/pytest_bdd_example/auth/forms.py b/pytest_bdd_example/auth/forms.py index f48344d..017c17a 100644 --- a/pytest_bdd_example/auth/forms.py +++ b/pytest_bdd_example/auth/forms.py @@ -1,9 +1,47 @@ from flask.ext.wtf import Form -from wtforms import TextField, PasswordField +from wtforms import TextField, PasswordField, BooleanField from wtforms.validators import Required +from flask.ext.security.forms import NextFormMixin +from flask.ext.security.utils import verify_and_update_password, get_message +from flask.ext.security.confirmable import requires_confirmation + +from .security import user_datastore -class LoginForm(Form): +class LoginForm(Form, NextFormMixin): username = TextField('Username', [Required()]) password = PasswordField('Password', [Required()]) + remember = BooleanField('Remember me') + + def validate(self): + if not super(LoginForm, self).validate(): + return False + + username = self.username.data.strip() + password = self.password.data.strip() + if username == '': + self.username.errors.append(get_message('EMAIL_NOT_PROVIDED')[0]) + return False + + if password == '': + self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0]) + return False + + self.user = user_datastore.find_user(username=username) + + if self.user is None: + self.username.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) + return False + + if not verify_and_update_password(password, self.user): + self.password.errors.append(get_message('INVALID_PASSWORD')[0]) + return False + + if requires_confirmation(self.user): + self.username.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) + return False + if not self.user.is_active(): + self.username.errors.append(get_message('DISABLED_ACCOUNT')[0]) + return False + return True diff --git a/pytest_bdd_example/auth/manager.py b/pytest_bdd_example/auth/manager.py index 0cf0aa5..ba899f5 100644 --- a/pytest_bdd_example/auth/manager.py +++ b/pytest_bdd_example/auth/manager.py @@ -2,15 +2,21 @@ from flask import request, current_app from flask.ext.login import LoginManager, current_user -from .models import User +from .security import user_datastore login_manager = LoginManager() -login_manager.login_view = 'auth.login' +login_manager.login_view = 'security.login' + + +PUBLIC_ENDPOINTS = [ + 'static', + 'security.login', +] def check_valid_login(): - if 'static' == request.endpoint: + if request.endpoint in PUBLIC_ENDPOINTS: return if getattr(current_app.view_functions.get(request.endpoint), 'is_public', False): @@ -22,4 +28,4 @@ def check_valid_login(): @login_manager.user_loader def load_user(userid): - return User.query.get(userid) + return user_datastore.find_user(id=userid) diff --git a/pytest_bdd_example/auth/models.py b/pytest_bdd_example/auth/models.py index 55a7df4..1e30670 100644 --- a/pytest_bdd_example/auth/models.py +++ b/pytest_bdd_example/auth/models.py @@ -1,12 +1,15 @@ from flask import current_app -from flask.ext.security import UserMixin +from flask.ext.security import UserMixin, RoleMixin db = current_app.extensions['sqlalchemy'].db -ROLE_USER = 0 -ROLE_ADMIN = 1 + +class Role(db.Model, RoleMixin): + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(80), unique=True) + description = db.Column(db.String(255)) class User(db.Model, UserMixin): @@ -14,9 +17,14 @@ class User(db.Model, UserMixin): username = db.Column(db.String(64), unique=True) email = db.Column(db.String(120), unique=True) password = db.Column(db.String(20)) - role = db.Column(db.SmallInteger, default=ROLE_USER) active = db.Column(db.Boolean, default=False) + roles = db.relationship( + Role, + secondary=lambda: roles_users, + backref=db.backref('users', lazy='dynamic'), + ) + def get_id(self): return unicode(self.id) @@ -25,3 +33,10 @@ class User(db.Model, UserMixin): def is_anonymous(self): return False + + +roles_users = db.Table( + 'roles_users', + db.Column('user_id', db.Integer(), db.ForeignKey(User.id)), + db.Column('role_id', db.Integer(), db.ForeignKey(Role.id)), +) diff --git a/pytest_bdd_example/auth/security.py b/pytest_bdd_example/auth/security.py new file mode 100644 index 0000000..4270433 --- /dev/null +++ b/pytest_bdd_example/auth/security.py @@ -0,0 +1,4 @@ +from flask.ext.security import SQLAlchemyUserDatastore +from .models import db, User, Role + +user_datastore = SQLAlchemyUserDatastore(db, User, Role) diff --git a/pytest_bdd_example/auth/views.py b/pytest_bdd_example/auth/views.py deleted file mode 100644 index d79843a..0000000 --- a/pytest_bdd_example/auth/views.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import request, url_for, redirect, render_template -from flask.ext.login import login_user - -from .blueprint import auth -from .decorators import public_endpoint -from .forms import LoginForm -from .models import User - - -@auth.route('/login/', methods=['GET', 'POST']) -@public_endpoint -def login(): - form = LoginForm() - - if form.validate_on_submit(): - user = ( - User.query - .filter(User.username == form.username.data) - .first() - ) - #TODO: if user is not found - login_user(user) - return redirect(request.args.get('next') or url_for('index')) - - return render_template('auth/login.html', form=form,) diff --git a/pytest_bdd_example/book/admin.py b/pytest_bdd_example/book/admin.py index a05b4b4..7bb253f 100644 --- a/pytest_bdd_example/book/admin.py +++ b/pytest_bdd_example/book/admin.py @@ -1,6 +1,37 @@ -from flask.ext.admin import Admin +from flask import current_app from flask.ext.admin.contrib.sqlamodel import ModelView -from pytest_bdd_example.book import db, Book -admin = Admin() -admin.add_view(ModelView(Book, db.session, 'books', endpoint='books')) +from flask.ext.principal import Permission, RoleNeed + +from pytest_bdd_example.book import db, Book, Author + + +admin = current_app.extensions['admin'][0] + + +class AuthorView(ModelView): + + def is_visible(self): + return Permission(RoleNeed('admin')).can() + + +admin.add_view( + AuthorView( + Author, + db.session, + 'authors', + endpoint='authors', + ) +) + + +admin.add_view( + ModelView( + Book, + db.session, + 'books', + endpoint='books', + ) +) + + diff --git a/pytest_bdd_example/book/blueprint.py b/pytest_bdd_example/book/blueprint.py index 5826e4b..b47402c 100644 --- a/pytest_bdd_example/book/blueprint.py +++ b/pytest_bdd_example/book/blueprint.py @@ -1,10 +1,7 @@ from flask import Blueprint - from .admin import admin book = Blueprint('book', __name__) -@book.record_once -def on_registered(state): - admin.init_app(state.app) +__all__ = ['book', 'admin'] diff --git a/pytest_bdd_example/dashboard/admin.py b/pytest_bdd_example/dashboard/admin.py new file mode 100644 index 0000000..521e6fc --- /dev/null +++ b/pytest_bdd_example/dashboard/admin.py @@ -0,0 +1,14 @@ +from flask.ext.admin import Admin +from flask.ext.admin.base import MenuLink + +admin = Admin( + name='Dashboard', + url='/', +) + +admin.add_link( + MenuLink( + name='Logout', + endpoint='security.logout', + ) +) diff --git a/pytest_bdd_example/dashboard/app.py b/pytest_bdd_example/dashboard/app.py index 312915a..d6d5a7a 100644 --- a/pytest_bdd_example/dashboard/app.py +++ b/pytest_bdd_example/dashboard/app.py @@ -3,6 +3,7 @@ from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy from pytest_bdd_example.dashboard import settings +from .admin import admin app = Flask( __name__, @@ -13,6 +14,7 @@ app = Flask( app.config.from_object('pytest_bdd_example.dashboard.settings') app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' +admin.init_app(app) db = SQLAlchemy(app) @@ -23,6 +25,5 @@ with app.app_context(): app.register_blueprint(auth, url_prefix='/auth') app.register_blueprint(book) - # Register the views from .views import * diff --git a/pytest_bdd_example/dashboard/views.py b/pytest_bdd_example/dashboard/views.py index 168389f..e69de29 100644 --- a/pytest_bdd_example/dashboard/views.py +++ b/pytest_bdd_example/dashboard/views.py @@ -1,8 +0,0 @@ -from flask import redirect, url_for - -from .app import app - - -@app.route('/') -def index(): - return redirect(url_for('admin.index')) diff --git a/pytest_bdd_example/templates/dashboard/auth/login.html b/pytest_bdd_example/templates/dashboard/auth/login.html index a6922a9..333e258 100644 --- a/pytest_bdd_example/templates/dashboard/auth/login.html +++ b/pytest_bdd_example/templates/dashboard/auth/login.html @@ -34,21 +34,23 @@ {% block menu %}{% endblock %} {% block content %} +
- {{ form.csrf_token }} + {{ login_user_form.csrf_token }}

Please sign in

- {{ form.username(placeholder=form.username.label.text, class="input-block-level") }} -
    {% for error in form.username.errors %}
  • {{ error|e }}
  • {% endfor %}
+ {{ login_user_form.username(placeholder=login_user_form.username.label.text, class="input-block-level") }} + + {% for error in login_user_form.username.errors %}
{{ error|e }}
{% endfor %}
- {{ form.password(placeholder=form.password.label.text, class="input-block-level") }} -
    {% for error in form.password.errors %}
  • {{ error|e }}
  • {% endfor %}
+ {{ login_user_form.password(placeholder=login_user_form.password.label.text, class="input-block-level") }} + {% for error in login_user_form.password.errors %}
{{ error|e }}
{% endfor %}
diff --git a/tests/helpers/mkdb/initialdata.py b/tests/helpers/mkdb/initialdata.py index 8568454..3f8e83f 100644 --- a/tests/helpers/mkdb/initialdata.py +++ b/tests/helpers/mkdb/initialdata.py @@ -1,20 +1,38 @@ from flask import current_app -from pytest_bdd_example.auth.models import User +from pytest_bdd_example.auth import user_datastore def create_initial_data(): print 'Populating the initial data...' db = current_app.extensions['sqlalchemy'].db - print 'Dashboard user: admin/asdasd' - admin = User( + print 'Creating roles...' + admin_role = user_datastore.create_role( + name='admin', + description='Administrators', + ) + + author_role = user_datastore.create_role( + name='author', + description='Book authors', + ) + + print 'Admin user: admin/asdasd' + admin = user_datastore.create_user( username='admin', password='asdasd', active=True, ) - db.session.add(admin) + user_datastore.add_role_to_user(admin, admin_role) + + print 'Author user: author/asdasd' + author = user_datastore.create_user( + username='author', + password='asdasd', + active=True, + ) + user_datastore.add_role_to_user(author, author_role) #TODO: add other data here... - db.session.commit()