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.

Samoth
  • 716
  • 15
  • 34
  • I don't know this library, but maybe [this](http://stackoverflow.com/questions/14874846/readonly-text-field-in-flask-admin-modelview) could be a solution – IcyTv Apr 07 '17 at 07:54
  • 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. – Samoth Apr 07 '17 at 08:18

3 Answers3

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!

Community
  • 1
  • 1
Sergey Shubin
  • 3,040
  • 4
  • 24
  • 36
  • Good day! I followed your 1st and 2nd parts: `class ReadOnlyStringField` and `form_overrides`; however, the `project name` does not become read-only. – Samoth Apr 08 '17 at 01:29
  • @Samoth 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 :) – Sergey Shubin Apr 08 '17 at 07:28
  • thanks but I set both `readonly_condition` to be `True` and `False`, he `project nam`e does not become read-only. – Samoth Apr 10 '17 at 02:37
  • @Samoth Could you post updated code? Maybe github is really more suitable. – Sergey Shubin Apr 12 '17 at 08:37
  • @ 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. – Samoth Apr 12 '17 at 08:58
  • OK, it is done and please visit the link, I finally put it under my R programming repo: https://github.com/samoth21/ProgrammingAssignment2/tree/master/auth – Samoth Apr 12 '17 at 09:03
  • @Samoth Great! But at the moment application uses Philip Martin solution which doesn't work unfortunately (see my comment under his post). – Sergey Shubin Apr 12 '17 at 09:12
  • Martin's solution does not work for me, the fields are not dynamically become read-only based o the value of `approve`. – Samoth Apr 12 '17 at 09:15
  • @ 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. – Samoth Apr 12 '17 at 09:43
  • @Samoth 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! – Sergey Shubin Apr 12 '17 at 10:03
  • @ 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. – Samoth Apr 12 '17 at 10:09
  • 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. – Samoth Apr 12 '17 at 10:12
  • @Samoth Just add `edit_form` method to your `ProjectView` (`SWProjectView`), it should not conflict with other code. Without it `readonly` condition will not by dynamic. – Sergey Shubin Apr 12 '17 at 10:47
  • @ Sergey Shubi, I am not sure I understand correctly what you just suggested. I wrote another class`ProjectView` 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. – Samoth Apr 12 '17 at 13:32
  • @Samoth No, add `edit_form` method and `form_overrides` attribute to `SWProjectView` just as they are posted in my example. – Sergey Shubin Apr 12 '17 at 13:43
  • @ 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? – Samoth Apr 13 '17 at 00:27
  • @Samoth 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](https://github.com/mrjoes/flask-admin/blob/master/flask_admin/templates/bootstrap3/admin/lib.html#L146) macro. You may want to create new question. I haven't found similar questions on SO — it can be useful for community. – Sergey Shubin Apr 13 '17 at 07:55
  • @ Sergey Shubin: If the field is a selectable drop down button, it seems cannot become read-only? Thanks. – Samoth Apr 18 '17 at 09:21
  • @Samoth Not found a way to make it, sorry. `QuerySelectField` which is used in dropdowns cannot be subclassed due to some flask-admin bug. – Sergey Shubin Apr 18 '17 at 12:34
  • @ 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. – Samoth Apr 19 '17 at 02:24
  • @Samoth Take a look at this [answer](http://stackoverflow.com/a/26351005/6682517) - 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. – Sergey Shubin Apr 19 '17 at 07:29
  • @ 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. – Samoth Apr 19 '17 at 07:44
  • @Samoth 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. – Sergey Shubin Apr 19 '17 at 11:00
  • @ Sergey Shubin: Thanks, so it looks like I should make a relationship between `class project` and `class user` since you mentioned `User.projects`.. – Samoth Apr 20 '17 at 00:59
  • @ 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. – Samoth Apr 26 '17 at 06:27
  • @Samoth Hi! It's quite easily fixed: declare `ReadOnlyTextAreaField(TextAreaField)` the same way as `ReadOnlyStringField` and use it for multi-line input. – Sergey Shubin Apr 26 '17 at 07:06
  • @ Sergey Shubin: Excuse me I've been stuck with an issue and cannot resolve it, would you please take a look at [this](http://stackoverflow.com/questions/43908969/flask-admin-sqlalchemy-exc-interfaceerrorerror-binding-parameter-8), 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. – Samoth May 12 '17 at 07:27
  • @Samoth 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](http://stackoverflow.com/questions/29888698/sqlalchemy-exc-interfaceerror-unprintable-interfaceerror-object). – Sergey Shubin May 12 '17 at 08:34
  • @ 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. – Samoth May 12 '17 at 08:43
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.

Phillip Martin
  • 1,910
  • 15
  • 30
  • 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'` – Samoth Apr 08 '17 at 01:07
  • 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. – Samoth Apr 08 '17 at 03:24
  • Also didn't work unfortunately: `self.model.approve` is model _class_ attribute not _instance_ attribute so it always typecasts to `True`. – Sergey Shubin Apr 12 '17 at 08:32
  • @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. – Phillip Martin Apr 12 '17 at 13:22
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
        }
    }
Vladyslav
  • 21
  • 3