From 305576071b847d1cf690c8f36158a210e84ccfd7 Mon Sep 17 00:00:00 2001 From: dpgaspar Date: Fri, 22 May 2015 22:50:00 +0100 Subject: [PATCH] register views generic for datamodels --- docs/versions.rst | 1 + flask_appbuilder/baseviews.py | 30 ++- flask_appbuilder/security/registerviews.py | 261 +++++++++++++++++++++ flask_appbuilder/security/sqla/models.py | 2 - flask_appbuilder/security/views.py | 12 +- 5 files changed, 289 insertions(+), 17 deletions(-) create mode 100644 flask_appbuilder/security/registerviews.py diff --git a/docs/versions.rst b/docs/versions.rst index b5f38093c..0108f03fa 100644 --- a/docs/versions.rst +++ b/docs/versions.rst @@ -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 diff --git a/flask_appbuilder/baseviews.py b/flask_appbuilder/baseviews.py index 779ff7dd7..f1279cd0e 100644 --- a/flask_appbuilder/baseviews.py +++ b/flask_appbuilder/baseviews.py @@ -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 @@ -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) @@ -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':[,,...]}),....] @@ -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]] @@ -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] """ ----------------------------------------------------- diff --git a/flask_appbuilder/security/registerviews.py b/flask_appbuilder/security/registerviews.py new file mode 100644 index 000000000..4ef8017b1 --- /dev/null +++ b/flask_appbuilder/security/registerviews.py @@ -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/') + 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) + + diff --git a/flask_appbuilder/security/sqla/models.py b/flask_appbuilder/security/sqla/models.py index ab30ef5cb..aeba958bc 100644 --- a/flask_appbuilder/security/sqla/models.py +++ b/flask_appbuilder/security/sqla/models.py @@ -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) diff --git a/flask_appbuilder/security/views.py b/flask_appbuilder/security/views.py index e3cb86dbd..edf5cb766 100644 --- a/flask_appbuilder/security/views.py +++ b/flask_appbuilder/security/views.py @@ -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): """ @@ -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'] @@ -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() @@ -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 = ''