From 5687f2f5a9c8585842a3e405054183e541525840 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 25 Jan 2013 16:52:50 -0800 Subject: [PATCH 1/8] Adds support for flask-peewee --- flask_security/__init__.py | 2 +- flask_security/datastore.py | 72 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/flask_security/__init__.py b/flask_security/__init__.py index f665380..b1176cb 100644 --- a/flask_security/__init__.py +++ b/flask_security/__init__.py @@ -13,7 +13,7 @@ __version__ = '1.5.4' from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user -from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore +from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore from .decorators import auth_token_required, http_auth_required, \ login_required, roles_accepted, roles_required from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \ diff --git a/flask_security/datastore.py b/flask_security/datastore.py index 7a958be..747740a 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -44,6 +44,15 @@ class MongoEngineDatastore(Datastore): model.delete() +class PeeweeDatastore(Datastore): + def put(self, model): + model.save() + return model + + def delete(self, model): + model.delete_instance() + + class UserDatastore(object): """Abstracted user datastore. @@ -179,3 +188,66 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore): def find_role(self, role): return self.role_model.objects(name=role).first() + + +class PeeweeUserDatastore(PeeweeDatastore, UserDatastore): + """A PeeweeD datastore implementation for Flask-Security that assumes + the use of the Flask-Peewee extension. + + :param user_model: A user model class definition + :param role_model: A role model class definition + :param role_link: A model implementing the many-to-many user-role relation + """ + def __init__(self, db, user_model, role_model, role_link): + PeeweeDatastore.__init__(self, db) + UserDatastore.__init__(self, user_model, role_model) + self.UserRole = role_link + + def find_user(self, **kwargs): + try: + return self.user_model.filter(**kwargs).get() + except self.user_model.DoesNotExist: + return None + + def find_role(self, role): + try: + return self.role_model.filter(name=role).get() + except self.role_model.DoesNotExist: + return None + + def create_user(self, **kwargs): + """Creates and returns a new user from the given parameters.""" + roles = kwargs.pop('roles', []) + user = self.user_model(**self._prepare_create_user_args(**kwargs)) + user = self.put(user) + for role in roles: + self.add_role_to_user(user, role) + return user + + + def add_role_to_user(self, user, role): + """Adds a role tp a user + + :param user: The user to manipulate + :param role: The role to add to the user + """ + user, role = self._prepare_role_modify_args(user, role) + if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count(): + return False + else: + self.UserRole.create(user=user, role=role) + return True + + def remove_role_from_user(self, user, role): + """Removes a role from a user + + :param user: The user to manipulate + :param role: The role to remove from the user + """ + user, role = self._prepare_role_modify_args(user, role) + if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count(): + self.UserRole.delete().where(self.UserRole.user==user, self.UserRole.role==role) + return True + else: + return False + From 70b11d9015f7f3b5b8bb31fd4b86c536e5fddf2e Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 25 Jan 2013 16:53:01 -0800 Subject: [PATCH 2/8] Unit-tests for flask-peewee --- .travis.yml | 2 +- setup.py | 1 + tests/functional_tests.py | 7 ++++ tests/test_app/peewee_app.py | 62 ++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/test_app/peewee_app.py diff --git a/.travis.yml b/.travis.yml index 42c3b47..a182d2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: install: - pip install . --quiet --use-mirrors - "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib --quiet --use-mirrors; fi" - - pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors + - pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors before_script: - mysql -e 'create database flask_security_test;' diff --git a/setup.py b/setup.py index 4025e75..05e8986 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ setup( 'nose', 'Flask-SQLAlchemy', 'Flask-MongoEngine', + 'Flask-Peewee', 'py-bcrypt', 'simplejson' ], diff --git a/tests/functional_tests.py b/tests/functional_tests.py index c68625a..fd8d60a 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -224,6 +224,13 @@ class MongoEngineSecurityTests(DefaultSecurityTests): return create_app(auth_config, **kwargs) +class PeeweeSecurityTests(DefaultSecurityTests): + + def _create_app(self, auth_config, **kwargs): + from tests.test_app.peewee_app import create_app + return create_app(auth_config, **kwargs) + + class DefaultDatastoreTests(SecurityTest): def test_add_role_to_user(self): diff --git a/tests/test_app/peewee_app.py b/tests/test_app/peewee_app.py new file mode 100644 index 0000000..ae404a2 --- /dev/null +++ b/tests/test_app/peewee_app.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +import sys +import os + +sys.path.pop(0) +sys.path.insert(0, os.getcwd()) + +from flask_peewee.db import Database +from peewee import * +from flask.ext.security import Security, UserMixin, RoleMixin, \ + PeeweeUserDatastore + +from tests.test_app import create_app as create_base_app, populate_data, \ + add_context_processors + +def create_app(config, **kwargs): + app = create_base_app(config) + app.config['DATABASE'] = { + 'name': 'example2.db', + 'engine': 'peewee.SqliteDatabase', + } + db = Database(app) + + class Role(db.Model, RoleMixin): + name = TextField(unique=True) + description = TextField(null=True) + + class User(db.Model, UserMixin): + email = TextField() + password = TextField() + last_login_at = DateTimeField(null=True) + current_login_at = DateTimeField(null=True) + last_login_ip = TextField(null=True) + current_login_ip = TextField(null=True) + login_count = IntegerField(null=True) + active = BooleanField(default=True) + confirmed_at = DateTimeField(null=True) + + class UserRoles(db.Model): + """ Peewee does not have built-in many-to-many support, so we have to + create this mapping class to link users to roles.""" + user = ForeignKeyField(User, related_name='roles') + role = ForeignKeyField(Role, related_name='users') + name = property(lambda self: self.role.name) + description = property(lambda self: self.role.description) + + @app.before_first_request + def before_first_request(): + for Model in (Role, User, UserRoles): + Model.drop_table(fail_silently=True) + Model.create_table(fail_silently=True) + populate_data(app.config.get('USER_COUNT', None)) + + app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs) + + add_context_processors(app.security) + + return app + +if __name__ == '__main__': + create_app({}).run() From 423e430e04d234be006dcb405c205a821f6b7e84 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 25 Jan 2013 16:54:18 -0800 Subject: [PATCH 3/8] Docs for flask-peewee --- docs/api.rst | 4 +++ docs/index.rst | 11 +++--- docs/models.rst | 14 ++++---- docs/quickstart.rst | 82 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 96 insertions(+), 15 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 4b86c60..c4ff952 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -49,6 +49,10 @@ Datastores :members: :inherited-members: +.. autoclass:: flask_security.datastore.PeeweeUserDatastore + :members: + :inherited-members: + Signals ------- diff --git a/docs/index.rst b/docs/index.rst index 97713c1..083de61 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ Flask application. They include: 4. Basic HTTP authentication 5. Token based authentication 6. Token based account activation (optional) -7. Token based password recovery/resetting (optional) +7. Token based password recovery / resetting (optional) 8. User registration (optional) 9. Login tracking (optional) @@ -27,10 +27,11 @@ and libraries. They include: Additionally, it assumes you'll be using a common library for your database connections and model definitions. Flask-Security supports the following Flask -extensions out of the box for data persistance: +extensions out of the box for data persistence: -1. `Flask-SQLAlchemy `_ -2. `Flask-MongoEngine `_ +1. `Flask-SQLAlchemy `_ +2. `Flask-MongoEngine `_ +3. `Flask-Peewee `_ -.. include:: contents.rst.inc \ No newline at end of file +.. include:: contents.rst.inc diff --git a/docs/models.rst b/docs/models.rst index 6ea6b15..6fea2e4 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -1,12 +1,12 @@ Models ====== -Flask-Security assumes you'll be using libraries such as SQLAlchemy or -MongoEngine to define a data model that includes a `User` and `Role` model. The -fields on your models must follow a particular convention depending on the -functionality your app requires. Aside from this, you're free to add any -additional fields to your model(s) if you want. At the bear minimum your `User` -and `Role` model should include the following fields: +Flask-Security assumes you'll be using libraries such as SQLAlchemy, +MongoEngine or Peewee to define a data model that includes a `User` and +`Role` model. The fields on your models must follow a particular convention +depending on the functionality your app requires. Aside from this, you're +free to add any additional fields to your model(s) if you want. At the bear +minimum your `User` and `Role` model should include the following fields: **User** @@ -48,4 +48,4 @@ additional fields: * ``current_login_at`` * ``last_login_ip`` * ``current_login_ip`` -* ``login_count`` \ No newline at end of file +* ``login_count`` diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 69aff7c..0443a0e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,6 +3,7 @@ Quick Start - `Basic SQLAlchemy Application <#basic-sqlalchemy-application>`_ - `Basic MongoEngine Application <#basic-mongoengine-application>`_ +- `Basic Peewee Application <#basic-peewee-application>`_ - `Mail Configuration <#mail-configuration>`_ Basic SQLAlchemy Application @@ -21,7 +22,7 @@ SQLAlchemy Application ~~~~~~~~~~~~~~~~~~~~~~ The following code sample illustrates how to get started as quickly as -possible using SQLAlchemy. +possible using SQLAlchemy: :: @@ -94,7 +95,9 @@ MongoEngine Application ~~~~~~~~~~~~~~~~~~~~~~~ The following code sample illustrates how to get started as quickly as -possible using MongoEngine:: +possible using MongoEngine: + +:: from flask import Flask, render_template from flask.ext.mongoengine import MongoEngine @@ -144,6 +147,79 @@ possible using MongoEngine:: app.run() +Basic Peewee Application +======================== + +Peewee Install requirements +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + $ mkvirtualenv + $ pip install flask-security flask-peewee + +Peewee Application +~~~~~~~~~~~~~~~~~~ + +The following code sample illustrates how to get started as quickly as +possible using Peewee: + +:: + + from flask import Flask, render_template + from flask_peewee.db import Database + from peewee import * + from flask.ext.security import Security, PeeweeUserDatastore, \ + UserMixin, RoleMixin, login_required + + # Create app + app = Flask(__name__) + app.config['DEBUG'] = True + app.config['SECRET_KEY'] = 'super-secret' + app.config['DATABASE'] = { + 'name': 'example.db', + 'engine': 'peewee.SqliteDatabase', + } + + # Create database connection object + db = Database(app) + + class Role(Model, RoleMixin): + name = TextField(unique=True) + description = TextField(null=True) + + class User(Model, UserMixin): + email = TextField() + password = TextField() + active = BooleanField(default=True) + confirmed_at = DateTimeField(null=True) + + class UserRoles(Model): + user = ForeignKeyField(User, related_name='roles') + role = ForeignKeyField(Role, related_name='users') + + # Setup Flask-Security + user_datastore = PeeweeUserDatastore(db, User, Role) + security = Security(app, user_datastore) + + # Create a user to test with + @app.before_first_request + def create_user(): + for Model in (Role, User, UserRoles): + Model.drop_table(fail_silently=True) + Model.create_table(fail_silently=True) + user_datastore.create_user(email='matt@nobien.net', password='password') + + # Views + @app.route('/') + @login_required + def home(): + return render_template('index.html') + + if __name__ == '__main__': + app.run() + + Mail Configuration =================== @@ -168,4 +244,4 @@ the basic application code in the previous section:: To learn more about the various Flask-Mail settings to configure it to work with your particular email server configuration, please see the -`Flask-Mail documentation `_. \ No newline at end of file +`Flask-Mail documentation `_. From 46c2355a7e3acfb4318b872349bc33a38bb01a28 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 25 Jan 2013 16:58:21 -0800 Subject: [PATCH 4/8] FIxes peewee description on quickstart --- docs/quickstart.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 0443a0e..8d288e8 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -195,11 +195,16 @@ possible using Peewee: confirmed_at = DateTimeField(null=True) class UserRoles(Model): + # Because peewee does not come with built-in many-to-many + # relationships, we need this intermediary class to link + # user to roles. user = ForeignKeyField(User, related_name='roles') role = ForeignKeyField(Role, related_name='users') + name = property(lambda self: self.role.name) + description = property(lambda self: self.role.description) # Setup Flask-Security - user_datastore = PeeweeUserDatastore(db, User, Role) + user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles) security = Security(app, user_datastore) # Create a user to test with From e3e96d546a223b3e919b9cf1d1879968412dd6b6 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 25 Jan 2013 16:59:48 -0800 Subject: [PATCH 5/8] Another small fix for the peewee docs --- docs/quickstart.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8d288e8..2e21d37 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -184,17 +184,17 @@ possible using Peewee: # Create database connection object db = Database(app) - class Role(Model, RoleMixin): + class Role(db.Model, RoleMixin): name = TextField(unique=True) description = TextField(null=True) - class User(Model, UserMixin): + class User(db.Model, UserMixin): email = TextField() password = TextField() active = BooleanField(default=True) confirmed_at = DateTimeField(null=True) - class UserRoles(Model): + class UserRoles(db.Model): # Because peewee does not come with built-in many-to-many # relationships, we need this intermediary class to link # user to roles. From aea5b916492826dd82e353f9436c182385c7f9ef Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 28 Jan 2013 18:57:19 -0800 Subject: [PATCH 6/8] Method stub parameters and docs for find_role didn't match implementations. --- flask_security/datastore.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flask_security/datastore.py b/flask_security/datastore.py index 747740a..f5ccac4 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -82,11 +82,11 @@ class UserDatastore(object): return kwargs def find_user(self, **kwargs): - """Returns a user matching the provided paramters.""" + """Returns a user matching the provided parameters.""" raise NotImplementedError - def find_role(self, **kwargs): - """Returns a role matching the provided paramters.""" + def find_role(self, name): + """Returns a role matching the provided name.""" raise NotImplementedError def add_role_to_user(self, user, role): From 462fb1ae7ed71d76f5a02ee3ce9e6f615ba0bf21 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 28 Jan 2013 18:58:11 -0800 Subject: [PATCH 7/8] Convenience method for finding or creating a role `datastore. find_or_create_role("admin")` will now always return a role with the name admin; useful for initialisation, --- flask_security/datastore.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flask_security/datastore.py b/flask_security/datastore.py index f5ccac4..293a9ac 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -146,6 +146,13 @@ class UserDatastore(object): role = self.role_model(**kwargs) return self.put(role) + def find_or_create_role(self, name, **kwargs): + """Returns a role matching the given name or creates it with any + additionally provided parameters + """ + kwrags["name"] = name + return self.find_role(name) or self.create_role(**kwargs) + def create_user(self, **kwargs): """Creates and returns a new user from the given parameters.""" From 51e06bdbb07c7a0182fded7d351fa5ac02166c5e Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 29 Jan 2013 15:46:59 -0800 Subject: [PATCH 8/8] Fixes typo in find_or_create_role --- flask_security/datastore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_security/datastore.py b/flask_security/datastore.py index 293a9ac..d0b3bba 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -150,7 +150,7 @@ class UserDatastore(object): """Returns a role matching the given name or creates it with any additionally provided parameters """ - kwrags["name"] = name + kwargs["name"] = name return self.find_role(name) or self.create_role(**kwargs) def create_user(self, **kwargs):