Repository: flask-admin/flask-admin

Issue:
Idea: Editable List View
It would be useful to be able to bulk edit records from the list view, like this:
![editable_listview2](https://cloud.githubusercontent.com/assets/992533/5567370/15760168-8f09-11e4-959d-a2f170ff0f5e.png)

I've made some progress on getting the form fields to show on the list view:
https://github.com/pawl/flask-admin/compare/list_editable

However, I'm having trouble with wtform's FieldList and processing the submitted data. Is there a way to do this that uses the existing inline form code? Or, does anyone have any suggestions for how to implement this?

Here's the gist I've been using to test my branch (with a new column_editable_list property): https://gist.github.com/pawl/ec6c51d1a10ffa75bac3



Discussion:
I made some more progress and now I have a working example!
![great_success](https://cloud.githubusercontent.com/assets/992533/5578640/b5d36e06-8ff0-11e4-901e-b73b8c6ee6bc.png)

Here's the link: http://104.236.24.168:5065/admin/model1/

If you want to try the code yourself, it's the same gist as before: https://gist.github.com/pawl/ec6c51d1a10ffa75bac3

This branch: https://github.com/pawl/flask-admin/compare/list_editable

Can you describe architecture in few words?

Example:
- Setting `column_editable_list = ['test1']` in the ModelView will allow editing the 'test1' column from the index_view using x-editable/ajax (http://vitalets.github.io/x-editable/).

How it works:
- An additional form is created for the index_view if "column_editable_list" is set. The new form only contains columns in "column_editable_list". The form is created using the existing get_form() code, but each field is wrapped in a WTForms FieldList. This is to allow POST'ing in this format: `('<field_name>-<primary_key>', u'<value>')`
- The FieldList fields have a custom widget called XEditableWidget. This widget determines how to display the x-editable/ajax form based on the field inside of the FieldList (StringField, IntegerField, etc).
- In list.html, if the column is in "column_editable_list", the x-editable fields are created with the primary key + value as parameters.
- The index_view accepts the ajax POST request. The validation and model update is similar to edit_view.

It's working on all field types now (including relations), here's the demo: http://104.236.24.168:5065/admin/model1/ (you might need to refresh if you saw the previous demo)

Nice! I have been working on something very similar but using gevent socketio to push changes in real-time to other connected clients as well. Good stuff!

Found one minor UI issue...

http://node.gordo4.com/snaps/37e652d9f501d90df8e156bdecda1e1e.png

Perhaps bump down default number of textarea rows from 7 to say... 4 or 5.

https://vitalets.github.io/x-editable/docs.html#textarea

I've been having an issue using this when my tables primary key is a UUID with a "-" character. It seems to only read a sub-string of the UUID; from the beginning of the UUID to the first "-" character.

The bottom of the stack trace is:

```
  DataError: (DataError) invalid input syntax for uuid: "edcda51d"
  LINE 3: WHERE products.product_uuid = 'edcda51d'
                                      ^
'SELECT products.product_uuid AS products_product_uuid, products.title AS products_title,      products.short_description AS products_short_description, products.long_description AS products_long_description, products.last_seen AS products_last_seen, products.price AS products_price, products.rating AS products_rating \nFROM products \nWHERE products.product_uuid = %(param_1)s' {'param_1': u'edcda51d'}
```

The UI goes a bit crazy too:
https://i.imgur.com/vAAUMzB.png

The issue is here: https://github.com/flask-admin/flask-admin/blob/a964d3fff65ebbc8ab31ea4f8c3e8be1a5b9b296/flask_admin/model/fields.py#L146

We can probably do some escaping here to make it work: https://github.com/flask-admin/flask-admin/blob/a964d3fff65ebbc8ab31ea4f8c3e8be1a5b9b296/flask_admin/model/fields.py#L158

@pawl There's helper for that.

https://github.com/flask-admin/flask-admin/blob/master/flask_admin/tools.py#L110 - pass list/tuple, get string.

https://github.com/flask-admin/flask-admin/blob/master/flask_admin/tools.py#L122 - pass string, get `list`.

@mrjoes Can you explain how the helper might be used to bypass this issue with the "-" character?

@karimtabet it encodes/decodes lists to/from string and handles escaping. 

So instead of concatenating strings like here: https://github.com/flask-admin/flask-admin/blob/a964d3fff65ebbc8ab31ea4f8c3e8be1a5b9b296/flask_admin/model/fields.py#L158

You can use helper to encode this tuple and then instead of using `split` - you can get your list back.

@pawl linked snippets that should be changed.

Unfortunately I didn't have time to make the change and test it.


Resolving patch:
diff --git a/flask_admin/model/fields.py b/flask_admin/model/fields.py
--- a/flask_admin/model/fields.py
+++ b/flask_admin/model/fields.py
@@ -141,11 +141,11 @@ def __init__(self, *args, **kwargs):
 
     def _extract_indices(self, prefix, formdata):
         offset = len(prefix) + 1
-        for k in formdata:
-            if k.startswith(prefix):
-                k = k[offset:].split('-', 1)[0]
-                # removed "if k.isdigit():"
-                yield k
+        for name in formdata:
+            # selects only relevant field (not CSRF, other fields, etc)
+            if name.startswith(prefix):
+                # exclude offset (prefix-), remaining text is the index
+                yield name[offset:]
 
     def _add_entry(self, formdata=None, data=unset_value, index=None):
         assert not self.max_entries or len(self.entries) < self.max_entries, \


Test patch:
diff --git a/flask_admin/tests/sqla/test_basic.py b/flask_admin/tests/sqla/test_basic.py
--- a/flask_admin/tests/sqla/test_basic.py
+++ b/flask_admin/tests/sqla/test_basic.py
@@ -118,7 +118,6 @@ def fill_db(db, Model1, Model2):
 def test_model():
     app, db, admin = setup()
     Model1, Model2 = create_models(db)
-    db.create_all()
 
     view = CustomModelView(Model1, db.session)
     admin.add_view(view)
@@ -408,6 +407,43 @@ def test_column_editable_list():
     ok_('test1_val_3' in data)
 
 
+def test_editable_list_special_pks():
+    ''' Tests editable list view + a primary key with special characters
+    '''
+    app, db, admin = setup()
+
+    class Model1(db.Model):
+        def __init__(self, id=None, val1=None):
+            self.id = id
+            self.val1 = val1
+
+        id = db.Column(db.String(20), primary_key=True)
+        val1 = db.Column(db.String(20))
+
+    db.create_all()
+
+    view = CustomModelView(Model1, db.session, column_editable_list=['val1'])
+    admin.add_view(view)
+
+    db.session.add(Model1('1-1', 'test1'))
+    db.session.add(Model1('1-5', 'test2'))
+    db.session.commit()
+
+    client = app.test_client()
+
+    # Form - Test basic in-line edit functionality
+    rv = client.post('/admin/model1/ajax/update/', data={
+        'val1-1-1': 'change-success-1',
+    })
+    data = rv.data.decode('utf-8')
+    ok_('Record was successfully saved.' == data)
+
+    # ensure the value has changed
+    rv = client.get('/admin/model1/')
+    data = rv.data.decode('utf-8')
+    ok_('change-success-1' in data)
+
+
 def test_column_filters():
     app, db, admin = setup()
 
@@ -1301,7 +1337,6 @@ def test_relations():
 def test_on_model_change_delete():
     app, db, admin = setup()
     Model1, _ = create_models(db)
-    db.create_all()
 
     class ModelView(CustomModelView):
         def on_model_change(self, form, model, is_created):
@@ -1666,7 +1701,6 @@ class Model2(db.Model):
 def test_safe_redirect():
     app, db, admin = setup()
     Model1, _ = create_models(db)
-    db.create_all()
 
     view = CustomModelView(Model1, db.session)
     admin.add_view(view)
@@ -1693,7 +1727,6 @@ def test_safe_redirect():
 def test_simple_list_pager():
     app, db, admin = setup()
     Model1, _ = create_models(db)
-    db.create_all()
 
     class TestModelView(CustomModelView):
         simple_list_pager = True