Add back-end support for multi-column grid sorting
or very nearly, anyway. front-end still just supports 1 column yet
This commit is contained in:
		
							parent
							
								
									4beca7af20
								
							
						
					
					
						commit
						6d7754cf2a
					
				
					 9 changed files with 222 additions and 202 deletions
				
			
		| 
						 | 
				
			
			@ -33,13 +33,7 @@ from cornice import resource, Service
 | 
			
		|||
 | 
			
		||||
from tailbone.api import APIView, api
 | 
			
		||||
from tailbone.db import Session
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SortColumn(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, field_name, model_name=None):
 | 
			
		||||
        self.field_name = field_name
 | 
			
		||||
        self.model_name = model_name
 | 
			
		||||
from tailbone.util import SortColumn
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIMasterView(APIView):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,12 +24,13 @@
 | 
			
		|||
Core Grid Classes
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from urllib.parse import urlencode
 | 
			
		||||
import warnings
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from six.moves import urllib
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from sqlalchemy import orm
 | 
			
		||||
from sa_filters import apply_sort
 | 
			
		||||
 | 
			
		||||
from rattail.db.types import GPCType
 | 
			
		||||
from rattail.util import prettify, pretty_boolean, pretty_quantity
 | 
			
		||||
| 
						 | 
				
			
			@ -552,48 +553,6 @@ class Grid(object):
 | 
			
		|||
            return self.url(obj)
 | 
			
		||||
        return self.url
 | 
			
		||||
 | 
			
		||||
    def make_webhelpers_grid(self):
 | 
			
		||||
        kwargs = dict(self._whgrid_kwargs)
 | 
			
		||||
        kwargs['request'] = self.request
 | 
			
		||||
        kwargs['url'] = self.make_url
 | 
			
		||||
 | 
			
		||||
        columns = list(self.columns)
 | 
			
		||||
        column_labels = kwargs.setdefault('column_labels', {})
 | 
			
		||||
        column_formats = kwargs.setdefault('column_formats', {})
 | 
			
		||||
 | 
			
		||||
        for key, value in self.labels.items():
 | 
			
		||||
            column_labels.setdefault(key, value)
 | 
			
		||||
 | 
			
		||||
        if self.checkboxes:
 | 
			
		||||
            columns.insert(0, 'checkbox')
 | 
			
		||||
            column_labels['checkbox'] = tags.checkbox('check-all')
 | 
			
		||||
            column_formats['checkbox'] = self.checkbox_column_format
 | 
			
		||||
 | 
			
		||||
        if self.renderers:
 | 
			
		||||
            kwargs['renderers'] = self.renderers
 | 
			
		||||
        if self.extra_row_class:
 | 
			
		||||
            kwargs['extra_record_class'] = self.extra_row_class
 | 
			
		||||
        if self.linked_columns:
 | 
			
		||||
            kwargs['linked_columns'] = list(self.linked_columns)
 | 
			
		||||
 | 
			
		||||
        if self.main_actions or self.more_actions:
 | 
			
		||||
            columns.append('actions')
 | 
			
		||||
            column_formats['actions'] = self.actions_column_format
 | 
			
		||||
 | 
			
		||||
        # TODO: pretty sure this factory doesn't serve all use cases yet?
 | 
			
		||||
        factory = CustomWebhelpersGrid
 | 
			
		||||
        # factory = webhelpers2_grid.Grid
 | 
			
		||||
        if self.sortable:
 | 
			
		||||
            # factory = CustomWebhelpersGrid
 | 
			
		||||
            kwargs['order_column'] = self.sortkey
 | 
			
		||||
            kwargs['order_direction'] = 'dsc' if self.sortdir == 'desc' else 'asc'
 | 
			
		||||
 | 
			
		||||
        grid = factory(self.make_visible_data(), columns, **kwargs)
 | 
			
		||||
        if self.sortable:
 | 
			
		||||
            grid.exclude_ordering = list([key for key in grid.exclude_ordering
 | 
			
		||||
                                          if key not in self.sorters])
 | 
			
		||||
        return grid
 | 
			
		||||
 | 
			
		||||
    def make_default_renderers(self, renderers):
 | 
			
		||||
        """
 | 
			
		||||
        Make the default set of column renderers for the grid.
 | 
			
		||||
| 
						 | 
				
			
			@ -638,19 +597,6 @@ class Grid(object):
 | 
			
		|||
    def actions_column_format(self, column_number, row_number, item):
 | 
			
		||||
        return HTML.td(self.render_actions(item, row_number), class_='actions')
 | 
			
		||||
 | 
			
		||||
    def render_grid(self, template='/grids/grid.mako', **kwargs):
 | 
			
		||||
        context = kwargs
 | 
			
		||||
        context['grid'] = self
 | 
			
		||||
        context['request'] = self.request
 | 
			
		||||
        grid_class = ''
 | 
			
		||||
        if self.width == 'full':
 | 
			
		||||
            grid_class = 'full'
 | 
			
		||||
        elif self.width == 'half':
 | 
			
		||||
            grid_class = 'half'
 | 
			
		||||
        context['grid_class'] = '{} {}'.format(grid_class, context.get('grid_class', ''))
 | 
			
		||||
        context.setdefault('grid_attrs', {})
 | 
			
		||||
        return render(template, context)
 | 
			
		||||
 | 
			
		||||
    def get_default_filters(self):
 | 
			
		||||
        """
 | 
			
		||||
        Returns the default set of filters provided by the grid.
 | 
			
		||||
| 
						 | 
				
			
			@ -761,6 +707,9 @@ class Grid(object):
 | 
			
		|||
                return query
 | 
			
		||||
            return query.order_by(getattr(column, direction)())
 | 
			
		||||
 | 
			
		||||
        sorter._class = class_
 | 
			
		||||
        sorter._column = column
 | 
			
		||||
 | 
			
		||||
        return sorter
 | 
			
		||||
 | 
			
		||||
    def make_simple_sorter(self, key, foldcase=False):
 | 
			
		||||
| 
						 | 
				
			
			@ -801,8 +750,12 @@ class Grid(object):
 | 
			
		|||
        # initial default settings
 | 
			
		||||
        settings = {}
 | 
			
		||||
        if self.sortable:
 | 
			
		||||
            settings['sortkey'] = self.default_sortkey
 | 
			
		||||
            settings['sortdir'] = self.default_sortdir
 | 
			
		||||
            if self.default_sortkey:
 | 
			
		||||
                settings['sorters.length'] = 1
 | 
			
		||||
                settings['sorters.1.key'] = self.default_sortkey
 | 
			
		||||
                settings['sorters.1.dir'] = self.default_sortdir
 | 
			
		||||
            else:
 | 
			
		||||
                settings['sorters.length'] = 0
 | 
			
		||||
        if self.pageable:
 | 
			
		||||
            settings['pagesize'] = self.get_default_pagesize()
 | 
			
		||||
            settings['page'] = self.default_page
 | 
			
		||||
| 
						 | 
				
			
			@ -875,8 +828,12 @@ class Grid(object):
 | 
			
		|||
                filtr.verb = settings['filter.{}.verb'.format(filtr.key)]
 | 
			
		||||
                filtr.value = settings['filter.{}.value'.format(filtr.key)]
 | 
			
		||||
        if self.sortable:
 | 
			
		||||
            self.sortkey = settings['sortkey']
 | 
			
		||||
            self.sortdir = settings['sortdir']
 | 
			
		||||
            self.active_sorters = []
 | 
			
		||||
            for i in range(1, settings['sorters.length'] + 1):
 | 
			
		||||
                self.active_sorters.append((
 | 
			
		||||
                    settings[f'sorters.{i}.key'],
 | 
			
		||||
                    settings[f'sorters.{i}.dir'],
 | 
			
		||||
                ))
 | 
			
		||||
        if self.pageable:
 | 
			
		||||
            self.pagesize = settings['pagesize']
 | 
			
		||||
            self.page = settings['page']
 | 
			
		||||
| 
						 | 
				
			
			@ -895,21 +852,36 @@ class Grid(object):
 | 
			
		|||
        # anything...
 | 
			
		||||
        session = Session()
 | 
			
		||||
        if user not in session:
 | 
			
		||||
            user = session.merge(user)
 | 
			
		||||
            # TODO: pretty sure there is no need to *merge* here..
 | 
			
		||||
            # but we shall see if any breakage happens maybe
 | 
			
		||||
            #user = session.merge(user)
 | 
			
		||||
            user = session.get(user.__class__, user.uuid)
 | 
			
		||||
 | 
			
		||||
        # User defaults should have all or nothing, so just check one key.
 | 
			
		||||
        key = 'tailbone.{}.grid.{}.sortkey'.format(user.uuid, self.key)
 | 
			
		||||
        app = self.request.rattail_config.get_app()
 | 
			
		||||
        return app.get_setting(Session(), key) is not None
 | 
			
		||||
 | 
			
		||||
        # user defaults should be all or nothing, so just check one key
 | 
			
		||||
        key = f'tailbone.{user.uuid}.grid.{self.key}.sorters.length'
 | 
			
		||||
        if app.get_setting(session, key) is not None:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        # TODO: this is deprecated but should work its way out of the
 | 
			
		||||
        # system in a little while (?)..then can remove this entirely
 | 
			
		||||
        key = f'tailbone.{user.uuid}.grid.{self.key}.sortkey'
 | 
			
		||||
        if app.get_setting(session, key) is not None:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def apply_user_defaults(self, settings):
 | 
			
		||||
        """
 | 
			
		||||
        Update the given settings dict with user defaults, if any exist.
 | 
			
		||||
        """
 | 
			
		||||
        app = self.request.rattail_config.get_app()
 | 
			
		||||
        session = Session()
 | 
			
		||||
        prefix = f'tailbone.{self.request.user.uuid}.grid.{self.key}'
 | 
			
		||||
 | 
			
		||||
        def merge(key, normalize=lambda v: v):
 | 
			
		||||
            skey = 'tailbone.{}.grid.{}.{}'.format(self.request.user.uuid, self.key, key)
 | 
			
		||||
            app = self.request.rattail_config.get_app()
 | 
			
		||||
            value = app.get_setting(Session(), skey)
 | 
			
		||||
            value = app.get_setting(session, f'{prefix}.{key}')
 | 
			
		||||
            settings[key] = normalize(value)
 | 
			
		||||
 | 
			
		||||
        if self.filterable:
 | 
			
		||||
| 
						 | 
				
			
			@ -919,8 +891,52 @@ class Grid(object):
 | 
			
		|||
                merge('filter.{}.value'.format(filtr.key))
 | 
			
		||||
 | 
			
		||||
        if self.sortable:
 | 
			
		||||
            merge('sortkey')
 | 
			
		||||
            merge('sortdir')
 | 
			
		||||
 | 
			
		||||
            # first clear existing settings for *sorting* only
 | 
			
		||||
            # nb. this is because number of sort settings will vary
 | 
			
		||||
            for key in list(settings):
 | 
			
		||||
                if key.startswith('sorters.'):
 | 
			
		||||
                    del settings[key]
 | 
			
		||||
 | 
			
		||||
            # check for *deprecated* settings, and use those if present
 | 
			
		||||
            # TODO: obviously should stop this, but must wait until
 | 
			
		||||
            # all old settings have been flushed out.  which in the
 | 
			
		||||
            # case of user-persisted settings, could be a while...
 | 
			
		||||
            sortkey = app.get_setting(session, f'{prefix}.sortkey')
 | 
			
		||||
            if sortkey:
 | 
			
		||||
                settings['sorters.length'] = 1
 | 
			
		||||
                settings['sorters.1.key'] = sortkey
 | 
			
		||||
                settings['sorters.1.dir'] = app.get_setting(session, f'{prefix}.sortdir')
 | 
			
		||||
 | 
			
		||||
                # nb. re-persist these user settings per new
 | 
			
		||||
                # convention, so deprecated settings go away and we
 | 
			
		||||
                # can remove this logic after a while..
 | 
			
		||||
                app = self.request.rattail_config.get_app()
 | 
			
		||||
                model = app.model
 | 
			
		||||
                prefix = f'tailbone.{self.request.user.uuid}.grid.{self.key}'
 | 
			
		||||
                query = Session.query(model.Setting)\
 | 
			
		||||
                               .filter(sa.or_(
 | 
			
		||||
                                   model.Setting.name.like(f'{prefix}.sorters.%'),
 | 
			
		||||
                                   model.Setting.name == f'{prefix}.sortkey',
 | 
			
		||||
                                   model.Setting.name == f'{prefix}.sortdir'))
 | 
			
		||||
                for setting in query.all():
 | 
			
		||||
                    Session.delete(setting)
 | 
			
		||||
                Session.flush()
 | 
			
		||||
 | 
			
		||||
                def persist(key):
 | 
			
		||||
                    app.save_setting(Session(),
 | 
			
		||||
                                     f'tailbone.{self.request.user.uuid}.grid.{self.key}.{key}',
 | 
			
		||||
                                     settings[key])
 | 
			
		||||
 | 
			
		||||
                persist('sorters.length')
 | 
			
		||||
                persist('sorters.1.key')
 | 
			
		||||
                persist('sorters.1.dir')
 | 
			
		||||
 | 
			
		||||
            else: # the future
 | 
			
		||||
                merge('sorters.length', int)
 | 
			
		||||
                for i in range(1, settings['sorters.length'] + 1):
 | 
			
		||||
                    merge(f'sorters.{i}.key')
 | 
			
		||||
                    merge(f'sorters.{i}.dir')
 | 
			
		||||
 | 
			
		||||
        if self.pageable:
 | 
			
		||||
            merge('pagesize', int)
 | 
			
		||||
| 
						 | 
				
			
			@ -939,10 +955,16 @@ class Grid(object):
 | 
			
		|||
                return True
 | 
			
		||||
 | 
			
		||||
        elif type_ == 'sort':
 | 
			
		||||
 | 
			
		||||
            # TODO: remove this eventually, but some links in the wild
 | 
			
		||||
            # may still include these params, so leave it for now
 | 
			
		||||
            for key in ['sortkey', 'sortdir']:
 | 
			
		||||
                if key in self.request.GET:
 | 
			
		||||
                    return True
 | 
			
		||||
 | 
			
		||||
            if 'sort1key' in self.request.GET:
 | 
			
		||||
                return True
 | 
			
		||||
 | 
			
		||||
        elif type_ == 'page':
 | 
			
		||||
            for key in ['pagesize', 'page']:
 | 
			
		||||
                if key in self.request.GET:
 | 
			
		||||
| 
						 | 
				
			
			@ -956,10 +978,12 @@ class Grid(object):
 | 
			
		|||
        """
 | 
			
		||||
        # session should have all or nothing, so just check a few keys which
 | 
			
		||||
        # should be guaranteed present if anything has been stashed
 | 
			
		||||
        for key in ['page', 'sortkey']:
 | 
			
		||||
            if 'grid.{}.{}'.format(self.key, key) in self.request.session:
 | 
			
		||||
        prefix = f'grid.{self.key}'
 | 
			
		||||
        for key in ['page', 'sorters.length']:
 | 
			
		||||
            if f'{prefix}.{key}' in self.request.session:
 | 
			
		||||
                return True
 | 
			
		||||
        return any([key.startswith('grid.{}.filter'.format(self.key)) for key in self.request.session])
 | 
			
		||||
        return any([key.startswith(f'{prefix}.filter')
 | 
			
		||||
                    for key in self.request.session])
 | 
			
		||||
 | 
			
		||||
    def get_setting(self, source, settings, key, normalize=lambda v: v, default=None):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -1044,8 +1068,46 @@ class Grid(object):
 | 
			
		|||
        """
 | 
			
		||||
        if not self.sortable:
 | 
			
		||||
            return
 | 
			
		||||
        settings['sortkey'] = self.get_setting(source, settings, 'sortkey')
 | 
			
		||||
        settings['sortdir'] = self.get_setting(source, settings, 'sortdir')
 | 
			
		||||
 | 
			
		||||
        if source == 'request':
 | 
			
		||||
 | 
			
		||||
            # TODO: remove this eventually, but some links in the wild
 | 
			
		||||
            # may still include these params, so leave it for now
 | 
			
		||||
            if 'sortkey' in self.request.GET:
 | 
			
		||||
                settings['sorters.length'] = 1
 | 
			
		||||
                settings['sorters.1.key'] = self.get_setting(source, settings, 'sortkey')
 | 
			
		||||
                settings['sorters.1.dir'] = self.get_setting(source, settings, 'sortdir')
 | 
			
		||||
 | 
			
		||||
            else: # the future
 | 
			
		||||
                i = 1
 | 
			
		||||
                while True:
 | 
			
		||||
                    skey = f'sort{i}key'
 | 
			
		||||
                    if skey in self.request.GET:
 | 
			
		||||
                        settings[f'sorters.{i}.key'] = self.get_setting(source, settings, skey)
 | 
			
		||||
                        settings[f'sorters.{i}.dir'] = self.get_setting(source, settings, f'sort{i}dir')
 | 
			
		||||
                    else:
 | 
			
		||||
                        break
 | 
			
		||||
                    i += 1
 | 
			
		||||
                settings['sorters.length'] = i - 1
 | 
			
		||||
 | 
			
		||||
        else: # session
 | 
			
		||||
 | 
			
		||||
            # TODO: definitely will remove this, but leave it for now
 | 
			
		||||
            # so it doesn't monkey with current user sessions when
 | 
			
		||||
            # next upgrade happens.  so, remove after all are upgraded
 | 
			
		||||
            sortkey = self.get_setting(source, settings, 'sortkey')
 | 
			
		||||
            if sortkey:
 | 
			
		||||
                settings['sorters.length'] = 1
 | 
			
		||||
                settings['sorters.1.key'] = sortkey
 | 
			
		||||
                settings['sorters.1.dir'] = self.get_setting(source, settings, 'sortdir')
 | 
			
		||||
 | 
			
		||||
            else: # the future
 | 
			
		||||
                settings['sorters.length'] = self.get_setting(source, settings,
 | 
			
		||||
                                                              'sorters.length', int)
 | 
			
		||||
                for i in range(1, settings['sorters.length'] + 1):
 | 
			
		||||
                    for key in ('key', 'dir'):
 | 
			
		||||
                        skey = f'sorters.{i}.{key}'
 | 
			
		||||
                        settings[skey] = self.get_setting(source, settings, skey)
 | 
			
		||||
 | 
			
		||||
    def update_page_settings(self, settings):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -1100,8 +1162,40 @@ class Grid(object):
 | 
			
		|||
                persist('filter.{}.value'.format(filtr.key))
 | 
			
		||||
 | 
			
		||||
        if self.sortable:
 | 
			
		||||
            persist('sortkey')
 | 
			
		||||
            persist('sortdir')
 | 
			
		||||
 | 
			
		||||
            # first clear existing settings for *sorting* only
 | 
			
		||||
            # nb. this is because number of sort settings will vary
 | 
			
		||||
            if to == 'defaults':
 | 
			
		||||
                model = self.request.rattail_config.get_model()
 | 
			
		||||
                prefix = f'tailbone.{self.request.user.uuid}.grid.{self.key}'
 | 
			
		||||
                query = Session.query(model.Setting)\
 | 
			
		||||
                               .filter(sa.or_(
 | 
			
		||||
                                   model.Setting.name.like(f'{prefix}.sorters.%'),
 | 
			
		||||
                                   # TODO: remove these eventually,
 | 
			
		||||
                                   # but probably should wait until
 | 
			
		||||
                                   # all nodes have been upgraded for
 | 
			
		||||
                                   # (quite) a while?
 | 
			
		||||
                                   model.Setting.name == f'{prefix}.sortkey',
 | 
			
		||||
                                   model.Setting.name == f'{prefix}.sortdir'))
 | 
			
		||||
                for setting in query.all():
 | 
			
		||||
                    Session.delete(setting)
 | 
			
		||||
                Session.flush()
 | 
			
		||||
            else: # session
 | 
			
		||||
                prefix = f'grid.{self.key}'
 | 
			
		||||
                for key in list(self.request.session):
 | 
			
		||||
                    if key.startswith(f'{prefix}.sorters.'):
 | 
			
		||||
                        del self.request.session[key]
 | 
			
		||||
                # TODO: definitely will remove these, but leave for
 | 
			
		||||
                # now so they don't monkey with current user sessions
 | 
			
		||||
                # when next upgrade happens.  so, remove after all are
 | 
			
		||||
                # upgraded
 | 
			
		||||
                self.request.session.pop(f'{prefix}.sortkey', None)
 | 
			
		||||
                self.request.session.pop(f'{prefix}.sortdir', None)
 | 
			
		||||
 | 
			
		||||
            persist('sorters.length')
 | 
			
		||||
            for i in range(1, settings['sorters.length'] + 1):
 | 
			
		||||
                persist(f'sorters.{i}.key')
 | 
			
		||||
                persist(f'sorters.{i}.dir')
 | 
			
		||||
 | 
			
		||||
        if self.pageable:
 | 
			
		||||
            persist('pagesize')
 | 
			
		||||
| 
						 | 
				
			
			@ -1131,21 +1225,32 @@ class Grid(object):
 | 
			
		|||
        """
 | 
			
		||||
        Sort the given query according to current settings, and return the result.
 | 
			
		||||
        """
 | 
			
		||||
        # Cannot sort unless we know which column to sort by.
 | 
			
		||||
        if not self.sortkey:
 | 
			
		||||
        # bail if no sort settings
 | 
			
		||||
        if not self.active_sorters:
 | 
			
		||||
            return data
 | 
			
		||||
 | 
			
		||||
        # Cannot sort unless we have a sort function.
 | 
			
		||||
        sortfunc = self.sorters.get(self.sortkey)
 | 
			
		||||
        if not sortfunc:
 | 
			
		||||
            return data
 | 
			
		||||
        # convert sort settings into a 'sortspec' for use with sa-filters
 | 
			
		||||
        full_spec = []
 | 
			
		||||
        for sortkey, sortdir in self.active_sorters:
 | 
			
		||||
            sortfunc = self.sorters.get(sortkey)
 | 
			
		||||
            if sortfunc:
 | 
			
		||||
                spec = {
 | 
			
		||||
                    'sortkey': sortkey,
 | 
			
		||||
                    'model': sortfunc._class.__name__,
 | 
			
		||||
                    'field': sortfunc._column.name,
 | 
			
		||||
                    'direction': sortdir or 'asc',
 | 
			
		||||
                }
 | 
			
		||||
                # spec.sortkey = sortkey
 | 
			
		||||
                full_spec.append(spec)
 | 
			
		||||
 | 
			
		||||
        # We can provide a default sort direction though.
 | 
			
		||||
        sortdir = getattr(self, 'sortdir', 'asc')
 | 
			
		||||
        if self.sortkey in self.joiners and self.sortkey not in self.joined:
 | 
			
		||||
            data = self.joiners[self.sortkey](data)
 | 
			
		||||
            self.joined.add(self.sortkey)
 | 
			
		||||
        return sortfunc(data, sortdir)
 | 
			
		||||
        # apply joins needed for this sort spec
 | 
			
		||||
        for spec in full_spec:
 | 
			
		||||
            sortkey = spec['sortkey']
 | 
			
		||||
            if sortkey in self.joiners and sortkey not in self.joined:
 | 
			
		||||
                data = self.joiners[sortkey](data)
 | 
			
		||||
                self.joined.add(sortkey)
 | 
			
		||||
 | 
			
		||||
        return apply_sort(data, full_spec)
 | 
			
		||||
 | 
			
		||||
    def paginate_data(self, data):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -1197,7 +1302,7 @@ class Grid(object):
 | 
			
		|||
            data = self.pager
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def render_complete(self, template='/grids/complete.mako', **kwargs):
 | 
			
		||||
    def render_complete(self, template='/grids/buefy.mako', **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Render the complete grid, including filters.
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -1717,5 +1822,5 @@ class URLMaker(object):
 | 
			
		|||
        params = self.request.GET.copy()
 | 
			
		||||
        params["page"] = page
 | 
			
		||||
        params["partial"] = "1"
 | 
			
		||||
        qs = urllib.parse.urlencode(params, True)
 | 
			
		||||
        qs = urlencode(params, True)
 | 
			
		||||
        return '{}?{}'.format(self.request.path, qs)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -202,7 +202,7 @@
 | 
			
		|||
       % endif
 | 
			
		||||
 | 
			
		||||
       % if grid.sortable:
 | 
			
		||||
       :default-sort="[sortField, sortOrder]"
 | 
			
		||||
       :default-sort="sortingPriority[0]"
 | 
			
		||||
       backend-sorting
 | 
			
		||||
       @sort="onSort"
 | 
			
		||||
       % endif
 | 
			
		||||
| 
						 | 
				
			
			@ -352,8 +352,9 @@
 | 
			
		|||
      firstItem: ${json.dumps(grid_data['first_item'] if grid.pageable else None)|n},
 | 
			
		||||
      lastItem: ${json.dumps(grid_data['last_item'] if grid.pageable else None)|n},
 | 
			
		||||
 | 
			
		||||
      sortField: ${json.dumps(grid.sortkey if grid.sortable else None)|n},
 | 
			
		||||
      sortOrder: ${json.dumps(grid.sortdir if grid.sortable else None)|n},
 | 
			
		||||
      % if grid.sortable:
 | 
			
		||||
          sortingPriority: ${json.dumps(grid.active_sorters)|n},
 | 
			
		||||
      % endif
 | 
			
		||||
 | 
			
		||||
      ## filterable: ${json.dumps(grid.filterable)|n},
 | 
			
		||||
      filters: ${json.dumps(filters_data if grid.filterable else None)|n},
 | 
			
		||||
| 
						 | 
				
			
			@ -454,8 +455,10 @@
 | 
			
		|||
          getBasicParams() {
 | 
			
		||||
              let params = {}
 | 
			
		||||
              % if grid.sortable:
 | 
			
		||||
                  params.sortkey = this.sortField
 | 
			
		||||
                  params.sortdir = this.sortOrder
 | 
			
		||||
                  for (let i = 1; i <= this.sortingPriority.length; i++) {
 | 
			
		||||
                      params['sort'+i+'key'] = this.sortingPriority[i-1][0]
 | 
			
		||||
                      params['sort'+i+'dir'] = this.sortingPriority[i-1][1]
 | 
			
		||||
                  }
 | 
			
		||||
              % endif
 | 
			
		||||
              % if grid.pageable:
 | 
			
		||||
                  params.pagesize = this.perPage
 | 
			
		||||
| 
						 | 
				
			
			@ -535,8 +538,7 @@
 | 
			
		|||
          },
 | 
			
		||||
 | 
			
		||||
          onSort(field, order) {
 | 
			
		||||
              this.sortField = field
 | 
			
		||||
              this.sortOrder = order
 | 
			
		||||
              this.sortingPriority = [[field, order]]
 | 
			
		||||
              // always reset to first page when changing sort options
 | 
			
		||||
              // TODO: i mean..right? would we ever not want that?
 | 
			
		||||
              this.currentPage = 1
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,38 +0,0 @@
 | 
			
		|||
## -*- coding: utf-8 -*-
 | 
			
		||||
<div class="grid-wrapper">
 | 
			
		||||
 | 
			
		||||
  <table class="grid-header">
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr>
 | 
			
		||||
 | 
			
		||||
        <td class="filters" rowspan="2">
 | 
			
		||||
          % if grid.filterable:
 | 
			
		||||
              ${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
 | 
			
		||||
          % endif
 | 
			
		||||
        </td>
 | 
			
		||||
 | 
			
		||||
        <td class="menu">
 | 
			
		||||
          % if context_menu:
 | 
			
		||||
              <ul id="context-menu">
 | 
			
		||||
                ${context_menu|n}
 | 
			
		||||
              </ul>
 | 
			
		||||
          % endif
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td class="tools">
 | 
			
		||||
          % if tools:
 | 
			
		||||
              <div class="grid-tools">
 | 
			
		||||
                ${tools|n}
 | 
			
		||||
              </div><!-- grid-tools -->
 | 
			
		||||
          % endif
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table><!-- grid-header -->
 | 
			
		||||
 | 
			
		||||
  ${grid.render_grid()|n}
 | 
			
		||||
 | 
			
		||||
</div><!-- grid-wrapper -->
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
## -*- coding: utf-8; -*-
 | 
			
		||||
<div class="grid ${grid_class}" data-delete-speedbump="${'true' if grid.delete_speedbump else 'false'}" ${h.HTML.render_attrs(grid_attrs)}>
 | 
			
		||||
  <table>
 | 
			
		||||
    ${grid.make_webhelpers_grid()}
 | 
			
		||||
  </table>
 | 
			
		||||
  % if grid.pageable and grid.pager:
 | 
			
		||||
      <div class="pager">
 | 
			
		||||
        <p class="showing">
 | 
			
		||||
          ${"showing {} thru {} of {:,d}".format(grid.pager.first_item, grid.pager.last_item, grid.pager.item_count)}
 | 
			
		||||
          % if grid.pager.page_count > 1:
 | 
			
		||||
              ${"(page {} of {:,d})".format(grid.pager.page, grid.pager.page_count)}
 | 
			
		||||
          % endif
 | 
			
		||||
        </p>
 | 
			
		||||
        <p class="page-links">
 | 
			
		||||
          ${h.select('pagesize', grid.pager.items_per_page, grid.get_pagesize_options())}
 | 
			
		||||
          per page 
 | 
			
		||||
          ${grid.pager.pager('$link_first $link_previous ~1~ $link_next $link_last', symbol_next='next', symbol_previous='prev')|n}
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
  % endif
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +44,17 @@ from webhelpers2.html import HTML, tags
 | 
			
		|||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SortColumn(object):
 | 
			
		||||
    """
 | 
			
		||||
    Generic representation of a sort column, for use with sorting grid
 | 
			
		||||
    data as well as with API.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, field_name, model_name=None):
 | 
			
		||||
        self.field_name = field_name
 | 
			
		||||
        self.model_name = model_name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_csrf_token(request):
 | 
			
		||||
    """
 | 
			
		||||
    Convenience function to retrieve the effective CSRF token for the given
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -476,36 +476,6 @@ class CustomerView(MasterView):
 | 
			
		|||
            items.append(HTML.tag('li', c=[link]))
 | 
			
		||||
        return HTML.tag('ul', c=items)
 | 
			
		||||
 | 
			
		||||
    # TODO: remove if no longer used
 | 
			
		||||
    def render_people_removable(self, customer, field):
 | 
			
		||||
        people = customer.people
 | 
			
		||||
        if not people:
 | 
			
		||||
            return ""
 | 
			
		||||
 | 
			
		||||
        route_prefix = self.get_route_prefix()
 | 
			
		||||
        permission_prefix = self.get_permission_prefix()
 | 
			
		||||
 | 
			
		||||
        view_url = lambda p, i: self.request.route_url('people.view', uuid=p.uuid)
 | 
			
		||||
        actions = [
 | 
			
		||||
            grids.GridAction('view', icon='zoomin', url=view_url),
 | 
			
		||||
        ]
 | 
			
		||||
        if self.people_detachable and self.request.has_perm('{}.detach_person'.format(permission_prefix)):
 | 
			
		||||
            url = lambda p, i: self.request.route_url('{}.detach_person'.format(route_prefix),
 | 
			
		||||
                                                      uuid=customer.uuid, person_uuid=p.uuid)
 | 
			
		||||
            actions.append(
 | 
			
		||||
                grids.GridAction('detach', icon='trash', url=url))
 | 
			
		||||
 | 
			
		||||
        columns = ['first_name', 'last_name', 'display_name']
 | 
			
		||||
        g = grids.Grid(
 | 
			
		||||
            key='{}.people'.format(route_prefix),
 | 
			
		||||
            data=customer.people,
 | 
			
		||||
            columns=columns,
 | 
			
		||||
            labels={'display_name': "Full Name"},
 | 
			
		||||
            url=lambda p: self.request.route_url('people.view', uuid=p.uuid),
 | 
			
		||||
            linked_columns=columns,
 | 
			
		||||
            main_actions=actions)
 | 
			
		||||
        return HTML.literal(g.render_grid())
 | 
			
		||||
 | 
			
		||||
    def render_shoppers(self, customer, field):
 | 
			
		||||
        route_prefix = self.get_route_prefix()
 | 
			
		||||
        permission_prefix = self.get_permission_prefix()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -340,11 +340,9 @@ class MasterView(View):
 | 
			
		|||
        if grid.pageable and hasattr(grid, 'pager'):
 | 
			
		||||
            self.first_visible_grid_index = grid.pager.first_item
 | 
			
		||||
 | 
			
		||||
        # return grid only, if partial page was requested
 | 
			
		||||
        # return grid data only, if partial page was requested
 | 
			
		||||
        if self.request.params.get('partial'):
 | 
			
		||||
            # render grid data only, as JSON
 | 
			
		||||
            return render_to_response('json', grid.get_buefy_data(),
 | 
			
		||||
                                      request=self.request)
 | 
			
		||||
            return self.json_response(grid.get_buefy_data())
 | 
			
		||||
 | 
			
		||||
        context = {
 | 
			
		||||
            'grid': grid,
 | 
			
		||||
| 
						 | 
				
			
			@ -1156,8 +1154,7 @@ class MasterView(View):
 | 
			
		|||
            # return grid only, if partial page was requested
 | 
			
		||||
            if self.request.params.get('partial'):
 | 
			
		||||
                # render grid data only, as JSON
 | 
			
		||||
                return render_to_response('json', grid.get_buefy_data(),
 | 
			
		||||
                                          request=self.request)
 | 
			
		||||
                return self.json_response(grid.get_buefy_data())
 | 
			
		||||
 | 
			
		||||
        context = {
 | 
			
		||||
            'instance': instance,
 | 
			
		||||
| 
						 | 
				
			
			@ -1284,8 +1281,7 @@ class MasterView(View):
 | 
			
		|||
        # return grid only, if partial page was requested
 | 
			
		||||
        if self.request.params.get('partial'):
 | 
			
		||||
            # render grid data only, as JSON
 | 
			
		||||
            return render_to_response('json', grid.get_buefy_data(),
 | 
			
		||||
                                      request=self.request)
 | 
			
		||||
            return self.json_response(grid.get_buefy_data())
 | 
			
		||||
 | 
			
		||||
        return self.render_to_response('versions', {
 | 
			
		||||
            'instance': instance,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -461,7 +461,8 @@ class MemberEquityPaymentView(MasterView):
 | 
			
		|||
        g.set_renderer(field, self.render_member_key)
 | 
			
		||||
        g.set_filter(field, attr,
 | 
			
		||||
                     label=self.get_member_key_label(),
 | 
			
		||||
                     default_active=True)
 | 
			
		||||
                     default_active=True,
 | 
			
		||||
                     default_verb='equal')
 | 
			
		||||
        g.set_sorter(field, attr)
 | 
			
		||||
 | 
			
		||||
        # member (name)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue