From d2b065a8fc5b662577e1abb85c7a167ec7697db5 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 14 Aug 2015 15:30:38 -0500 Subject: [PATCH] Add basic checkbox support to new grids. Also: * Add 'creatable', 'editable' etc. to master view class. * Add styles for warning/notice grid rows. * Misc. other tweaks. --- tailbone/newgrids/alchemy.py | 5 + tailbone/newgrids/core.py | 51 +++++--- tailbone/static/css/base.css | 1 - tailbone/static/css/newgrids.css | 34 +++++- tailbone/static/js/jquery.ui.tailbone.js | 23 ++++ tailbone/static/js/tailbone.js | 5 - tailbone/templates/base.mako | 1 - tailbone/templates/master/edit.mako | 4 +- tailbone/templates/master/index.mako | 12 +- tailbone/templates/master/view.mako | 4 +- tailbone/templates/newgrids/grid.mako | 14 +-- tailbone/templates/progress.mako | 1 - tailbone/views/master.py | 145 ++++++++++++++++++----- 13 files changed, 229 insertions(+), 71 deletions(-) diff --git a/tailbone/newgrids/alchemy.py b/tailbone/newgrids/alchemy.py index 8719359f..40fc3ba5 100644 --- a/tailbone/newgrids/alchemy.py +++ b/tailbone/newgrids/alchemy.py @@ -159,5 +159,10 @@ class AlchemyGrid(Grid): self._fa_grid._set_active(row, orm.object_session(row)) yield row + def get_row_key(self, row): + mapper = orm.object_mapper(row) + assert len(mapper.primary_key) == 1 + return getattr(row, mapper.primary_key[0].key) + def render_cell(self, row, column): return column.field.render_readonly() diff --git a/tailbone/newgrids/core.py b/tailbone/newgrids/core.py index 0b1397c3..d4b2658c 100644 --- a/tailbone/newgrids/core.py +++ b/tailbone/newgrids/core.py @@ -42,7 +42,8 @@ class Grid(object): joiners={}, filterable=False, filters={}, sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', pageable=False, default_pagesize=20, default_page=1, - width='auto', checkboxes=False, **kwargs): + width='auto', checkboxes=False, row_attrs={}, cell_attrs={}, + **kwargs): self.key = key self.request = request self.columns = columns @@ -73,6 +74,8 @@ class Grid(object): self.width = width self.checkboxes = checkboxes + self.row_attrs = row_attrs + self.cell_attrs = cell_attrs def get_default_filters(self): """ @@ -473,6 +476,8 @@ class Grid(object): classes = ['newgrid'] if self.width == 'full': classes.append('full') + if self.checkboxes: + classes.append('selectable') return {'class_': ' '.join(classes), 'data-url': self.request.current_route_url(_query=None), 'data-permalink': self.request.current_route_url()} @@ -533,16 +538,14 @@ class Grid(object): def get_row_attrs(self, row, i): """ - Returns a properly-formatted set of attributes which will be applied to - the ```` element for the given row. Note that ``i`` will be a - 1-based index value for the row within its table. The meaning of - ``row`` is basically not defined; it depends on the type of data the - grid deals with. + Returns a dict of HTML attributes which is to be applied to the row's + ```` element. Note that ``i`` will be a 1-based index value for + the row within its table. The meaning of ``row`` is basically not + defined; it depends on the type of data the grid deals with. """ - # attrs = {'class_': self.get_row_class(row, i)} - # attrs = {} - # return format_attrs(**attrs) - return {} + if callable(self.row_attrs): + return self.row_attrs(row, i) + return self.row_attrs # def get_row_class(self, row, i): # class_ = self.default_row_class(row, i) @@ -552,18 +555,34 @@ class Grid(object): # class_ = '{0} {1}'.format(class_, extra) # return class_ - # def checkbox(self, key): - # """ - # Render a checkbox using the given key. - # """ - # return tags.checkbox('checkbox-{0}-{1}'.format(self.key, key)) + def get_row_key(self, row): + raise NotImplementedError + + def checkbox(self, row): + return True + + def checked(self, row): + return False + + def render_checkbox(self, row): + """ + Returns a boolean indicating whether ot not a checkbox should be + rendererd for the given row. Default implementation returns ``True`` + in all cases. + """ + if not self.checkbox(row): + return '' + return tags.checkbox('checkbox-{0}-{1}'.format(self.key, self.get_row_key(row)), + checked=self.checked(row)) def get_cell_attrs(self, row, column): """ Returns a dictionary of HTML attributes which should be applied to the ```` element in which the given row and column "intersect". """ - return {} + if callable(self.cell_attrs): + return self.cell_attrs(row, column) + return self.cell_attrs def render_cell(self, row, column): return '' diff --git a/tailbone/static/css/base.css b/tailbone/static/css/base.css index b596d96b..f939d915 100644 --- a/tailbone/static/css/base.css +++ b/tailbone/static/css/base.css @@ -44,7 +44,6 @@ p { } .right { - float: right; text-align: right; } diff --git a/tailbone/static/css/newgrids.css b/tailbone/static/css/newgrids.css index ae49fe5f..057d5918 100644 --- a/tailbone/static/css/newgrids.css +++ b/tailbone/static/css/newgrids.css @@ -133,18 +133,46 @@ * tbody ******************************/ -.newgrid table tbody td { +.newgrid tbody td { padding: 5px 6px; } -.newgrid table tbody tr:nth-child(odd) { +.newgrid.selectable tbody td { + cursor: default; +} + +.newgrid tbody tr:nth-child(odd) { background-color: #e0e0e0; } -.newgrid table tbody tr.hovering { +.newgrid tbody tr.hovering { background-color: #bbbbbb; } +.newgrid tbody tr.notice { + background-color: #fd6; +} + +.newgrid tbody tr.notice:nth-child(odd) { + background-color: #fe8; +} + +.newgrid tbody tr.notice.hovering { + background-color: #ec7; +} + +.newgrid tbody tr.warning { + background-color: #fcc; +} + +.newgrid tbody tr.warning:nth-child(odd) { + background-color: #ebb; +} + +.newgrid tbody tr.warning.hovering { + background-color: #daa; +} + /****************************** * main actions diff --git a/tailbone/static/js/jquery.ui.tailbone.js b/tailbone/static/js/jquery.ui.tailbone.js index f0f810a6..c23618f6 100644 --- a/tailbone/static/js/jquery.ui.tailbone.js +++ b/tailbone/static/js/jquery.ui.tailbone.js @@ -115,6 +115,29 @@ $(this).removeClass('hovering'); }); + // Do some extra stuff for grids with checkboxes. + if (this.grid.hasClass('selectable')) { + + // (Un-)Check all rows when clicking check-all box in header. + this.element.on('click', 'thead th.checkbox input', function() { + var checked = $(this).prop('checked'); + that.grid.find('tbody td.checkbox input').prop('checked', checked); + }); + + // Select current row when clicked, unless clicking checkbox + // (since that already does select the row) or a link (since + // that does something completely different). + this.element.on('click', 'tbody td.checkbox input', function(event) { + event.stopPropagation(); + }); + this.element.on('click', 'tbody a', function(event) { + event.stopPropagation(); + }); + this.element.on('click', 'tbody tr', function() { + $(this).find('td.checkbox input').click(); + }); + } + // Show 'more' actions when user hovers over 'more' link. this.element.on('mouseenter', '.actions a.more', function() { that.grid.find('.actions div.more').hide(); diff --git a/tailbone/static/js/tailbone.js b/tailbone/static/js/tailbone.js index eacfacc3..fb84dd4b 100644 --- a/tailbone/static/js/tailbone.js +++ b/tailbone/static/js/tailbone.js @@ -112,11 +112,6 @@ $(function() { $('input[type=submit]').button(); $('input[type=reset]').button(); - /* - * Enhance new-style grids. - */ - $('.newgrid-wrapper').gridwrapper(); - /* * When filter labels are clicked, (un)check the associated checkbox. */ diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index 83ad9c3e..8ef4abcb 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -131,7 +131,6 @@ ${h.javascript_link('https://code.jquery.com/ui/1.11.4/jquery-ui.min.js')} ${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.ui.menubar.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.loadmask.min.js'))} - ${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.js'))} diff --git a/tailbone/templates/master/edit.mako b/tailbone/templates/master/edit.mako index df7f379c..a1c9dd40 100644 --- a/tailbone/templates/master/edit.mako +++ b/tailbone/templates/master/edit.mako @@ -5,10 +5,10 @@ <%def name="context_menu_items()">
  • ${h.link_to("Back to {0}".format(model_title_plural), url(route_prefix))}
  • - % if request.has_perm('{0}.view'.format(permission_prefix)): + % if master.viewable and request.has_perm('{0}.view'.format(permission_prefix)):
  • ${h.link_to("View this {0}".format(model_title), action_url('view', instance))}
  • % endif - % if request.has_perm('{0}.delete'.format(permission_prefix)): + % if master.deletable and request.has_perm('{0}.delete'.format(permission_prefix)):
  • ${h.link_to("Delete this {0}".format(model_title), action_url('delete', instance))}
  • % endif diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index 6a6bfe0c..eccb7bac 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -9,8 +9,18 @@ <%def name="title()">${grid.model_title_plural} +<%def name="head_tags()"> + ${parent.head_tags()} + ${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))} + + + <%def name="context_menu_items()"> - % if request.has_perm('{0}.create'.format(grid.permission_prefix)): + % if master.creatable and request.has_perm('{0}.create'.format(grid.permission_prefix)):
  • ${h.link_to("Create a new {0}".format(grid.model_title), url('{0}.create'.format(grid.route_prefix)))}
  • % endif diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako index 2579d4d7..d419d9a8 100644 --- a/tailbone/templates/master/view.mako +++ b/tailbone/templates/master/view.mako @@ -5,10 +5,10 @@ <%def name="context_menu_items()">
  • ${h.link_to("Back to {0}".format(model_title_plural), url(route_prefix))}
  • - % if request.has_perm('{0}.edit'.format(permission_prefix)): + % if master.editable and request.has_perm('{0}.edit'.format(permission_prefix)):
  • ${h.link_to("Edit this {0}".format(model_title), action_url('edit', instance))}
  • % endif - % if request.has_perm('{0}.delete'.format(permission_prefix)): + % if master.deletable and master.deletable_instance(instance) and request.has_perm('{0}.delete'.format(permission_prefix)):
  • ${h.link_to("Delete this {0}".format(model_title), action_url('delete', instance))}
  • % endif diff --git a/tailbone/templates/newgrids/grid.mako b/tailbone/templates/newgrids/grid.mako index 4f3f1fe2..9cb73c31 100644 --- a/tailbone/templates/newgrids/grid.mako +++ b/tailbone/templates/newgrids/grid.mako @@ -3,9 +3,9 @@ -## % if grid.checkboxes: -## -## % endif + % if grid.checkboxes: + + % endif % for column in grid.iter_visible_columns(): ${grid.column_header(column)} % endfor @@ -17,9 +17,9 @@ % for i, row in enumerate(grid.iter_rows(), 1): -## % if grid.checkboxes: -## -## % endif + % if grid.checkboxes: + + % endif % for column in grid.iter_visible_columns(): % endfor @@ -32,7 +32,7 @@ % endfor
    ${h.checkbox('check-all')}${h.checkbox('check-all')}
    ${grid.checkbox(row)}${grid.render_checkbox(row)}${grid.render_cell(row, column)}
    - % if grid.pageable: + % if grid.pageable and grid.pager:

    showing ${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count} diff --git a/tailbone/templates/progress.mako b/tailbone/templates/progress.mako index 01b337d9..2d33e8bc 100644 --- a/tailbone/templates/progress.mako +++ b/tailbone/templates/progress.mako @@ -7,7 +7,6 @@ ${h.javascript_link('https://code.jquery.com/jquery-1.11.3.min.js')} ${h.javascript_link('https://code.jquery.com/ui/1.11.4/jquery-ui.min.js')} ${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.ui.menubar.js'))} - ${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.js'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/base.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/layout.css'))} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index ae4a031a..c4fcf918 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -45,11 +45,19 @@ class MasterView(View): """ Base "master" view class. All model master views should derive from this. """ + creatable = True + viewable = True + editable = True + deletable = True + creating = False viewing = False editing = False deleting = False + row_attrs = {} + cell_attrs = {} + ############################## # Available Views ############################## @@ -123,13 +131,10 @@ class MasterView(View): if result is not None: return result - # Flush immediately to force any pending integrity errors etc.; that - # way we don't set flash message until we know we have success. - Session.delete(instance) - Session.flush() + self.delete_instance(instance) self.request.session.flash("{0} {1} has been deleted.".format( self.get_model_title(), instance)) - return HTTPFound(location=self.get_index_url()) + return self.redirect(self.get_after_delete_url(instance)) ############################## @@ -237,6 +242,7 @@ class MasterView(View): the template prefix. """ data.update({ + 'master': self, 'model_title': self.get_model_title(), 'model_title_plural': self.get_model_title_plural(), 'route_prefix': self.get_route_prefix(), @@ -300,28 +306,55 @@ class MasterView(View): 'pageable': True, 'main_actions': self.get_main_actions(), 'more_actions': self.get_more_actions(), + 'checkbox': self.checkbox, + 'checked': self.checked, + 'row_attrs': self.get_row_attrs, + 'cell_attrs': self.get_cell_attrs, 'model_title': self.get_model_title(), 'model_title_plural': self.get_model_title_plural(), 'permission_prefix': self.get_permission_prefix(), 'route_prefix': self.get_route_prefix(), } + def get_row_attrs(self, row, i): + """ + Returns a dict of HTML attributes which is to be applied to the row's + ```` element. Note that ``i`` will be a 1-based index value for + the row within its table. The meaning of ``row`` is basically not + defined; it depends on the type of data the grid deals with. + """ + if callable(self.row_attrs): + return self.row_attrs(row, i) + return self.row_attrs + + def get_cell_attrs(self, row, column): + """ + Returns a dictionary of HTML attributes which should be applied to the + ```` element in which the given row and column "intersect". + """ + if callable(self.cell_attrs): + return self.cell_attrs(row, column) + return self.cell_attrs + def get_main_actions(self): """ Return a list of 'main' actions for the grid. """ - return [ - self.make_action('view', icon='zoomin'), - ] + actions = [] + if self.viewable: + actions.append(self.make_action('view', icon='zoomin')) + return actions def get_more_actions(self): """ Return a list of 'more' actions for the grid. """ - return [ - self.make_action('edit', icon='pencil'), - self.make_action('delete', icon='trash'), - ] + actions = [] + if self.editable: + actions.append(self.make_action('edit', icon='pencil')) + if self.deletable: + actions.append(self.make_action('delete', icon='trash')) + return actions def make_action(self, key, **kwargs): """ @@ -350,6 +383,7 @@ class MasterView(View): data = self.make_query() kwargs = self.make_grid_kwargs() grid = factory(key, self.request, data=data, model_class=self.model_class, **kwargs) + grid._fa_grid.prettify = prettify self.configure_grid(grid) grid.load_settings() return grid @@ -387,6 +421,21 @@ class MasterView(View): """ return session.query(self.model_class) + def checkbox(self, instance): + """ + Returns a boolean indicating whether ot not a checkbox should be + rendererd for the given row. Default implementation returns ``True`` + in all cases. + """ + return True + + def checked(self, instance): + """ + Returns a boolean indicating whether ot not a checkbox should be + checked by default, for the given row. Default implementation returns + ``False`` in all cases. + """ + return False ############################## # CRUD Stuff @@ -445,11 +494,39 @@ class MasterView(View): Event hook, called just after an existing instance is saved. """ + def deletable_instance(self, instance): + """ + Returns boolean indicating whether or not the given instance can be + considered "deletable". Returns ``True`` by default; override as + necessary. + """ + return True + def before_delete(self, instance): """ Event hook, called just before deletion is attempted. """ + def delete_instance(self, instance): + """ + Delete the instance, or mark it as deleted, or whatever you need to do. + """ + # Flush immediately to force any pending integrity errors etc.; that + # way we don't set flash message until we know we have success. + Session.delete(instance) + Session.flush() + + def get_after_delete_url(self, instance): + """ + Returns the URL to which the user should be redirected after + successfully "deleting" the given instance. + """ + if hasattr(self, 'after_delete_url'): + if callable(self.after_delete_url): + return self.after_delete_url(instance) + return self.after_delete_url + return self.get_index_url() + ############################## # Config Stuff ############################## @@ -476,29 +553,33 @@ class MasterView(View): "List/Search {0}".format(model_title_plural)) # create - config.add_route('{0}.create'.format(route_prefix), '{0}/new'.format(url_prefix)) - config.add_view(cls, attr='create', route_name='{0}.create'.format(route_prefix), - permission='{0}.create'.format(permission_prefix)) - config.add_tailbone_permission(permission_prefix, '{0}.create'.format(permission_prefix), - "Create new {0}".format(model_title_plural)) + if cls.creatable: + config.add_route('{0}.create'.format(route_prefix), '{0}/new'.format(url_prefix)) + config.add_view(cls, attr='create', route_name='{0}.create'.format(route_prefix), + permission='{0}.create'.format(permission_prefix)) + config.add_tailbone_permission(permission_prefix, '{0}.create'.format(permission_prefix), + "Create new {0}".format(model_title)) # view - config.add_route('{0}.view'.format(route_prefix), '{0}/{{{1}}}'.format(url_prefix, model_key)) - config.add_view(cls, attr='view', route_name='{0}.view'.format(route_prefix), - permission='{0}.view'.format(permission_prefix)) - config.add_tailbone_permission(permission_prefix, '{0}.view'.format(permission_prefix), - "View {0} Details".format(model_title)) + if cls.viewable: + config.add_route('{0}.view'.format(route_prefix), '{0}/{{{1}}}'.format(url_prefix, model_key)) + config.add_view(cls, attr='view', route_name='{0}.view'.format(route_prefix), + permission='{0}.view'.format(permission_prefix)) + config.add_tailbone_permission(permission_prefix, '{0}.view'.format(permission_prefix), + "View {0} Details".format(model_title)) # edit - config.add_route('{0}.edit'.format(route_prefix), '{0}/{{{1}}}/edit'.format(url_prefix, model_key)) - config.add_view(cls, attr='edit', route_name='{0}.edit'.format(route_prefix), - permission='{0}.edit'.format(permission_prefix)) - config.add_tailbone_permission(permission_prefix, '{0}.edit'.format(permission_prefix), - "Edit {0}".format(model_title_plural)) + if cls.editable: + config.add_route('{0}.edit'.format(route_prefix), '{0}/{{{1}}}/edit'.format(url_prefix, model_key)) + config.add_view(cls, attr='edit', route_name='{0}.edit'.format(route_prefix), + permission='{0}.edit'.format(permission_prefix)) + config.add_tailbone_permission(permission_prefix, '{0}.edit'.format(permission_prefix), + "Edit {0}".format(model_title)) # delete - config.add_route('{0}.delete'.format(route_prefix), '{0}/{{{1}}}/delete'.format(url_prefix, model_key)) - config.add_view(cls, attr='delete', route_name='{0}.delete'.format(route_prefix), - permission='{0}.delete'.format(permission_prefix)) - config.add_tailbone_permission(permission_prefix, '{0}.delete'.format(permission_prefix), - "Delete {0}".format(model_title_plural)) + if cls.deletable: + config.add_route('{0}.delete'.format(route_prefix), '{0}/{{{1}}}/delete'.format(url_prefix, model_key)) + config.add_view(cls, attr='delete', route_name='{0}.delete'.format(route_prefix), + permission='{0}.delete'.format(permission_prefix)) + config.add_tailbone_permission(permission_prefix, '{0}.delete'.format(permission_prefix), + "Delete {0}".format(model_title))