feat: add basic autocomplete support, for Person
URL endpoint only for now, form widget to come later
This commit is contained in:
		
							parent
							
								
									4bf2bb42fb
								
							
						
					
					
						commit
						9d261de45a
					
				
					 6 changed files with 391 additions and 203 deletions
				
			
		| 
						 | 
				
			
			@ -263,6 +263,12 @@ class MasterView(View):
 | 
			
		|||
 | 
			
		||||
       This is optional; see also :meth:`get_form_fields()`.
 | 
			
		||||
 | 
			
		||||
    .. attribute:: has_autocomplete
 | 
			
		||||
 | 
			
		||||
       Boolean indicating whether the view model supports
 | 
			
		||||
       "autocomplete" - i.e. it should have an :meth:`autocomplete()`
 | 
			
		||||
       view.  Default is ``False``.
 | 
			
		||||
 | 
			
		||||
    .. attribute:: configurable
 | 
			
		||||
 | 
			
		||||
       Boolean indicating whether the master view supports
 | 
			
		||||
| 
						 | 
				
			
			@ -286,6 +292,7 @@ class MasterView(View):
 | 
			
		|||
    viewable = True
 | 
			
		||||
    editable = True
 | 
			
		||||
    deletable = True
 | 
			
		||||
    has_autocomplete = False
 | 
			
		||||
    configurable = False
 | 
			
		||||
 | 
			
		||||
    # current action
 | 
			
		||||
| 
						 | 
				
			
			@ -573,6 +580,84 @@ class MasterView(View):
 | 
			
		|||
        session = self.app.get_session(obj)
 | 
			
		||||
        session.delete(obj)
 | 
			
		||||
 | 
			
		||||
    ##############################
 | 
			
		||||
    # autocomplete methods
 | 
			
		||||
    ##############################
 | 
			
		||||
 | 
			
		||||
    def autocomplete(self):
 | 
			
		||||
        """
 | 
			
		||||
        View which accepts a single ``term`` param, and returns a JSON
 | 
			
		||||
        list of autocomplete results to match.
 | 
			
		||||
 | 
			
		||||
        By default, this view is included only if
 | 
			
		||||
        :attr:`has_autocomplete` is true.  It usually maps to a URL
 | 
			
		||||
        like ``/widgets/autocomplete``.
 | 
			
		||||
 | 
			
		||||
        Subclass generally does not need to override this method, but
 | 
			
		||||
        rather should override the others which this calls:
 | 
			
		||||
 | 
			
		||||
        * :meth:`autocomplete_data()`
 | 
			
		||||
        * :meth:`autocomplete_normalize()`
 | 
			
		||||
        """
 | 
			
		||||
        term = self.request.GET.get('term', '')
 | 
			
		||||
        if not term:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        data = self.autocomplete_data(term)
 | 
			
		||||
        if not data:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        max_results = 100       # TODO
 | 
			
		||||
 | 
			
		||||
        results = []
 | 
			
		||||
        for obj in data[:max_results]:
 | 
			
		||||
            normal = self.autocomplete_normalize(obj)
 | 
			
		||||
            if normal:
 | 
			
		||||
                results.append(normal)
 | 
			
		||||
 | 
			
		||||
        return results
 | 
			
		||||
 | 
			
		||||
    def autocomplete_data(self, term):
 | 
			
		||||
        """
 | 
			
		||||
        Should return the data/query for the "matching" model records,
 | 
			
		||||
        based on autocomplete search term.  This is called by
 | 
			
		||||
        :meth:`autocomplete()`.
 | 
			
		||||
 | 
			
		||||
        Subclass must override this; default logic returns no data.
 | 
			
		||||
 | 
			
		||||
        :param term: String search term as-is from user, e.g. "foo bar".
 | 
			
		||||
 | 
			
		||||
        :returns: List of data records, or SQLAlchemy query.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    def autocomplete_normalize(self, obj):
 | 
			
		||||
        """
 | 
			
		||||
        Should return a "normalized" version of the given model
 | 
			
		||||
        record, suitable for autocomplete JSON results.  This is
 | 
			
		||||
        called by :meth:`autocomplete()`.
 | 
			
		||||
 | 
			
		||||
        Subclass may need to override this; default logic is
 | 
			
		||||
        simplistic but will work for basic models.  It returns the
 | 
			
		||||
        "autocomplete results" dict for the object::
 | 
			
		||||
 | 
			
		||||
           {
 | 
			
		||||
               'value': obj.uuid,
 | 
			
		||||
               'label': str(obj),
 | 
			
		||||
           }
 | 
			
		||||
 | 
			
		||||
        The 2 keys shown are required; any other keys will be ignored
 | 
			
		||||
        by the view logic but may be useful on the frontend widget.
 | 
			
		||||
 | 
			
		||||
        :param obj: Model record/instance.
 | 
			
		||||
 | 
			
		||||
        :returns: Dict of "autocomplete results" format, as shown
 | 
			
		||||
           above.
 | 
			
		||||
        """
 | 
			
		||||
        return {
 | 
			
		||||
            'value': obj.uuid,
 | 
			
		||||
            'label': str(obj),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    ##############################
 | 
			
		||||
    # configure methods
 | 
			
		||||
    ##############################
 | 
			
		||||
| 
						 | 
				
			
			@ -1888,6 +1973,15 @@ class MasterView(View):
 | 
			
		|||
                                        f'{permission_prefix}.delete',
 | 
			
		||||
                                        f"Delete {model_title}")
 | 
			
		||||
 | 
			
		||||
        # autocomplete
 | 
			
		||||
        if cls.has_autocomplete:
 | 
			
		||||
            config.add_route(f'{route_prefix}.autocomplete',
 | 
			
		||||
                             f'{url_prefix}/autocomplete')
 | 
			
		||||
            config.add_view(cls, attr='autocomplete',
 | 
			
		||||
                            route_name=f'{route_prefix}.autocomplete',
 | 
			
		||||
                            renderer='json',
 | 
			
		||||
                            permission=f'{route_prefix}.list')
 | 
			
		||||
 | 
			
		||||
        # configure
 | 
			
		||||
        if cls.configurable:
 | 
			
		||||
            config.add_route(f'{route_prefix}.configure',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,8 @@
 | 
			
		|||
Views for people
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.db.model import Person
 | 
			
		||||
from wuttaweb.views import MasterView
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +48,7 @@ class PersonView(MasterView):
 | 
			
		|||
    model_title_plural = "People"
 | 
			
		||||
    route_prefix = 'people'
 | 
			
		||||
    sort_defaults = 'full_name'
 | 
			
		||||
    has_autocomplete = True
 | 
			
		||||
 | 
			
		||||
    grid_columns = [
 | 
			
		||||
        'full_name',
 | 
			
		||||
| 
						 | 
				
			
			@ -85,6 +88,17 @@ class PersonView(MasterView):
 | 
			
		|||
        if 'users' in f:
 | 
			
		||||
            f.fields.remove('users')
 | 
			
		||||
 | 
			
		||||
    def autocomplete_query(self, term):
 | 
			
		||||
        """ """
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        session = self.Session()
 | 
			
		||||
        query = session.query(model.Person)
 | 
			
		||||
        criteria = [model.Person.full_name.ilike(f'%{word}%')
 | 
			
		||||
                    for word in term.split()]
 | 
			
		||||
        query = query.filter(sa.and_(*criteria))\
 | 
			
		||||
                     .order_by(model.Person.full_name)
 | 
			
		||||
        return query
 | 
			
		||||
 | 
			
		||||
    def view_profile(self, session=None):
 | 
			
		||||
        """ """
 | 
			
		||||
        person = self.get_instance(session=session)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -197,6 +197,14 @@ class SettingView(MasterView):
 | 
			
		|||
    model_title = "Raw Setting"
 | 
			
		||||
    sort_defaults = 'name'
 | 
			
		||||
 | 
			
		||||
    # TODO: master should handle this (per model key)
 | 
			
		||||
    def configure_grid(self, g):
 | 
			
		||||
        """ """
 | 
			
		||||
        super().configure_grid(g)
 | 
			
		||||
 | 
			
		||||
        # name
 | 
			
		||||
        g.set_link('name')
 | 
			
		||||
 | 
			
		||||
    def configure_form(self, f):
 | 
			
		||||
        """ """
 | 
			
		||||
        super().configure_form(f)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue