5

I've built an app with a table called Project which is stored in sqlite, I want allow only super users can view the approve column when creating, editing data.

The Project data is retrieved in the "class Project", and I've added if current_user.has_role('superuser') in "class ProjectView":

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_security import SQLAlchemyUserDatastore, current_user,UserMixin,        
     RoleMixin
from flask_admin.contrib import sqla

 # Create Flask application
 app = Flask(__name__)
 app.config.from_pyfile('config.py')
 db = SQLAlchemy(app)

 # Define models
 roles_users = db.Table(
     'roles_users',
     db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
     db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))
 )

 class Role(db.Model, RoleMixin):
     id = db.Column(db.Integer(), primary_key=True)
     name = db.Column(db.String(80), unique=True)
     description = db.Column(db.String(255))

     def __str__(self):
         return self.name

 class User(db.Model, UserMixin):
     id = db.Column(db.Integer, primary_key=True)
     email = db.Column(db.String(255), unique=True)
     password = db.Column(db.String(255))
     roles = db.relationship('Role', secondary=roles_users,
                        backref=db.backref('users', lazy='dynamic'))

     def __str__(self):
         return self.email

 class Project(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    team = db.Column(db.Unicode(64))
    project_name = db.Column(db.Unicode(128))
    approve = db.Column(db.Boolean())

 # Setup Flask-Security
 user_datastore = SQLAlchemyUserDatastore(db, User, Role)
 security = Security(app, user_datastore)


  # Create customized model view class
 class MyModelView(sqla.ModelView):

     def is_accessible(self):
        if not current_user.is_active or not current_user.is_authenticated:
            return False

        if current_user.has_role('superuser'):
            return True

        return False

     def _handle_view(self, name, **kwargs):
        if not self.is_accessible():
            if current_user.is_authenticated:
                # permission denied
               abort(403)
        else:
            # login
            return redirect(url_for('security.login', next=request.url))

 class ProjectView(sqla.ModelView):

    def is_accessible(self):
        if not current_user.is_active:
            return False
        else:
            return True

    def _handle_view(self, name, **kwargs):
        if not self.is_accessible():
            if current_user.is_authenticated:
                # permission denied
               abort(403)
            else:
               if current_user.has_role('superuser'):
                 form_create_rules = [
                 rules.FieldSet(('team'), 'Personal Info'),
                 rules.Header('Project Info'),
                 rules.Field('project_name'),
                 'approve',
                 rules.Container('rule_demo.wrap', rules.Field('notes'))
                 ]

             else: 
                 form_create_rules = [
                 rules.FieldSet(('team'), 'Personal Info'),
                 rules.Header('Project Info'),
                 rules.Field('project_name'),
                 #'approve',
                 rules.Container('rule_demo.wrap', rules.Field('notes'))
                 ]
             form_edit_rules = form_create_rules

             create_template = 'rule_create.html'
             edit_template = 'rule_edit.html'

    @app.route('/')
    def index():
      return render_template('index.html')

    # Create admin
    admin = flask_admin.Admin(
          app,
          'Release Control System',
          # log in success page
          base_template='my_master.html',   
          template_mode='bootstrap3',
        )

   # Add model views
   admin.add_view(MyModelView(Role, db.session))
   admin.add_view(MyModelView(User, db.session))
   admin.add_view(ProjectView(Project, db.session))

But it still not works and all users still can view the approve column. Kindly advise. Thanks in advance.

Samoth
  • 716
  • 15
  • 34

4 Answers4

7

You can use BaseModelView.column_list attribute to specify dynamically calculated list of accessible columns, just make it a property. However different "field" attributes of ModelView are cached on application launch so you need to override their caches:

from flask import has_app_context

class ProjectView(sqla.ModelView):
    @property
    def _list_columns(self):
        return self.get_list_columns()

    @_list_columns.setter
    def _list_columns(self, value):
        pass

    @property
    def column_list(self):
        if not has_app_context() or current_user.has_role('superuser'):
            return ['team', 'project_name', 'approve']
        else:
            return ['team', 'project_name']

column_list attribute is used during application initialisation when current_user is not available. Use flask.has_app_context() method to check this state and pass application a full list of columns on launch.

If you need to specify different set of columns for editing you need form_rules attributes (you already used them in your question):

from flask_admin.form import rules

class ProjectView(sqla.ModelView):
    @property
    def _form_edit_rules(self):
        return rules.RuleSet(self, self.form_rules)

    @_form_edit_rules.setter
    def _form_edit_rules(self, value):
        pass

    @property
    def _form_create_rules(self):
        return rules.RuleSet(self, self.form_rules)

    @_form_create_rules.setter
    def _form_create_rules(self, value):
        pass

    @property
    def form_rules(self):
        form_rules = [
            rules.FieldSet(('team',), 'Personal Info'),
            rules.Header('Project Info'),
            rules.Field('project_name')
        ]
        if not has_app_context() or current_user.has_role('superuser'):
            form_rules.append('approve')
        form_rules.append(rules.Container('rule_demo.wrap', rules.Field('notes')))
        return form_rules

Also you do not need to use _handle_view to redirect user to login page. For this purpose BaseView.inaccessible_callback method is used:

def inaccessible_callback(self, name, **kwargs):
    if current_user.is_authenticated:
        abort(403)
    else:
        return redirect(url_for('security.login', next=request.url))
Sergey Shubin
  • 3,040
  • 4
  • 24
  • 36
  • thanks. But it shows `AttributeError: 'NoneType' object has no attribute 'has_role'` after revising the code. And if I comment `@property`, then it shows `TypeError: 'instancemethod' object is not iterable`. – Samoth Mar 02 '17 at 01:18
  • @Samoth I forgot that application uses `column_list` several times during initialisation when there is no `current_user` available. Try to use `flask.has_app_context()` method to give application full list of columns on launch. I updated the example above. – Sergey Shubin Mar 02 '17 at 07:05
  • thanks but it still not works and the user still can see the **approve** column. I posted the `class ProjectView` code below. Thanks . – Samoth Mar 02 '17 at 07:24
  • @Samoth Thanks for you tries! Made one more attempt to override cached attributes. – Sergey Shubin Mar 02 '17 at 07:58
  • Thanks and this time an error occured: `TypeError: _list_columns() takes exactly 1 argument (2 given)`. I updated the code below. – Samoth Mar 02 '17 at 08:11
  • @Samoth Mistyped `_list_columns` **setter** definition. Change it to `def _list_columns(self, value):` – Sergey Shubin Mar 02 '17 at 08:51
  • Thanks, this time worked like a miracle! Thanks again ! – Samoth Mar 02 '17 at 09:00
  • @Samoth Great! I shall edit my answer so it will be more usable for other users. – Sergey Shubin Mar 02 '17 at 09:03
  • I found something really weird, now **approve** does not show in the list but still appeared in the **create** and **edit** page, I posted the screen shots in the below answer. – Samoth Mar 02 '17 at 09:12
  • @Samoth Found a solution: `form_rules` attributes which you have already used. Updated the answer. Thanks for the updates! – Sergey Shubin Mar 02 '17 at 11:51
  • @ Sergey Shubin, it's me need to say thank you! After update, the error occurs: `TypeError: 'NoneType' object is not iterable`, I'm thinking it might be `@property` usually cause the error. – Samoth Mar 03 '17 at 01:02
  • @Samoth Oh, I forgot to return the value from `form_rules`, now it will work. – Sergey Shubin Mar 03 '17 at 04:54
  • @ Sergey Shubin, thanks but now when clicked **create** and **edit**, it shows `ValueError: Form does not have field t`. I'll post the code and pics in another answer. – Samoth Mar 03 '17 at 05:11
0
 class ProjectView(sqla.ModelView):
    '''
    def inaccessible_callback(self, name, **kwargs):
       if current_user.is_authenticated:
          abort(403)
       else:
          return redirect(url_for('security.login', next=request.url))
    '''
    def is_accessible(self):
        if not current_user.is_active or not current_user.is_authenticated:
            return False
        else:
            return True

    @property
    def _list_columns(self):
        return self.get_list_columns()

    @_list_columns.setter
    def _list_columns(self,value):
       pass

    @property
    def column_list(self):
       if not has_app_context() or current_user.has_role('superuser'):
          return ['team', 'project_name', 'approve']
       else:
          return ['team', 'project_name']

    form_edit_rules = column_list
    create_template = 'rule_create.html'
    edit_template = 'rule_edit.html'

enter image description here

enter image description here

enter image description here

Samoth
  • 716
  • 15
  • 34
0
class ProjectView(sqla.ModelView):

  def inaccessible_callback(self, name, **kwargs):
     if current_user.is_authenticated:
        abort(403)
     else:
        return redirect(url_for('security.login', next=request.url))

  @property
  def _form_edit_rules(self):
      return rules.RuleSet(self, self.form_rules)

  @_form_edit_rules.setter
  def _form_edit_rules(self, value):
      pass

  @property
  def _form_create_rules(self):
      return rules.RuleSet(self, self.form_rules)

  @_form_create_rules.setter
  def _form_create_rules(self, value):
      pass

  @property
  def form_rules(self):
      form_rules = [
          rules.FieldSet(('team'), 'Personal Info'),
          rules.Header('Project Info'),
          rules.Field('project_name')
      ]
      if not has_app_context() or current_user.has_role('superuser'):
          form_rules.append('approve')
      form_rules.append(rules.Container('rule_demo.wrap', 
          rules.Field('notes')))
      return form_rules

enter image description here

Samoth
  • 716
  • 15
  • 34
  • 1
    It's already another issue: change `('team')` to `('team',)` :) The first argument of `FieldSet` should be tuple or list but `('team')` is handled as string. Look at this question: [Why do tuples with only one element get converted to strings?](http://stackoverflow.com/questions/12876177/why-do-tuples-with-only-one-element-get-converted-to-strings) – Sergey Shubin Mar 03 '17 at 07:02
  • Yes, now it works like a charm, I don't kwow how to express my appreciation! Although the `rules.Container('rule_demo.wrap', rules.Field('notes'))` does not work. The most important part is **approve** , which only can be seen and edited by the superusers, thanks again. – Samoth Mar 03 '17 at 07:32
  • 1
    Thank you, too! The answer now looks more useful for others with all the updates. – Sergey Shubin Mar 03 '17 at 07:57
0

I'd like to combine the FileAdmin(example here: https://github.com/flask-admin/flask-admin/tree/master/examples/file).

Which means only after the project is being approved(see pic), then the users can upload the file to the system automatically specified path(say: /Reviewer1/Reviewer2/file)

enter image description here

Samoth
  • 716
  • 15
  • 34