From 923b1f55e2ae60dd58da59e894ae6a6f09cb0154 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Sat, 19 Apr 2014 15:27:25 -0400 Subject: [PATCH] Revert User#set_password and User#check_password for Migrate compatibility --- {{cookiecutter.app_name}}/manage.py | 10 ++-- {{cookiecutter.app_name}}/tests/conftest.py | 1 - {{cookiecutter.app_name}}/tests/factories.py | 5 +- .../tests/test_models.py | 22 ++++---- .../tests/test_webtests.py | 1 - .../{{cookiecutter.app_name}}/database.py | 51 +------------------ .../{{cookiecutter.app_name}}/public/forms.py | 2 +- .../{{cookiecutter.app_name}}/user/models.py | 17 +++++-- 8 files changed, 35 insertions(+), 74 deletions(-) diff --git a/{{cookiecutter.app_name}}/manage.py b/{{cookiecutter.app_name}}/manage.py index d77c8a9..45f0038 100644 --- a/{{cookiecutter.app_name}}/manage.py +++ b/{{cookiecutter.app_name}}/manage.py @@ -20,19 +20,19 @@ manager = Manager(app) TEST_CMD = "py.test tests" def _make_context(): - '''Return context dict for a shell session so you can access + """Return context dict for a shell session so you can access app, db, and the User model by default. - ''' + """ return {'app': app, 'db': db, 'User': User} @manager.command def test(): - '''Run the tests.''' + """Run the tests.""" status = subprocess.call(TEST_CMD, shell=True) sys.exit(status) -manager.add_command("server", Server()) -manager.add_command("shell", Shell(make_context=_make_context)) +manager.add_command('server', Server()) +manager.add_command('shell', Shell(make_context=_make_context)) manager.add_command('db', MigrateCommand) if __name__ == '__main__': diff --git a/{{cookiecutter.app_name}}/tests/conftest.py b/{{cookiecutter.app_name}}/tests/conftest.py index 91f21bd..2d3240b 100644 --- a/{{cookiecutter.app_name}}/tests/conftest.py +++ b/{{cookiecutter.app_name}}/tests/conftest.py @@ -9,7 +9,6 @@ from {{ cookiecutter.app_name }}.settings import TestConfig from {{cookiecutter.app_name}}.app import create_app from {{cookiecutter.app_name}}.database import db as _db -from .factories import ALL_FACTORIES @pytest.yield_fixture(scope='session') def app(): diff --git a/{{cookiecutter.app_name}}/tests/factories.py b/{{cookiecutter.app_name}}/tests/factories.py index a1f7ffc..cf7714a 100644 --- a/{{cookiecutter.app_name}}/tests/factories.py +++ b/{{cookiecutter.app_name}}/tests/factories.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from factory import Sequence +from factory import Sequence, PostGenerationMethodCall from factory.alchemy import SQLAlchemyModelFactory from {{cookiecutter.app_name}}.user.models import User @@ -24,7 +24,6 @@ class UserFactory(BaseFactory): username = Sequence(lambda n: "user{0}".format(n)) email = Sequence(lambda n: "user{0}@example.com".format(n)) - password = 'example' + password = PostGenerationMethodCall('set_password', 'example') active = True -ALL_FACTORIES = [UserFactory] diff --git a/{{cookiecutter.app_name}}/tests/test_models.py b/{{cookiecutter.app_name}}/tests/test_models.py index fa97242..1951c1a 100644 --- a/{{cookiecutter.app_name}}/tests/test_models.py +++ b/{{cookiecutter.app_name}}/tests/test_models.py @@ -5,42 +5,42 @@ import datetime as dt import pytest from {{ cookiecutter.app_name }}.user.models import User, Role -from .base import DbTestCase from .factories import UserFactory +@pytest.mark.usefixtures('db') class TestUser: - def test_created_at_defaults_to_datetime(self, db): + def test_created_at_defaults_to_datetime(self): user = User(username='foo', email='foo@bar.com') user.save() assert bool(user.created_at) - assert isinstance(user.created_at, dt.datetime) is True + assert isinstance(user.created_at, dt.datetime) - def test_password_is_nullable(self, db): + def test_password_is_nullable(self): user = User(username='foo', email='foo@bar.com') user.save() assert user.password is None - def test_factory(self, db): + def test_factory(self): user = UserFactory(password="myprecious") assert bool(user.username) assert bool(user.email) assert bool(user.created_at) assert user.is_admin is False assert user.active is True - assert user.password == "myprecious" + assert user.check_password('myprecious') - def test_check_password_with_equality_operators(self, db): + def test_check_password(self): user = User.create(username="foo", email="foo@bar.com", password="foobarbaz123") - assert user.password == 'foobarbaz123' - assert user.password != "barfoobaz" + assert user.check_password('foobarbaz123') is True + assert user.check_password("barfoobaz") is False - def test_full_name(self, db): + def test_full_name(self): user = UserFactory(first_name="Foo", last_name="Bar") assert user.full_name == "Foo Bar" - def test_roles(self, db): + def test_roles(self): role = Role(name='admin') role.save() u = UserFactory() diff --git a/{{cookiecutter.app_name}}/tests/test_webtests.py b/{{cookiecutter.app_name}}/tests/test_webtests.py index 26c81a0..1c99da1 100644 --- a/{{cookiecutter.app_name}}/tests/test_webtests.py +++ b/{{cookiecutter.app_name}}/tests/test_webtests.py @@ -8,7 +8,6 @@ from flask import url_for from {{cookiecutter.app_name}}.user.models import User -from .base import DbTestCase from .factories import UserFactory @pytest.fixture diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py index e490200..296f29a 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py @@ -3,9 +3,8 @@ utilities. """ from sqlalchemy.orm import relationship -from sqlalchemy.types import TypeDecorator -from .extensions import db, bcrypt +from .extensions import db Column = db.Column relationship = relationship @@ -51,56 +50,10 @@ class CRUDMixin(object): db.session.delete(self) return commit and db.session.commit() -# From Mike Bayer's "atmcraft" example app +# From Mike Bayer's "Building the app" talk # https://speakerdeck.com/zzzeek/building-the-app def ReferenceCol(tablename, nullable=False, **kwargs): """Column that adds primary key foreign key reference.""" return db.Column( db.ForeignKey("{0}.id".format(tablename)), nullable=nullable, **kwargs) - - -class Password(str): - """Coerce a string to a bcrypt password. - - Rationale: for an easy string comparison, - so we can say ``some_password == 'hello123'`` - - .. seealso:: - - https://pypi.python.org/pypi/bcrypt/ - - """ - - def __new__(cls, value, crypt=True): - if value is None: - return None - if isinstance(value, unicode): - value = value.encode('utf-8') - if crypt: - value = bcrypt.generate_password_hash(value) - return str.__new__(cls, value) - - def __eq__(self, other): - if other and not isinstance(other, Password): - return bcrypt.check_password_hash(self, other) - return str.__eq__(self, other) - - def __ne__(self, other): - return not self.__eq__(other) - - -class BcryptType(TypeDecorator): - """Coerce strings to bcrypted Password objects for the database. - """ - impl = db.String(128) - - def process_bind_param(self, value, dialect): - return Password(value) - - def process_result_value(self, value, dialect): - # already crypted, so don't crypt again - return Password(value, crypt=False) - - def __repr__(self): - return "BcryptType()" diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py index 1c00e77..5659c74 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py @@ -22,7 +22,7 @@ class LoginForm(Form): self.username.errors.append("Unknown username") return False - if self.user != self.password.data: + if not self.user.check_password(self.password.data): self.password.errors.append("Invalid password") return False diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py index 6baf4f5..b93509f 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py @@ -3,13 +3,13 @@ import datetime as dt from flask.ext.login import UserMixin +from {{cookiecutter.app_name}}.extensions import bcrypt from {{cookiecutter.app_name}}.database import ( db, CRUDMixin, ReferenceCol, relationship, Column, - BcryptType, ) @@ -27,15 +27,26 @@ class User(UserMixin, CRUDMixin, db.Model): __tablename__ = 'users' username = Column(db.String(80), unique=True, nullable=False) email = Column(db.String(80), unique=True, nullable=False) - password = Column(BcryptType, nullable=True) + #: The hashed password + password = Column(db.String(128), nullable=True) created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow) first_name = Column(db.String(30), nullable=True) last_name = Column(db.String(30), nullable=True) active = Column(db.Boolean(), default=False) is_admin = Column(db.Boolean(), default=False) - def __init__(self, username, email, **kwargs): + def __init__(self, username, email, password=None, **kwargs): db.Model.__init__(self, username=username, email=email, **kwargs) + if password: + self.set_password(password) + else: + self.password = None + + def set_password(self, password): + self.password = bcrypt.generate_password_hash(password) + + def check_password(self, value): + return bcrypt.check_password_hash(self.password, value) @property def full_name(self):