Skip to content

Commit

Permalink
register views generic for datamodels
Browse files Browse the repository at this point in the history
  • Loading branch information
dpgaspar committed May 22, 2015
1 parent 33796dc commit 3055760
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 17 deletions.
1 change: 1 addition & 0 deletions docs/versions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Improvements and Bug fixes on 1.4.0
- New, SimpleFormView and PublicFormView now subclass BaseFormView.
- New, class method for BaseView's get_default_url, returns the default_view url.
- New, OAuth authentication method.
- New, Search for role with a particular set of permissions on views or menus.
- TODO, add_exclude_columns
- TODO, edit_exclude_columns
- TODO, show_exclude_columns
Expand Down
30 changes: 23 additions & 7 deletions flask_appbuilder/baseviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ class MyView(ModelView):
search_columns = ['name','address']
"""
search_exclude_columns = None
"""
List with columns to exclude from search. Will include all possible columns by default
"""
label_columns = None
"""
Dictionary of labels for your columns, override this if you want diferent pretify labels
Expand Down Expand Up @@ -375,9 +379,15 @@ def _init_titles(self):
def _init_properties(self):
self.label_columns = self.label_columns or {}
self.base_filters = self.base_filters or []
self.search_exclude_columns = self.search_exclude_columns or []
self.search_columns = self.search_columns or []

self._base_filters = self.datamodel.get_filters().add_filter_list(self.base_filters)
list_cols = self.datamodel.get_columns_list()
self.search_columns = self.search_columns or self.datamodel.get_search_columns_list()
search_columns = self.datamodel.get_search_columns_list()
if not self.search_columns:
self.search_columns = [x for x in search_columns if x not in self.search_exclude_columns]

self._gen_labels_columns(list_cols)
self._filters = self.datamodel.get_filters(self.search_columns)

Expand Down Expand Up @@ -460,15 +470,20 @@ class MyView(ModelView):
"""
A list of columns to exclude from the show view. By default all columns are included.
"""

add_exclude_columns = None
"""
A list of columns to exclude from the add form. By default all columns are included.
"""
edit_exclude_columns = None
"""
A list of columns to exclude from the edit form. By default all columns are included.
"""
order_columns = None
""" Allowed order columns """

page_size = 10
"""
Use this property to change default page size
"""

show_fieldsets = None
"""
show fieldsets django style [(<'TITLE'|None>, {'fields':[<F1>,<F2>,...]}),....]
Expand Down Expand Up @@ -652,6 +667,8 @@ def _init_properties(self):
self.add_form_extra_fields = self.add_form_extra_fields or {}
self.edit_form_extra_fields = self.edit_form_extra_fields or {}
self.show_exclude_columns = self.show_exclude_columns or []
self.add_exclude_columns = self.add_exclude_columns or []
self.edit_exclude_columns = self.edit_exclude_columns or []
# Generate base props
list_cols = self.datamodel.get_user_columns_list()
self.list_columns = self.list_columns or [list_cols[0]]
Expand All @@ -670,15 +687,14 @@ def _init_properties(self):
self.add_columns = self.add_columns + list(fieldset_item[1].get('fields'))
else:
if not self.add_columns:

self.add_columns = list_cols
self.add_columns = [x for x in list_cols if x not in self.add_exclude_columns]
if self.edit_fieldsets:
self.edit_columns = []
for fieldset_item in self.edit_fieldsets:
self.edit_columns = self.edit_columns + list(fieldset_item[1].get('fields'))
else:
if not self.edit_columns:
self.edit_columns = list_cols
self.edit_columns = [x for x in list_cols if x not in self.edit_exclude_columns]

"""
-----------------------------------------------------
Expand Down
261 changes: 261 additions & 0 deletions flask_appbuilder/security/registerviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
__author__ = 'dpgaspar'

import logging

from werkzeug.security import generate_password_hash
from flask import flash, redirect, session, url_for, request
from openid.consumer.consumer import Consumer, SUCCESS, CANCEL

from flask_openid import SessionWrapper, OpenIDResponse
from ..views import expose, PublicFormView
from flask_babelpkg import lazy_gettext
#from .models import User, RegisterUser
from .forms import RegisterUserOIDForm, RegisterUserDBForm, LoginForm_oid
from ..models.sqla.interface import SQLAInterface
from ..validators import Unique
from .._compat import as_unicode
from .. import const as c

log = logging.getLogger(__name__)


def get_first_last_name(fullname):
names = fullname.split()
if len(names) > 1:
return names[0], ' '.join(names[1:])
elif names:
return names[0], ''


class BaseRegisterUser(PublicFormView):
"""
Make your own user registration view and inherit from this class if you
want to implement a completely different registration process. If not,
just inherit from RegisterUserDBView or RegisterUserOIDView depending on
your authentication method.
then override SecurityManager property that defines the class to use::
from flask.ext.appbuilder.security.registerviews import RegisterUserDBView
class MyRegisterUserDBView(BaseRegisterUser):
email_template = 'register_mail.html'
...
class MySecurityManager(SecurityManager):
registeruserdbview = MyRegisterUserDBView
When instantiating AppBuilder set your own SecurityManager class::
appbuilder = AppBuilder(app, db.session, security_manager_class=MySecurityManager)
"""
route_base = '/register'
email_template = 'appbuilder/general/security/register_mail.html'
""" The template used to generate the email sent to the user """
email_subject = lazy_gettext('Account activation')
""" The email subject sent to the user """
activation_template = 'appbuilder/general/security/activation.html'
""" The activation template, shown when the user is activated """
message = lazy_gettext('Registration sent to your email')
""" The message shown on a successful registration """
error_message = lazy_gettext('Not possible to register you at the moment, try again later')
""" The message shown on an unsuccessful registration """
false_error_message = lazy_gettext('Registration not found')
""" The message shown on an unsuccessful registration """
form_title = lazy_gettext('Fill out the registration form')
""" The form title """

def send_email(self, register_user):
"""
Method for sending the registration Email to the user
"""
try:
from flask_mail import Mail, Message
except:
log.error("Install Flask-Mail to use User registration")
return False
mail = Mail(self.appbuilder.get_app)
msg = Message()
msg.subject = self.email_subject
url = url_for('.activation', _external=True, activation_hash=register_user.registration_hash)
msg.html = self.render_template(self.email_template,
url=url,
username=register_user.username,
first_name=register_user.first_name,
last_name=register_user.last_name)
msg.recipients = [register_user.email]
try:
mail.send(msg)
except Exception as e:
log.error("Send email exception: {0}".format(str(e)))
return False
return True

def add_registration(self, username, first_name, last_name, email, password=''):
"""
Add a registration request for the user.
:rtype : RegisterUser
"""
register_user = self.appbuilder.sm.add_register_user(username, first_name, last_name, email, password)
if register_user:
if self.send_email(register_user):
flash(as_unicode(self.message), 'info')
return register_user
else:
flash(as_unicode(self.error_message), 'danger')
self.appbuilder.sm.del_register_user(register_user)
return None

@expose('/activation/<string:activation_hash>')
def activation(self, activation_hash):
"""
Endpoint to expose an activation url, this url
is sent to the user by email, when accessed the user is inserted
and activated
"""
reg = self.appbuilder.sm.find_register_user(activation_hash)
if not reg:
log.error(c.LOGMSG_ERR_SEC_NO_REGISTER_HASH.format(activation_hash))
flash(as_unicode(self.false_error_message), 'danger')
return redirect(self.appbuilder.get_url_for_index)
if not self.appbuilder.sm.add_user(username=reg.username,
email=reg.email,
first_name=reg.first_name,
last_name=reg.last_name,
role=self.appbuilder.sm.find_role(
self.appbuilder.sm.auth_user_registration_role),
hashed_password=reg.password):
flash(as_unicode(self.error_message), 'danger')
return redirect(self.appbuilder.get_url_for_index)
else:
self.appbuilder.sm.del_register_user(reg)
return self.render_template(self.activation_template,
username=reg.username,
first_name=reg.first_name,
last_name=reg.last_name,
appbuilder=self.appbuilder)

def add_form_unique_validations(self, form):
datamodel_user = self.appbuilder.sm.get_user_datamodel
datamodel_register_user = self.appbuilder.sm.get_register_user_datamodel
if len(form.username.validators) == 1:
form.username.validators.append(Unique(datamodel_user, 'username'))
form.username.validators.append(Unique(datamodel_register_user, 'username'))
if len(form.email.validators) == 2:
form.email.validators.append(Unique(datamodel_user, 'email'))
form.email.validators.append(Unique(datamodel_register_user, 'email'))


class RegisterUserDBView(BaseRegisterUser):
"""
View for Registering a new user, auth db mode
"""
form = RegisterUserDBForm
""" The WTForm form presented to the user to register himself """
redirect_url = '/'

def form_get(self, form):
self.add_form_unique_validations(form)

def form_post(self, form):
self.add_registration(username=form.username.data,
first_name=form.first_name.data,
last_name=form.last_name.data,
email=form.email.data,
password=form.password.data)


class RegisterUserOIDView(BaseRegisterUser):
"""
View for Registering a new user, auth OID mode
"""
route_base = '/register'

form = RegisterUserOIDForm
default_view = 'form_oid_post'

@expose("/formoidone", methods=['GET', 'POST'])
def form_oid_post(self, flag=True):
if flag:
self.oid_login_handler(self.form_oid_post, self.appbuilder.sm.oid)
form = LoginForm_oid()
if form.validate_on_submit():
session['remember_me'] = form.remember_me.data
return self.appbuilder.sm.oid.try_login(form.openid.data, ask_for=['email', 'fullname'])
resp = session.pop('oid_resp', None)
if resp:
self._init_vars()
form = self.form.refresh()
self.form_get(form)
form.username.data = resp.email
first_name, last_name = get_first_last_name(resp.fullname)
form.first_name.data = first_name
form.last_name.data = last_name
form.email.data = resp.email
widgets = self._get_edit_widget(form=form)
#self.update_redirect()
return self.render_template(self.form_template,
title=self.form_title,
widgets=widgets,
form_action='form',
appbuilder=self.appbuilder)
else:
flash(as_unicode(self.error_message), 'warning')
return redirect(self.get_redirect())

def oid_login_handler(self, f, oid):
"""
Hackish method to make use of oid.login_handler decorator.
"""
if request.args.get('openid_complete') != u'yes':
return f(False)
consumer = Consumer(SessionWrapper(self), oid.store_factory())
openid_response = consumer.complete(request.args.to_dict(),
oid.get_current_url())
if openid_response.status == SUCCESS:
return self.after_login(OpenIDResponse(openid_response, []))
elif openid_response.status == CANCEL:
oid.signal_error(u'The request was cancelled')
return redirect(oid.get_current_url())
oid.signal_error(u'OpenID authentication error')
return redirect(oid.get_current_url())

def after_login(self, resp):
"""
Method that adds the return OpenID response object on the session
this session key will be deleted
"""
session['oid_resp'] = resp

def form_get(self, form):
self.add_form_unique_validations(form)

def form_post(self, form):
self.add_registration(username=form.username.data,
first_name=form.first_name.data,
last_name=form.last_name.data,
email=form.email.data)

class RegisterUserOAuthView(BaseRegisterUser):
"""
View for Registering a new user, auth OID mode
"""
form = RegisterUserOIDForm

def form_get(self, form):
self.add_form_unique_validations(form)
# fills the register form with the collected data from OAuth
form.username.data = request.args.get('username', '')
form.first_name.data = request.args.get('first_name', '')
form.last_name.data = request.args.get('last_name', '')
form.email.data = request.args.get('email', '')

def form_post(self, form):
log.debug('Adding Registration')
self.add_registration(username=form.username.data,
first_name=form.first_name.data,
last_name=form.last_name.data,
email=form.email.data)


2 changes: 0 additions & 2 deletions flask_appbuilder/security/sqla/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ class User(Model):
last_login = Column(DateTime)
login_count = Column(Integer)
fail_login_count = Column(Integer)
#role_id = Column(Integer, default=1, ForeignKey('ab_role.id'))
#role = relationship('Role')
roles = relationship('Role', secondary=assoc_user_role, backref='user')
created_on = Column(DateTime, default=datetime.datetime.now, nullable=True)
changed_on = Column(DateTime, default=datetime.datetime.now, nullable=True)
Expand Down
12 changes: 4 additions & 8 deletions flask_appbuilder/security/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ class PermissionViewModelView(ModelView):

label_columns = {'permission': lazy_gettext('Permission'), 'view_menu': lazy_gettext('View/Menu')}
list_columns = ['permission', 'view_menu']
show_columns = ['permission', 'view_menu']
search_columns = ['permission', 'view_menu']



class ResetMyPasswordView(SimpleFormView):
"""
Expand Down Expand Up @@ -146,8 +144,7 @@ class UserModelView(ModelView):
{'fields': ['first_name', 'last_name', 'email'], 'expanded': True}),
]

search_columns = ['first_name', 'last_name', 'username', 'email', 'roles', 'active',
'created_by', 'changed_by', 'changed_on', 'changed_by', 'login_count']
search_exclude_columns = ['password']

add_columns = ['first_name', 'last_name', 'username', 'active', 'email', 'roles']
edit_columns = ['first_name', 'last_name', 'username', 'active', 'email', 'roles']
Expand Down Expand Up @@ -300,10 +297,8 @@ class RoleModelView(ModelView):

label_columns = {'name': lazy_gettext('Name'), 'permissions': lazy_gettext('Permissions')}
list_columns = ['name', 'permissions']
show_columns = ['name', 'permissions']
order_columns = ['name']
search_columns = ['name']


@action("Copy Role", lazy_gettext('Copy Role'), lazy_gettext('Copy the selected roles?'), icon='fa-copy', single=False)
def copy_role(self, items):
self.update_redirect()
Expand All @@ -323,6 +318,7 @@ class RegisterUserModelView(ModelView):
show_title = lazy_gettext('Show Registration')
list_columns = ['username','registration_date','email']
show_exclude_columns = ['password']
search_exclude_columns = ['password']

class AuthView(BaseView):
route_base = ''
Expand Down

0 comments on commit 3055760

Please sign in to comment.