Compare commits
	
		
			2 commits
		
	
	
		
			2ad1ae9c49
			...
			4c467f5267
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
							 | 
						4c467f5267 | ||
| 
							 | 
						754e0989e4 | 
					 22 changed files with 1372 additions and 81 deletions
				
			
		
							
								
								
									
										6
									
								
								docs/api/wuttaweb/grids.base.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wuttaweb/grids.base.rst
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``wuttaweb.grids.base``
 | 
				
			||||||
 | 
					=======================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: wuttaweb.grids.base
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
							
								
								
									
										6
									
								
								docs/api/wuttaweb/grids.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wuttaweb/grids.rst
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``wuttaweb.grids``
 | 
				
			||||||
 | 
					==================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: wuttaweb.grids
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,8 @@
 | 
				
			||||||
   db
 | 
					   db
 | 
				
			||||||
   forms
 | 
					   forms
 | 
				
			||||||
   forms.base
 | 
					   forms.base
 | 
				
			||||||
 | 
					   grids
 | 
				
			||||||
 | 
					   grids.base
 | 
				
			||||||
   handler
 | 
					   handler
 | 
				
			||||||
   helpers
 | 
					   helpers
 | 
				
			||||||
   menus
 | 
					   menus
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ Forms Library
 | 
				
			||||||
The ``wuttaweb.forms`` namespace contains the following:
 | 
					The ``wuttaweb.forms`` namespace contains the following:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* :class:`~wuttaweb.forms.base.Form`
 | 
					* :class:`~wuttaweb.forms.base.Form`
 | 
				
			||||||
 | 
					* :class:`~wuttaweb.forms.base.FieldList`
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .base import Form
 | 
					from .base import Form, FieldList
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,8 @@ class FieldList(list):
 | 
				
			||||||
    of :class:`python:list`.
 | 
					    of :class:`python:list`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    You normally would not need to instantiate this yourself, but it
 | 
					    You normally would not need to instantiate this yourself, but it
 | 
				
			||||||
    is used under the hood for e.g. :attr:`Form.fields`.
 | 
					    is used under the hood for :attr:`Form.fields` as well as
 | 
				
			||||||
 | 
					    :attr:`~wuttaweb.grids.base.Grid.columns`.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def insert_before(self, field, newfield):
 | 
					    def insert_before(self, field, newfield):
 | 
				
			||||||
| 
						 | 
					@ -105,15 +106,48 @@ class Form:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Form instances contain the following attributes:
 | 
					    Form instances contain the following attributes:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Reference to current :term:`request` object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. attribute:: fields
 | 
					    .. attribute:: fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       :class:`FieldList` instance containing string field names for
 | 
					       :class:`FieldList` instance containing string field names for
 | 
				
			||||||
       the form.  By default, fields will appear in the same order as
 | 
					       the form.  By default, fields will appear in the same order as
 | 
				
			||||||
       they are in this list.
 | 
					       they are in this list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. attribute:: request
 | 
					    .. attribute:: schema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       Reference to current :term:`request` object.
 | 
					       Colander-based schema object for the form.  This is optional;
 | 
				
			||||||
 | 
					       if not specified an attempt will be made to construct one
 | 
				
			||||||
 | 
					       automatically.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       See also :meth:`get_schema()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: model_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Optional "class" for the model.  If set, this usually would be
 | 
				
			||||||
 | 
					       a SQLAlchemy mapped class.  This may be used instead of
 | 
				
			||||||
 | 
					       specifying the :attr:`schema`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: model_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Optional instance from which initial form data should be
 | 
				
			||||||
 | 
					       obtained.  In simple cases this might be a dict, or maybe an
 | 
				
			||||||
 | 
					       instance of :attr:`model_class`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Note that this also may be used instead of specifying the
 | 
				
			||||||
 | 
					       :attr:`schema`, if the instance belongs to a class which is
 | 
				
			||||||
 | 
					       SQLAlchemy-mapped.  (In that case :attr:`model_class` can be
 | 
				
			||||||
 | 
					       determined automatically.)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: readonly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Boolean indicating the form does not allow submit.  In practice
 | 
				
			||||||
 | 
					       this means there will not even be a ``<form>`` tag involved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Default for this is ``False`` in which case the ``<form>`` tag
 | 
				
			||||||
 | 
					       will exist and submit is allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. attribute:: action_url
 | 
					    .. attribute:: action_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,6 +194,9 @@ class Form:
 | 
				
			||||||
            request,
 | 
					            request,
 | 
				
			||||||
            fields=None,
 | 
					            fields=None,
 | 
				
			||||||
            schema=None,
 | 
					            schema=None,
 | 
				
			||||||
 | 
					            model_class=None,
 | 
				
			||||||
 | 
					            model_instance=None,
 | 
				
			||||||
 | 
					            readonly=False,
 | 
				
			||||||
            labels={},
 | 
					            labels={},
 | 
				
			||||||
            action_url=None,
 | 
					            action_url=None,
 | 
				
			||||||
            vue_tagname='wutta-form',
 | 
					            vue_tagname='wutta-form',
 | 
				
			||||||
| 
						 | 
					@ -171,6 +208,7 @@ class Form:
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        self.request = request
 | 
					        self.request = request
 | 
				
			||||||
        self.schema = schema
 | 
					        self.schema = schema
 | 
				
			||||||
 | 
					        self.readonly = readonly
 | 
				
			||||||
        self.labels = labels or {}
 | 
					        self.labels = labels or {}
 | 
				
			||||||
        self.action_url = action_url
 | 
					        self.action_url = action_url
 | 
				
			||||||
        self.vue_tagname = vue_tagname
 | 
					        self.vue_tagname = vue_tagname
 | 
				
			||||||
| 
						 | 
					@ -183,6 +221,9 @@ class Form:
 | 
				
			||||||
        self.config = self.request.wutta_config
 | 
					        self.config = self.request.wutta_config
 | 
				
			||||||
        self.app = self.config.get_app()
 | 
					        self.app = self.config.get_app()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.model_class = model_class
 | 
				
			||||||
 | 
					        self.model_instance = model_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if fields is not None:
 | 
					        if fields is not None:
 | 
				
			||||||
            self.set_fields(fields)
 | 
					            self.set_fields(fields)
 | 
				
			||||||
        elif self.schema:
 | 
					        elif self.schema:
 | 
				
			||||||
| 
						 | 
					@ -259,8 +300,21 @@ class Form:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return the :class:`colander:colander.Schema` object for the
 | 
					        Return the :class:`colander:colander.Schema` object for the
 | 
				
			||||||
        form, generating it automatically if necessary.
 | 
					        form, generating it automatically if necessary.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Note that if :attr:`schema` is already set, that will be
 | 
				
			||||||
 | 
					        returned as-is.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not self.schema:
 | 
					        if not self.schema:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if self.fields:
 | 
				
			||||||
 | 
					                schema = colander.Schema()
 | 
				
			||||||
 | 
					                for name in self.fields:
 | 
				
			||||||
 | 
					                    schema.add(colander.SchemaNode(
 | 
				
			||||||
 | 
					                        colander.String(),
 | 
				
			||||||
 | 
					                        name=name))
 | 
				
			||||||
 | 
					                self.schema = schema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            else: # no fields
 | 
				
			||||||
                raise NotImplementedError
 | 
					                raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.schema
 | 
					        return self.schema
 | 
				
			||||||
| 
						 | 
					@ -272,7 +326,12 @@ class Form:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not hasattr(self, 'deform_form'):
 | 
					        if not hasattr(self, 'deform_form'):
 | 
				
			||||||
            schema = self.get_schema()
 | 
					            schema = self.get_schema()
 | 
				
			||||||
            form = deform.Form(schema)
 | 
					            kwargs = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if self.model_instance:
 | 
				
			||||||
 | 
					                kwargs['appstruct'] = self.model_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            form = deform.Form(schema, **kwargs)
 | 
				
			||||||
            self.deform_form = form
 | 
					            self.deform_form = form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.deform_form
 | 
					        return self.deform_form
 | 
				
			||||||
| 
						 | 
					@ -332,7 +391,7 @@ class Form:
 | 
				
			||||||
        output = render(template, context)
 | 
					        output = render(template, context)
 | 
				
			||||||
        return HTML.literal(output)
 | 
					        return HTML.literal(output)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_vue_field(self, fieldname):
 | 
					    def render_vue_field(self, fieldname, readonly=None):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Render the given field completely, i.e. ``<b-field>`` wrapper
 | 
					        Render the given field completely, i.e. ``<b-field>`` wrapper
 | 
				
			||||||
        with label and containing a widget.
 | 
					        with label and containing a widget.
 | 
				
			||||||
| 
						 | 
					@ -349,11 +408,19 @@ class Form:
 | 
				
			||||||
              <!-- widget element(s) -->
 | 
					              <!-- widget element(s) -->
 | 
				
			||||||
           </b-field>
 | 
					           </b-field>
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        dform = self.get_deform()
 | 
					
 | 
				
			||||||
        field = dform[fieldname]
 | 
					        if readonly is None:
 | 
				
			||||||
 | 
					            readonly = self.readonly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # render the field widget or whatever
 | 
					        # render the field widget or whatever
 | 
				
			||||||
        html = field.serialize()
 | 
					        dform = self.get_deform()
 | 
				
			||||||
 | 
					        field = dform[fieldname]
 | 
				
			||||||
 | 
					        kw = {}
 | 
				
			||||||
 | 
					        if readonly:
 | 
				
			||||||
 | 
					            kw['readonly'] = True
 | 
				
			||||||
 | 
					        html = field.serialize(**kw)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # mark all that as safe
 | 
				
			||||||
        html = HTML.literal(html)
 | 
					        html = HTML.literal(html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # render field label
 | 
					        # render field label
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/wuttaweb/grids/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/wuttaweb/grids/__init__.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					################################################################################
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  wuttaweb -- Web App for Wutta Framework
 | 
				
			||||||
 | 
					#  Copyright © 2024 Lance Edgar
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  This file is part of Wutta Framework.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Wutta Framework is free software: you can redistribute it and/or modify it
 | 
				
			||||||
 | 
					#  under the terms of the GNU General Public License as published by the Free
 | 
				
			||||||
 | 
					#  Software Foundation, either version 3 of the License, or (at your option) any
 | 
				
			||||||
 | 
					#  later version.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Wutta Framework is distributed in the hope that it will be useful, but
 | 
				
			||||||
 | 
					#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 | 
				
			||||||
 | 
					#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 | 
				
			||||||
 | 
					#  more details.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  You should have received a copy of the GNU General Public License along with
 | 
				
			||||||
 | 
					#  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					################################################################################
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Grids Library
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The ``wuttaweb.grids`` namespace contains the following:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* :class:`~wuttaweb.grids.base.Grid`
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .base import Grid, GridAction
 | 
				
			||||||
							
								
								
									
										344
									
								
								src/wuttaweb/grids/base.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								src/wuttaweb/grids/base.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,344 @@
 | 
				
			||||||
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					################################################################################
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  wuttaweb -- Web App for Wutta Framework
 | 
				
			||||||
 | 
					#  Copyright © 2024 Lance Edgar
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  This file is part of Wutta Framework.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Wutta Framework is free software: you can redistribute it and/or modify it
 | 
				
			||||||
 | 
					#  under the terms of the GNU General Public License as published by the Free
 | 
				
			||||||
 | 
					#  Software Foundation, either version 3 of the License, or (at your option) any
 | 
				
			||||||
 | 
					#  later version.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Wutta Framework is distributed in the hope that it will be useful, but
 | 
				
			||||||
 | 
					#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 | 
				
			||||||
 | 
					#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 | 
				
			||||||
 | 
					#  more details.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  You should have received a copy of the GNU General Public License along with
 | 
				
			||||||
 | 
					#  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					################################################################################
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Base grid classes
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pyramid.renderers import render
 | 
				
			||||||
 | 
					from webhelpers2.html import HTML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from wuttaweb.forms import FieldList
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Grid:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Base class for all grids.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param request: Reference to current :term:`request` object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param columns: List of column names for the grid.  This is
 | 
				
			||||||
 | 
					       optional; if not specified an attempt will be made to deduce
 | 
				
			||||||
 | 
					       the list automatically.  See also :attr:`columns`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. note::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Some parameters are not explicitly described above.  However
 | 
				
			||||||
 | 
					       their corresponding attributes are described below.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Grid instances contain the following attributes:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Presumably unique key for the grid; used to track per-grid
 | 
				
			||||||
 | 
					       sort/filter settings etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: columns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       :class:`~wuttaweb.forms.base.FieldList` instance containing
 | 
				
			||||||
 | 
					       string column names for the grid.  Columns will appear in the
 | 
				
			||||||
 | 
					       same order as they are in this list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       See also :meth:`set_columns()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Data set for the grid.  This should either be a list of dicts
 | 
				
			||||||
 | 
					       (or objects with dict-like access to fields, corresponding to
 | 
				
			||||||
 | 
					       model records) or else an object capable of producing such a
 | 
				
			||||||
 | 
					       list, e.g. SQLAlchemy query.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: actions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       List of :class:`GridAction` instances represenging action links
 | 
				
			||||||
 | 
					       to be shown for each record in the grid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: vue_tagname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       String name for Vue component tag.  By default this is
 | 
				
			||||||
 | 
					       ``'wutta-grid'``.  See also :meth:`render_vue_tag()`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(
 | 
				
			||||||
 | 
					            self,
 | 
				
			||||||
 | 
					            request,
 | 
				
			||||||
 | 
					            key=None,
 | 
				
			||||||
 | 
					            columns=None,
 | 
				
			||||||
 | 
					            data=None,
 | 
				
			||||||
 | 
					            actions=[],
 | 
				
			||||||
 | 
					            vue_tagname='wutta-grid',
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        self.request = request
 | 
				
			||||||
 | 
					        self.key = key
 | 
				
			||||||
 | 
					        self.data = data
 | 
				
			||||||
 | 
					        self.actions = actions or []
 | 
				
			||||||
 | 
					        self.vue_tagname = vue_tagname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.config = self.request.wutta_config
 | 
				
			||||||
 | 
					        self.app = self.config.get_app()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if columns is not None:
 | 
				
			||||||
 | 
					            self.set_columns(columns)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.columns = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def vue_component(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        String name for the Vue component, e.g. ``'WuttaGrid'``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is a generated value based on :attr:`vue_tagname`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        words = self.vue_tagname.split('-')
 | 
				
			||||||
 | 
					        return ''.join([word.capitalize() for word in words])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_columns(self, columns):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Explicitly set the list of grid columns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This will overwrite :attr:`columns` with a new
 | 
				
			||||||
 | 
					        :class:`~wuttaweb.forms.base.FieldList` instance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param columns: List of string column names.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.columns = FieldList(columns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_vue_tag(self, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Render the Vue component tag for the grid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        By default this simply returns:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .. code-block:: html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           <wutta-grid></wutta-grid>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The actual output will depend on various grid attributes, in
 | 
				
			||||||
 | 
					        particular :attr:`vue_tagname`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return HTML.tag(self.vue_tagname, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_vue_template(
 | 
				
			||||||
 | 
					            self,
 | 
				
			||||||
 | 
					            template='/grids/vue_template.mako',
 | 
				
			||||||
 | 
					            **context):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Render the Vue template block for the grid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This returns something like:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .. code-block:: none
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           <script type="text/x-template" id="wutta-grid-template">
 | 
				
			||||||
 | 
					             <b-table>
 | 
				
			||||||
 | 
					               <!-- columns etc. -->
 | 
				
			||||||
 | 
					             </b-table>
 | 
				
			||||||
 | 
					           </script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .. todo::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           Why can't Sphinx render the above code block as 'html' ?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           It acts like it can't handle a ``<script>`` tag at all?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Actual output will of course depend on grid attributes,
 | 
				
			||||||
 | 
					        :attr:`vue_tagname` and :attr:`columns` etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param template: Path to Mako template which is used to render
 | 
				
			||||||
 | 
					           the output.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        context['grid'] = self
 | 
				
			||||||
 | 
					        context.setdefault('request', self.request)
 | 
				
			||||||
 | 
					        output = render(template, context)
 | 
				
			||||||
 | 
					        return HTML.literal(output)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_vue_columns(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns a list of Vue-compatible column definitions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This uses :attr:`columns` as the basis; each definition
 | 
				
			||||||
 | 
					        returned will be a dict in this format::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           {
 | 
				
			||||||
 | 
					               'field': 'foo',
 | 
				
			||||||
 | 
					               'label': "Foo",
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See also :meth:`get_vue_data()`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not self.columns:
 | 
				
			||||||
 | 
					            raise ValueError(f"you must define columns for the grid! key = {self.key}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        columns = []
 | 
				
			||||||
 | 
					        for name in self.columns:
 | 
				
			||||||
 | 
					            columns.append({
 | 
				
			||||||
 | 
					                'field': name,
 | 
				
			||||||
 | 
					                'label': self.app.make_title(name),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        return columns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_vue_data(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns a list of Vue-compatible data records.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This uses :attr:`data` as the basis, but may add some extra
 | 
				
			||||||
 | 
					        values to each record for sake of action URLs etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See also :meth:`get_vue_columns()`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # use data as-is unless we have actions
 | 
				
			||||||
 | 
					        if not self.actions:
 | 
				
			||||||
 | 
					            return self.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # we have action(s), so add URL(s) for each record in data
 | 
				
			||||||
 | 
					        data = []
 | 
				
			||||||
 | 
					        for i, record in enumerate(self.data):
 | 
				
			||||||
 | 
					            record = dict(record)
 | 
				
			||||||
 | 
					            for action in self.actions:
 | 
				
			||||||
 | 
					                url = action.get_url(record, i)
 | 
				
			||||||
 | 
					                key = f'_action_url_{action.key}'
 | 
				
			||||||
 | 
					                record[key] = url
 | 
				
			||||||
 | 
					            data.append(record)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GridAction:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Represents a "row action" hyperlink within a grid context.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    All such actions are displayed as a group, in a dedicated
 | 
				
			||||||
 | 
					    **Actions** column in the grid.  So each row in the grid has its
 | 
				
			||||||
 | 
					    own set of action links.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    A :class:`Grid` can have one (or zero) or more of these in its
 | 
				
			||||||
 | 
					    :attr:`~Grid.actions` list.  You can call
 | 
				
			||||||
 | 
					    :meth:`~wuttaweb.views.base.View.make_grid_action()` to add custom
 | 
				
			||||||
 | 
					    actions from within a view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param request: Current :term:`request` object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. note::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Some parameters are not explicitly described above.  However
 | 
				
			||||||
 | 
					       their corresponding attributes are described below.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       String key for the action (e.g. ``'edit'``), unique within the
 | 
				
			||||||
 | 
					       grid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Label to be displayed for the action link.  If not set, will be
 | 
				
			||||||
 | 
					       generated from :attr:`key` by calling
 | 
				
			||||||
 | 
					       :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.make_title()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       See also :meth:`render_label()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       URL for the action link, if applicable.  This *can* be a simple
 | 
				
			||||||
 | 
					       string, however that will cause every row in the grid to have
 | 
				
			||||||
 | 
					       the same URL for this action.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       A better way is to specify a callable which can return a unique
 | 
				
			||||||
 | 
					       URL for each record.  The callable should expect ``(obj, i)``
 | 
				
			||||||
 | 
					       args, for instance::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def myurl(obj, i):
 | 
				
			||||||
 | 
					              return request.route_url('widgets.view', uuid=obj.uuid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          action = GridAction(request, 'view', url=myurl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       See also :meth:`get_url()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: icon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Name of icon to be shown for the action link.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       See also :meth:`render_icon()`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(
 | 
				
			||||||
 | 
					            self,
 | 
				
			||||||
 | 
					            request,
 | 
				
			||||||
 | 
					            key,
 | 
				
			||||||
 | 
					            label=None,
 | 
				
			||||||
 | 
					            url=None,
 | 
				
			||||||
 | 
					            icon=None,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        self.request = request
 | 
				
			||||||
 | 
					        self.config = self.request.wutta_config
 | 
				
			||||||
 | 
					        self.app = self.config.get_app()
 | 
				
			||||||
 | 
					        self.key = key
 | 
				
			||||||
 | 
					        self.url = url
 | 
				
			||||||
 | 
					        self.label = label or self.app.make_title(key)
 | 
				
			||||||
 | 
					        self.icon = icon or key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_icon(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Render the HTML snippet for the action link icon.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This uses :attr:`icon` to identify the named icon to be shown.
 | 
				
			||||||
 | 
					        Output is something like (here ``'trash'`` is the icon name):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .. code-block:: html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           <i class="fas fa-trash"></i>
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.request.use_oruga:
 | 
				
			||||||
 | 
					            raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return HTML.tag('i', class_=f'fas fa-{self.icon}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_label(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Render the label text for the action link.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Default behavior is to return :attr:`label` as-is.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_url(self, obj, i=None):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the action link URL for the given object (model
 | 
				
			||||||
 | 
					        instance).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        If :attr:`url` is a simple string, it is returned as-is.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        But if :attr:`url` is a callable (which is typically the most
 | 
				
			||||||
 | 
					        useful), that will be called with the same ``(obj, i)`` args
 | 
				
			||||||
 | 
					        passed along.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param obj: Model instance of whatever type the parent grid is
 | 
				
			||||||
 | 
					           setup to use.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param i: Zero-based sequence for the object, within the
 | 
				
			||||||
 | 
					           parent grid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See also :attr:`url`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if callable(self.url):
 | 
				
			||||||
 | 
					            return self.url(obj, i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self.url
 | 
				
			||||||
| 
						 | 
					@ -122,6 +122,11 @@ class MenuHandler(GenericHandler):
 | 
				
			||||||
                    'route': 'appinfo',
 | 
					                    'route': 'appinfo',
 | 
				
			||||||
                    'perm': 'appinfo.list',
 | 
					                    'perm': 'appinfo.list',
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    'title': "Raw Settings",
 | 
				
			||||||
 | 
					                    'route': 'settings',
 | 
				
			||||||
 | 
					                    'perm': 'settings.list',
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@
 | 
				
			||||||
      % endfor
 | 
					      % endfor
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    % if not form.readonly:
 | 
				
			||||||
        <div style="margin-top: 1.5rem; display: flex; gap: 0.5rem; justify-content: ${'end' if form.align_buttons_right else 'start'}; width: 100%; padding-left: 10rem;">
 | 
					        <div style="margin-top: 1.5rem; display: flex; gap: 0.5rem; justify-content: ${'end' if form.align_buttons_right else 'start'}; width: 100%; padding-left: 10rem;">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          % if form.show_button_reset:
 | 
					          % if form.show_button_reset:
 | 
				
			||||||
| 
						 | 
					@ -33,6 +34,7 @@
 | 
				
			||||||
          </b-button>
 | 
					          </b-button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					    % endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ${h.end_form()}
 | 
					  ${h.end_form()}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										48
									
								
								src/wuttaweb/templates/grids/vue_template.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/wuttaweb/templates/grids/vue_template.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script type="text/x-template" id="${grid.vue_tagname}-template">
 | 
				
			||||||
 | 
					  <${b}-table :data="data"
 | 
				
			||||||
 | 
					              hoverable
 | 
				
			||||||
 | 
					              :loading="loading">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    % for column in grid.get_vue_columns():
 | 
				
			||||||
 | 
					        <${b}-table-column field="${column['field']}"
 | 
				
			||||||
 | 
					                           label="${column['label']}"
 | 
				
			||||||
 | 
					                           v-slot="props"
 | 
				
			||||||
 | 
					                           cell-class="c_${column['field']}">
 | 
				
			||||||
 | 
					          <span v-html="props.row.${column['field']}"></span>
 | 
				
			||||||
 | 
					        </${b}-table-column>
 | 
				
			||||||
 | 
					    % endfor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    % if grid.actions:
 | 
				
			||||||
 | 
					        <${b}-table-column field="actions"
 | 
				
			||||||
 | 
					                           label="Actions"
 | 
				
			||||||
 | 
					                           v-slot="props">
 | 
				
			||||||
 | 
					          % for action in grid.actions:
 | 
				
			||||||
 | 
					              <a :href="props.row._action_url_${action.key}">
 | 
				
			||||||
 | 
					                ${action.render_icon()}
 | 
				
			||||||
 | 
					                ${action.render_label()}
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					               
 | 
				
			||||||
 | 
					          % endfor
 | 
				
			||||||
 | 
					        </${b}-table-column>
 | 
				
			||||||
 | 
					    % endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </${b}-table>
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let ${grid.vue_component} = {
 | 
				
			||||||
 | 
					      template: '#${grid.vue_tagname}-template',
 | 
				
			||||||
 | 
					      methods: {},
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let ${grid.vue_component}Data = {
 | 
				
			||||||
 | 
					      data: ${grid.vue_component}CurrentData,
 | 
				
			||||||
 | 
					      loading: false,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										5
									
								
								src/wuttaweb/templates/master/form.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/wuttaweb/templates/master/form.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					<%inherit file="/form.mako" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${parent.body()}
 | 
				
			||||||
| 
						 | 
					@ -3,11 +3,30 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="title()">${index_title}</%def>
 | 
					<%def name="title()">${index_title}</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## nb. avoid hero bar for index page
 | 
				
			||||||
<%def name="content_title()"></%def>
 | 
					<%def name="content_title()"></%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="page_content()">
 | 
					<%def name="page_content()">
 | 
				
			||||||
  <p>TODO: index page content</p>
 | 
					  % if grid is not Undefined:
 | 
				
			||||||
 | 
					      ${grid.render_vue_tag()}
 | 
				
			||||||
 | 
					  % endif
 | 
				
			||||||
</%def>
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="render_this_page_template()">
 | 
				
			||||||
 | 
					  ${parent.render_this_page_template()}
 | 
				
			||||||
 | 
					  % if grid is not Undefined:
 | 
				
			||||||
 | 
					      ${grid.render_vue_template()}
 | 
				
			||||||
 | 
					  % endif
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="finalize_this_page_vars()">
 | 
				
			||||||
 | 
					  ${parent.finalize_this_page_vars()}
 | 
				
			||||||
 | 
					  % if grid is not Undefined:
 | 
				
			||||||
 | 
					      <script>
 | 
				
			||||||
 | 
					        ${grid.vue_component}.data = function() { return ${grid.vue_component}Data }
 | 
				
			||||||
 | 
					        Vue.component('${grid.vue_tagname}', ${grid.vue_component})
 | 
				
			||||||
 | 
					      </script>
 | 
				
			||||||
 | 
					  % endif
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
${parent.body()}
 | 
					${parent.body()}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/wuttaweb/templates/master/view.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/wuttaweb/templates/master/view.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					<%inherit file="/master/form.mako" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="title()">${index_title} » ${instance_title}</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="content_title()">${instance_title}</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${parent.body()}
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ Base Logic for Views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from pyramid import httpexceptions
 | 
					from pyramid import httpexceptions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb import forms
 | 
					from wuttaweb import forms, grids
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class View:
 | 
					class View:
 | 
				
			||||||
| 
						 | 
					@ -68,11 +68,31 @@ class View:
 | 
				
			||||||
        Make and return a new :class:`~wuttaweb.forms.base.Form`
 | 
					        Make and return a new :class:`~wuttaweb.forms.base.Form`
 | 
				
			||||||
        instance, per the given ``kwargs``.
 | 
					        instance, per the given ``kwargs``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        This is the "default" form factory which merely invokes
 | 
					        This is the "base" factory which merely invokes the
 | 
				
			||||||
        the constructor.
 | 
					        constructor.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return forms.Form(self.request, **kwargs)
 | 
					        return forms.Form(self.request, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_grid(self, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Make and return a new :class:`~wuttaweb.grids.base.Grid`
 | 
				
			||||||
 | 
					        instance, per the given ``kwargs``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is the "base" factory which merely invokes the
 | 
				
			||||||
 | 
					        constructor.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return grids.Grid(self.request, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_grid_action(self, key, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Make and return a new :class:`~wuttaweb.grids.base.GridAction`
 | 
				
			||||||
 | 
					        instance, per the given ``key`` and ``kwargs``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is the "base" factory which merely invokes the
 | 
				
			||||||
 | 
					        constructor.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return grids.GridAction(self.request, key, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def notfound(self):
 | 
					    def notfound(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Convenience method, to raise a HTTP 404 Not Found exception::
 | 
					        Convenience method, to raise a HTTP 404 Not Found exception::
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,6 +100,25 @@ class MasterView(View):
 | 
				
			||||||
       Code should not access this directly but instead call
 | 
					       Code should not access this directly but instead call
 | 
				
			||||||
       :meth:`get_model_title_plural()`.
 | 
					       :meth:`get_model_title_plural()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: model_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Optional override for the view's "model key" - e.g. ``'id'``
 | 
				
			||||||
 | 
					       (string for simple case) or composite key such as
 | 
				
			||||||
 | 
					       ``('id_field', 'name_field')``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       If :attr:`model_class` is set to a SQLAlchemy mapped class, the
 | 
				
			||||||
 | 
					       model key can be determined automatically.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Code should not access this directly but instead call
 | 
				
			||||||
 | 
					       :meth:`get_model_key()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: grid_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Optional override for the view's grid key, e.g. ``'widgets'``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Code should not access this directly but instead call
 | 
				
			||||||
 | 
					       :meth:`get_grid_key()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. attribute:: config_title
 | 
					    .. attribute:: config_title
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       Optional override for the view's "config" title, e.g. ``"Wutta
 | 
					       Optional override for the view's "config" title, e.g. ``"Wutta
 | 
				
			||||||
| 
						 | 
					@ -138,6 +157,29 @@ class MasterView(View):
 | 
				
			||||||
       i.e. it should have an :meth:`index()` view.  Default value is
 | 
					       i.e. it should have an :meth:`index()` view.  Default value is
 | 
				
			||||||
       ``True``.
 | 
					       ``True``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: has_grid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Boolean indicating whether the :meth:`index()` view should
 | 
				
			||||||
 | 
					       include a grid.  Default value is ``True``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: grid_columns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       List of columns for the :meth:`index()` view grid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       This is optional; see also :meth:`index_get_grid_columns()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: viewable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Boolean indicating whether the view model supports "viewing" -
 | 
				
			||||||
 | 
					       i.e. it should have a :meth:`view()` view.  Default value is
 | 
				
			||||||
 | 
					       ``True``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: form_fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       List of columns for the model form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       This is optional; see also :meth:`get_form_fields()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. attribute:: configurable
 | 
					    .. attribute:: configurable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       Boolean indicating whether the master view supports
 | 
					       Boolean indicating whether the master view supports
 | 
				
			||||||
| 
						 | 
					@ -151,9 +193,13 @@ class MasterView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # features
 | 
					    # features
 | 
				
			||||||
    listable = True
 | 
					    listable = True
 | 
				
			||||||
 | 
					    has_grid = True
 | 
				
			||||||
 | 
					    viewable = True
 | 
				
			||||||
    configurable = False
 | 
					    configurable = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # current action
 | 
					    # current action
 | 
				
			||||||
 | 
					    listing = False
 | 
				
			||||||
 | 
					    viewing = False
 | 
				
			||||||
    configuring = False
 | 
					    configuring = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
| 
						 | 
					@ -170,12 +216,158 @@ class MasterView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        By default, this view is included only if :attr:`listable` is
 | 
					        By default, this view is included only if :attr:`listable` is
 | 
				
			||||||
        true.
 | 
					        true.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The default view logic will show a "grid" (table) with the
 | 
				
			||||||
 | 
					        model data (unless :attr:`has_grid` is false).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See also related methods, which are called by this one:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * :meth:`index_make_grid()`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        self.listing = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        context = {
 | 
					        context = {
 | 
				
			||||||
            'index_url': None,  # avoid title link since this *is* the index
 | 
					            'index_url': None, # nb. avoid title link since this *is* the index
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.has_grid:
 | 
				
			||||||
 | 
					            context['grid'] = self.index_make_grid()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.render_to_response('index', context)
 | 
					        return self.render_to_response('index', context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def index_make_grid(self, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Create and return a :class:`~wuttaweb.grids.base.Grid`
 | 
				
			||||||
 | 
					        instance for use with the :meth:`index()` view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See also related methods, which are called by this one:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * :meth:`get_grid_key()`
 | 
				
			||||||
 | 
					        * :meth:`index_get_grid_columns()`
 | 
				
			||||||
 | 
					        * :meth:`index_get_grid_data()`
 | 
				
			||||||
 | 
					        * :meth:`index_configure_grid()`
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if 'key' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['key'] = self.get_grid_key()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'columns' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['columns'] = self.index_get_grid_columns()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'data' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['data'] = self.index_get_grid_data()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'actions' not in kwargs:
 | 
				
			||||||
 | 
					            actions = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # TODO: should split this off into index_get_grid_actions() ?
 | 
				
			||||||
 | 
					            if self.viewable:
 | 
				
			||||||
 | 
					                actions.append(self.make_grid_action('view', icon='eye',
 | 
				
			||||||
 | 
					                                                     url=self.get_action_url_view))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            kwargs['actions'] = actions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        grid = self.make_grid(**kwargs)
 | 
				
			||||||
 | 
					        self.index_configure_grid(grid)
 | 
				
			||||||
 | 
					        return grid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def index_get_grid_columns(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the default list of grid column names, for the
 | 
				
			||||||
 | 
					        :meth:`index()` view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is called by :meth:`index_make_grid()`; in the resulting
 | 
				
			||||||
 | 
					        :class:`~wuttaweb.grids.base.Grid` instance, this becomes
 | 
				
			||||||
 | 
					        :attr:`~wuttaweb.grids.base.Grid.columns`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This method may return ``None``, in which case the grid may
 | 
				
			||||||
 | 
					        (try to) generate its own default list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Subclass may define :attr:`grid_columns` for simple cases, or
 | 
				
			||||||
 | 
					        can override this method if needed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Also note that :meth:`index_configure_grid()` may be used to
 | 
				
			||||||
 | 
					        further modify the final column set, regardless of what this
 | 
				
			||||||
 | 
					        method returns.  So a common pattern is to declare all
 | 
				
			||||||
 | 
					        "supported" columns by setting :attr:`grid_columns` but then
 | 
				
			||||||
 | 
					        optionally remove or replace some of those within
 | 
				
			||||||
 | 
					        :meth:`index_configure_grid()`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if hasattr(self, 'grid_columns'):
 | 
				
			||||||
 | 
					            return self.grid_columns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def index_get_grid_data(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the grid data for the :meth:`index()` view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is called by :meth:`index_make_grid()`; in the resulting
 | 
				
			||||||
 | 
					        :class:`~wuttaweb.grids.base.Grid` instance, this becomes
 | 
				
			||||||
 | 
					        :attr:`~wuttaweb.grids.base.Grid.data`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        As of now there is not yet a "sane" default for this method;
 | 
				
			||||||
 | 
					        it simply returns an empty list.  Subclass should override as
 | 
				
			||||||
 | 
					        needed.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_action_url_view(self, obj, i):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the "view" grid action URL for the given object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Most typically this is like ``/widgets/XXX`` where ``XXX``
 | 
				
			||||||
 | 
					        represents the object's key/ID.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        route_prefix = self.get_route_prefix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        kw = {}
 | 
				
			||||||
 | 
					        for key in self.get_model_key():
 | 
				
			||||||
 | 
					            kw[key] = obj[key]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self.request.route_url(f'{route_prefix}.view', **kw)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def index_configure_grid(self, grid):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Configure the grid for the :meth:`index()` view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is called by :meth:`index_make_grid()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        There is no default logic here; subclass should override as
 | 
				
			||||||
 | 
					        needed.  The ``grid`` param will already be "complete" and
 | 
				
			||||||
 | 
					        ready to use as-is, but this method can further modify it
 | 
				
			||||||
 | 
					        based on request details etc.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ##############################
 | 
				
			||||||
 | 
					    # view methods
 | 
				
			||||||
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def view(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        View to "view" details of an existing model record.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This usually corresponds to a URL like ``/widgets/XXX``
 | 
				
			||||||
 | 
					        where ``XXX`` represents the key/ID for the record.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        By default, this view is included only if :attr:`viewable` is
 | 
				
			||||||
 | 
					        true.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The default view logic will show a read-only form with field
 | 
				
			||||||
 | 
					        values displayed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See also related methods, which are called by this one:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * :meth:`make_model_form()`
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.viewing = True
 | 
				
			||||||
 | 
					        instance = self.get_instance()
 | 
				
			||||||
 | 
					        form = self.make_model_form(instance, readonly=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        context = {
 | 
				
			||||||
 | 
					            'instance': instance,
 | 
				
			||||||
 | 
					            'instance_title': self.get_instance_title(instance),
 | 
				
			||||||
 | 
					            'form': form,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return self.render_to_response('view', context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # configure methods
 | 
					    # configure methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
| 
						 | 
					@ -451,14 +643,12 @@ class MasterView(View):
 | 
				
			||||||
        Save the given settings to the DB; this is called by
 | 
					        Save the given settings to the DB; this is called by
 | 
				
			||||||
        :meth:`configure()`.
 | 
					        :meth:`configure()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        This method expected a list of name/value dicts and will
 | 
					        This method expects a list of name/value dicts and will simply
 | 
				
			||||||
        simply save each to the DB, with no "conversion" logic.
 | 
					        save each to the DB, with no "conversion" logic.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param settings: List of normalized setting definitions, as
 | 
					        :param settings: List of normalized setting definitions, as
 | 
				
			||||||
           returned by :meth:`configure_gather_settings()`.
 | 
					           returned by :meth:`configure_gather_settings()`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # app = self.get_rattail_app()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # nb. must avoid self.Session here in case that does not point
 | 
					        # nb. must avoid self.Session here in case that does not point
 | 
				
			||||||
        # to our primary app DB
 | 
					        # to our primary app DB
 | 
				
			||||||
        session = Session()
 | 
					        session = Session()
 | 
				
			||||||
| 
						 | 
					@ -470,26 +660,6 @@ class MasterView(View):
 | 
				
			||||||
    # support methods
 | 
					    # support methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_index_title(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Returns the main index title for the master view.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        By default this returns the value from
 | 
					 | 
				
			||||||
        :meth:`get_model_title_plural()`.  Subclass may override as
 | 
					 | 
				
			||||||
        needed.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return self.get_model_title_plural()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_index_url(self, **kwargs):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Returns the URL for master's :meth:`index()` view.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        NB. this returns ``None`` if :attr:`listable` is false.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if self.listable:
 | 
					 | 
				
			||||||
            route_prefix = self.get_route_prefix()
 | 
					 | 
				
			||||||
            return self.request.route_url(route_prefix, **kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_to_response(self, template, context):
 | 
					    def render_to_response(self, template, context):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Locate and render an appropriate template, with the given
 | 
					        Locate and render an appropriate template, with the given
 | 
				
			||||||
| 
						 | 
					@ -568,6 +738,110 @@ class MasterView(View):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return [f'/master/{template}.mako']
 | 
					        return [f'/master/{template}.mako']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_index_title(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the main index title for the master view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        By default this returns the value from
 | 
				
			||||||
 | 
					        :meth:`get_model_title_plural()`.  Subclass may override as
 | 
				
			||||||
 | 
					        needed.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.get_model_title_plural()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_index_url(self, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the URL for master's :meth:`index()` view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        NB. this returns ``None`` if :attr:`listable` is false.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.listable:
 | 
				
			||||||
 | 
					            route_prefix = self.get_route_prefix()
 | 
				
			||||||
 | 
					            return self.request.route_url(route_prefix, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_instance(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        This should return the "current" model instance based on the
 | 
				
			||||||
 | 
					        request details (e.g. route kwargs).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        If the instance cannot be found, this should raise a HTTP 404
 | 
				
			||||||
 | 
					        exception, i.e. :meth:`~wuttaweb.views.base.View.notfound()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        There is no "sane" default logic here; subclass *must*
 | 
				
			||||||
 | 
					        override or else a ``NotImplementedError`` is raised.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        raise NotImplementedError("you must define get_instance() method "
 | 
				
			||||||
 | 
					                                  f" for view class: {self.__class__}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_instance_title(self, instance):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return the human-friendly "title" for the instance, to be used
 | 
				
			||||||
 | 
					        in the page title when viewing etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Default logic returns the value from ``str(instance)``;
 | 
				
			||||||
 | 
					        subclass may override if needed.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return str(instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_model_form(self, model_instance=None, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Create and return a :class:`~wuttaweb.forms.base.Form`
 | 
				
			||||||
 | 
					        for the view model.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Note that this method is called for multiple "CRUD" views,
 | 
				
			||||||
 | 
					        e.g.:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * :meth:`view()`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See also related methods, which are called by this one:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * :meth:`get_form_fields()`
 | 
				
			||||||
 | 
					        * :meth:`configure_form()`
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        kwargs['model_instance'] = model_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'fields' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['fields'] = self.get_form_fields()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        form = self.make_form(**kwargs)
 | 
				
			||||||
 | 
					        self.configure_form(form)
 | 
				
			||||||
 | 
					        return form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_form_fields(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the initial list of field names for the model form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is called by :meth:`make_model_form()`; in the resulting
 | 
				
			||||||
 | 
					        :class:`~wuttaweb.forms.base.Form` instance, this becomes
 | 
				
			||||||
 | 
					        :attr:`~wuttaweb.forms.base.Form.fields`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This method may return ``None``, in which case the form may
 | 
				
			||||||
 | 
					        (try to) generate its own default list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Subclass may define :attr:`form_fields` for simple cases, or
 | 
				
			||||||
 | 
					        can override this method if needed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Note that :meth:`configure_form()` may be used to further
 | 
				
			||||||
 | 
					        modify the final field list, regardless of what this method
 | 
				
			||||||
 | 
					        returns.  So a common pattern is to declare all "supported"
 | 
				
			||||||
 | 
					        fields by setting :attr:`form_fields` but then optionally
 | 
				
			||||||
 | 
					        remove or replace some in :meth:`configure_form()`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if hasattr(self, 'form_fields'):
 | 
				
			||||||
 | 
					            return self.form_fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure_form(self, form):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Configure the given model form, as needed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is called by :meth:`make_model_form()` - for multiple
 | 
				
			||||||
 | 
					        CRUD views.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        There is no default logic here; subclass should override if
 | 
				
			||||||
 | 
					        needed.  The ``form`` param will already be "complete" and
 | 
				
			||||||
 | 
					        ready to use as-is, but this method can further modify it
 | 
				
			||||||
 | 
					        based on request details etc.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # class methods
 | 
					    # class methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
| 
						 | 
					@ -663,6 +937,32 @@ class MasterView(View):
 | 
				
			||||||
        model_title = cls.get_model_title()
 | 
					        model_title = cls.get_model_title()
 | 
				
			||||||
        return f"{model_title}s"
 | 
					        return f"{model_title}s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_model_key(cls):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the "model key" for the master view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This should return a tuple containing one or more "field
 | 
				
			||||||
 | 
					        names" corresponding to the primary key for data records.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        In the most simple/common scenario, where the master view
 | 
				
			||||||
 | 
					        represents a Wutta-based SQLAlchemy model, the return value
 | 
				
			||||||
 | 
					        for this method is: ``('uuid',)``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        But there is no "sane" default for other scenarios, in which
 | 
				
			||||||
 | 
					        case subclass should define :attr:`model_key`.  If the model
 | 
				
			||||||
 | 
					        key cannot be determined, raises ``AttributeError``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :returns: Tuple of field names comprising the model key.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if hasattr(cls, 'model_key'):
 | 
				
			||||||
 | 
					            keys = cls.model_key
 | 
				
			||||||
 | 
					            if isinstance(keys, str):
 | 
				
			||||||
 | 
					                keys = [keys]
 | 
				
			||||||
 | 
					            return tuple(keys)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        raise AttributeError(f"you must define model_key for view class: {cls}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def get_route_prefix(cls):
 | 
					    def get_route_prefix(cls):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -713,6 +1013,27 @@ class MasterView(View):
 | 
				
			||||||
        route_prefix = cls.get_route_prefix()
 | 
					        route_prefix = cls.get_route_prefix()
 | 
				
			||||||
        return f'/{route_prefix}'
 | 
					        return f'/{route_prefix}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_instance_url_prefix(cls):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Generate the URL prefix specific to an instance for this model
 | 
				
			||||||
 | 
					        view.  This will include model key param placeholders; it
 | 
				
			||||||
 | 
					        winds up looking like:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * ``/widgets/{uuid}``
 | 
				
			||||||
 | 
					        * ``/resources/{foo}|{bar}|{baz}``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The former being the most simple/common, and the latter
 | 
				
			||||||
 | 
					        showing what a "composite" model key looks like, with pipe
 | 
				
			||||||
 | 
					        symbols separating the key parts.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        prefix = cls.get_url_prefix() + '/'
 | 
				
			||||||
 | 
					        for i, key in enumerate(cls.get_model_key()):
 | 
				
			||||||
 | 
					            if i:
 | 
				
			||||||
 | 
					                prefix += '|'
 | 
				
			||||||
 | 
					            prefix += f'{{{key}}}'
 | 
				
			||||||
 | 
					        return prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def get_template_prefix(cls):
 | 
					    def get_template_prefix(cls):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -738,6 +1059,26 @@ class MasterView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return cls.get_url_prefix()
 | 
					        return cls.get_url_prefix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_grid_key(cls):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the (presumably) unique key to be used for the primary
 | 
				
			||||||
 | 
					        grid in the :meth:`index()` view.  This key may also be used
 | 
				
			||||||
 | 
					        as the basis (key prefix) for secondary grids.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is called from :meth:`index_make_grid()`; in the
 | 
				
			||||||
 | 
					        resulting :class:`~wuttaweb.grids.base.Grid` instance, this
 | 
				
			||||||
 | 
					        becomes :attr:`~wuttaweb.grids.base.Grid.key`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The default logic for this method will call
 | 
				
			||||||
 | 
					        :meth:`get_route_prefix()` and return that value as-is.  A
 | 
				
			||||||
 | 
					        subclass may override by assigning :attr:`grid_key`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if hasattr(cls, 'grid_key'):
 | 
				
			||||||
 | 
					            return cls.grid_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return cls.get_route_prefix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def get_config_title(cls):
 | 
					    def get_config_title(cls):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -794,6 +1135,13 @@ class MasterView(View):
 | 
				
			||||||
            config.add_view(cls, attr='index',
 | 
					            config.add_view(cls, attr='index',
 | 
				
			||||||
                            route_name=route_prefix)
 | 
					                            route_name=route_prefix)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # view
 | 
				
			||||||
 | 
					        if cls.viewable:
 | 
				
			||||||
 | 
					            instance_url_prefix = cls.get_instance_url_prefix()
 | 
				
			||||||
 | 
					            config.add_route(f'{route_prefix}.view', instance_url_prefix)
 | 
				
			||||||
 | 
					            config.add_view(cls, attr='view',
 | 
				
			||||||
 | 
					                            route_name=f'{route_prefix}.view')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # configure
 | 
					        # configure
 | 
				
			||||||
        if cls.configurable:
 | 
					        if cls.configurable:
 | 
				
			||||||
            config.add_route(f'{route_prefix}.configure',
 | 
					            config.add_route(f'{route_prefix}.configure',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,8 +26,11 @@ Views for app settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from collections import OrderedDict
 | 
					from collections import OrderedDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from wuttjamaican.db.model import Setting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb.views import MasterView
 | 
					from wuttaweb.views import MasterView
 | 
				
			||||||
from wuttaweb.util import get_libver, get_liburl
 | 
					from wuttaweb.util import get_libver, get_liburl
 | 
				
			||||||
 | 
					from wuttaweb.db import Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppInfoView(MasterView):
 | 
					class AppInfoView(MasterView):
 | 
				
			||||||
| 
						 | 
					@ -38,10 +41,16 @@ class AppInfoView(MasterView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    * ``/appinfo/``
 | 
					    * ``/appinfo/``
 | 
				
			||||||
    * ``/appinfo/configure``
 | 
					    * ``/appinfo/configure``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    See also :class:`SettingView`.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    model_name = 'AppInfo'
 | 
					    model_name = 'AppInfo'
 | 
				
			||||||
    model_title_plural = "App Info"
 | 
					    model_title_plural = "App Info"
 | 
				
			||||||
    route_prefix = 'appinfo'
 | 
					    route_prefix = 'appinfo'
 | 
				
			||||||
 | 
					    has_grid = False
 | 
				
			||||||
 | 
					    viewable = False
 | 
				
			||||||
 | 
					    editable = False
 | 
				
			||||||
 | 
					    deletable = False
 | 
				
			||||||
    configurable = True
 | 
					    configurable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_get_simple_settings(self):
 | 
					    def configure_get_simple_settings(self):
 | 
				
			||||||
| 
						 | 
					@ -103,8 +112,6 @@ class AppInfoView(MasterView):
 | 
				
			||||||
            ('bb_vue_fontawesome', "(BB) @fortawesome/vue-fontawesome"),
 | 
					            ('bb_vue_fontawesome', "(BB) @fortawesome/vue-fontawesome"),
 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # import ipdb; ipdb.set_trace()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for key in weblibs:
 | 
					        for key in weblibs:
 | 
				
			||||||
            title = weblibs[key]
 | 
					            title = weblibs[key]
 | 
				
			||||||
            weblibs[key] = {
 | 
					            weblibs[key] = {
 | 
				
			||||||
| 
						 | 
					@ -127,12 +134,79 @@ class AppInfoView(MasterView):
 | 
				
			||||||
        return context
 | 
					        return context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SettingView(MasterView):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Master view for the "raw" settings table.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Notable URLs provided by this class:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * ``/settings/``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    See also :class:`AppInfoView`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    model_class = Setting
 | 
				
			||||||
 | 
					    model_title = "Raw Setting"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: this should be deduced by master
 | 
				
			||||||
 | 
					    model_key = 'name'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: try removing these
 | 
				
			||||||
 | 
					    grid_columns = [
 | 
				
			||||||
 | 
					        'name',
 | 
				
			||||||
 | 
					        'value',
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    form_fields = list(grid_columns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: should define query, let master handle the rest
 | 
				
			||||||
 | 
					    def index_get_grid_data(self, session=None):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        session = session or Session()
 | 
				
			||||||
 | 
					        query = session.query(model.Setting)\
 | 
				
			||||||
 | 
					                          .order_by(model.Setting.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        settings = []
 | 
				
			||||||
 | 
					        for setting in query:
 | 
				
			||||||
 | 
					            settings.append(self.normalize_setting(setting))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: master should handle this (but not as dict)
 | 
				
			||||||
 | 
					    def normalize_setting(self, setting):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            'name': setting.name,
 | 
				
			||||||
 | 
					            'value': setting.value,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: master should handle this
 | 
				
			||||||
 | 
					    def get_instance(self, session=None):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					        session = session or Session()
 | 
				
			||||||
 | 
					        name = self.request.matchdict['name']
 | 
				
			||||||
 | 
					        setting = session.query(model.Setting).get(name)
 | 
				
			||||||
 | 
					        if setting:
 | 
				
			||||||
 | 
					            return self.normalize_setting(setting)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self.notfound()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: master should handle this
 | 
				
			||||||
 | 
					    def get_instance_title(self, setting):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					        return setting['name']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):
 | 
				
			||||||
    base = globals()
 | 
					    base = globals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    AppInfoView = kwargs.get('AppInfoView', base['AppInfoView'])
 | 
					    AppInfoView = kwargs.get('AppInfoView', base['AppInfoView'])
 | 
				
			||||||
    AppInfoView.defaults(config)
 | 
					    AppInfoView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SettingView = kwargs.get('SettingView', base['SettingView'])
 | 
				
			||||||
 | 
					    SettingView.defaults(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):
 | 
				
			||||||
    defaults(config)
 | 
					    defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,8 +59,8 @@ class TestForm(TestCase):
 | 
				
			||||||
    def tearDown(self):
 | 
					    def tearDown(self):
 | 
				
			||||||
        testing.tearDown()
 | 
					        testing.tearDown()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_form(self, request=None, **kwargs):
 | 
					    def make_form(self, **kwargs):
 | 
				
			||||||
        return base.Form(request or self.request, **kwargs)
 | 
					        return base.Form(self.request, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_schema(self):
 | 
					    def make_schema(self):
 | 
				
			||||||
        schema = colander.Schema(children=[
 | 
					        schema = colander.Schema(children=[
 | 
				
			||||||
| 
						 | 
					@ -124,19 +124,33 @@ class TestForm(TestCase):
 | 
				
			||||||
        self.assertIs(form.schema, schema)
 | 
					        self.assertIs(form.schema, schema)
 | 
				
			||||||
        self.assertIs(form.get_schema(), schema)
 | 
					        self.assertIs(form.get_schema(), schema)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # auto-generating schema not yet supported
 | 
					        # schema is auto-generated if fields provided
 | 
				
			||||||
        form = self.make_form(fields=['foo', 'bar'])
 | 
					        form = self.make_form(fields=['foo', 'bar'])
 | 
				
			||||||
 | 
					        schema = form.get_schema()
 | 
				
			||||||
 | 
					        self.assertEqual(len(schema.children), 2)
 | 
				
			||||||
 | 
					        self.assertEqual(schema['foo'].name, 'foo')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # but auto-generating without fields is not supported
 | 
				
			||||||
 | 
					        form = self.make_form()
 | 
				
			||||||
        self.assertIsNone(form.schema)
 | 
					        self.assertIsNone(form.schema)
 | 
				
			||||||
        self.assertRaises(NotImplementedError, form.get_schema)
 | 
					        self.assertRaises(NotImplementedError, form.get_schema)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_deform(self):
 | 
					    def test_get_deform(self):
 | 
				
			||||||
        schema = self.make_schema()
 | 
					        schema = self.make_schema()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # basic
 | 
				
			||||||
        form = self.make_form(schema=schema)
 | 
					        form = self.make_form(schema=schema)
 | 
				
			||||||
        self.assertFalse(hasattr(form, 'deform_form'))
 | 
					        self.assertFalse(hasattr(form, 'deform_form'))
 | 
				
			||||||
        dform = form.get_deform()
 | 
					        dform = form.get_deform()
 | 
				
			||||||
        self.assertIsInstance(dform, deform.Form)
 | 
					        self.assertIsInstance(dform, deform.Form)
 | 
				
			||||||
        self.assertIs(form.deform_form, dform)
 | 
					        self.assertIs(form.deform_form, dform)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # with model instance / cstruct
 | 
				
			||||||
 | 
					        myobj = {'foo': 'one', 'bar': 'two'}
 | 
				
			||||||
 | 
					        form = self.make_form(schema=schema, model_instance=myobj)
 | 
				
			||||||
 | 
					        dform = form.get_deform()
 | 
				
			||||||
 | 
					        self.assertEqual(dform.cstruct, myobj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_label(self):
 | 
					    def test_get_label(self):
 | 
				
			||||||
        form = self.make_form(fields=['foo', 'bar'])
 | 
					        form = self.make_form(fields=['foo', 'bar'])
 | 
				
			||||||
        self.assertEqual(form.get_label('foo'), "Foo")
 | 
					        self.assertEqual(form.get_label('foo'), "Foo")
 | 
				
			||||||
| 
						 | 
					@ -193,6 +207,13 @@ class TestForm(TestCase):
 | 
				
			||||||
        # nb. no error message
 | 
					        # nb. no error message
 | 
				
			||||||
        self.assertNotIn('message', html)
 | 
					        self.assertNotIn('message', html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # readonly
 | 
				
			||||||
 | 
					        html = form.render_vue_field('foo', readonly=True)
 | 
				
			||||||
 | 
					        self.assertIn('<b-field :horizontal="true" label="Foo">', html)
 | 
				
			||||||
 | 
					        self.assertNotIn('<b-input name="foo"', html)
 | 
				
			||||||
 | 
					        # nb. no error message
 | 
				
			||||||
 | 
					        self.assertNotIn('message', html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # with single "static" error
 | 
					        # with single "static" error
 | 
				
			||||||
        dform['foo'].error = MagicMock(msg="something is wrong")
 | 
					        dform['foo'].error = MagicMock(msg="something is wrong")
 | 
				
			||||||
        html = form.render_vue_field('foo')
 | 
					        html = form.render_vue_field('foo')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										0
									
								
								tests/grids/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/grids/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										150
									
								
								tests/grids/test_base.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								tests/grids/test_base.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,150 @@
 | 
				
			||||||
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from unittest import TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pyramid import testing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from wuttjamaican.conf import WuttaConfig
 | 
				
			||||||
 | 
					from wuttaweb.grids import base
 | 
				
			||||||
 | 
					from wuttaweb.forms import FieldList
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestGrid(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        self.config = WuttaConfig(defaults={
 | 
				
			||||||
 | 
					            'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.pyramid_config = testing.setUp(request=self.request, settings={
 | 
				
			||||||
 | 
					            'mako.directories': ['wuttaweb:templates'],
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        testing.tearDown()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_grid(self, request=None, **kwargs):
 | 
				
			||||||
 | 
					        return base.Grid(request or self.request, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_constructor(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # empty
 | 
				
			||||||
 | 
					        grid = self.make_grid()
 | 
				
			||||||
 | 
					        self.assertIsNone(grid.key)
 | 
				
			||||||
 | 
					        self.assertIsNone(grid.columns)
 | 
				
			||||||
 | 
					        self.assertIsNone(grid.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # now with columns
 | 
				
			||||||
 | 
					        grid = self.make_grid(columns=['foo', 'bar'])
 | 
				
			||||||
 | 
					        self.assertIsInstance(grid.columns, FieldList)
 | 
				
			||||||
 | 
					        self.assertEqual(grid.columns, ['foo', 'bar'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_vue_tagname(self):
 | 
				
			||||||
 | 
					        grid = self.make_grid()
 | 
				
			||||||
 | 
					        self.assertEqual(grid.vue_tagname, 'wutta-grid')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_vue_component(self):
 | 
				
			||||||
 | 
					        grid = self.make_grid()
 | 
				
			||||||
 | 
					        self.assertEqual(grid.vue_component, 'WuttaGrid')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_render_vue_tag(self):
 | 
				
			||||||
 | 
					        grid = self.make_grid(columns=['foo', 'bar'])
 | 
				
			||||||
 | 
					        html = grid.render_vue_tag()
 | 
				
			||||||
 | 
					        self.assertEqual(html, '<wutta-grid></wutta-grid>')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_render_vue_template(self):
 | 
				
			||||||
 | 
					        self.pyramid_config.include('pyramid_mako')
 | 
				
			||||||
 | 
					        self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
 | 
				
			||||||
 | 
					                                           'pyramid.events.BeforeRender')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        grid = self.make_grid(columns=['foo', 'bar'])
 | 
				
			||||||
 | 
					        html = grid.render_vue_template()
 | 
				
			||||||
 | 
					        self.assertIn('<script type="text/x-template" id="wutta-grid-template">', html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_vue_columns(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # error if no columns are set
 | 
				
			||||||
 | 
					        grid = self.make_grid()
 | 
				
			||||||
 | 
					        self.assertRaises(ValueError, grid.get_vue_columns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # otherwise get back field/label dicts
 | 
				
			||||||
 | 
					        grid = self.make_grid(columns=['foo', 'bar'])
 | 
				
			||||||
 | 
					        columns = grid.get_vue_columns()
 | 
				
			||||||
 | 
					        first = columns[0]
 | 
				
			||||||
 | 
					        self.assertEqual(first['field'], 'foo')
 | 
				
			||||||
 | 
					        self.assertEqual(first['label'], 'Foo')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_vue_data(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # null by default
 | 
				
			||||||
 | 
					        grid = self.make_grid()
 | 
				
			||||||
 | 
					        data = grid.get_vue_data()
 | 
				
			||||||
 | 
					        self.assertIsNone(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # is usually a list
 | 
				
			||||||
 | 
					        mydata = [
 | 
				
			||||||
 | 
					            {'foo': 'bar'},
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        grid = self.make_grid(data=mydata)
 | 
				
			||||||
 | 
					        data = grid.get_vue_data()
 | 
				
			||||||
 | 
					        self.assertIs(data, mydata)
 | 
				
			||||||
 | 
					        self.assertEqual(data, [{'foo': 'bar'}])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # if grid has actions, that list may be supplemented
 | 
				
			||||||
 | 
					        grid.actions.append(base.GridAction(self.request, 'view', url='/blarg'))
 | 
				
			||||||
 | 
					        data = grid.get_vue_data()
 | 
				
			||||||
 | 
					        self.assertIsNot(data, mydata)
 | 
				
			||||||
 | 
					        self.assertEqual(data, [{'foo': 'bar', '_action_url_view': '/blarg'}])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestGridAction(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        self.config = WuttaConfig()
 | 
				
			||||||
 | 
					        self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_action(self, key, **kwargs):
 | 
				
			||||||
 | 
					        return base.GridAction(self.request, key, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_render_icon(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # icon is derived from key by default
 | 
				
			||||||
 | 
					        action = self.make_action('blarg')
 | 
				
			||||||
 | 
					        html = action.render_icon()
 | 
				
			||||||
 | 
					        self.assertIn('<i class="fas fa-blarg">', html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # oruga not yet supported
 | 
				
			||||||
 | 
					        self.request.use_oruga = True
 | 
				
			||||||
 | 
					        self.assertRaises(NotImplementedError, action.render_icon)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_render_label(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # label is derived from key by default
 | 
				
			||||||
 | 
					        action = self.make_action('blarg')
 | 
				
			||||||
 | 
					        label = action.render_label()
 | 
				
			||||||
 | 
					        self.assertEqual(label, "Blarg")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # otherwise use what caller provides
 | 
				
			||||||
 | 
					        action = self.make_action('foo', label="Bar")
 | 
				
			||||||
 | 
					        label = action.render_label()
 | 
				
			||||||
 | 
					        self.assertEqual(label, "Bar")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_url(self):
 | 
				
			||||||
 | 
					        obj = {'foo': 'bar'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # null by default
 | 
				
			||||||
 | 
					        action = self.make_action('blarg')
 | 
				
			||||||
 | 
					        url = action.get_url(obj)
 | 
				
			||||||
 | 
					        self.assertIsNone(url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # or can be "static"
 | 
				
			||||||
 | 
					        action = self.make_action('blarg', url='/foo')
 | 
				
			||||||
 | 
					        url = action.get_url(obj)
 | 
				
			||||||
 | 
					        self.assertEqual(url, '/foo')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # or can be "dynamic"
 | 
				
			||||||
 | 
					        action = self.make_action('blarg', url=lambda o, i: '/yeehaw')
 | 
				
			||||||
 | 
					        url = action.get_url(obj)
 | 
				
			||||||
 | 
					        self.assertEqual(url, '/yeehaw')
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
 | 
				
			||||||
from wuttjamaican.conf import WuttaConfig
 | 
					from wuttjamaican.conf import WuttaConfig
 | 
				
			||||||
from wuttaweb.views import base
 | 
					from wuttaweb.views import base
 | 
				
			||||||
from wuttaweb.forms import Form
 | 
					from wuttaweb.forms import Form
 | 
				
			||||||
 | 
					from wuttaweb.grids import Grid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestView(TestCase):
 | 
					class TestView(TestCase):
 | 
				
			||||||
| 
						 | 
					@ -31,6 +32,10 @@ class TestView(TestCase):
 | 
				
			||||||
        form = self.view.make_form()
 | 
					        form = self.view.make_form()
 | 
				
			||||||
        self.assertIsInstance(form, Form)
 | 
					        self.assertIsInstance(form, Form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_make_grid(self):
 | 
				
			||||||
 | 
					        grid = self.view.make_grid()
 | 
				
			||||||
 | 
					        self.assertIsInstance(grid, Grid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_notfound(self):
 | 
					    def test_notfound(self):
 | 
				
			||||||
        error = self.view.notfound()
 | 
					        error = self.view.notfound()
 | 
				
			||||||
        self.assertIsInstance(error, HTTPNotFound)
 | 
					        self.assertIsInstance(error, HTTPNotFound)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_defaults(self):
 | 
					    def test_defaults(self):
 | 
				
			||||||
        master.MasterView.model_name = 'Widget'
 | 
					        master.MasterView.model_name = 'Widget'
 | 
				
			||||||
 | 
					        with patch.object(master.MasterView, 'viewable', new=False):
 | 
				
			||||||
            # TODO: should inspect pyramid routes after this, to be certain
 | 
					            # TODO: should inspect pyramid routes after this, to be certain
 | 
				
			||||||
            master.MasterView.defaults(self.pyramid_config)
 | 
					            master.MasterView.defaults(self.pyramid_config)
 | 
				
			||||||
        del master.MasterView.model_name
 | 
					        del master.MasterView.model_name
 | 
				
			||||||
| 
						 | 
					@ -122,6 +123,16 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
        self.assertEqual(master.MasterView.get_model_title_plural(), "Dinosaurs")
 | 
					        self.assertEqual(master.MasterView.get_model_title_plural(), "Dinosaurs")
 | 
				
			||||||
        del master.MasterView.model_class
 | 
					        del master.MasterView.model_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_model_key(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # error by default (since no model class)
 | 
				
			||||||
 | 
					        self.assertRaises(AttributeError, master.MasterView.get_model_key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # subclass may specify model key
 | 
				
			||||||
 | 
					        master.MasterView.model_key = 'uuid'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_model_key(), ('uuid',))
 | 
				
			||||||
 | 
					        del master.MasterView.model_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_route_prefix(self):
 | 
					    def test_get_route_prefix(self):
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # error by default (since no model class)
 | 
					        # error by default (since no model class)
 | 
				
			||||||
| 
						 | 
					@ -179,6 +190,25 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
        self.assertEqual(master.MasterView.get_url_prefix(), '/machines')
 | 
					        self.assertEqual(master.MasterView.get_url_prefix(), '/machines')
 | 
				
			||||||
        del master.MasterView.model_class
 | 
					        del master.MasterView.model_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_instance_url_prefix(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # error by default (since no model class)
 | 
				
			||||||
 | 
					        self.assertRaises(AttributeError, master.MasterView.get_instance_url_prefix)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # typical example with url_prefix and simple key
 | 
				
			||||||
 | 
					        master.MasterView.url_prefix = '/widgets'
 | 
				
			||||||
 | 
					        master.MasterView.model_key = 'uuid'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_instance_url_prefix(), '/widgets/{uuid}')
 | 
				
			||||||
 | 
					        del master.MasterView.url_prefix
 | 
				
			||||||
 | 
					        del master.MasterView.model_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # typical example with composite key
 | 
				
			||||||
 | 
					        master.MasterView.url_prefix = '/widgets'
 | 
				
			||||||
 | 
					        master.MasterView.model_key = ('foo', 'bar')
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_instance_url_prefix(), '/widgets/{foo}|{bar}')
 | 
				
			||||||
 | 
					        del master.MasterView.url_prefix
 | 
				
			||||||
 | 
					        del master.MasterView.model_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_template_prefix(self):
 | 
					    def test_get_template_prefix(self):
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # error by default (since no model class)
 | 
					        # error by default (since no model class)
 | 
				
			||||||
| 
						 | 
					@ -215,6 +245,37 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
        self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
 | 
					        self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
 | 
				
			||||||
        del master.MasterView.model_class
 | 
					        del master.MasterView.model_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_grid_key(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # error by default (since no model class)
 | 
				
			||||||
 | 
					        self.assertRaises(AttributeError, master.MasterView.get_grid_key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # subclass may specify grid key
 | 
				
			||||||
 | 
					        master.MasterView.grid_key = 'widgets'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_grid_key(), 'widgets')
 | 
				
			||||||
 | 
					        del master.MasterView.grid_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # or it may specify route prefix
 | 
				
			||||||
 | 
					        master.MasterView.route_prefix = 'trucks'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_grid_key(), 'trucks')
 | 
				
			||||||
 | 
					        del master.MasterView.route_prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # or it may specify *normalized* model name
 | 
				
			||||||
 | 
					        master.MasterView.model_name_normalized = 'blaster'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_grid_key(), 'blasters')
 | 
				
			||||||
 | 
					        del master.MasterView.model_name_normalized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # or it may specify *standard* model name
 | 
				
			||||||
 | 
					        master.MasterView.model_name = 'Dinosaur'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_grid_key(), 'dinosaurs')
 | 
				
			||||||
 | 
					        del master.MasterView.model_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # or it may specify model class
 | 
				
			||||||
 | 
					        MyModel = MagicMock(__name__='Machine')
 | 
				
			||||||
 | 
					        master.MasterView.model_class = MyModel
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_grid_key(), 'machines')
 | 
				
			||||||
 | 
					        del master.MasterView.model_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_config_title(self):
 | 
					    def test_get_config_title(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # error by default (since no model class)
 | 
					        # error by default (since no model class)
 | 
				
			||||||
| 
						 | 
					@ -250,12 +311,6 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
    # support methods
 | 
					    # support methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_index_title(self):
 | 
					 | 
				
			||||||
        master.MasterView.model_title_plural = "Wutta Widgets"
 | 
					 | 
				
			||||||
        view = master.MasterView(self.request)
 | 
					 | 
				
			||||||
        self.assertEqual(view.get_index_title(), "Wutta Widgets")
 | 
					 | 
				
			||||||
        del master.MasterView.model_title_plural
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_render_to_response(self):
 | 
					    def test_render_to_response(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def widgets(request): return {}
 | 
					        def widgets(request): return {}
 | 
				
			||||||
| 
						 | 
					@ -286,21 +341,50 @@ class TestMasterView(WebTestCase):
 | 
				
			||||||
        self.assertRaises(IOError, view.render_to_response, 'nonexistent', {})
 | 
					        self.assertRaises(IOError, view.render_to_response, 'nonexistent', {})
 | 
				
			||||||
        del master.MasterView.model_name
 | 
					        del master.MasterView.model_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_index_title(self):
 | 
				
			||||||
 | 
					        master.MasterView.model_title_plural = "Wutta Widgets"
 | 
				
			||||||
 | 
					        view = master.MasterView(self.request)
 | 
				
			||||||
 | 
					        self.assertEqual(view.get_index_title(), "Wutta Widgets")
 | 
				
			||||||
 | 
					        del master.MasterView.model_title_plural
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_instance(self):
 | 
				
			||||||
 | 
					        view = master.MasterView(self.request)
 | 
				
			||||||
 | 
					        self.assertRaises(NotImplementedError, view.get_instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # view methods
 | 
					    # view methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_index(self):
 | 
					    def test_index(self):
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # basic sanity check using /appinfo
 | 
					        # sanity/coverage check using /settings/
 | 
				
			||||||
        master.MasterView.model_name = 'AppInfo'
 | 
					        master.MasterView.model_name = 'Setting'
 | 
				
			||||||
        master.MasterView.route_prefix = 'appinfo'
 | 
					        master.MasterView.model_key = 'name'
 | 
				
			||||||
        master.MasterView.template_prefix = '/appinfo'
 | 
					        master.MasterView.grid_columns = ['name', 'value']
 | 
				
			||||||
        view = master.MasterView(self.request)
 | 
					        view = master.MasterView(self.request)
 | 
				
			||||||
        response = view.index()
 | 
					        response = view.index()
 | 
				
			||||||
 | 
					        # then again with data, to include view action url
 | 
				
			||||||
 | 
					        data = [{'name': 'foo', 'value': 'bar'}]
 | 
				
			||||||
 | 
					        with patch.object(view, 'index_get_grid_data', return_value=data):
 | 
				
			||||||
 | 
					            response = view.index()
 | 
				
			||||||
        del master.MasterView.model_name
 | 
					        del master.MasterView.model_name
 | 
				
			||||||
        del master.MasterView.route_prefix
 | 
					        del master.MasterView.model_key
 | 
				
			||||||
        del master.MasterView.template_prefix
 | 
					        del master.MasterView.grid_columns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_view(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # sanity/coverage check using /settings/XXX
 | 
				
			||||||
 | 
					        master.MasterView.model_name = 'Setting'
 | 
				
			||||||
 | 
					        master.MasterView.grid_columns = ['name', 'value']
 | 
				
			||||||
 | 
					        master.MasterView.form_fields = ['name', 'value']
 | 
				
			||||||
 | 
					        view = master.MasterView(self.request)
 | 
				
			||||||
 | 
					        setting = {'name': 'foo.bar', 'value': 'baz'}
 | 
				
			||||||
 | 
					        self.request.matchdict = {'name': 'foo.bar'}
 | 
				
			||||||
 | 
					        with patch.object(view, 'get_instance', return_value=setting):
 | 
				
			||||||
 | 
					            response = view.view()
 | 
				
			||||||
 | 
					        del master.MasterView.model_name
 | 
				
			||||||
 | 
					        del master.MasterView.grid_columns
 | 
				
			||||||
 | 
					        del master.MasterView.form_fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_configure(self):
 | 
					    def test_configure(self):
 | 
				
			||||||
        model = self.app.model
 | 
					        model = self.app.model
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,22 +2,66 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from tests.views.utils import WebTestCase
 | 
					from tests.views.utils import WebTestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pyramid.httpexceptions import HTTPNotFound
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb.views import settings
 | 
					from wuttaweb.views import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestAppInfoView(WebTestCase):
 | 
					class TestAppInfoView(WebTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_view(self):
 | 
				
			||||||
 | 
					        return settings.AppInfoView(self.request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_index(self):
 | 
					    def test_index(self):
 | 
				
			||||||
        # sanity/coverage check
 | 
					        # sanity/coverage check
 | 
				
			||||||
        view = settings.AppInfoView(self.request)
 | 
					        view = self.make_view()
 | 
				
			||||||
        response = view.index()
 | 
					        response = view.index()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_configure_get_simple_settings(self):
 | 
					    def test_configure_get_simple_settings(self):
 | 
				
			||||||
        # sanity/coverage check
 | 
					        # sanity/coverage check
 | 
				
			||||||
        view = settings.AppInfoView(self.request)
 | 
					        view = self.make_view()
 | 
				
			||||||
        simple = view.configure_get_simple_settings()
 | 
					        simple = view.configure_get_simple_settings()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_configure_get_context(self):
 | 
					    def test_configure_get_context(self):
 | 
				
			||||||
        # sanity/coverage check
 | 
					        # sanity/coverage check
 | 
				
			||||||
        view = settings.AppInfoView(self.request)
 | 
					        view = self.make_view()
 | 
				
			||||||
        context = view.configure_get_context()
 | 
					        context = view.configure_get_context()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestSettingView(WebTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_view(self):
 | 
				
			||||||
 | 
					        return settings.SettingView(self.request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_index_get_grid_data(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # empty data by default
 | 
				
			||||||
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					        data = view.index_get_grid_data(session=self.session)
 | 
				
			||||||
 | 
					        self.assertEqual(len(data), 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # unless we save some settings
 | 
				
			||||||
 | 
					        self.app.save_setting(self.session, 'foo', 'bar')
 | 
				
			||||||
 | 
					        self.session.commit()
 | 
				
			||||||
 | 
					        data = view.index_get_grid_data(session=self.session)
 | 
				
			||||||
 | 
					        self.assertEqual(len(data), 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_instance(self):
 | 
				
			||||||
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					        self.request.matchdict = {'name': 'foo'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # setting not found
 | 
				
			||||||
 | 
					        setting = view.get_instance(session=self.session)
 | 
				
			||||||
 | 
					        self.assertIsInstance(setting, HTTPNotFound)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # setting is returned
 | 
				
			||||||
 | 
					        self.app.save_setting(self.session, 'foo', 'bar')
 | 
				
			||||||
 | 
					        self.session.commit()
 | 
				
			||||||
 | 
					        setting = view.get_instance(session=self.session)
 | 
				
			||||||
 | 
					        self.assertEqual(setting, {'name': 'foo', 'value': 'bar'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_instance_title(self):
 | 
				
			||||||
 | 
					        setting = {'name': 'foo', 'value': 'bar'}
 | 
				
			||||||
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					        title = view.get_instance_title(setting)
 | 
				
			||||||
 | 
					        self.assertEqual(title, 'foo')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue