From 74fbd9dac47d4c7ba06bc8d81083413c31e95a3c Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 12 Aug 2020 13:57:52 +1000 Subject: [PATCH 1/6] Implemented Delete with Simple Table example Added JQuery for confirmation prompt - You don't have to use this. --- controllers.py | 6 ++++++ libs/simple_table.py | 6 +++--- templates/libs/simple_table.html | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/controllers.py b/controllers.py index edcd328..67ff563 100644 --- a/controllers.py +++ b/controllers.py @@ -67,6 +67,12 @@ def zip_code(zip_code_id): return dict(form=form) +#DELETE +@action('zip_code/delete/', method=['GET', 'POST']) +@action.uses(session, db, auth, 'simple_table.html') +def zip_code(zip_code_id): + result = db(db.zip_code.id == zip_code_id).delete() + redirect(URL('index', vars=dict(user_signature=request.query.get('user_signature')))) @action('grid', method=['POST', 'GET']) @action.uses(session, db, auth, 'index.html') diff --git a/libs/simple_table.py b/libs/simple_table.py index 7c7a290..4f5bff6 100644 --- a/libs/simple_table.py +++ b/libs/simple_table.py @@ -273,7 +273,7 @@ def __repr__(self): _html.append(_top_div) - _table = TABLE(_class='table is-bordered is-striped is-hoverable is-fullwidth') + _table = TABLE(_class='table is-bordered is-striped is-hoverable is-fullwidth', _id="table") # build the header _thead = THEAD() @@ -332,7 +332,7 @@ def __repr__(self): if self.delete_url and self.delete_url != '': if self.include_action_button_text: _a = A(_href=self.delete_url + '/%s?user_signature=%s' % (row.id, self.user_signature), - _class='button is-small') + _class='confirmation button is-small') _span = SPAN(_class='icon is-small action-button-image') _span.append(I(_class='fas fa-trash')) _a.append(_span) @@ -340,7 +340,7 @@ def __repr__(self): else: _a = A(I(_class='fas fa-trash'), _href=self.delete_url + '/%s?user_signature=%s' % (row.id, self.user_signature), - _class='button is-small') + _class='confirmation button is-small', _message='Delete record ' +str(row.id) ) _td.append(_a) _tr.append(_td) _table.append(_tr) diff --git a/templates/libs/simple_table.html b/templates/libs/simple_table.html index baa0deb..ceb9e8e 100644 --- a/templates/libs/simple_table.html +++ b/templates/libs/simple_table.html @@ -3,3 +3,17 @@
[[=XML(grid)]]
+ + + + + From 71865cb444763e73f3d0b3501b9ce476730c757a Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 12 Aug 2020 14:03:06 +1000 Subject: [PATCH 2/6] Removed _id="table" as was unnecessary --- databases/storage.db | Bin 8146944 -> 8146944 bytes libs/simple_table.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/databases/storage.db b/databases/storage.db index 631cfb31df2f31b1d12cdac43147c139513fa29b..2fd31f5944cf56d3aed4115691f80d92774802b1 100644 GIT binary patch delta 665 zcmWmAyHAs09L4eHEpI8d)YewtYC))YK|u>tyi^2K6e}v;5%Grl^8zYf@QN`3k|$Lr zz~NbObt)3`P7JPY{sAU#CM>p1h=YTEJAAg2lbka?S123Dbd?pEsqXSdRI)WOAzCag z9xY{BY%N|b#i=YAw?; z6J0j(&rZxV6`g5MpxgagbaaIsT2KM^Z++X4?JoRtdh@n5tWw6Cj|~KStOm=kmee=( zNllr-gk58%^0A+`QYw)hDa5cA>kvm3s<9q5s6{Ea0d?4jP1uYrsK-`pLjxMI9XpUj z6PnS26n3H&yRaL3uor1}q_FQp(&zhQr9G3%^*1}CmYMk8(Pw^q^hC_B`E%XzWKjI?#y&IEX{&!eJaiH;!^tru>~9tW@ufH5vAc#J1_icvhnbG*PyyuxdYx#7h(`Pu)tyAy=~ delta 637 zcmW;I%UetV7=ZC}&YY9XG-igJgQlb?iV?Y02$3Unf4vu$lP zJ?S~C7PkEdtgPEO)ALwa@Ur;*0q;9LRgR70eT|vUOu zs4P*bR9UJLS4pT;sU%gFsiaiWDjAjKDl1f~RaVZ5JN479w!Rp__vwy&*k5vERxq&W z4qK_2@av+hm3BPlS>f;It`R$&`Qzpz+c1pwz=^!U|qsFV-0GMMJ?*E7WG(%1~g(lHlPX3*oaMNK`S<63v$?sHf+Oov||T$ zVi$H}4?4m^x${#lG)tWmR#T;N>&AJC4V8zPWw^9PC1@YOwai^hCUp}37kY8r*Il)a2Drq9v9G$i@1aV4B|4b z;3}@+I)-oqH*pJs+Ze_j+{Fk+aS!+L00lh67#`s Date: Fri, 14 Aug 2020 18:58:09 +1000 Subject: [PATCH 3/6] ignore databases --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 13dd987..d559cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ /dist/* /qlf.egg-info/* .deposit +databases/ From 170f1dd741a64dbf77e1fc4a3fc59f95f2e2a132 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 19 Aug 2020 16:25:43 +1000 Subject: [PATCH 4/6] Bring up to date with upstream --- .gitignore | 1 - README.md | 17 +- controllers.py | 42 ++++- databases/README.md | 1 + ...1b96a99be45f5a23f4277867ce_auth_user.table | Bin 0 -> 950 bytes ...f5a23f4277867ce_auth_user_tag_groups.table | Bin 0 -> 372 bytes ...81b96a99be45f5a23f4277867ce_zip_code.table | Bin 0 -> 852 bytes databases/sql.log | 35 ++++ libs/datatables.py | 61 ++++++- libs/simple_table.py | 77 ++++++++- static/components/mtable.html | 100 +++++++++++ static/components/mtable.js | 162 ++++++++++++++++++ static/css/main.css | 3 + static/js/utils.js | 12 +- templates/ajax_grid.html | 5 + templates/auth.html | 12 +- templates/datatables.html | 104 +---------- templates/layout.html | 3 +- templates/libs/edit.html | 27 ++- 19 files changed, 525 insertions(+), 137 deletions(-) create mode 100644 databases/README.md create mode 100644 databases/fb87181b96a99be45f5a23f4277867ce_auth_user.table create mode 100644 databases/fb87181b96a99be45f5a23f4277867ce_auth_user_tag_groups.table create mode 100644 databases/fb87181b96a99be45f5a23f4277867ce_zip_code.table create mode 100644 databases/sql.log create mode 100644 static/components/mtable.html create mode 100644 static/components/mtable.js create mode 100644 static/css/main.css create mode 100644 templates/ajax_grid.html diff --git a/.gitignore b/.gitignore index d559cb3..13dd987 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,4 @@ /dist/* /qlf.egg-info/* .deposit -databases/ diff --git a/README.md b/README.md index 1dfa234..92b86f7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ A collection of different ways to display a grid in py4web. * Simple Table - My shot at a reusable table * HTML Grid - the HTML grid from py4web * Datatables.net - sample implementation of a reusable datatables.net table +* py4web AJAX grid - AJAX grid using mtable +* py4web Vue.js grid - a Vue.js grid in py4web All three examples are working over the same sqlite database of zip code I downloaded freely from the web which contains just over 42,000 records. @@ -25,7 +27,16 @@ Search is different that web2py SQLFORM.grid. In essense, you provide your own * formatting - I seem to recall Massimo saying we needed to add some standardized css classes so people can modify the look and feel. #### datatables.net -* biggest concern here is formatting/presentation. I'm using bulma in py4web but there is no official bulma skin for datatables.net. Need to look into formatting options. UPDATE 8/11/2020 - I've made lots of formatting changes. More to come but got a good start tonight. -* edit controls - I have the edit controls placed where I want them - just need to insert the urls for the edit pages - Coming soon -* my implementation uses a specific view page. I need to make it more granular so you can include the javascript in one spot and specify the element id for where the table lives +* Formatting was a concern but now I'm happy with how it is working +* Edit and Delete controls are working - need a confirmation popup before delete + +#### AJAX Grid +* I don't care for the layout +* No paging controls, only allows you to retrieve more rows which are then added on to the end of the rows you'd already retrieved +* Confusing search control - I can't figure out how to search by Primary City in my app +* I don't think I'll be looking at this any more as I just don't like the way it worked. + +#### Vue.js Grid +* Not sure how this is supposed to work. I couldn't get a sample working wiht my table + diff --git a/controllers.py b/controllers.py index 9660dc3..dba3803 100644 --- a/controllers.py +++ b/controllers.py @@ -3,11 +3,16 @@ from functools import reduce from py4web import action, request, redirect, URL, Field -from py4web.utils.form import Form, FormStyleBulma +from py4web.utils.form import Form, FormStyleBulma, FormStyleDefault from py4web.utils.grid import Grid +from pydal.validators import IS_NULL_OR, IS_IN_SET from .common import db, session, auth, unauthenticated from .libs.datatables import DataTablesField, DataTablesRequest, DataTablesResponse -from .libs.simple_table import SimpleTable, get_signature, get_filter_value +from .libs.simple_table import SimpleTable, get_signature, get_storage_value +from py4web.utils.publisher import Publisher, ALLOW_ALL_POLICY # for ajax_grid + +# exposes services necessary to access the db.thing via ajax +publisher = Publisher(db, policy=ALLOW_ALL_POLICY) @action('index', method=['POST', 'GET']) @@ -24,13 +29,30 @@ def index(action=None, tablename=None, record_id=None): # check session to see if we've saved a default value user_signature = get_signature() - search_filter = get_filter_value(user_signature, 'search_filter', None) + search_state = get_storage_value(user_signature, 'search_state', None) + search_type = get_storage_value(user_signature, 'search_type', None) + search_filter = get_storage_value(user_signature, 'search_filter', None) # build the search form - search_form = Form([Field('search', length=50, default=search_filter)], - keep_values=True, formstyle=FormStyleBulma) + zip_type_requires = IS_NULL_OR(IS_IN_SET([x.zip_type for x in db(db.zip_code.id > 0).select(db.zip_code.zip_type, + orderby=db.zip_code.zip_type, + distinct=True)])) + zip_state_requires = IS_NULL_OR(IS_IN_SET([x.state for x in db(db.zip_code.id > 0).select(db.zip_code.state, + orderby=db.zip_code.state, + distinct=True)])) + search_form = Form([Field('state', length=20, requires=zip_state_requires, + default=search_state, + _title='Filter by State'), + Field('zip_type', length=20, requires=zip_type_requires, + default=search_type, + _title='Select Filter by ZIP Type'), + Field('search', length=50, default=search_filter, _placeholder='...search text...', + _title='Enter search text and click on Filter')], + keep_values=True, formstyle=FormStyleSimpleTable, ) if search_form.accepted: + search_state = search_form.vars['state'] + search_type = search_form.vars['zip_type'] search_filter = search_form.vars['search'] queries = [(db.zip_code.id > 0)] @@ -40,6 +62,8 @@ def index(action=None, tablename=None, record_id=None): (db.zip_code.primary_city.contains(search_filter)) | (db.zip_code.county.contains(search_filter)) | (db.zip_code.state.contains(search_filter))) + if search_state: + queries.append(db.zip_code.state == search_state) if search_type: queries.append(db.zip_code.zip_type == search_type) @@ -61,7 +85,9 @@ def index(action=None, tablename=None, record_id=None): grid = SimpleTable(queries, fields=fields, search_form=search_form, - filter_values=dict(search_filter=search_filter), + storage_values=dict(search_state=search_state, + search_type=search_type, + search_filter=search_filter), orderby=orderby, create=True, details=True, @@ -122,9 +148,9 @@ def datatables(): @unauthenticated -@action('_datatables_data', method=['GET', 'POST']) +@action('datatables_data', method=['GET', 'POST']) @action.uses(session, db, auth) -def _datatables_data(): +def datatables_data(): """ datatables.net makes an ajax call to this method to get the data diff --git a/databases/README.md b/databases/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/databases/README.md @@ -0,0 +1 @@ + diff --git a/databases/fb87181b96a99be45f5a23f4277867ce_auth_user.table b/databases/fb87181b96a99be45f5a23f4277867ce_auth_user.table new file mode 100644 index 0000000000000000000000000000000000000000..dfc29702a0da55c45f12b4c880507d47d01554a0 GIT binary patch literal 950 zcmZ{i&2Q5%7{*7N=A#{G5`znu;?uYwfy9MdHAN6rDI=s3J4I$SwMFJ^v8`!`NgP+{ zJ(|B_|KuDeSyPtfk}p5M=XpQ=I{)s5d-l4~M=)S3UESTB@U^5m-W!0cIJ;CtZ@}Y) zkFc-7>439*YGx4q~Vt^nVuw3JWEce^YC;!Tc+Ql#c>kG z;Ud*ADmgwkG6%60(h!vRDyu(HUxBX#sozWdpt$16RnB@wKJ_92|e0 zB}bn=e0(=uE~4+tP}2#FQL9IqKJU;JjL11=c~#~vc1S&X@D$HOR%Kxgz+}9@LR=N4 z?O6lc)EOOeievn|<1YiOe;+Ad@tZ8bJnm!TlY@smKe z7dSx=63gollTDiVLJq&Bw3s>s>JxTLGH~;_u literal 0 HcmV?d00001 diff --git a/databases/fb87181b96a99be45f5a23f4277867ce_auth_user_tag_groups.table b/databases/fb87181b96a99be45f5a23f4277867ce_auth_user_tag_groups.table new file mode 100644 index 0000000000000000000000000000000000000000..4d0c3ead1b881632332c6dd1257c837918c47087 GIT binary patch literal 372 zcmY+9Pfx-?5XJdZ3Jvk(7g+ZM7ZXi9cx=k3q*BaM51g7xSGGxAVB3x1VB)pO9M|vb z7L@2*W-^)g-fupgue{wl)Q1^iC(lgN-Ga+?CCseVL08Lsr@5)IFE^FcMPbK5xlvX6 zQg9Qt(W&-ZZVqDXmb=2>#WafH9vFFI)0xkfB!ngLpX2#73K+~FiVgOZOy8``p?O9P zdMbTSjw>4ul@KSF;yKh;F;1Frtfpv{f1M9^1Gr7p#Vk2h(WBQgD9r3sjjHbN~PV literal 0 HcmV?d00001 diff --git a/databases/sql.log b/databases/sql.log new file mode 100644 index 0000000..5be50d6 --- /dev/null +++ b/databases/sql.log @@ -0,0 +1,35 @@ +timestamp: 2020-05-26T08:40:18.512195 +CREATE TABLE "auth_user"( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "username" CHAR(512) UNIQUE, + "email" CHAR(512) UNIQUE, + "password" CHAR(512), + "first_name" CHAR(512), + "last_name" CHAR(512), + "sso_id" CHAR(512), + "action_token" CHAR(512), + "last_password_change" TIMESTAMP, + "past_passwords_hash" TEXT +); +success! +timestamp: 2020-05-26T08:40:18.521975 +CREATE TABLE "auth_user_tag_groups"( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "path" CHAR(512), + "record_id" INTEGER REFERENCES "auth_user" ("id") ON DELETE CASCADE +); +success! +timestamp: 2020-07-11T13:27:53.362545 +CREATE TABLE "zip_code"( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "zip_code" CHAR(5) UNIQUE, + "zip_type" CHAR(512), + "primary_city" CHAR(512), + "state" CHAR(512), + "county" CHAR(512), + "timezone" CHAR(512), + "area_code" CHAR(512), + "latitude" DOUBLE, + "longitude" DOUBLE +); +success! diff --git a/libs/datatables.py b/libs/datatables.py index b70b3d2..7a95baf 100644 --- a/libs/datatables.py +++ b/libs/datatables.py @@ -1,20 +1,69 @@ +from yatl.helpers import DIV, TABLE, TR, TD, TH, A, SPAN, I, THEAD, P, TAG, TBODY, SCRIPT +from py4web import URL + + class DataTablesResponse: - def __init__(self, fields=None, data_function=None, edit_function=None, page_length=15, sort_sequence=None): + def __init__(self, fields=None, data_url=None, create_url=None, + edit_url=None, delete_url=None, page_length=15, + sort_sequence=None): """ - A dataholder class so we can simply pass data from controller to web page with some defaults + All the data we need to build a datatable + Contains helper methods to write out the table :param fields: list of DataTablesField objects to display on the page - :param data_function: the controller function to call to get the data - :param edit_function: edit function to call to edit a page - Not in use at this time + :param data_url: the url for the call to get the data + :param edit_url: edit url to the edit page for the data :param page_length: default=15 - number of rows to display by default :param sort_sequence: list of a list of columns to sort by """ self.fields = fields - self.data_function = data_function - self.edit_function = edit_function + self.data_url = data_url + self.create_url = create_url + self.edit_url = edit_url + self.delete_url = delete_url self.page_length = page_length self.sort_sequence = sort_sequence if sort_sequence else [] + def style(self): + return """ + +""" def script(self): js = (' diff --git a/templates/datatables.html b/templates/datatables.html index 245aaa9..2957ccb 100644 --- a/templates/datatables.html +++ b/templates/datatables.html @@ -1,102 +1,6 @@ [[extend 'layout.html']] - - +[[=XML(dt.style())]] +[[=XML(dt.script())]]
- - - - [[for field in dt.fields:]] - - [[pass]] - - - - - -
[[=field.label]]ACTIONS
-
\ No newline at end of file + [[=XML(dt.table())]] + diff --git a/templates/layout.html b/templates/layout.html index 0a18080..5dfa0d5 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -5,6 +5,7 @@ + @@ -67,7 +68,7 @@ [[include]] -