flask-admin: How to make columns read-only according to other columns' value?
Asked Answered
F

3

7

I've built a system that allows users to apply for code review and wait for manager to approve.

And now what I want to achieve is as below:

  1. If it's approved, enter image description here then all the fields become read-only(I manually set Project name as read-only here):

enter image description here

  1. If it's rejected,

    enter image description here

    then all the fields become editable. Of course, when creating a new project, all the fields should be editable.
    enter image description here

    The code of class Project and ProjectView are as below:

     from flask_sqlalchemy import SQLAlchemy
     from flask_admin.contrib import sqla
     from flask_security import current_user
    
     # Create Flask application
     app = Flask(__name__)
     app.config.from_pyfile('config.py')
     db = SQLAlchemy(app)
    
     class Project(db.Model):
    
            id = db.Column(db.Integer, primary_key=True)
            project_name = db.Column(db.Unicode(128))
            version = db.Column(db.Unicode(128))
            SVN = db.Column(db.UnicodeText)
            approve = db.Column(db.Boolean())
    
            def __unicode__(self):
                return self.name
    
     class ProjectView(sqla.ModelView):
         def is_accessible(self):
             if not current_user.is_active or not current_user.is_authenticated:
                 return False
             return False
    
         @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.Field('project_name'),
             rules.Field('version'),
             rules.Field('SVN'),
         ]
         if not has_app_context() or current_user.has_role('superuser'):
             form_rules.append('approve')
    

    In my opinion, since approve is a boolean variable, there should be a condition judgement to tell if it is 0 or 1 and then the field become read-only or editable accordingly.

    Thanks for any advise in advance.

Flavescent answered 5/4, 2017 at 5:43 Comment(2)
I don't know this library, but maybe this could be a solutionBuckeye
Yes, I checked link you provided, but what I want is set the columns read-only dynamically, instead of fixed to read-only all the time.Flavescent
A
6

As you already noticed setting readonly property for a field is rather simple but making it dynamic is a bit tricky.

First of all you need a custom field class:

from wtforms.fields import StringField

class ReadOnlyStringField(StringField):
    @staticmethod
    def readonly_condition():
        # Dummy readonly condition
        return False

    def __call__(self, *args, **kwargs):
        # Adding `readonly` property to `input` field
        if self.readonly_condition():
            kwargs.setdefault('readonly', True)
        return super(ReadOnlyStringField, self).__call__(*args, **kwargs)

    def populate_obj(self, obj, name):
        # Preventing application from updating field value
        # (user can modify web page and update the field)
        if not self.readonly_condition():
            super(ReadOnlyStringField, self).populate_obj(obj, name)

Set form_overrides attribute for your view:

class ProjectView(sqla.ModelView):
    form_overrides = {
        'project_name': ReadOnlyStringField
    }

You need to pass custom readonly_condition function to ReadOnlyStringField instance. The easiest way I found is overriding edit_form method:

class ProjectView(sqla.ModelView):
    def edit_form(self, obj=None):
        def readonly_condition():
            if obj is None:
                return False
            return obj.approve
        form = super(ProjectView, self).edit_form(obj)
        form.project_name.readonly_condition = readonly_condition
        return form

Happy coding!

Astridastride answered 7/4, 2017 at 10:19 Comment(29)
Good day! I followed your 1st and 2nd parts: class ReadOnlyStringField and form_overrides; however, the project name does not become read-only.Flavescent
@Flavescent Greetings! Without 3rd part readonly status will be statically based on dummy readonly_condition method which always return False. You can change it to True to check it actually sets readonly status. I just thought the field should be readable by default :)Astridastride
thanks but I set both readonly_condition to be True and False, he project name does not become read-only.Flavescent
@Flavescent Could you post updated code? Maybe github is really more suitable.Astridastride
@ Sergey Shubin, Yes, I've been trying to put my code on github these days but in vain. Ii seems I have to use command line to commit code and cannot upload directly.Flavescent
OK, it is done and please visit the link, I finally put it under my R programming repo: github.com/samoth21/ProgrammingAssignment2/tree/master/authFlavescent
@Flavescent Great! But at the moment application uses Philip Martin solution which doesn't work unfortunately (see my comment under his post).Astridastride
Martin's solution does not work for me, the fields are not dynamically become read-only based o the value of approve.Flavescent
@ Sergey Shubin, kindly let me know if the application can be run on your machine since in my time zone it's time to take a rest now, haha.Flavescent
@Flavescent I think it will be better to switch application to my solution. I can then make code review and find possible problems why it is not working in your case :) Nice resting!Astridastride
@ Sergey Shubin, Yes sure I am willing to do that, but actually I am not quite sure how to integrate your third part (class ProjectView(sqla.ModelView)) with my application.Flavescent
I already added your 1st part in my application( class ReadOnlyStringField(StringField)) , and per my understanding the 2nd part s just for a demo, so I did not add them.Flavescent
@Flavescent Just add edit_form method to your ProjectView (SWProjectView), it should not conflict with other code. Without it readonly condition will not by dynamic.Astridastride
@ Sergey Shubi, I am not sure I understand correctly what you just suggested. I wrote another classProjectView to inherit SWProjectView and then add edit form in it. And I add the view using admin.add_view(ProjectView(Project, db.session)). Though it still not work.Flavescent
@Flavescent No, add edit_form method and form_overrides attribute to SWProjectView just as they are posted in my example.Astridastride
@ Sergey Shub, the solution works well and will be accepted as the best answer. By the way, I've been trying to change the save button on the edit and create form to other string something like confrim or submit but could not find the file to edit, would you please advise?Flavescent
@Flavescent Thanks, you are welcome! Button text can't be easily changed, you need to override several macros and blocks in your "edit.html" and "create.html" templates to change render_form_buttons macro. You may want to create new question. I haven't found similar questions on SO — it can be useful for community.Astridastride
@ Sergey Shubin: If the field is a selectable drop down button, it seems cannot become read-only? Thanks.Flavescent
@Flavescent Not found a way to make it, sorry. QuerySelectField which is used in dropdowns cannot be subclassed due to some flask-admin bug.Astridastride
@ Sergey Shubin: I think there is a big defect in my application: every users can see all the project list. Is it possible to restrict the user can only can see his/her own project? Since flask-admin examples provided are all role based until last weekend I suddenly noticed the defect. Thanks so much for any advise in advance.Flavescent
@Flavescent Take a look at this answer - you will need to override get_query and get_count_query methods in your ProjectView. I already tried this solution in my project and it worked.Astridastride
@ Sergey Shubin: Thanks, and what is the variable I need to replace paid in my application? Would you please explain more because I am not so sure what is the variable here to restrict the user can only can see his/her own project.Flavescent
@Flavescent You need to pass SQLAlchemy filter. In your case return value may look like this self.session.query(self.model).filter(self.model.reviewers.user_id == current_user.id). Not sure how it will work: your User.projects relationship is commented and don't know if current_user.id is your actual user ID.Astridastride
@ Sergey Shubin: Thanks, so it looks like I should make a relationship between class project and class user since you mentioned User.projects..Flavescent
@ Sergey Shubin: There's one more question on the answer. If I set the field as ReadOnlyStringField, then the filed's height become fixed(one liner) and cannot be extended; this is not comfortable when user is editing it. Thanks.Flavescent
@Flavescent Hi! It's quite easily fixed: declare ReadOnlyTextAreaField(TextAreaField) the same way as ReadOnlyStringField and use it for multi-line input.Astridastride
@ Sergey Shubin: Excuse me I've been stuck with an issue and cannot resolve it, would you please take a look at this, whether I use __str__ and __repr__ both not worked. The query worked and returned user names correctly but the data type is still an memory location thus cannot be store in database.Flavescent
@Flavescent Hi! It looks like your last issue doesn't refer to your last question. This error is mentioned in wtforms questions, try to look here.Astridastride
@ Sergey Shubin: Yes I saw this post before but the situation seems different. I define many-to-many relationship between role&user and team &user. And it works fine. But when I try to query it(by user's id and teams), the error occurred.Flavescent
R
3

The previous answer I put on here had a major flaw. The following uses a different approach by analyzing the form itself and adding readonly: True to render_kw for a particular form if a certain condition is met.

class ProjectView(sqla.ModelView):
    # ... other class code

    def edit_form(self, obj=None):
        # grab form from super
        form = super(ProjectView, self).edit_form(obj)

        # form.approved.data should be the same as approved
        # if approved is included in the form
        if form.approved.data:
            if form.project_name.render_kw:
                form.project_name.render_kw.update({
                    'readonly': True
                })
            else:
                form.project_name.render_kw = {'readonly': True}
        return form

This is a bit hacky, and it requires that approved be in the edit form. If you used this solution, you could either add approved as a readonly field or, instead of readonly, you could remove the approved field from the form in the above class method.

Rsfsr answered 7/4, 2017 at 18:29 Comment(4)
thanks and I revised my code as follow: def _form_edit_rules(self): return { CustomizableField('project_name', field_args={ 'readonly': not self.model.approve }), rules.RuleSet(self, self.edit_form_rules) } but the error occurs: AttributeError: 'set' object has no attribute 'visible_fields'Flavescent
I found 2 situations: not self.model.approve: then the fields are editable all the time; and self.model.approve: then the fields are read-only all the time. It's not dynamically based on the approve value.Flavescent
Also didn't work unfortunately: self.model.approve is model class attribute not instance attribute so it always typecasts to True.Astridastride
@SergeyShubin, thanks for pointing that out. That was an oversight on my part. I changed my answer to edit the form directly based on the content of the form.Rsfsr
T
2

For me this trick was the simplest way to do it:

from flask_sqlalchemy import SQLAlchemy
from flask_admin.contrib.sqla import ModelView
from flask_admin.form.rules import Field


class Example(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    not_editable = db.Column(db.Unicode(128))
    editable = db.Column(db.Unicode(128))


class ReadonlyFiledRule(Field):
    def __call__(self, form, form_opts=None, field_args={}):
        field_args['readonly'] = True
        return super(ReadonlyFiledRule, self).__call__(form, form_opts, field_args)


class ExampleView(ModelView):
    form_edit_rules = (ReadonlyFiledRule('not_editable'), 'editable', )

Update (the easiest way):

class ExampleView(ModelView):
    form_widget_args = {
        'not_editable': {
            'readonly': True
        }
    }
Tithing answered 19/2, 2018 at 16:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.