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/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..2e21d37 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,84 @@ 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(db.Model, RoleMixin):
+ name = TextField(unique=True)
+ description = TextField(null=True)
+
+ class User(db.Model, UserMixin):
+ email = TextField()
+ password = TextField()
+ active = BooleanField(default=True)
+ confirmed_at = DateTimeField(null=True)
+
+ 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.
+ 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, UserRoles)
+ 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 +249,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 `_.
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..d0b3bba 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.
@@ -73,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):
@@ -137,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
+ """
+ kwargs["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."""
@@ -179,3 +195,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
+
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 f1e070d..1196dfb 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()