Add basic "mobile index" master view, plus support for demo mode
This commit is contained in:
		
							parent
							
								
									9808bb3a91
								
							
						
					
					
						commit
						581a21bd9d
					
				
					 16 changed files with 301 additions and 16 deletions
				
			
		| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
# -*- coding: utf-8 -*-
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  Rattail -- Retail Software Framework
 | 
					#  Rattail -- Retail Software Framework
 | 
				
			||||||
#  Copyright © 2010-2015 Lance Edgar
 | 
					#  Copyright © 2010-2017 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Rattail.
 | 
					#  This file is part of Rattail.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,9 @@
 | 
				
			||||||
Grids and Friends
 | 
					Grids and Friends
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from __future__ import unicode_literals, absolute_import
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import filters
 | 
					from . import filters
 | 
				
			||||||
from .core import Grid, GridColumn, GridAction
 | 
					from .core import Grid, GridColumn, GridAction
 | 
				
			||||||
from .alchemy import AlchemyGrid
 | 
					from .alchemy import AlchemyGrid
 | 
				
			||||||
 | 
					from .mobile import MobileGrid
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										71
									
								
								tailbone/newgrids/mobile.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								tailbone/newgrids/mobile.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,71 @@
 | 
				
			||||||
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					################################################################################
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Rattail -- Retail Software Framework
 | 
				
			||||||
 | 
					#  Copyright © 2010-2017 Lance Edgar
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  This file is part of Rattail.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Rattail is free software: you can redistribute it and/or modify it under the
 | 
				
			||||||
 | 
					#  terms of the GNU Affero General Public License as published by the Free
 | 
				
			||||||
 | 
					#  Software Foundation, either version 3 of the License, or (at your option)
 | 
				
			||||||
 | 
					#  any later version.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Rattail 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 Affero General Public License for
 | 
				
			||||||
 | 
					#  more details.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  You should have received a copy of the GNU Affero General Public License
 | 
				
			||||||
 | 
					#  along with Rattail.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					################################################################################
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Mobile Grids
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from __future__ import unicode_literals, absolute_import
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from webhelpers.html import tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Grid(object):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Base class for all grids
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    configured = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, key, data=None, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Grid constructor
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.key = key
 | 
				
			||||||
 | 
					        self.data = data
 | 
				
			||||||
 | 
					        for k, v in kwargs.items():
 | 
				
			||||||
 | 
					            setattr(self, k, v)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __iter__(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        This grid supports iteration, over its data
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return iter(self.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure(self, include=None):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Configure the grid.  This must define which columns to display and in
 | 
				
			||||||
 | 
					        which order, etc.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.configured = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def view_url(self, obj, mobile=False):
 | 
				
			||||||
 | 
					        route = '{}{}.view'.format('mobile.' if mobile else '', self.route_prefix)
 | 
				
			||||||
 | 
					        return self.request.route_url(route, uuid=obj.uuid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MobileGrid(Grid):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Base class for all mobile grids
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_object(self, obj):
 | 
				
			||||||
 | 
					        return tags.link_to(obj, self.view_url(obj, mobile=True))
 | 
				
			||||||
| 
						 | 
					@ -32,3 +32,9 @@ div.field-wrapper div.field input[type="password"] {
 | 
				
			||||||
div.buttons input {
 | 
					div.buttons input {
 | 
				
			||||||
    margin: auto 5px;
 | 
					    margin: auto 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* this is for "login as chuck" tip in demo mode */
 | 
				
			||||||
 | 
					.tips {
 | 
				
			||||||
 | 
					    margin-top: 2em;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,3 +38,9 @@
 | 
				
			||||||
${self.logo()}
 | 
					${self.logo()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
${self.login_form()}
 | 
					${self.login_form()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					% if request.rattail_config.demo():
 | 
				
			||||||
 | 
					    <p class="tips">
 | 
				
			||||||
 | 
					      Login with <strong>chuck / admin</strong> for full demo access
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					% endif
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								tailbone/templates/mobile/master/index.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tailbone/templates/mobile/master/index.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					## ##############################################################################
 | 
				
			||||||
 | 
					## 
 | 
				
			||||||
 | 
					## Default master 'index' template for mobile.  Features a somewhat abbreviated
 | 
				
			||||||
 | 
					## data table and (hopefully) exposes a way to filter and sort the data, etc.
 | 
				
			||||||
 | 
					## 
 | 
				
			||||||
 | 
					## ##############################################################################
 | 
				
			||||||
 | 
					<%inherit file="/mobile/base.mako" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="title()">${model_title_plural}</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<ul data-role="listview">
 | 
				
			||||||
 | 
					  % for obj in grid:
 | 
				
			||||||
 | 
					      <li>${grid.render_object(obj)}</li>
 | 
				
			||||||
 | 
					  % endfor
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
							
								
								
									
										12
									
								
								tailbone/templates/mobile/master/view.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								tailbone/templates/mobile/master/view.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					## ##############################################################################
 | 
				
			||||||
 | 
					## 
 | 
				
			||||||
 | 
					## Default master 'view' template for mobile.  Features a basic field list, and
 | 
				
			||||||
 | 
					## links to edit/delete the object when appropriate.
 | 
				
			||||||
 | 
					## 
 | 
				
			||||||
 | 
					## ##############################################################################
 | 
				
			||||||
 | 
					<%inherit file="/mobile/base.mako" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="title()">${model_title}: ${instance_title}</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>TODO: display fieldset for object</p>
 | 
				
			||||||
| 
						 | 
					@ -162,6 +162,10 @@ class AuthenticationView(View):
 | 
				
			||||||
        if not self.request.user:
 | 
					        if not self.request.user:
 | 
				
			||||||
            return self.redirect(self.request.route_url('home'))
 | 
					            return self.redirect(self.request.route_url('home'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.rattail_config.demo() and self.request.user.username == 'chuck':
 | 
				
			||||||
 | 
					            self.request.session.flash("Cannot change password for 'chuck' in demo mode", 'error')
 | 
				
			||||||
 | 
					            return self.redirect(self.request.get_referrer())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        form = Form(self.request, schema=ChangePassword, state=self.request.user)
 | 
					        form = Form(self.request, schema=ChangePassword, state=self.request.user)
 | 
				
			||||||
        if form.validate():
 | 
					        if form.validate():
 | 
				
			||||||
            set_user_password(self.request.user, form.data['new_password'])
 | 
					            set_user_password(self.request.user, form.data['new_password'])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -401,7 +401,7 @@ class BatchMasterView(MasterView):
 | 
				
			||||||
        del batch.data_rows[:]
 | 
					        del batch.data_rows[:]
 | 
				
			||||||
        super(BatchMasterView, self).delete_instance(batch)
 | 
					        super(BatchMasterView, self).delete_instance(batch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_fallback_templates(self, template):
 | 
					    def get_fallback_templates(self, template, mobile=False):
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            '/newbatch/{}.mako'.format(template),
 | 
					            '/newbatch/{}.mako'.format(template),
 | 
				
			||||||
            '/master/{}.mako'.format(template),
 | 
					            '/master/{}.mako'.format(template),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,6 +45,7 @@ class CustomersView(MasterView):
 | 
				
			||||||
    Master view for the Customer class.
 | 
					    Master view for the Customer class.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    model_class = model.Customer
 | 
					    model_class = model.Customer
 | 
				
			||||||
 | 
					    supports_mobile = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):
 | 
					    def configure_grid(self, g):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -81,6 +82,10 @@ class CustomersView(MasterView):
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            readonly=True)
 | 
					            readonly=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_mobile_data(self, session=None):
 | 
				
			||||||
 | 
					        # TODO: hacky!
 | 
				
			||||||
 | 
					        return self.get_data(session=session).order_by(model.Customer.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_instance(self):
 | 
					    def get_instance(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            instance = super(CustomersView, self).get_instance()
 | 
					            instance = super(CustomersView, self).get_instance()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -140,6 +140,16 @@ class ProfilesView(MasterView):
 | 
				
			||||||
    def get_instance_title(self, email):
 | 
					    def get_instance_title(self, email):
 | 
				
			||||||
        return email['_email'].get_complete_subject(render=False)
 | 
					        return email['_email'].get_complete_subject(render=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def editable_instance(self, profile):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return profile['key'] != 'user_feedback'
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deletable_instance(self, profile):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return profile['key'] != 'user_feedback'
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_form(self, email, **kwargs):
 | 
					    def make_form(self, email, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Make a simple form for use with CRUD views.
 | 
					        Make a simple form for use with CRUD views.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -111,6 +111,16 @@ class EmployeesView(MasterView):
 | 
				
			||||||
            q = q.filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)
 | 
					            q = q.filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)
 | 
				
			||||||
        return q
 | 
					        return q
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def editable_instance(self, employee):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return not bool(employee.user and employee.username == 'chuck')
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deletable_instance(self, employee):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return not bool(employee.user and employee.username == 'chuck')
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _preconfigure_fieldset(self, fs):
 | 
					    def _preconfigure_fieldset(self, fs):
 | 
				
			||||||
        fs.append(forms.AssociationProxyField('first_name'))
 | 
					        fs.append(forms.AssociationProxyField('first_name'))
 | 
				
			||||||
        fs.append(forms.AssociationProxyField('last_name'))
 | 
					        fs.append(forms.AssociationProxyField('last_name'))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,7 @@ from webhelpers.html import HTML, tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from tailbone import forms, newgrids as grids
 | 
					from tailbone import forms, newgrids as grids
 | 
				
			||||||
from tailbone.views import View
 | 
					from tailbone.views import View
 | 
				
			||||||
from tailbone.newgrids import filters, AlchemyGrid, GridAction
 | 
					from tailbone.newgrids import filters, AlchemyGrid, GridAction, MobileGrid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MasterView(View):
 | 
					class MasterView(View):
 | 
				
			||||||
| 
						 | 
					@ -57,6 +57,8 @@ class MasterView(View):
 | 
				
			||||||
    bulk_deletable = False
 | 
					    bulk_deletable = False
 | 
				
			||||||
    mergeable = False
 | 
					    mergeable = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    supports_mobile = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    listing = False
 | 
					    listing = False
 | 
				
			||||||
    creating = False
 | 
					    creating = False
 | 
				
			||||||
    viewing = False
 | 
					    viewing = False
 | 
				
			||||||
| 
						 | 
					@ -122,6 +124,83 @@ class MasterView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.render_to_response('index', {'grid': grid})
 | 
					        return self.render_to_response('index', {'grid': grid})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def mobile_index(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Mobile "home" page for the data model
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.listing = True
 | 
				
			||||||
 | 
					        grid = self.make_mobile_grid()
 | 
				
			||||||
 | 
					        return self.render_to_response('index', {'grid': grid}, mobile=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_mobile_grid(self, **kwargs):
 | 
				
			||||||
 | 
					        factory = self.get_mobile_grid_factory()
 | 
				
			||||||
 | 
					        key = self.get_mobile_grid_key()
 | 
				
			||||||
 | 
					        data = self.get_mobile_data(session=kwargs.get('session'))
 | 
				
			||||||
 | 
					        kwargs = self.make_mobile_grid_kwargs(**kwargs)
 | 
				
			||||||
 | 
					        kwargs.setdefault('key', key)
 | 
				
			||||||
 | 
					        kwargs.setdefault('request', self.request)
 | 
				
			||||||
 | 
					        kwargs.setdefault('data', data)
 | 
				
			||||||
 | 
					        kwargs.setdefault('model_class', self.get_model_class(error=False))
 | 
				
			||||||
 | 
					        grid = factory(**kwargs)
 | 
				
			||||||
 | 
					        self.preconfigure_mobile_grid(grid)
 | 
				
			||||||
 | 
					        self.configure_mobile_grid(grid)
 | 
				
			||||||
 | 
					        return grid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_mobile_grid_factory(cls):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Must return a callable to be used when creating new mobile grid
 | 
				
			||||||
 | 
					        instances.  Instead of overriding this, you can set
 | 
				
			||||||
 | 
					        :attr:`mobile_grid_factory`.  Default factory is :class:`MobileGrid`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return getattr(cls, 'mobile_grid_factory', MobileGrid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_mobile_grid_key(cls):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Must return a unique "config key" for the mobile grid, for sort/filter
 | 
				
			||||||
 | 
					        purposes etc.  (It need only be unique among *mobile* grids.)  Instead
 | 
				
			||||||
 | 
					        of overriding this, you can set :attr:`mobile_grid_key`.  Default is
 | 
				
			||||||
 | 
					        the value returned by :meth:`get_route_prefix()`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if hasattr(cls, 'mobile_grid_key'):
 | 
				
			||||||
 | 
					            return cls.mobile_grid_key
 | 
				
			||||||
 | 
					        return cls.get_route_prefix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_mobile_data(self, session=None):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Must return the "raw" / full data set for the mobile grid.  This data
 | 
				
			||||||
 | 
					        should *not* yet be sorted or filtered in any way; that happens later.
 | 
				
			||||||
 | 
					        Default is the value returned by :meth:`get_data()`, in which case all
 | 
				
			||||||
 | 
					        records visible in the traditional view, are visible in mobile too.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.get_data(session=session)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_mobile_grid_kwargs(self, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Must return a dictionary of kwargs to be passed to the factory when
 | 
				
			||||||
 | 
					        creating new mobile grid instances.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        defaults = {
 | 
				
			||||||
 | 
					            'route_prefix': self.get_route_prefix(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        defaults.update(kwargs)
 | 
				
			||||||
 | 
					        return defaults
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def preconfigure_mobile_grid(self, grid):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Optionally perform pre-configuration for the mobile grid, to establish
 | 
				
			||||||
 | 
					        some sane defaults etc.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure_mobile_grid(self, grid):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Configure the mobile grid.  The primary objective here is to define
 | 
				
			||||||
 | 
					        which columns to show and in which order etc.  Along the way you're
 | 
				
			||||||
 | 
					        free to customize any column(s) you like, as needed.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        grid.configure()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self):
 | 
					    def create(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        View for creating a new model record.
 | 
					        View for creating a new model record.
 | 
				
			||||||
| 
						 | 
					@ -185,6 +264,23 @@ class MasterView(View):
 | 
				
			||||||
                                                        tools=self.make_row_grid_tools(instance))
 | 
					                                                        tools=self.make_row_grid_tools(instance))
 | 
				
			||||||
        return self.render_to_response('view', context)
 | 
					        return self.render_to_response('view', context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def mobile_view(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Mobile view for displaying a single object's details
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.viewing = True
 | 
				
			||||||
 | 
					        instance = self.get_instance()
 | 
				
			||||||
 | 
					        # form = self.make_form(instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        context = {
 | 
				
			||||||
 | 
					            'instance': instance,
 | 
				
			||||||
 | 
					            'instance_title': self.get_instance_title(instance),
 | 
				
			||||||
 | 
					            # 'instance_editable': self.editable_instance(instance),
 | 
				
			||||||
 | 
					            # 'instance_deletable': self.deletable_instance(instance),
 | 
				
			||||||
 | 
					            # 'form': form,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return self.render_to_response('view', context, mobile=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_default_row_grid_tools(self, obj):
 | 
					    def make_default_row_grid_tools(self, obj):
 | 
				
			||||||
        if self.rows_creatable:
 | 
					        if self.rows_creatable:
 | 
				
			||||||
            link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
 | 
					            link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
 | 
				
			||||||
| 
						 | 
					@ -614,7 +710,7 @@ class MasterView(View):
 | 
				
			||||||
        return self.request.route_url('{0}.{1}'.format(self.get_route_prefix(), action),
 | 
					        return self.request.route_url('{0}.{1}'.format(self.get_route_prefix(), action),
 | 
				
			||||||
                                      **self.get_action_route_kwargs(instance))
 | 
					                                      **self.get_action_route_kwargs(instance))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_to_response(self, template, data):
 | 
					    def render_to_response(self, template, data, mobile=False):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a response with the given template rendered with the given data.
 | 
					        Return a response with the given template rendered with the given data.
 | 
				
			||||||
        Note that ``template`` must only be a "key" (e.g. 'index' or 'view').
 | 
					        Note that ``template`` must only be a "key" (e.g. 'index' or 'view').
 | 
				
			||||||
| 
						 | 
					@ -650,14 +746,17 @@ class MasterView(View):
 | 
				
			||||||
            context.update(getattr(self, 'template_kwargs_{}'.format(template))(**context))
 | 
					            context.update(getattr(self, 'template_kwargs_{}'.format(template))(**context))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # First try the template path most specific to the view.
 | 
					        # First try the template path most specific to the view.
 | 
				
			||||||
 | 
					        if mobile:
 | 
				
			||||||
 | 
					            mako_path = '/mobile{}/{}.mako'.format(self.get_template_prefix(), template)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            mako_path = '{}/{}.mako'.format(self.get_template_prefix(), template)
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return render_to_response('{}/{}.mako'.format(self.get_template_prefix(), template),
 | 
					            return render_to_response(mako_path, context, request=self.request)
 | 
				
			||||||
                                      context, request=self.request)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except IOError:
 | 
					        except IOError:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Failing that, try one or more fallback templates.
 | 
					            # Failing that, try one or more fallback templates.
 | 
				
			||||||
            for fallback in self.get_fallback_templates(template):
 | 
					            for fallback in self.get_fallback_templates(template, mobile=mobile):
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    return render_to_response(fallback, context, request=self.request)
 | 
					                    return render_to_response(fallback, context, request=self.request)
 | 
				
			||||||
                except IOError:
 | 
					                except IOError:
 | 
				
			||||||
| 
						 | 
					@ -704,7 +803,9 @@ class MasterView(View):
 | 
				
			||||||
            return render('{}/{}.mako'.format(self.get_template_prefix(), template),
 | 
					            return render('{}/{}.mako'.format(self.get_template_prefix(), template),
 | 
				
			||||||
                          context, request=self.request)
 | 
					                          context, request=self.request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_fallback_templates(self, template):
 | 
					    def get_fallback_templates(self, template, mobile=False):
 | 
				
			||||||
 | 
					        if mobile:
 | 
				
			||||||
 | 
					            return ['/mobile/master/{}.mako'.format(template)]
 | 
				
			||||||
        return ['/master/{}.mako'.format(template)]
 | 
					        return ['/master/{}.mako'.format(template)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def template_kwargs(self, **kwargs):
 | 
					    def template_kwargs(self, **kwargs):
 | 
				
			||||||
| 
						 | 
					@ -1297,11 +1398,15 @@ class MasterView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # list/search
 | 
					        # list/search
 | 
				
			||||||
        if cls.listable:
 | 
					        if cls.listable:
 | 
				
			||||||
 | 
					            config.add_tailbone_permission(permission_prefix, '{}.list'.format(permission_prefix),
 | 
				
			||||||
 | 
					                                           "List / search {}".format(model_title_plural))
 | 
				
			||||||
            config.add_route(route_prefix, '{}/'.format(url_prefix))
 | 
					            config.add_route(route_prefix, '{}/'.format(url_prefix))
 | 
				
			||||||
            config.add_view(cls, attr='index', route_name=route_prefix,
 | 
					            config.add_view(cls, attr='index', route_name=route_prefix,
 | 
				
			||||||
                            permission='{}.list'.format(permission_prefix))
 | 
					                            permission='{}.list'.format(permission_prefix))
 | 
				
			||||||
            config.add_tailbone_permission(permission_prefix, '{}.list'.format(permission_prefix),
 | 
					            if cls.supports_mobile:
 | 
				
			||||||
                                           "List / search {}".format(model_title_plural))
 | 
					                config.add_route('mobile.{}'.format(route_prefix), '/mobile{}/'.format(url_prefix))
 | 
				
			||||||
 | 
					                config.add_view(cls, attr='mobile_index', route_name='mobile.{}'.format(route_prefix),
 | 
				
			||||||
 | 
					                                permission='{}.list'.format(permission_prefix))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # create
 | 
					        # create
 | 
				
			||||||
        if cls.creatable:
 | 
					        if cls.creatable:
 | 
				
			||||||
| 
						 | 
					@ -1344,6 +1449,10 @@ class MasterView(View):
 | 
				
			||||||
            config.add_route('{}.view'.format(route_prefix), '{}/{{{}}}'.format(url_prefix, model_key))
 | 
					            config.add_route('{}.view'.format(route_prefix), '{}/{{{}}}'.format(url_prefix, model_key))
 | 
				
			||||||
            config.add_view(cls, attr='view', route_name='{}.view'.format(route_prefix),
 | 
					            config.add_view(cls, attr='view', route_name='{}.view'.format(route_prefix),
 | 
				
			||||||
                            permission='{}.view'.format(permission_prefix))
 | 
					                            permission='{}.view'.format(permission_prefix))
 | 
				
			||||||
 | 
					            if cls.supports_mobile:
 | 
				
			||||||
 | 
					                config.add_route('mobile.{}.view'.format(route_prefix), '/mobile{}/{{{}}}'.format(url_prefix, model_key))
 | 
				
			||||||
 | 
					                config.add_view(cls, attr='mobile_view', route_name='mobile.{}.view'.format(route_prefix),
 | 
				
			||||||
 | 
					                                permission='{}.view'.format(permission_prefix))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # edit
 | 
					        # edit
 | 
				
			||||||
        if cls.editable:
 | 
					        if cls.editable:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -107,6 +107,16 @@ class PeopleView(MasterView):
 | 
				
			||||||
            return instance.person
 | 
					            return instance.person
 | 
				
			||||||
        raise HTTPNotFound
 | 
					        raise HTTPNotFound
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def editable_instance(self, person):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return not bool(person.user and person.user.username == 'chuck')
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deletable_instance(self, person):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return not bool(person.user and person.user.username == 'chuck')
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _preconfigure_fieldset(self, fs):
 | 
					    def _preconfigure_fieldset(self, fs):
 | 
				
			||||||
        fs.display_name.set(label="Full Name")
 | 
					        fs.display_name.set(label="Full Name")
 | 
				
			||||||
        fs.phone.set(label="Phone Number", readonly=True)
 | 
					        fs.phone.set(label="Phone Number", readonly=True)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,10 +38,10 @@ class PrincipalMasterView(MasterView):
 | 
				
			||||||
    Master view base class for security principal models, i.e. User and Role.
 | 
					    Master view base class for security principal models, i.e. User and Role.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_fallback_templates(self, template):
 | 
					    def get_fallback_templates(self, template, mobile=False):
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            '/principal/{}.mako'.format(template),
 | 
					            '/principal/{}.mako'.format(template),
 | 
				
			||||||
        ] + super(PrincipalMasterView, self).get_fallback_templates(template)
 | 
					        ] + super(PrincipalMasterView, self).get_fallback_templates(template, mobile=mobile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def find_by_perm(self):
 | 
					    def find_by_perm(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
# -*- coding: utf-8 -*-
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  Rattail -- Retail Software Framework
 | 
					#  Rattail -- Retail Software Framework
 | 
				
			||||||
#  Copyright © 2010-2015 Lance Edgar
 | 
					#  Copyright © 2010-2017 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Rattail.
 | 
					#  This file is part of Rattail.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,9 @@
 | 
				
			||||||
Settings Views
 | 
					Settings Views
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from __future__ import unicode_literals
 | 
					from __future__ import unicode_literals, absolute_import
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from rattail.db import model
 | 
					from rattail.db import model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +38,7 @@ class SettingsView(MasterView):
 | 
				
			||||||
    Master view for the settings model.
 | 
					    Master view for the settings model.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    model_class = model.Setting
 | 
					    model_class = model.Setting
 | 
				
			||||||
 | 
					    feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_grid(self, g):
 | 
					    def configure_grid(self, g):
 | 
				
			||||||
        g.filters['name'].default_active = True
 | 
					        g.filters['name'].default_active = True
 | 
				
			||||||
| 
						 | 
					@ -57,6 +60,16 @@ class SettingsView(MasterView):
 | 
				
			||||||
        if self.editing:
 | 
					        if self.editing:
 | 
				
			||||||
            fs.name.set(readonly=True)
 | 
					            fs.name.set(readonly=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def editable_instance(self, setting):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return not bool(self.feedback.match(setting.name))
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deletable_instance(self, setting):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return not bool(self.feedback.match(setting.name))
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def includeme(config):
 | 
					def includeme(config):
 | 
				
			||||||
    SettingsView.defaults(config)
 | 
					    SettingsView.defaults(config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -201,6 +201,16 @@ class UsersView(PrincipalMasterView):
 | 
				
			||||||
            del fs.password
 | 
					            del fs.password
 | 
				
			||||||
            del fs.confirm_password
 | 
					            del fs.confirm_password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def editable_instance(self, user):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return user.username != 'chuck'
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deletable_instance(self, user):
 | 
				
			||||||
 | 
					        if self.rattail_config.demo():
 | 
				
			||||||
 | 
					            return user.username != 'chuck'
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def find_principals_with_permission(self, session, permission):
 | 
					    def find_principals_with_permission(self, session, permission):
 | 
				
			||||||
        # TODO: this should search Permission table instead, and work backward to User?
 | 
					        # TODO: this should search Permission table instead, and work backward to User?
 | 
				
			||||||
        all_users = session.query(model.User)\
 | 
					        all_users = session.query(model.User)\
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue