Please hammer this py4web CRUD example & tell me what to improve

252 views
Skip to first unread message

Tom Campbell

unread,
Jul 5, 2020, 4:59:24 PM7/5/20
to py4web
Working on the simplest robust CRUD example I can think of. This is a rudimentary task list that lets you create and edit to do items. Please tell me what it's missing, other than:
  • Paging. Right now it only shows you the most recent 10 tasks
  • Click column to sort by priority, date, etc.
  • Search
Goals are to keep line count to a minimum while demonstrating as many production practices as possible.

Here it is:
https://gist.github.com/tomcam/7b723cbb5f6542f54532d45c1dbc2d19




Jim Steil

unread,
Jul 5, 2020, 7:26:39 PM7/5/20
to py4web
Tom:

Here is what I've been working on.  Not fully baked yet, but working to get it there.  I have this saved in my app as libs/html.py

from py4web import request, URL
from .. models import db
from .. import settings
from yatl.helpers import DIV, TABLE, TR, TD, TH, A, SPAN, I, THEAD, P, XML, TAG
from py4web.utils.form import FormStyleDefault
from functools import reduce
from html import unescape, escape

NAV
= TAG.nav
HEADER
= TAG.header


class QueryTable:
   
def __init__(self,
                 endpoint
,
                 queries
,
                 tablename
,
                 search_form
=None,
                 fields
=None,
                 hidden_fields
=None,
                 show_id
=False,
                 order_by
=None,
                 left
=None,
                 headings
=None,
                 per_page
=settings.DEFAULT_ROWS_PER_PAGE,
                 create_url
='',
                 edit_url
='',
                 delete_url
=''):
       
#  get the current query parms
       
self.query_parms = dict()
       
self.filter_by = dict()
       
if request.query_string and isinstance(request.query_string, str):
           
#  split the key/value pairs
            kvp
= request.query_string.split('&')
           
for query_parm in kvp:
               
#  split the parm into key and value
                key
, value = query_parm.split('=')
               
if key[:7] == 'filter_':
                    queries
.append(db[tablename][key[7:]].contains(value.replace('%20', ' ')))
               
else:
                   
self.query_parms[key] = value

       
#  get instance arguments
       
self.endpoint = endpoint

       
self.search_form = search_form

       
self.query = reduce(lambda a, b: (a & b), queries)

       
self.fields = []
       
if fields:
           
if isinstance(fields, list):
               
self.fields = fields
           
else:
               
self.fields = [fields]

       
self.hidden_fields = []
       
if hidden_fields:
           
if isinstance(hidden_fields, list):
               
self.hidden_fields = hidden_fields
           
else:
               
self.hidden_fields = [hidden_fields]

       
self.show_id = show_id
       
self.order_by = order_by
       
self.left = left

       
self.headings = []
       
if headings:
           
if isinstance(headings, list):
               
self.headings = headings
           
else:
               
self.headings = [headings]

       
self.per_page = per_page
        current_page_number
= request.query.get('page', 1)
       
self.current_page_number = current_page_number if isinstance(current_page_number, int) \
           
else int(current_page_number)

       
self.edit_url = edit_url
       
self.delete_url = delete_url
       
self.create_url = create_url

        parms
= dict()
        sort_order
= request.query.get('sort', self.order_by)
       
if sort_order:
           
#  can be an int or a PyDAL field
           
try:
                index
= int(sort_order)
               
if request.query.get('sort_dir') and request.query.get('sort_dir') == 'desc':
                    parms
['orderby'] = ~self.fields[index]
               
else:
                    parms
['orderby'] = self.fields[index]
           
except:
               
#  if not an int, then assume PyDAL field
                parms
['orderby'] = sort_order
       
else:
           
for field in self.fields:
               
if field not in self.hidden_fields and (field.name != 'id' or field.name == 'id' and self.show_id):
                    parms
['orderby'] = field

       
if self.left:
            parms
['left'] = self.left

       
if self.fields:
           
self.total_rows = db(self.query).select(*fields, **parms)
       
else:
           
self.total_rows = db(self.query).select(**parms)

       
if len(self.total_rows) > self.per_page:
           
self.page_start = self.per_page * (self.current_page_number - 1)
           
self.page_end = self.page_start + self.per_page
            parms
['limitby'] = (self.page_start, self.page_end)
       
else:
           
self.page_start = 0
           
if len(self.total_rows) > 1:
               
self.page_start = 1
           
self.page_end = len(self.total_rows)

       
if self.fields:
           
self.rows = db(self.query).select(*fields, **parms)
       
else:
           
self.rows = db(self.query).select(**parms)

       
self.number_of_pages = len(self.total_rows) // self.per_page
       
if len(self.total_rows) % self.per_page > 0:
           
self.number_of_pages += 1

   
def iter_pages(self, left_edge=1, right_edge=1, left_current=1, right_current=2):
        current
= 1
        last_blank
= False
       
while current <= self.number_of_pages:
           
#  current page
           
if current == self.current_page_number:
                last_blank
= False
               
yield current

           
#  left edge
           
elif current <= left_edge:
                last_blank
= False
               
yield current

           
#  right edge
           
elif current >= self.number_of_pages - right_edge:
                last_blank
= False
               
yield current

           
#  left of current
           
elif self.current_page_number - left_current <= current < self.current_page_number:
                last_blank
= False
               
yield current

           
#  right of current
           
elif self.current_page_number < current <= self.current_page_number + right_current:
                last_blank
= False
               
yield current
           
else:
               
if not last_blank:
                   
yield None
                    last_blank
= True

            current
+= 1

   
def __repr__(self):
       
"""
        build the query table

        :return: html representation of the table
        """

        _html
= DIV(_class='field')
        _top_div
= DIV(_style='padding-bottom: 1rem;')
       
if self.create_url and self.create_url != '':
            _a
= A('', _href=self.create_url, _class='button')
            _span
= SPAN(_class='icon is-small')
            _span
.append(I(_class='fas fa-plus'))
            _a
.append(_span)
            _a
.append(SPAN('New'))
            _top_div
.append(_a)

       
#  build the search form if provided
       
if self.search_form:
            _sf
= DIV(_class='is-pulled-right')
            _sf
.append(self.search_form.custom['begin'])
            _tr
= TR()
           
for field in self.search_form.table:
                _fs
= SPAN(_style='padding-right: .5rem;')
                _td
= TD(_style='padding-right: .5rem;')
               
if field.type == 'boolean':
                    _fs
.append(self.search_form.custom['widgets'][field.name])
                    _fs
.append(field.label)
                    _td
.append(self.search_form.custom['widgets'][field.name])
                    _td
.append(field.label)
               
else:
                    _fs
.append(self.search_form.custom['widgets'][field.name])
                    _td
.append(self.search_form.custom['widgets'][field.name])
               
if field.name in self.search_form.custom['errors'] and self.search_form.custom['errors'][field.name]:
                    _fs
.append(SPAN(self.search_form.custom['errors'][field.name], _style="color:#ff0000"))
                    _td
.append(DIV(self.search_form.custom['errors'][field.name], _style="color:#ff0000"))
                _tr
.append(_td)
            _tr
.append(TD(self.search_form.custom['submit']))
            _sf
.append(TABLE(_tr))
           
for hidden_widget in self.search_form.custom['hidden_widgets'].keys():
                _sf
.append(self.search_form.custom['hidden_widgets'][hidden_widget])

            _sf
.append(self.search_form.custom['end'])
            _top_div
.append(_sf)

        _html
.append(_top_div)

        _table
= TABLE(_class='table is-bordered is-striped is-hoverable is-fullwidth')

       
# build the header
        _thead
= THEAD()
       
for index, field in enumerate(self.fields):
           
if field not in self.hidden_fields and (field.name != 'id' or field.name == 'id' and self.show_id):
               
try:
                    heading
= self.headings[index]
               
except:
                    heading
= field.label
               
#  add the sort order query parm
                sort_query_parms
= dict(self.query_parms)
                sort_query_parms
['sort'] = index
                current_sort_dir
= 'asc'

                _h
= A(heading.replace('_', ' ').upper(),
                       _href
=URL(self.endpoint, vars=sort_query_parms))
               
if 'sort_dir' in sort_query_parms:
                    current_sort_dir
= sort_query_parms['sort_dir']
                   
del sort_query_parms['sort_dir']
               
if index == int(request.query.get('sort', 0)) and current_sort_dir == 'asc':
                    sort_query_parms
['sort_dir'] = 'desc'

                _th
= TH()
                _th
.append(_h)

                _thead
.append(_th)

        _thead
.append(TH('ACTIONS'))

        _table
.append(_thead)

       
#  build the rows
       
for row in self.rows:
            _tr
= TR()
           
for field in self.fields:
               
if field not in self.hidden_fields and (field.name != 'id' or field.name == 'id' and self.show_id):
                    _tr
.append(TD(row[field.name] if row and field and field.name in row and row[field.name] else ''))

            _td
= None
           
if (self.edit_url and self.edit_url != '') or (self.delete_url and self.delete_url != ''):
                _td
= TD(_class='center', _style='text-align: center;')
               
if self.edit_url and self.edit_url != '':
                    _a
= A(I(_class='fas fa-edit'), _href=self.edit_url + '/%s' % row.id, _class='button is-small')
                   
# _span = SPAN(_class='icon is-small')
                   
# _span.append(I(_class='fas fa-edit'))
                   
# _a.append(_span)
                   
# _a.append(SPAN('Edit'))
                    _td
.append(_a)
               
if self.delete_url and self.delete_url != '':
                    _a
= A(I(_class='fas fa-trash'), _href=self.delete_url + '/%s' % row.id, _class='button is-small')
                   
# _span = SPAN(_class='icon is-small action-button-image')
                   
# _span.append(I(_class='fas fa-trash'))
                   
# _a.append(_span)
                   
# _a.append(SPAN('Delete'))
                    _td
.append(_a)
                _tr
.append(_td)
                _table
.append(_tr)

        _html
.append(_table)

        _row_count
= DIV(_class='is-pulled-left')
        _row_count
.append(
            P
('Displaying rows %s thru %s of %s' % (self.page_start + 1 if self.number_of_pages > 1 else 1,
                                                   
self.page_end if self.page_end < len(self.total_rows) else len(
                                                       
self.total_rows),
                                                    len
(self.total_rows))))
        _html
.append(_row_count)

       
#  build the pager
        _pager
= DIV(_class='is-pulled-right')
       
for page_number in self.iter_pages():
           
# self.rows.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2):
           
if page_number:
                pager_query_parms
= dict(self.query_parms)
                pager_query_parms
['page'] = page_number
               
if self.current_page_number == page_number:
                    _pager
.append(A(page_number, _class='button is-primary is-small',
                                    _href
=URL(self.endpoint, vars=pager_query_parms)))
               
else:
                    _pager
.append(A(page_number, _class='button is-small',
                                    _href
=URL(self.endpoint, vars=pager_query_parms)))
           
else:
                _pager
.append('...')

       
if self.number_of_pages > 1:
            _html
.append(_pager)

       
return str(_html)


my controller

from py4web import action, request, abort, redirect, URL, response, Field
from yatl.helpers import A
from .. common import db, session, T, cache, auth, logger, authenticated
from py4web.utils.form import Form, FormStyleBulma
from pydal.validators import IS_DATE, IS_IN_SET
from py4web.utils.publisher import Publisher, ALLOW_ALL_POLICY
from .. libs.html import QueryTable, ConnectMenu
import datetime

page
= dict(title='Administration', sub_title='')
publisher
= Publisher(db, policy=ALLOW_ALL_POLICY)


@action("administration/index", method="GET")
@action.uses(session, db, T, auth.user, "administration/index.html")
def index():
    page
['sub_title'] = A('MENU', _href=URL('index'))

    parent_link
= db(db.link.name == 'Administration').select().first()
    menu
= ConnectMenu(parent_link.id)

   
return dict(page=page, menu=menu)


@action('administration/links', method=['POST', 'GET'])
@action.uses(session, db, auth.user, 'grid.html')
def links():
    url_path
= 'administration/links'
    page
['sub_title'] = A('LINKS', _href=URL(url_path))

    fields
= [db.link.id,
              db
.link.name,
              db
.link.table_name,
              db
.link.permission_name,
              db
.link.method]

   
#  check session to see if we've saved a default value
    search_filter
= session.get('links_search', None)

   
#  build the search form
    form
= Form([Field('search', length=50, default=search_filter)],
                keep_values
=True, formstyle=FormStyleBulma)
   
if form.accepted:
        search_filter
= form.vars['search']

       
#  store in session
        session
['links_search'] = search_filter

    queries
= [(db.link.id > 0)]
   
if search_filter:
        queries
.append((db.link.name.contains(search_filter)) |
                       
(db.link.table_name.contains(search_filter)) |
                       
(db.link.permission_name.contains(search_filter)) |
                       
(db.link.method.contains(search_filter)) |
                       
(db.link.heading.contains(search_filter)))

    grid
= QueryTable(url_path, queries, fields=fields,
                      tablename
='link', search_form=form,
                      create_url
=URL('administration/link/0'),
                      edit_url
=URL('administration/link'),
                      delete_url
=URL('administration/link/delete'))

   
return dict(page=page, grid=grid, form=form)


@action('administration/link/<link_id>', methods=['GET', 'POST'])
@action.uses(session, db, auth.user, 'administration/link.html')
def link(link_id):
    page
['sub_title'] = A('LINKS', _href=URL('administration', 'links'))

    form
= Form(db.link, record=link_id, formstyle=FormStyleBulma)

   
return dict(page=page, form=form)

And my template

[[extend 'layout.html']]

[[=XML(grid)]]

I'm not sure I've given you all the relevant components, but I'll work on packaging it up as you did in a github repo and then share it.

I've attached a couple screenshots.

Let me know if anyone is interested in seeing more...

-Jim
Screenshot from 2020-07-05 18-23-55.png
Screenshot from 2020-07-05 18-24-49.png

Jim Steil

unread,
Jul 5, 2020, 7:36:37 PM7/5/20
to py4web
Well, google didn't get my entire post.  But, you can see in the screen shots where it is going. 

If people are interested I'll get a sample app built and share on github.

-Jim

               
if field not in self.hidden_fields and (<span style="color:

Jim Steil

unread,
Jul 5, 2020, 7:37:33 PM7/5/20
to py4web
Dang, now google IS displaying my entire original post.  Oh well...

Wanderson Santiago dos Reis

unread,
Jul 5, 2020, 7:49:47 PM7/5/20
to py4web
Great job! thanks for sharing and go ahead.

Tom Campbell

unread,
Jul 5, 2020, 7:53:24 PM7/5/20
to Jim Steil, py4web
Looking awesome, Jim! Thanks for the share. Definitely put it up on GitHub!

--
You received this message because you are subscribed to the Google Groups "py4web" group.
To unsubscribe from this group and stop receiving emails from it, send an email to py4web+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/py4web/fa3c39fa-9de5-4948-92eb-4eaa851bdee4o%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages