initial rattail v0.4 port (savepoint)
|
@ -28,7 +28,41 @@
|
||||||
|
|
||||||
from rattail.pyramid._version import __version__
|
from rattail.pyramid._version import __version__
|
||||||
|
|
||||||
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
|
|
||||||
|
|
||||||
|
Session = scoped_session(sessionmaker())
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
|
import rattail
|
||||||
|
import rattail.db
|
||||||
|
from zope.sqlalchemy import ZopeTransactionExtension
|
||||||
|
|
||||||
|
# Configure Beaker session.
|
||||||
|
config.include('pyramid_beaker')
|
||||||
|
|
||||||
|
# Bring in transaction manager.
|
||||||
|
config.include('pyramid_tm')
|
||||||
|
|
||||||
|
# Configure SQLAlchemy session.
|
||||||
|
rattail.init_modules(['rattail.db'])
|
||||||
|
Session.configure(bind=rattail.db.engine)
|
||||||
|
Session.configure(extension=ZopeTransactionExtension())
|
||||||
|
|
||||||
|
# Configure user authentication / authorization.
|
||||||
|
from pyramid.authentication import SessionAuthenticationPolicy
|
||||||
|
config.set_authentication_policy(SessionAuthenticationPolicy())
|
||||||
|
from rattail.pyramid.auth import RattailAuthorizationPolicy
|
||||||
|
config.set_authorization_policy(RattailAuthorizationPolicy())
|
||||||
|
|
||||||
|
# Add forbidden view.
|
||||||
|
config.add_forbidden_view('rattail.pyramid.views.core:forbidden')
|
||||||
|
|
||||||
|
# Add static views.
|
||||||
|
config.add_static_view('rattail', 'rattail.pyramid:static')
|
||||||
|
|
||||||
|
# Add subscriber hooks.
|
||||||
config.include('rattail.pyramid.subscribers')
|
config.include('rattail.pyramid.subscribers')
|
||||||
config.include('rattail.pyramid.views')
|
# config.include('rattail.pyramid.views')
|
||||||
|
|
52
rattail/pyramid/auth.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.auth`` -- Authorization Policy
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implementer
|
||||||
|
from pyramid.interfaces import IAuthorizationPolicy
|
||||||
|
from pyramid.security import Everyone, Authenticated
|
||||||
|
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.auth import has_permission
|
||||||
|
from rattail.db.model import User
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(IAuthorizationPolicy)
|
||||||
|
class RattailAuthorizationPolicy(object):
|
||||||
|
|
||||||
|
def permits(self, context, principals, permission):
|
||||||
|
for userid in principals:
|
||||||
|
if userid not in (Everyone, Authenticated):
|
||||||
|
user = Session.query(User).get(userid)
|
||||||
|
assert user
|
||||||
|
return has_permission(Session(), user, permission)
|
||||||
|
if Everyone in principals:
|
||||||
|
return has_permission(Session(), None, permission)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# def principals_allowed_by_permission(self, context, permission):
|
||||||
|
# raise NotImplementedError
|
|
@ -1,85 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# Rattail -- Retail Software Framework
|
|
||||||
# Copyright © 2010-2012 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/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
``rattail.pyramid.forms`` -- Rattail Forms
|
|
||||||
"""
|
|
||||||
|
|
||||||
from webhelpers.html import literal
|
|
||||||
|
|
||||||
import formalchemy
|
|
||||||
|
|
||||||
from edbob.pyramid.forms import pretty_datetime
|
|
||||||
|
|
||||||
import rattail
|
|
||||||
from rattail.gpc import GPC
|
|
||||||
|
|
||||||
|
|
||||||
class GPCFieldRenderer(formalchemy.TextFieldRenderer):
|
|
||||||
"""
|
|
||||||
Renderer for :class:`rattail.gpc.GPC` fields.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def length(self):
|
|
||||||
# Hm, should maybe consider hard-coding this...?
|
|
||||||
return len(str(GPC(0)))
|
|
||||||
|
|
||||||
|
|
||||||
class PriceFieldRenderer(formalchemy.TextFieldRenderer):
|
|
||||||
"""
|
|
||||||
Renderer for fields which reference a :class:`ProductPrice` instance.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def render_readonly(self, **kwargs):
|
|
||||||
price = self.field.raw_value
|
|
||||||
if price:
|
|
||||||
if price.price is not None and price.pack_price is not None:
|
|
||||||
if price.multiple > 1:
|
|
||||||
return literal('$ %0.2f / %u ($ %0.2f / %u)' % (
|
|
||||||
price.price, price.multiple,
|
|
||||||
price.pack_price, price.pack_multiple))
|
|
||||||
return literal('$ %0.2f ($ %0.2f / %u)' % (
|
|
||||||
price.price, price.pack_price, price.pack_multiple))
|
|
||||||
if price.price is not None:
|
|
||||||
if price.multiple > 1:
|
|
||||||
return '$ %0.2f / %u' % (price.price, price.multiple)
|
|
||||||
return '$ %0.2f' % price.price
|
|
||||||
if price.pack_price is not None:
|
|
||||||
return '$ %0.2f / %u' % (price.pack_price, price.pack_multiple)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
class PriceWithExpirationFieldRenderer(PriceFieldRenderer):
|
|
||||||
"""
|
|
||||||
Price field renderer which also displays the expiration date, if present.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def render_readonly(self, **kwargs):
|
|
||||||
res = super(PriceWithExpirationFieldRenderer, self).render_readonly(**kwargs)
|
|
||||||
if res:
|
|
||||||
price = self.field.raw_value
|
|
||||||
if price.ends:
|
|
||||||
res += ' (%s)' % pretty_datetime(price.ends, from_='utc')
|
|
||||||
return res
|
|
31
rattail/pyramid/forms/__init__.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.forms`` -- Forms
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rattail.pyramid.forms.core import *
|
||||||
|
from rattail.pyramid.forms.formalchemy import *
|
||||||
|
from rattail.pyramid.forms.simpleform import *
|
111
rattail/pyramid/forms/core.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.forms.core`` -- Core Forms
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from sqlalchemy.util import OrderedDict
|
||||||
|
|
||||||
|
from webhelpers.html import literal, tags
|
||||||
|
|
||||||
|
import rattail
|
||||||
|
from rattail.time import localize
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['Form']
|
||||||
|
|
||||||
|
|
||||||
|
class Form(rattail.Object):
|
||||||
|
"""
|
||||||
|
Generic form class.
|
||||||
|
|
||||||
|
This class exists primarily so that rendering calls may mimic those used by
|
||||||
|
FormAlchemy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
readonly = False
|
||||||
|
successive = False
|
||||||
|
|
||||||
|
action_url = None
|
||||||
|
home_route = None
|
||||||
|
home_url = None
|
||||||
|
# template = None
|
||||||
|
|
||||||
|
render_fields = OrderedDict()
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
# def __init__(self, request=None, action_url=None, home_url=None, template=None, **kwargs):
|
||||||
|
def __init__(self, request=None, action_url=None, home_url=None, **kwargs):
|
||||||
|
super(Form, self).__init__(**kwargs)
|
||||||
|
self.request = request
|
||||||
|
if action_url:
|
||||||
|
self.action_url = action_url
|
||||||
|
if request and not self.action_url:
|
||||||
|
self.action_url = request.current_route_url()
|
||||||
|
if home_url:
|
||||||
|
self.home_url = home_url
|
||||||
|
if request and not self.home_url:
|
||||||
|
home = self.home_route if self.home_route else 'home'
|
||||||
|
self.home_url = request.route_url(home)
|
||||||
|
# if template:
|
||||||
|
# self.template = template
|
||||||
|
# if not self.template:
|
||||||
|
# self.template = '%s.mako' % self.action_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def action_url(self):
|
||||||
|
return self.request.current_route_url()
|
||||||
|
|
||||||
|
def standard_buttons(self, submit="Save"):
|
||||||
|
return literal(tags.submit('submit', submit) + ' ' + self.cancel_button())
|
||||||
|
|
||||||
|
def cancel_button(self):
|
||||||
|
return literal('<button type="button" class="cancel">Cancel</button>')
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Renders the form as HTML. All keyword arguments are passed on to the
|
||||||
|
template context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_datetime(value, from_='local', to='local'):
|
||||||
|
"""
|
||||||
|
Formats a ``datetime.datetime`` instance and returns a "pretty"
|
||||||
|
human-readable string from it, e.g. "42 minutes ago". ``value`` is
|
||||||
|
rendered directly as a string if no date/time can be parsed from it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(value, datetime.datetime):
|
||||||
|
return str(value) if value else ''
|
||||||
|
if not value.tzinfo:
|
||||||
|
value = localize(value, from_=from_, to=to)
|
||||||
|
return literal('<span title="%s">%s</span>' % (
|
||||||
|
value.strftime('%Y-%m-%d %H:%M:%S %Z%z'),
|
||||||
|
pretty.date(value)))
|
32
rattail/pyramid/forms/formalchemy/__init__.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.forms.formalchemy`` -- FormAlchemy Forms
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rattail.pyramid.forms.formalchemy.core import *
|
||||||
|
from rattail.pyramid.forms.formalchemy.fieldset import *
|
||||||
|
from rattail.pyramid.forms.formalchemy.fields import *
|
||||||
|
from rattail.pyramid.forms.formalchemy.renderers import *
|
108
rattail/pyramid/forms/formalchemy/core.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.forms.formalchemy.core`` -- FormAlchemy Core
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from pyramid.renderers import render
|
||||||
|
|
||||||
|
import formalchemy
|
||||||
|
from formalchemy.validators import accepts_none
|
||||||
|
|
||||||
|
import rattail
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['AlchemyForm', 'required']
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateEngine(formalchemy.templates.TemplateEngine):
|
||||||
|
"""
|
||||||
|
Mako template engine for FormAlchemy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def render(self, template, prefix='/forms/', suffix='.mako', **kwargs):
|
||||||
|
template = ''.join((prefix, template, suffix))
|
||||||
|
return render(template, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# Make our TemplateEngine the default.
|
||||||
|
engine = TemplateEngine()
|
||||||
|
formalchemy.config.engine = engine
|
||||||
|
|
||||||
|
|
||||||
|
class AlchemyForm(rattail.Object):
|
||||||
|
"""
|
||||||
|
Form to contain a :class:`formalchemy.FieldSet` instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
create_label = "Create"
|
||||||
|
update_label = "Update"
|
||||||
|
|
||||||
|
allow_successive_creates = False
|
||||||
|
|
||||||
|
def __init__(self, request, fieldset, **kwargs):
|
||||||
|
super(AlchemyForm, self).__init__(**kwargs)
|
||||||
|
self.request = request
|
||||||
|
self.fieldset = fieldset
|
||||||
|
|
||||||
|
def _get_readonly(self):
|
||||||
|
return self.fieldset.readonly
|
||||||
|
|
||||||
|
def _set_readonly(self, val):
|
||||||
|
self.fieldset.readonly = val
|
||||||
|
|
||||||
|
readonly = property(_get_readonly, _set_readonly)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def successive_create_label(self):
|
||||||
|
return "%s and continue" % self.create_label
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
kwargs['form'] = self
|
||||||
|
if self.readonly:
|
||||||
|
template = '/forms/form_readonly.mako'
|
||||||
|
else:
|
||||||
|
template = '/forms/form.mako'
|
||||||
|
return render(template, kwargs)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.fieldset.sync()
|
||||||
|
Session.flush()
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
self.fieldset.rebind(data=self.request.params)
|
||||||
|
return self.fieldset.validate()
|
||||||
|
|
||||||
|
|
||||||
|
@accepts_none
|
||||||
|
def required(value, field=None):
|
||||||
|
if value is None or value == '':
|
||||||
|
msg = "Please provide a value"
|
||||||
|
if field:
|
||||||
|
msg = "You must provide a value for %s" % field.label()
|
||||||
|
raise formalchemy.ValidationError(msg)
|
75
rattail/pyramid/forms/formalchemy/fields.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.forms.formalchemy.fields`` -- Field Classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
import formalchemy
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['AssociationProxyField', 'ChildGridField', 'PropertyField']
|
||||||
|
|
||||||
|
|
||||||
|
def AssociationProxyField(name, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a :class:`Field` class which is aware of SQLAlchemy association
|
||||||
|
proxies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class ProxyField(formalchemy.Field):
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
if not self.is_readonly():
|
||||||
|
setattr(self.parent.model, self.name,
|
||||||
|
self.renderer.deserialize())
|
||||||
|
|
||||||
|
kwargs.setdefault('value', lambda x: getattr(x, name))
|
||||||
|
return ProxyField(name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ChildGridField(formalchemy.Field):
|
||||||
|
"""
|
||||||
|
Convenience class for including a child grid within a fieldset as a
|
||||||
|
read-only field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, value, *args, **kwargs):
|
||||||
|
super(ChildGridField, self).__init__(name, *args, **kwargs)
|
||||||
|
self.set(value=value)
|
||||||
|
self.set(readonly=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyField(formalchemy.Field):
|
||||||
|
"""
|
||||||
|
Convenience class for fields which simply involve a read-only property
|
||||||
|
value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, attr=None, *args, **kwargs):
|
||||||
|
super(PropertyField, self).__init__(name, *args, **kwargs)
|
||||||
|
if not attr:
|
||||||
|
attr = name
|
||||||
|
self.set(value=lambda x: getattr(x, attr))
|
||||||
|
self.set(readonly=True)
|
72
rattail/pyramid/forms/formalchemy/fieldset.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.forms.formalchemy.fieldset`` -- FormAlchemy FieldSet
|
||||||
|
"""
|
||||||
|
|
||||||
|
import formalchemy
|
||||||
|
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.util import prettify
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['FieldSet', 'make_fieldset']
|
||||||
|
|
||||||
|
|
||||||
|
class FieldSet(formalchemy.FieldSet):
|
||||||
|
"""
|
||||||
|
Adds a little magic to the :class:`formalchemy.FieldSet` class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
prettify = staticmethod(prettify)
|
||||||
|
|
||||||
|
def __init__(self, model, class_name=None, crud_title=None, url=None,
|
||||||
|
route_name=None, action_url='', home_url=None, **kwargs):
|
||||||
|
super(FieldSet, self).__init__(model, **kwargs)
|
||||||
|
self.class_name = class_name or self._original_cls.__name__.lower()
|
||||||
|
self.crud_title = crud_title or prettify(self.class_name)
|
||||||
|
self.edit = isinstance(model, self._original_cls)
|
||||||
|
self.route_name = route_name or (self.class_name + 's')
|
||||||
|
self.action_url = action_url
|
||||||
|
self.home_url = home_url
|
||||||
|
self.allow_continue = kwargs.pop('allow_continue', False)
|
||||||
|
|
||||||
|
def get_display_text(self):
|
||||||
|
return unicode(self.model)
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
kwargs.setdefault('class_', self.class_name)
|
||||||
|
return super(FieldSet, self).render(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def make_fieldset(model, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a :class:`FieldSet` equipped with the current scoped
|
||||||
|
:class:`rattail.db.Session` instance (unless ``session`` is provided as a
|
||||||
|
keyword argument).
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs.setdefault('session', Session())
|
||||||
|
return FieldSet(model, **kwargs)
|
188
rattail/pyramid/forms/formalchemy/renderers.py
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.forms.formalchemy.renderers`` -- Field Renderers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import formalchemy
|
||||||
|
|
||||||
|
from webhelpers.html import literal
|
||||||
|
|
||||||
|
from pyramid.renderers import render
|
||||||
|
|
||||||
|
from rattail.barcodes import GPC
|
||||||
|
from rattail.pyramid.forms.core import pretty_datetime
|
||||||
|
from rattail.pyramid.forms.formalchemy.fieldset import FieldSet
|
||||||
|
from rattail.time import local_time
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer',
|
||||||
|
'StrippingFieldRenderer', 'YesNoFieldRenderer',
|
||||||
|
'DateTimeFieldRenderer']
|
||||||
|
|
||||||
|
|
||||||
|
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
|
||||||
|
"""
|
||||||
|
Autocomplete renderer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class AutocompleteFieldRenderer(formalchemy.fields.FieldRenderer):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def focus_name(self):
|
||||||
|
return self.name + '-textbox'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def needs_focus(self):
|
||||||
|
return not bool(self.value or field_value)
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
kwargs.setdefault('field_name', self.name)
|
||||||
|
kwargs.setdefault('field_value', self.value or field_value)
|
||||||
|
kwargs.setdefault('field_display', self.raw_value or field_display)
|
||||||
|
kwargs.setdefault('service_url', service_url)
|
||||||
|
kwargs.setdefault('width', width)
|
||||||
|
return render('/forms/field_autocomplete.mako', kwargs)
|
||||||
|
|
||||||
|
return AutocompleteFieldRenderer
|
||||||
|
|
||||||
|
|
||||||
|
def EnumFieldRenderer(enum):
|
||||||
|
"""
|
||||||
|
Adds support for enumeration fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Renderer(formalchemy.fields.SelectFieldRenderer):
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
value = self.raw_value
|
||||||
|
if value is None:
|
||||||
|
return ''
|
||||||
|
if value in enum:
|
||||||
|
return enum[value]
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
opts = [(enum[x], x) for x in sorted(enum)]
|
||||||
|
return formalchemy.fields.SelectFieldRenderer.render(self, opts, **kwargs)
|
||||||
|
|
||||||
|
return Renderer
|
||||||
|
|
||||||
|
|
||||||
|
class StrippingFieldRenderer(formalchemy.TextFieldRenderer):
|
||||||
|
"""
|
||||||
|
Standard text field renderer, which strips whitespace from either end of
|
||||||
|
the input value on deserialization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def deserialize(self):
|
||||||
|
value = super(StrippingFieldRenderer, self).deserialize()
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class YesNoFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer):
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
value = self.raw_value
|
||||||
|
if value is None:
|
||||||
|
return ''
|
||||||
|
return 'Yes' if value else 'No'
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
|
||||||
|
"""
|
||||||
|
Leverages Rattail time system to coerce timestamp to local time zone before
|
||||||
|
displaying it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
value = self.raw_value
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
value = local_time(value)
|
||||||
|
return value.strftime(self.format)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
FieldSet.default_renderers[formalchemy.types.DateTime] = DateTimeFieldRenderer
|
||||||
|
|
||||||
|
|
||||||
|
def PrettyDateTimeFieldRenderer(from_='local', to='local'):
|
||||||
|
|
||||||
|
class PrettyDateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
return pretty_datetime(self.raw_value, from_=from_, to=to)
|
||||||
|
|
||||||
|
return PrettyDateTimeFieldRenderer
|
||||||
|
|
||||||
|
|
||||||
|
class GPCFieldRenderer(formalchemy.TextFieldRenderer):
|
||||||
|
"""
|
||||||
|
Renderer for :class:`rattail.GPC` fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
# Hm, should maybe consider hard-coding this...?
|
||||||
|
return len(str(GPC(0)))
|
||||||
|
|
||||||
|
|
||||||
|
class PriceFieldRenderer(formalchemy.TextFieldRenderer):
|
||||||
|
"""
|
||||||
|
Renderer for fields which reference a :class:`ProductPrice` instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
price = self.field.raw_value
|
||||||
|
if price:
|
||||||
|
if price.price is not None and price.pack_price is not None:
|
||||||
|
if price.multiple > 1:
|
||||||
|
return literal('$ %0.2f / %u ($ %0.2f / %u)' % (
|
||||||
|
price.price, price.multiple,
|
||||||
|
price.pack_price, price.pack_multiple))
|
||||||
|
return literal('$ %0.2f ($ %0.2f / %u)' % (
|
||||||
|
price.price, price.pack_price, price.pack_multiple))
|
||||||
|
if price.price is not None:
|
||||||
|
if price.multiple > 1:
|
||||||
|
return '$ %0.2f / %u' % (price.price, price.multiple)
|
||||||
|
return '$ %0.2f' % price.price
|
||||||
|
if price.pack_price is not None:
|
||||||
|
return '$ %0.2f / %u' % (price.pack_price, price.pack_multiple)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
class PriceWithExpirationFieldRenderer(PriceFieldRenderer):
|
||||||
|
"""
|
||||||
|
Price field renderer which also displays the expiration date, if present.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
res = super(PriceWithExpirationFieldRenderer, self).render_readonly(**kwargs)
|
||||||
|
if res:
|
||||||
|
price = self.field.raw_value
|
||||||
|
if price.ends:
|
||||||
|
res += ' (%s)' % pretty_datetime(price.ends, from_='utc')
|
||||||
|
return res
|
66
rattail/pyramid/forms/simpleform.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.forms.simpleform`` -- pyramid_simpleform Forms
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyramid.renderers import render
|
||||||
|
|
||||||
|
import formencode
|
||||||
|
import pyramid_simpleform
|
||||||
|
from pyramid_simpleform.renderers import FormRenderer
|
||||||
|
|
||||||
|
from rattail.pyramid.forms.core import Form
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['Schema', 'SimpleForm']
|
||||||
|
|
||||||
|
|
||||||
|
class Schema(formencode.Schema):
|
||||||
|
"""
|
||||||
|
Subclass of ``formencode.Schema``, which exists only to ignore extra
|
||||||
|
fields. These normally would cause a schema instance to be deemed invalid,
|
||||||
|
and pretty much *every* form has a submit button which would be considered
|
||||||
|
an extra field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
allow_extra_fields = True
|
||||||
|
filter_extra_fields = True
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleForm(Form):
|
||||||
|
|
||||||
|
template = None
|
||||||
|
|
||||||
|
def __init__(self, request, **kwargs):
|
||||||
|
super(SimpleForm, self).__init__(request, **kwargs)
|
||||||
|
self.form = pyramid_simpleform.Form(request)
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
kw = {
|
||||||
|
'form': self,
|
||||||
|
}
|
||||||
|
kw.update(kwargs)
|
||||||
|
return render('/forms/form.mako', kw)
|
32
rattail/pyramid/grids/__init__.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.grids`` -- Grids
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rattail.pyramid.grids.core import *
|
||||||
|
from rattail.pyramid.grids.alchemy import *
|
||||||
|
from rattail.pyramid.grids import util
|
||||||
|
from rattail.pyramid.grids import search
|
124
rattail/pyramid/grids/alchemy.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.grids.alchemy`` -- FormAlchemy Grid
|
||||||
|
"""
|
||||||
|
|
||||||
|
from webhelpers.html import tags
|
||||||
|
from webhelpers.html import HTML
|
||||||
|
|
||||||
|
import formalchemy
|
||||||
|
|
||||||
|
import rattail
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.pyramid.grids.core import Grid
|
||||||
|
from rattail.util import prettify
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['AlchemyGrid']
|
||||||
|
|
||||||
|
|
||||||
|
class AlchemyGrid(Grid):
|
||||||
|
|
||||||
|
sort_map = {}
|
||||||
|
|
||||||
|
pager = None
|
||||||
|
pager_format = '$link_first $link_previous ~1~ $link_next $link_last'
|
||||||
|
|
||||||
|
def __init__(self, request, cls, instances, **kwargs):
|
||||||
|
super(AlchemyGrid, self).__init__(request, **kwargs)
|
||||||
|
self._formalchemy_grid = formalchemy.Grid(
|
||||||
|
cls, instances, session=Session(), request=request)
|
||||||
|
self._formalchemy_grid.prettify = prettify
|
||||||
|
self.noclick_fields = []
|
||||||
|
|
||||||
|
def __delattr__(self, attr):
|
||||||
|
delattr(self._formalchemy_grid, attr)
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._formalchemy_grid, attr)
|
||||||
|
|
||||||
|
def cell_class(self, field):
|
||||||
|
classes = [field.name]
|
||||||
|
if field.name in self.noclick_fields:
|
||||||
|
classes.append('noclick')
|
||||||
|
return ' '.join(classes)
|
||||||
|
|
||||||
|
def checkbox(self, row):
|
||||||
|
return tags.checkbox('check-'+row.uuid)
|
||||||
|
|
||||||
|
def click_route_kwargs(self, row):
|
||||||
|
return {'uuid': row.uuid}
|
||||||
|
|
||||||
|
def column_header(self, field):
|
||||||
|
class_ = None
|
||||||
|
label = field.label()
|
||||||
|
if field.key in self.sort_map:
|
||||||
|
class_ = 'sortable'
|
||||||
|
if field.key == self.config['sort']:
|
||||||
|
class_ += ' sorted ' + self.config['dir']
|
||||||
|
label = tags.link_to(label, '#')
|
||||||
|
return HTML.tag('th', class_=class_, field=field.key,
|
||||||
|
title=self.column_titles.get(field.key), c=label)
|
||||||
|
|
||||||
|
def edit_route_kwargs(self, row):
|
||||||
|
return {'uuid': row.uuid}
|
||||||
|
|
||||||
|
def delete_route_kwargs(self, row):
|
||||||
|
return {'uuid': row.uuid}
|
||||||
|
|
||||||
|
def iter_fields(self):
|
||||||
|
return self._formalchemy_grid.render_fields.itervalues()
|
||||||
|
|
||||||
|
def iter_rows(self):
|
||||||
|
for row in self._formalchemy_grid.rows:
|
||||||
|
self._formalchemy_grid._set_active(row)
|
||||||
|
yield row
|
||||||
|
|
||||||
|
def page_count_options(self):
|
||||||
|
options = rattail.config.get('rattail.pyramid', 'grid.page_count_options')
|
||||||
|
if options:
|
||||||
|
options = options.split(',')
|
||||||
|
options = [int(x.strip()) for x in options]
|
||||||
|
else:
|
||||||
|
options = [5, 10, 20, 50, 100]
|
||||||
|
return options
|
||||||
|
|
||||||
|
def page_links(self):
|
||||||
|
return self.pager.pager(self.pager_format,
|
||||||
|
symbol_next='next',
|
||||||
|
symbol_previous='prev',
|
||||||
|
onclick="grid_navigate_page(this, '$partial_url'); return false;")
|
||||||
|
|
||||||
|
def render_field(self, field):
|
||||||
|
if self._formalchemy_grid.readonly:
|
||||||
|
return field.render_readonly()
|
||||||
|
return field.render()
|
||||||
|
|
||||||
|
def row_attrs(self, row, i):
|
||||||
|
attrs = super(AlchemyGrid, self).row_attrs(row, i)
|
||||||
|
if hasattr(row, 'uuid'):
|
||||||
|
attrs['uuid'] = row.uuid
|
||||||
|
return attrs
|
138
rattail/pyramid/grids/core.py
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.grids.core`` -- Core Grid Classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections import OrderedDict
|
||||||
|
except ImportError:
|
||||||
|
from ordereddict import OrderedDict
|
||||||
|
|
||||||
|
from webhelpers.html import HTML
|
||||||
|
from webhelpers.html.builder import format_attrs
|
||||||
|
|
||||||
|
from pyramid.renderers import render
|
||||||
|
|
||||||
|
from rattail import Object
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['Grid']
|
||||||
|
|
||||||
|
|
||||||
|
class Grid(Object):
|
||||||
|
|
||||||
|
full = False
|
||||||
|
hoverable = True
|
||||||
|
clickable = False
|
||||||
|
checkboxes = False
|
||||||
|
editable = False
|
||||||
|
deletable = False
|
||||||
|
|
||||||
|
partial_only = False
|
||||||
|
|
||||||
|
click_route_name = None
|
||||||
|
click_route_kwargs = None
|
||||||
|
|
||||||
|
edit_route_name = None
|
||||||
|
edit_route_kwargs = None
|
||||||
|
|
||||||
|
delete_route_name = None
|
||||||
|
delete_route_kwargs = None
|
||||||
|
|
||||||
|
def __init__(self, request, **kwargs):
|
||||||
|
kwargs.setdefault('fields', OrderedDict())
|
||||||
|
kwargs.setdefault('column_titles', {})
|
||||||
|
kwargs.setdefault('extra_columns', [])
|
||||||
|
super(Grid, self).__init__(**kwargs)
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
def add_column(self, name, label, callback):
|
||||||
|
self.extra_columns.append(
|
||||||
|
Object(name=name, label=label, callback=callback))
|
||||||
|
|
||||||
|
def column_header(self, field):
|
||||||
|
return HTML.tag('th', field=field.name,
|
||||||
|
title=self.column_titles.get(field.name),
|
||||||
|
c=field.label)
|
||||||
|
|
||||||
|
def div_attrs(self):
|
||||||
|
classes = ['grid']
|
||||||
|
if self.full:
|
||||||
|
classes.append('full')
|
||||||
|
if self.clickable:
|
||||||
|
classes.append('clickable')
|
||||||
|
if self.hoverable:
|
||||||
|
classes.append('hoverable')
|
||||||
|
return format_attrs(
|
||||||
|
class_=' '.join(classes),
|
||||||
|
url=self.request.current_route_url())
|
||||||
|
|
||||||
|
def get_delete_url(self, row):
|
||||||
|
kwargs = {}
|
||||||
|
if self.delete_route_kwargs:
|
||||||
|
if callable(self.delete_route_kwargs):
|
||||||
|
kwargs = self.delete_route_kwargs(row)
|
||||||
|
else:
|
||||||
|
kwargs = self.delete_route_kwargs
|
||||||
|
return self.request.route_url(self.delete_route_name, **kwargs)
|
||||||
|
|
||||||
|
def get_edit_url(self, row):
|
||||||
|
kwargs = {}
|
||||||
|
if self.edit_route_kwargs:
|
||||||
|
if callable(self.edit_route_kwargs):
|
||||||
|
kwargs = self.edit_route_kwargs(row)
|
||||||
|
else:
|
||||||
|
kwargs = self.edit_route_kwargs
|
||||||
|
return self.request.route_url(self.edit_route_name, **kwargs)
|
||||||
|
|
||||||
|
def get_row_attrs(self, row, i):
|
||||||
|
attrs = self.row_attrs(row, i)
|
||||||
|
if self.clickable:
|
||||||
|
kwargs = {}
|
||||||
|
if self.click_route_kwargs:
|
||||||
|
if callable(self.click_route_kwargs):
|
||||||
|
kwargs = self.click_route_kwargs(row)
|
||||||
|
else:
|
||||||
|
kwargs = self.click_route_kwargs
|
||||||
|
attrs['url'] = self.request.route_url(self.click_route_name, **kwargs)
|
||||||
|
return format_attrs(**attrs)
|
||||||
|
|
||||||
|
def iter_fields(self):
|
||||||
|
return self.fields.itervalues()
|
||||||
|
|
||||||
|
def iter_rows(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def render(self, template='/grids/grid.mako', **kwargs):
|
||||||
|
kwargs.setdefault('grid', self)
|
||||||
|
return render(template, kwargs)
|
||||||
|
|
||||||
|
def render_field(self, field):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def row_attrs(self, row, i):
|
||||||
|
attrs = {'class_': 'odd' if i % 2 else 'even'}
|
||||||
|
return attrs
|
274
rattail/pyramid/grids/search.py
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.grids.search`` -- Grid Search Filters
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
from webhelpers.html import tags
|
||||||
|
from webhelpers.html import literal
|
||||||
|
|
||||||
|
from pyramid.renderers import render
|
||||||
|
from pyramid_simpleform import Form
|
||||||
|
from pyramid_simpleform.renderers import FormRenderer
|
||||||
|
|
||||||
|
from rattail.util import Object, prettify
|
||||||
|
|
||||||
|
|
||||||
|
class SearchFilter(Object):
|
||||||
|
"""
|
||||||
|
Base class and default implementation for search filters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, label=None, **kwargs):
|
||||||
|
Object.__init__(self, **kwargs)
|
||||||
|
self.name = name
|
||||||
|
self.label = label or prettify(name)
|
||||||
|
|
||||||
|
def types_select(self):
|
||||||
|
types = [
|
||||||
|
('is', "is"),
|
||||||
|
('nt', "is not"),
|
||||||
|
('lk', "contains"),
|
||||||
|
('nl', "doesn't contain"),
|
||||||
|
]
|
||||||
|
options = []
|
||||||
|
filter_map = self.search.filter_map[self.name]
|
||||||
|
for value, label in types:
|
||||||
|
if value in filter_map:
|
||||||
|
options.append((value, label))
|
||||||
|
return tags.select('filter_type_'+self.name,
|
||||||
|
self.search.config.get('filter_type_'+self.name),
|
||||||
|
options, class_='filter-type')
|
||||||
|
|
||||||
|
def value_control(self):
|
||||||
|
return tags.text(self.name, self.search.config.get(self.name))
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanSearchFilter(SearchFilter):
|
||||||
|
"""
|
||||||
|
Boolean search filter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def value_control(self):
|
||||||
|
return tags.select(self.name, self.search.config.get(self.name),
|
||||||
|
["True", "False"])
|
||||||
|
|
||||||
|
|
||||||
|
class SearchForm(Form):
|
||||||
|
"""
|
||||||
|
Generic form class which aggregates :class:`SearchFilter` instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request, filter_map, config, *args, **kwargs):
|
||||||
|
super(SearchForm, self).__init__(request, *args, **kwargs)
|
||||||
|
self.filter_map = filter_map
|
||||||
|
self.config = config
|
||||||
|
self.filters = {}
|
||||||
|
|
||||||
|
def add_filter(self, filter_):
|
||||||
|
filter_.search = self
|
||||||
|
self.filters[filter_.name] = filter_
|
||||||
|
|
||||||
|
|
||||||
|
class SearchFormRenderer(FormRenderer):
|
||||||
|
"""
|
||||||
|
Renderer for :class:`SearchForm` instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, form, *args, **kwargs):
|
||||||
|
super(SearchFormRenderer, self).__init__(form, *args, **kwargs)
|
||||||
|
self.request = form.request
|
||||||
|
self.filters = form.filters
|
||||||
|
self.config = form.config
|
||||||
|
|
||||||
|
def add_filter(self, visible):
|
||||||
|
options = ['add a filter']
|
||||||
|
for f in self.sorted_filters():
|
||||||
|
options.append((f.name, f.label))
|
||||||
|
return self.select('add-filter', options,
|
||||||
|
style='display: none;' if len(visible) == len(self.filters) else None)
|
||||||
|
|
||||||
|
def checkbox(self, name, checked=None, **kwargs):
|
||||||
|
if name.startswith('include_filter_'):
|
||||||
|
if checked is None:
|
||||||
|
checked = self.config[name]
|
||||||
|
return tags.checkbox(name, checked=checked, **kwargs)
|
||||||
|
if checked is None:
|
||||||
|
checked = False
|
||||||
|
return super(SearchFormRenderer, self).checkbox(name, checked=checked, **kwargs)
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
kwargs['search'] = self
|
||||||
|
return literal(render('/grids/search.mako', kwargs))
|
||||||
|
|
||||||
|
def sorted_filters(self):
|
||||||
|
return sorted(self.filters.values(), key=lambda x: x.label)
|
||||||
|
|
||||||
|
def text(self, name, **kwargs):
|
||||||
|
return tags.text(name, value=self.config.get(name), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_exact(field):
|
||||||
|
"""
|
||||||
|
Convenience function which returns a filter map entry, with typical logic
|
||||||
|
built in for "exact match" queries applied to ``field``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
'is':
|
||||||
|
lambda q, v: q.filter(field == v) if v else q,
|
||||||
|
'nt':
|
||||||
|
lambda q, v: q.filter(field != v) if v else q,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def filter_ilike(field):
|
||||||
|
"""
|
||||||
|
Convenience function which returns a filter map entry, with typical logic
|
||||||
|
built in for "ILIKE" queries applied to ``field``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def ilike(query, value):
|
||||||
|
if value:
|
||||||
|
query = query.filter(field.ilike('%%%s%%' % value))
|
||||||
|
return query
|
||||||
|
|
||||||
|
def not_ilike(query, value):
|
||||||
|
if value:
|
||||||
|
query = query.filter(or_(
|
||||||
|
field == None,
|
||||||
|
~field.ilike('%%%s%%' % value),
|
||||||
|
))
|
||||||
|
return query
|
||||||
|
|
||||||
|
return {'lk': ilike, 'nl': not_ilike}
|
||||||
|
|
||||||
|
|
||||||
|
def get_filter_config(prefix, request, filter_map, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a configuration dictionary for a search form.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
def update_config(dict_, prefix='', exclude_by_default=False):
|
||||||
|
"""
|
||||||
|
Updates the ``config`` dictionary based on the contents of ``dict_``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for field in filter_map:
|
||||||
|
if prefix+'include_filter_'+field in dict_:
|
||||||
|
include = dict_[prefix+'include_filter_'+field]
|
||||||
|
include = bool(include) and include != '0'
|
||||||
|
config['include_filter_'+field] = include
|
||||||
|
elif exclude_by_default:
|
||||||
|
config['include_filter_'+field] = False
|
||||||
|
if prefix+'filter_type_'+field in dict_:
|
||||||
|
config['filter_type_'+field] = dict_[prefix+'filter_type_'+field]
|
||||||
|
if prefix+field in dict_:
|
||||||
|
config[field] = dict_[prefix+field]
|
||||||
|
|
||||||
|
# Update config to exclude all filters by default.
|
||||||
|
for field in filter_map:
|
||||||
|
config['include_filter_'+field] = False
|
||||||
|
|
||||||
|
# Update config to honor default settings.
|
||||||
|
config.update(kwargs)
|
||||||
|
|
||||||
|
# Update config with data cached in session.
|
||||||
|
update_config(request.session, prefix=prefix+'.')
|
||||||
|
|
||||||
|
# Update config with data from GET/POST request.
|
||||||
|
if request.params.get('filters') == 'true':
|
||||||
|
update_config(request.params, exclude_by_default=True)
|
||||||
|
|
||||||
|
# Cache filter data in session.
|
||||||
|
for key in config:
|
||||||
|
if (not key.startswith('filter_factory_')
|
||||||
|
and not key.startswith('filter_label_')):
|
||||||
|
request.session[prefix+'.'+key] = config[key]
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def get_filter_map(cls, exact=[], ilike=[], **kwargs):
|
||||||
|
"""
|
||||||
|
Convenience function which returns a "filter map" for ``cls``.
|
||||||
|
|
||||||
|
``exact``, if provided, should be a list of field names for which "exact"
|
||||||
|
filtering is to be allowed.
|
||||||
|
|
||||||
|
``ilike``, if provided, should be a list of field names for which "ILIKE"
|
||||||
|
filtering is to be allowed.
|
||||||
|
|
||||||
|
Any remaining ``kwargs`` are assumed to be filter map entries themselves,
|
||||||
|
and are added directly to the map.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fmap = {}
|
||||||
|
for name in exact:
|
||||||
|
fmap[name] = filter_exact(getattr(cls, name))
|
||||||
|
for name in ilike:
|
||||||
|
fmap[name] = filter_ilike(getattr(cls, name))
|
||||||
|
fmap.update(kwargs)
|
||||||
|
return fmap
|
||||||
|
|
||||||
|
|
||||||
|
def get_search_form(request, filter_map, config):
|
||||||
|
"""
|
||||||
|
Returns a :class:`SearchForm` instance with a :class:`SearchFilter` for
|
||||||
|
each filter in ``filter_map``, using configuration from ``config``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
search = SearchForm(request, filter_map, config)
|
||||||
|
for field in filter_map:
|
||||||
|
factory = config.get('filter_factory_%s' % field, SearchFilter)
|
||||||
|
label = config.get('filter_label_%s' % field)
|
||||||
|
search.add_filter(factory(field, label=label))
|
||||||
|
return search
|
||||||
|
|
||||||
|
|
||||||
|
def filter_query(query, config, filter_map, join_map):
|
||||||
|
"""
|
||||||
|
Filters ``query`` according to ``config`` and ``filter_map``. ``join_map``
|
||||||
|
is used, if necessary, to join additional tables to the base query. The
|
||||||
|
filtered query is returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
joins = config.setdefault('joins', [])
|
||||||
|
for key in config:
|
||||||
|
if key.startswith('include_filter_') and config[key]:
|
||||||
|
field = key[15:]
|
||||||
|
if field in join_map and field not in joins:
|
||||||
|
query = join_map[field](query)
|
||||||
|
joins.append(field)
|
||||||
|
value = config.get(field)
|
||||||
|
if value:
|
||||||
|
fmap = filter_map[field]
|
||||||
|
filt = fmap[config['filter_type_'+field]]
|
||||||
|
query = filt(query, value)
|
||||||
|
return query
|
138
rattail/pyramid/grids/util.py
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.grids.util`` -- Grid Utilities
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||||
|
|
||||||
|
from webhelpers.html import literal
|
||||||
|
|
||||||
|
from pyramid.response import Response
|
||||||
|
|
||||||
|
from rattail.pyramid.grids.search import SearchFormRenderer
|
||||||
|
|
||||||
|
|
||||||
|
def get_sort_config(name, request, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a configuration dictionary for grid sorting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initial config uses some default values.
|
||||||
|
config = {
|
||||||
|
'dir': 'asc',
|
||||||
|
'per_page': 20,
|
||||||
|
'page': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Override with defaults provided by caller.
|
||||||
|
config.update(kwargs)
|
||||||
|
|
||||||
|
# Override with values from GET/POST request and/or session.
|
||||||
|
for key in config:
|
||||||
|
full_key = name+'_'+key
|
||||||
|
if request.params.get(key):
|
||||||
|
value = request.params[key]
|
||||||
|
config[key] = value
|
||||||
|
request.session[full_key] = value
|
||||||
|
elif request.session.get(full_key):
|
||||||
|
value = request.session[full_key]
|
||||||
|
config[key] = value
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def get_sort_map(cls, names=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Convenience function which returns a sort map for ``cls``.
|
||||||
|
|
||||||
|
If ``names`` is not specified, the map will include all "standard" fields
|
||||||
|
present on the mapped class. Otherwise, the map will be limited to only
|
||||||
|
the fields which are named.
|
||||||
|
|
||||||
|
All remaining ``kwargs`` are assumed to be sort map entries, and will be
|
||||||
|
added to the map directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
smap = {}
|
||||||
|
if names is None:
|
||||||
|
names = []
|
||||||
|
for attr in cls.__dict__:
|
||||||
|
obj = getattr(cls, attr)
|
||||||
|
if isinstance(obj, InstrumentedAttribute):
|
||||||
|
if obj.key != 'uuid':
|
||||||
|
names.append(obj.key)
|
||||||
|
for name in names:
|
||||||
|
smap[name] = sorter(getattr(cls, name))
|
||||||
|
smap.update(kwargs)
|
||||||
|
return smap
|
||||||
|
|
||||||
|
|
||||||
|
def render_grid(grid, search_form=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Convenience function to render ``grid`` (which should be a
|
||||||
|
:class:`rattail.pyramid.grids.Grid` instance).
|
||||||
|
|
||||||
|
This "usually" will return a dictionary to be used as context for rendering
|
||||||
|
the final view template.
|
||||||
|
|
||||||
|
However, if a partial grid is requested (or mandated), then the grid body
|
||||||
|
will be rendered and a :class:`pyramid.response.Response` object will be
|
||||||
|
returned instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if grid.partial_only or grid.request.params.get('partial'):
|
||||||
|
return Response(body=grid.render(), content_type='text/html')
|
||||||
|
kwargs['grid'] = literal(grid.render())
|
||||||
|
if search_form:
|
||||||
|
kwargs['search'] = SearchFormRenderer(search_form)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def sort_query(query, config, sort_map, join_map={}):
|
||||||
|
"""
|
||||||
|
Sorts ``query`` according to ``config`` and ``sort_map``. ``join_map`` is
|
||||||
|
used, if necessary, to join additional tables to the base query. The
|
||||||
|
sorted query is returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field = config.get('sort')
|
||||||
|
if not field:
|
||||||
|
return query
|
||||||
|
joins = config.setdefault('joins', [])
|
||||||
|
if field in join_map and field not in joins:
|
||||||
|
query = join_map[field](query)
|
||||||
|
joins.append(field)
|
||||||
|
sort = sort_map[field]
|
||||||
|
return sort(query, config['dir'])
|
||||||
|
|
||||||
|
|
||||||
|
def sorter(field):
|
||||||
|
"""
|
||||||
|
Returns a function suitable for a sort map callable, with typical logic
|
||||||
|
built in for sorting applied to ``field``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return lambda q, d: q.order_by(getattr(field, d)())
|
33
rattail/pyramid/helpers.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.helpers`` -- Template Context Helpers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from webhelpers.html import *
|
||||||
|
from webhelpers.html.tags import *
|
78
rattail/pyramid/progress.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.progress`` -- Progress Indicator
|
||||||
|
"""
|
||||||
|
|
||||||
|
from beaker.session import Session
|
||||||
|
|
||||||
|
|
||||||
|
def get_progress_session(session, key):
|
||||||
|
request = session.request
|
||||||
|
id = '%s.progress.%s' % (session.id, key)
|
||||||
|
session = Session(request, id)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
class SessionProgress(object):
|
||||||
|
"""
|
||||||
|
Provides a session-based progress bar mechanism.
|
||||||
|
|
||||||
|
This class is only responsible for keeping the progress *data* current. It
|
||||||
|
is the responsibility of some client-side AJAX (etc.) to consume the data
|
||||||
|
for display to the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, session, key):
|
||||||
|
self.session = get_progress_session(session, key)
|
||||||
|
self.canceled = False
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def __call__(self, message, maximum):
|
||||||
|
self.clear()
|
||||||
|
self.session['message'] = message
|
||||||
|
self.session['maximum'] = maximum
|
||||||
|
self.session['value'] = 0
|
||||||
|
self.session.save()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.session.clear()
|
||||||
|
self.session['complete'] = False
|
||||||
|
self.session['error'] = False
|
||||||
|
self.session['canceled'] = False
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
|
def update(self, value):
|
||||||
|
self.session.load()
|
||||||
|
if self.session.get('canceled'):
|
||||||
|
self.canceled = True
|
||||||
|
else:
|
||||||
|
self.session['value'] = value
|
||||||
|
self.session.save()
|
||||||
|
return not self.canceled
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
pass
|
24
rattail/pyramid/static/css/autocomplete.css
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Autocomplete
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.autocomplete {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.autocomplete div {
|
||||||
|
background-color: #dddddd;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.autocomplete strong {
|
||||||
|
margin: 0px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.autocomplete .selected {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #aaaaaa;
|
||||||
|
}
|
98
rattail/pyramid/static/css/base.css
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* General
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 12pt;
|
||||||
|
margin: 20px auto 10px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.buttons {
|
||||||
|
clear: both;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* div.controls { */
|
||||||
|
/* font-weight: bold; */
|
||||||
|
/* margin: 10px auto; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
/* div.controls div { */
|
||||||
|
/* margin: 5px; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
/* div.controls label { */
|
||||||
|
/* display: block; */
|
||||||
|
/* float: left; */
|
||||||
|
/* width: 120px; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
div.dialog {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.flash-message {
|
||||||
|
background-color: #dddddd;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.error {
|
||||||
|
color: #dd6666;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.error {
|
||||||
|
color: #dd6666;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.error li {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
24
rattail/pyramid/static/css/filters.css
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Filters
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.filters div.filter {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.filters div.filter label {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.filters div.filter select.filter-type {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.filters div.filter div.value {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.filters div.buttons * {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
97
rattail/pyramid/static/css/forms.css
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Form Wrapper
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.form-wrapper {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Context Menu
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.form-wrapper ul.context-menu {
|
||||||
|
float: right;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Forms
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.form,
|
||||||
|
div.fieldset-form,
|
||||||
|
div.fieldset {
|
||||||
|
float: left;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Fieldsets
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.field-wrapper {
|
||||||
|
clear: both;
|
||||||
|
min-height: 30px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper.error {
|
||||||
|
background-color: #ddcccc;
|
||||||
|
border: 2px solid #dd6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper label {
|
||||||
|
color: #000000;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
width: 140px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper div.field-error {
|
||||||
|
/* clear: both; */
|
||||||
|
color: #dd6666;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper div.field {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper div.field input[type=text],
|
||||||
|
div.field-wrapper div.field input[type=password],
|
||||||
|
div.field-wrapper div.field select,
|
||||||
|
div.field-wrapper div.field textarea {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label input[type=checkbox] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Buttons
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.buttons {
|
||||||
|
clear: both;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.buttons * {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
178
rattail/pyramid/static/css/grids.css
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Grid Header
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
table.grid-header {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Form (Filters etc.)
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
table.grid-header td.form {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Context Menu
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
table.grid-header td.context-menu {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.grid-header td.context-menu ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Tools
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
table.grid-header td.tools {
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Grid
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.grid {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 9pt;
|
||||||
|
line-height: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid.full table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table th,
|
||||||
|
div.grid table td {
|
||||||
|
border-right: 1px solid black;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
padding: 2px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table th.sortable a {
|
||||||
|
display: block;
|
||||||
|
padding-right: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table th.sorted {
|
||||||
|
background-position: right center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table th.sorted.asc {
|
||||||
|
background-image: url(../img/sort_arrow_up.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table th.sorted.desc {
|
||||||
|
background-image: url(../img/sort_arrow_down.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody td {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody td.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody td.right {
|
||||||
|
float: none;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tr.odd {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* div.grid table thead th.checkbox, */
|
||||||
|
/* div.grid table tbody td.checkbox { */
|
||||||
|
/* text-align: center; */
|
||||||
|
/* vertical-align: middle; */
|
||||||
|
/* width: 15px; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
div.grid table tbody tr.hovering {
|
||||||
|
background-color: #bbbbbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table.hoverable tbody tr {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid.clickable table tbody tr,
|
||||||
|
div.grid table.selectable tbody tr,
|
||||||
|
div.grid table.checkable tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid.clickable table tbody tr td.noclick {
|
||||||
|
cursor: default;
|
||||||
|
text-align: center;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody tr td.noclick.edit,
|
||||||
|
div.grid table tbody tr td.noclick.delete {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody tr td.noclick.edit {
|
||||||
|
background-image: url(../img/edit.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody tr td.noclick.delete {
|
||||||
|
background-image: url(../img/delete.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* div.grid table.selectable tbody tr.selected, */
|
||||||
|
/* div.grid table.checkable tbody tr.selected { */
|
||||||
|
/* background-color: #666666; */
|
||||||
|
/* color: white; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
div.pager {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pager p {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pager p.showing {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pager #grid-page-count {
|
||||||
|
font-size: 8pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pager p.page-links {
|
||||||
|
float: right;
|
||||||
|
}
|
75
rattail/pyramid/static/css/layout.css
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Main Layout
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
html, body, #container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > #container {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
border-bottom: 1px solid #000000;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#body {
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
clear: both;
|
||||||
|
margin-top: -4em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Header
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
#header h1 {
|
||||||
|
margin: 0px 5px 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
margin: 8px 20px auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login a.username {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-menu {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#home-link {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-links {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-menu {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
clear: both;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-menu li {
|
||||||
|
display: inline;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
29
rattail/pyramid/static/css/login.css
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* login.css
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.form {
|
||||||
|
margin: auto;
|
||||||
|
float: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper {
|
||||||
|
margin: 10px auto;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper label {
|
||||||
|
margin: 0px;
|
||||||
|
text-align: right;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper input {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.buttons input {
|
||||||
|
margin: auto 5px;
|
||||||
|
}
|
33
rattail/pyramid/static/css/perms.css
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Permission Lists
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.field-wrapper.permissions div.field div.group {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper.permissions div.field div.group p {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper.permissions div.field label {
|
||||||
|
float: none;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper.permissions div.field label input {
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper.permissions div.field div.group p.perm {
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.field-wrapper.permissions div.field div.group p.perm span {
|
||||||
|
font-family: monospace;
|
||||||
|
/* font-weight: bold; */
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 178 B |
After Width: | Height: | Size: 120 B |
After Width: | Height: | Size: 105 B |
After Width: | Height: | Size: 111 B |
After Width: | Height: | Size: 110 B |
After Width: | Height: | Size: 119 B |
After Width: | Height: | Size: 101 B |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
489
rattail/pyramid/static/css/smoothness/jquery-ui-1.8.2.custom.css
vendored
Normal file
|
@ -0,0 +1,489 @@
|
||||||
|
/*
|
||||||
|
* jQuery UI CSS Framework
|
||||||
|
* Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
|
||||||
|
* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Layout helpers
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-helper-hidden { display: none; }
|
||||||
|
.ui-helper-hidden-accessible { position: absolute; left: -99999999px; }
|
||||||
|
.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
|
||||||
|
.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
|
||||||
|
.ui-helper-clearfix { display: inline-block; }
|
||||||
|
/* required comment for clearfix to work in Opera \*/
|
||||||
|
* html .ui-helper-clearfix { height:1%; }
|
||||||
|
.ui-helper-clearfix { display:block; }
|
||||||
|
/* end clearfix */
|
||||||
|
.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
|
||||||
|
|
||||||
|
|
||||||
|
/* Interaction Cues
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-state-disabled { cursor: default !important; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Icons
|
||||||
|
----------------------------------*/
|
||||||
|
|
||||||
|
/* states and images */
|
||||||
|
.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Misc visuals
|
||||||
|
----------------------------------*/
|
||||||
|
|
||||||
|
/* Overlays */
|
||||||
|
.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* jQuery UI CSS Framework
|
||||||
|
* Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
|
||||||
|
* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
|
||||||
|
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/* Component containers
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; }
|
||||||
|
.ui-widget .ui-widget { font-size: 1em; }
|
||||||
|
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
|
||||||
|
.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; }
|
||||||
|
.ui-widget-content a { color: #222222; }
|
||||||
|
.ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
|
||||||
|
.ui-widget-header a { color: #222222; }
|
||||||
|
|
||||||
|
/* Interaction states
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #555555; }
|
||||||
|
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; }
|
||||||
|
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999; background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; }
|
||||||
|
.ui-state-hover a, .ui-state-hover a:hover { color: #212121; text-decoration: none; }
|
||||||
|
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; }
|
||||||
|
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; text-decoration: none; }
|
||||||
|
.ui-widget :active { outline: none; }
|
||||||
|
|
||||||
|
/* Interaction Cues
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fcefa1; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; }
|
||||||
|
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; }
|
||||||
|
.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; }
|
||||||
|
.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; }
|
||||||
|
.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; }
|
||||||
|
.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
|
||||||
|
.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
|
||||||
|
.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
|
||||||
|
|
||||||
|
/* Icons
|
||||||
|
----------------------------------*/
|
||||||
|
|
||||||
|
/* states and images */
|
||||||
|
.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); }
|
||||||
|
.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
|
||||||
|
.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
|
||||||
|
.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png); }
|
||||||
|
.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
|
||||||
|
.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
|
||||||
|
.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); }
|
||||||
|
.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); }
|
||||||
|
|
||||||
|
/* positioning */
|
||||||
|
.ui-icon-carat-1-n { background-position: 0 0; }
|
||||||
|
.ui-icon-carat-1-ne { background-position: -16px 0; }
|
||||||
|
.ui-icon-carat-1-e { background-position: -32px 0; }
|
||||||
|
.ui-icon-carat-1-se { background-position: -48px 0; }
|
||||||
|
.ui-icon-carat-1-s { background-position: -64px 0; }
|
||||||
|
.ui-icon-carat-1-sw { background-position: -80px 0; }
|
||||||
|
.ui-icon-carat-1-w { background-position: -96px 0; }
|
||||||
|
.ui-icon-carat-1-nw { background-position: -112px 0; }
|
||||||
|
.ui-icon-carat-2-n-s { background-position: -128px 0; }
|
||||||
|
.ui-icon-carat-2-e-w { background-position: -144px 0; }
|
||||||
|
.ui-icon-triangle-1-n { background-position: 0 -16px; }
|
||||||
|
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
|
||||||
|
.ui-icon-triangle-1-e { background-position: -32px -16px; }
|
||||||
|
.ui-icon-triangle-1-se { background-position: -48px -16px; }
|
||||||
|
.ui-icon-triangle-1-s { background-position: -64px -16px; }
|
||||||
|
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
|
||||||
|
.ui-icon-triangle-1-w { background-position: -96px -16px; }
|
||||||
|
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
|
||||||
|
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
|
||||||
|
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
|
||||||
|
.ui-icon-arrow-1-n { background-position: 0 -32px; }
|
||||||
|
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
|
||||||
|
.ui-icon-arrow-1-e { background-position: -32px -32px; }
|
||||||
|
.ui-icon-arrow-1-se { background-position: -48px -32px; }
|
||||||
|
.ui-icon-arrow-1-s { background-position: -64px -32px; }
|
||||||
|
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
|
||||||
|
.ui-icon-arrow-1-w { background-position: -96px -32px; }
|
||||||
|
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
|
||||||
|
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
|
||||||
|
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
|
||||||
|
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
|
||||||
|
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
|
||||||
|
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
|
||||||
|
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
|
||||||
|
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
|
||||||
|
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
|
||||||
|
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
|
||||||
|
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
|
||||||
|
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
|
||||||
|
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
|
||||||
|
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
|
||||||
|
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
|
||||||
|
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
|
||||||
|
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
|
||||||
|
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
|
||||||
|
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
|
||||||
|
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
|
||||||
|
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
|
||||||
|
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
|
||||||
|
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
|
||||||
|
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
|
||||||
|
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
|
||||||
|
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
|
||||||
|
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
|
||||||
|
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
|
||||||
|
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
|
||||||
|
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
|
||||||
|
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
|
||||||
|
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
|
||||||
|
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
|
||||||
|
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
|
||||||
|
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
|
||||||
|
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
|
||||||
|
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
|
||||||
|
.ui-icon-arrow-4 { background-position: 0 -80px; }
|
||||||
|
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
|
||||||
|
.ui-icon-extlink { background-position: -32px -80px; }
|
||||||
|
.ui-icon-newwin { background-position: -48px -80px; }
|
||||||
|
.ui-icon-refresh { background-position: -64px -80px; }
|
||||||
|
.ui-icon-shuffle { background-position: -80px -80px; }
|
||||||
|
.ui-icon-transfer-e-w { background-position: -96px -80px; }
|
||||||
|
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
|
||||||
|
.ui-icon-folder-collapsed { background-position: 0 -96px; }
|
||||||
|
.ui-icon-folder-open { background-position: -16px -96px; }
|
||||||
|
.ui-icon-document { background-position: -32px -96px; }
|
||||||
|
.ui-icon-document-b { background-position: -48px -96px; }
|
||||||
|
.ui-icon-note { background-position: -64px -96px; }
|
||||||
|
.ui-icon-mail-closed { background-position: -80px -96px; }
|
||||||
|
.ui-icon-mail-open { background-position: -96px -96px; }
|
||||||
|
.ui-icon-suitcase { background-position: -112px -96px; }
|
||||||
|
.ui-icon-comment { background-position: -128px -96px; }
|
||||||
|
.ui-icon-person { background-position: -144px -96px; }
|
||||||
|
.ui-icon-print { background-position: -160px -96px; }
|
||||||
|
.ui-icon-trash { background-position: -176px -96px; }
|
||||||
|
.ui-icon-locked { background-position: -192px -96px; }
|
||||||
|
.ui-icon-unlocked { background-position: -208px -96px; }
|
||||||
|
.ui-icon-bookmark { background-position: -224px -96px; }
|
||||||
|
.ui-icon-tag { background-position: -240px -96px; }
|
||||||
|
.ui-icon-home { background-position: 0 -112px; }
|
||||||
|
.ui-icon-flag { background-position: -16px -112px; }
|
||||||
|
.ui-icon-calendar { background-position: -32px -112px; }
|
||||||
|
.ui-icon-cart { background-position: -48px -112px; }
|
||||||
|
.ui-icon-pencil { background-position: -64px -112px; }
|
||||||
|
.ui-icon-clock { background-position: -80px -112px; }
|
||||||
|
.ui-icon-disk { background-position: -96px -112px; }
|
||||||
|
.ui-icon-calculator { background-position: -112px -112px; }
|
||||||
|
.ui-icon-zoomin { background-position: -128px -112px; }
|
||||||
|
.ui-icon-zoomout { background-position: -144px -112px; }
|
||||||
|
.ui-icon-search { background-position: -160px -112px; }
|
||||||
|
.ui-icon-wrench { background-position: -176px -112px; }
|
||||||
|
.ui-icon-gear { background-position: -192px -112px; }
|
||||||
|
.ui-icon-heart { background-position: -208px -112px; }
|
||||||
|
.ui-icon-star { background-position: -224px -112px; }
|
||||||
|
.ui-icon-link { background-position: -240px -112px; }
|
||||||
|
.ui-icon-cancel { background-position: 0 -128px; }
|
||||||
|
.ui-icon-plus { background-position: -16px -128px; }
|
||||||
|
.ui-icon-plusthick { background-position: -32px -128px; }
|
||||||
|
.ui-icon-minus { background-position: -48px -128px; }
|
||||||
|
.ui-icon-minusthick { background-position: -64px -128px; }
|
||||||
|
.ui-icon-close { background-position: -80px -128px; }
|
||||||
|
.ui-icon-closethick { background-position: -96px -128px; }
|
||||||
|
.ui-icon-key { background-position: -112px -128px; }
|
||||||
|
.ui-icon-lightbulb { background-position: -128px -128px; }
|
||||||
|
.ui-icon-scissors { background-position: -144px -128px; }
|
||||||
|
.ui-icon-clipboard { background-position: -160px -128px; }
|
||||||
|
.ui-icon-copy { background-position: -176px -128px; }
|
||||||
|
.ui-icon-contact { background-position: -192px -128px; }
|
||||||
|
.ui-icon-image { background-position: -208px -128px; }
|
||||||
|
.ui-icon-video { background-position: -224px -128px; }
|
||||||
|
.ui-icon-script { background-position: -240px -128px; }
|
||||||
|
.ui-icon-alert { background-position: 0 -144px; }
|
||||||
|
.ui-icon-info { background-position: -16px -144px; }
|
||||||
|
.ui-icon-notice { background-position: -32px -144px; }
|
||||||
|
.ui-icon-help { background-position: -48px -144px; }
|
||||||
|
.ui-icon-check { background-position: -64px -144px; }
|
||||||
|
.ui-icon-bullet { background-position: -80px -144px; }
|
||||||
|
.ui-icon-radio-off { background-position: -96px -144px; }
|
||||||
|
.ui-icon-radio-on { background-position: -112px -144px; }
|
||||||
|
.ui-icon-pin-w { background-position: -128px -144px; }
|
||||||
|
.ui-icon-pin-s { background-position: -144px -144px; }
|
||||||
|
.ui-icon-play { background-position: 0 -160px; }
|
||||||
|
.ui-icon-pause { background-position: -16px -160px; }
|
||||||
|
.ui-icon-seek-next { background-position: -32px -160px; }
|
||||||
|
.ui-icon-seek-prev { background-position: -48px -160px; }
|
||||||
|
.ui-icon-seek-end { background-position: -64px -160px; }
|
||||||
|
.ui-icon-seek-start { background-position: -80px -160px; }
|
||||||
|
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
|
||||||
|
.ui-icon-seek-first { background-position: -80px -160px; }
|
||||||
|
.ui-icon-stop { background-position: -96px -160px; }
|
||||||
|
.ui-icon-eject { background-position: -112px -160px; }
|
||||||
|
.ui-icon-volume-off { background-position: -128px -160px; }
|
||||||
|
.ui-icon-volume-on { background-position: -144px -160px; }
|
||||||
|
.ui-icon-power { background-position: 0 -176px; }
|
||||||
|
.ui-icon-signal-diag { background-position: -16px -176px; }
|
||||||
|
.ui-icon-signal { background-position: -32px -176px; }
|
||||||
|
.ui-icon-battery-0 { background-position: -48px -176px; }
|
||||||
|
.ui-icon-battery-1 { background-position: -64px -176px; }
|
||||||
|
.ui-icon-battery-2 { background-position: -80px -176px; }
|
||||||
|
.ui-icon-battery-3 { background-position: -96px -176px; }
|
||||||
|
.ui-icon-circle-plus { background-position: 0 -192px; }
|
||||||
|
.ui-icon-circle-minus { background-position: -16px -192px; }
|
||||||
|
.ui-icon-circle-close { background-position: -32px -192px; }
|
||||||
|
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
|
||||||
|
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
|
||||||
|
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
|
||||||
|
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
|
||||||
|
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
|
||||||
|
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
|
||||||
|
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
|
||||||
|
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
|
||||||
|
.ui-icon-circle-zoomin { background-position: -176px -192px; }
|
||||||
|
.ui-icon-circle-zoomout { background-position: -192px -192px; }
|
||||||
|
.ui-icon-circle-check { background-position: -208px -192px; }
|
||||||
|
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
|
||||||
|
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
|
||||||
|
.ui-icon-circlesmall-close { background-position: -32px -208px; }
|
||||||
|
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
|
||||||
|
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
|
||||||
|
.ui-icon-squaresmall-close { background-position: -80px -208px; }
|
||||||
|
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
|
||||||
|
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
|
||||||
|
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
|
||||||
|
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
|
||||||
|
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
|
||||||
|
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Misc visuals
|
||||||
|
----------------------------------*/
|
||||||
|
|
||||||
|
/* Corner radius */
|
||||||
|
.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; }
|
||||||
|
.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
|
||||||
|
.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
|
||||||
|
.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||||
|
.ui-corner-top { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
|
||||||
|
.ui-corner-bottom { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||||
|
.ui-corner-right { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
|
||||||
|
.ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
|
||||||
|
.ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px; }
|
||||||
|
|
||||||
|
/* Overlays */
|
||||||
|
.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
|
||||||
|
.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/* Resizable
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-resizable { position: relative;}
|
||||||
|
.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
|
||||||
|
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
|
||||||
|
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
|
||||||
|
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
|
||||||
|
.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
|
||||||
|
.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
|
||||||
|
.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
|
||||||
|
.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
|
||||||
|
.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
|
||||||
|
.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* Selectable
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-selectable-helper { border:1px dotted black }
|
||||||
|
/* Accordion
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
|
||||||
|
.ui-accordion .ui-accordion-li-fix { display: inline; }
|
||||||
|
.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
|
||||||
|
.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
|
||||||
|
/* IE7-/Win - Fix extra vertical space in lists */
|
||||||
|
.ui-accordion a { zoom: 1; }
|
||||||
|
.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
|
||||||
|
.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
|
||||||
|
.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
|
||||||
|
.ui-accordion .ui-accordion-content-active { display: block; }/* Autocomplete
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-autocomplete { position: absolute; cursor: default; }
|
||||||
|
.ui-autocomplete-loading { background: white url('images/ui-anim_basic_16x16.gif') right center no-repeat; }
|
||||||
|
|
||||||
|
/* workarounds */
|
||||||
|
* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
|
||||||
|
|
||||||
|
/* Menu
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-menu {
|
||||||
|
list-style:none;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 0;
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
.ui-menu .ui-menu {
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
||||||
|
.ui-menu .ui-menu-item {
|
||||||
|
margin:0;
|
||||||
|
padding: 0;
|
||||||
|
zoom: 1;
|
||||||
|
float: left;
|
||||||
|
clear: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ui-menu .ui-menu-item a {
|
||||||
|
text-decoration:none;
|
||||||
|
display:block;
|
||||||
|
padding:.2em .4em;
|
||||||
|
line-height:1.5;
|
||||||
|
zoom:1;
|
||||||
|
}
|
||||||
|
.ui-menu .ui-menu-item a.ui-state-hover,
|
||||||
|
.ui-menu .ui-menu-item a.ui-state-active {
|
||||||
|
font-weight: normal;
|
||||||
|
margin: -1px;
|
||||||
|
}
|
||||||
|
/* Button
|
||||||
|
----------------------------------*/
|
||||||
|
|
||||||
|
.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
|
||||||
|
.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
|
||||||
|
button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
|
||||||
|
.ui-button-icons-only { width: 3.4em; }
|
||||||
|
button.ui-button-icons-only { width: 3.7em; }
|
||||||
|
|
||||||
|
/*button text element */
|
||||||
|
.ui-button .ui-button-text { display: block; line-height: 1.4; }
|
||||||
|
.ui-button-text-only .ui-button-text { padding: .4em 1em; }
|
||||||
|
.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
|
||||||
|
.ui-button-text-icon .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
|
||||||
|
.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
|
||||||
|
/* no icon support for input elements, provide padding by default */
|
||||||
|
input.ui-button { padding: .4em 1em; }
|
||||||
|
|
||||||
|
/*button icon element(s) */
|
||||||
|
.ui-button-icon-only .ui-icon, .ui-button-text-icon .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
|
||||||
|
.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
|
||||||
|
.ui-button-text-icon .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
|
||||||
|
.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
|
||||||
|
|
||||||
|
/*button sets*/
|
||||||
|
.ui-buttonset { margin-right: 7px; }
|
||||||
|
.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
|
||||||
|
|
||||||
|
/* workarounds */
|
||||||
|
button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Dialog
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
|
||||||
|
.ui-dialog .ui-dialog-titlebar { padding: .5em 1em .3em; position: relative; }
|
||||||
|
.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .2em 0; }
|
||||||
|
.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
|
||||||
|
.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
|
||||||
|
.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
|
||||||
|
.ui-dialog .ui-dialog-content { border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
|
||||||
|
.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
|
||||||
|
.ui-dialog .ui-dialog-buttonpane button { float: right; margin: .5em .4em .5em 0; cursor: pointer; padding: .2em .6em .3em .6em; line-height: 1.4em; width:auto; overflow:visible; }
|
||||||
|
.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
|
||||||
|
.ui-draggable .ui-dialog-titlebar { cursor: move; }
|
||||||
|
/* Slider
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-slider { position: relative; text-align: left; }
|
||||||
|
.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
|
||||||
|
.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
|
||||||
|
|
||||||
|
.ui-slider-horizontal { height: .8em; }
|
||||||
|
.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
|
||||||
|
.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
|
||||||
|
.ui-slider-horizontal .ui-slider-range-min { left: 0; }
|
||||||
|
.ui-slider-horizontal .ui-slider-range-max { right: 0; }
|
||||||
|
|
||||||
|
.ui-slider-vertical { width: .8em; height: 100px; }
|
||||||
|
.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
|
||||||
|
.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
|
||||||
|
.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
|
||||||
|
.ui-slider-vertical .ui-slider-range-max { top: 0; }/* Tabs
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
|
||||||
|
.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
|
||||||
|
.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; }
|
||||||
|
.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
|
||||||
|
.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }
|
||||||
|
.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
|
||||||
|
.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
|
||||||
|
.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
|
||||||
|
.ui-tabs .ui-tabs-hide { display: none !important; }
|
||||||
|
/* Datepicker
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-datepicker { width: 17em; padding: .2em .2em 0; }
|
||||||
|
.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
|
||||||
|
.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
|
||||||
|
.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
|
||||||
|
.ui-datepicker .ui-datepicker-prev { left:2px; }
|
||||||
|
.ui-datepicker .ui-datepicker-next { right:2px; }
|
||||||
|
.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
|
||||||
|
.ui-datepicker .ui-datepicker-next-hover { right:1px; }
|
||||||
|
.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
|
||||||
|
.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
|
||||||
|
.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
|
||||||
|
.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
|
||||||
|
.ui-datepicker select.ui-datepicker-month,
|
||||||
|
.ui-datepicker select.ui-datepicker-year { width: 49%;}
|
||||||
|
.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
|
||||||
|
.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
|
||||||
|
.ui-datepicker td { border: 0; padding: 1px; }
|
||||||
|
.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
|
||||||
|
.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
|
||||||
|
.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
|
||||||
|
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
|
||||||
|
|
||||||
|
/* with multiple calendars */
|
||||||
|
.ui-datepicker.ui-datepicker-multi { width:auto; }
|
||||||
|
.ui-datepicker-multi .ui-datepicker-group { float:left; }
|
||||||
|
.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
|
||||||
|
.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
|
||||||
|
.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
|
||||||
|
.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
|
||||||
|
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
|
||||||
|
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
|
||||||
|
.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
|
||||||
|
.ui-datepicker-row-break { clear:both; width:100%; }
|
||||||
|
|
||||||
|
/* RTL support */
|
||||||
|
.ui-datepicker-rtl { direction: rtl; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-group { float:right; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
|
||||||
|
|
||||||
|
/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
|
||||||
|
.ui-datepicker-cover {
|
||||||
|
display: none; /*sorry for IE5*/
|
||||||
|
display/**/: block; /*sorry for IE5*/
|
||||||
|
position: absolute; /*must have*/
|
||||||
|
z-index: -1; /*must have*/
|
||||||
|
filter: mask(); /*must have*/
|
||||||
|
top: -4px; /*must have*/
|
||||||
|
left: -4px; /*must have*/
|
||||||
|
width: 200px; /*must have*/
|
||||||
|
height: 200px; /*must have*/
|
||||||
|
}/* Progressbar
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-progressbar { height:2em; text-align: left; }
|
||||||
|
.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }
|
BIN
rattail/pyramid/static/img/delete.png
Normal file
After Width: | Height: | Size: 641 B |
BIN
rattail/pyramid/static/img/edit.png
Normal file
After Width: | Height: | Size: 533 B |
BIN
rattail/pyramid/static/img/logo.jpg
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
rattail/pyramid/static/img/sort_arrow_down.png
Normal file
After Width: | Height: | Size: 158 B |
BIN
rattail/pyramid/static/img/sort_arrow_up.png
Normal file
After Width: | Height: | Size: 169 B |
392
rattail/pyramid/static/js/jquery.autocomplete.js
Normal file
|
@ -0,0 +1,392 @@
|
||||||
|
/**
|
||||||
|
* Ajax Autocomplete for jQuery, version 1.1.3
|
||||||
|
* (c) 2010 Tomas Kirda
|
||||||
|
*
|
||||||
|
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
|
||||||
|
* For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
|
||||||
|
*
|
||||||
|
* Last Review: 04/19/2010
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*jslint onevar: true, evil: true, nomen: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
|
||||||
|
/*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
|
||||||
|
var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g');
|
||||||
|
|
||||||
|
function fnFormatResult(value, data, currentValue) {
|
||||||
|
var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
|
||||||
|
return value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function Autocomplete(el, options) {
|
||||||
|
this.el = $(el);
|
||||||
|
this.el.attr('autocomplete', 'off');
|
||||||
|
this.suggestions = [];
|
||||||
|
this.data = [];
|
||||||
|
this.badQueries = [];
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
this.currentValue = this.el.val();
|
||||||
|
this.intervalId = 0;
|
||||||
|
this.cachedResponse = [];
|
||||||
|
this.onChangeInterval = null;
|
||||||
|
this.ignoreValueChange = false;
|
||||||
|
this.serviceUrl = options.serviceUrl;
|
||||||
|
this.isLocal = false;
|
||||||
|
this.options = {
|
||||||
|
autoSubmit: false,
|
||||||
|
minChars: 1,
|
||||||
|
maxHeight: 300,
|
||||||
|
deferRequestBy: 0,
|
||||||
|
width: 0,
|
||||||
|
highlight: true,
|
||||||
|
params: {},
|
||||||
|
fnFormatResult: fnFormatResult,
|
||||||
|
delimiter: null,
|
||||||
|
zIndex: 9999
|
||||||
|
};
|
||||||
|
this.initialize();
|
||||||
|
this.setOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.fn.autocomplete = function(options) {
|
||||||
|
return new Autocomplete(this.get(0)||$('<input />'), options);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Autocomplete.prototype = {
|
||||||
|
|
||||||
|
killerFn: null,
|
||||||
|
|
||||||
|
initialize: function() {
|
||||||
|
|
||||||
|
var me, uid, autocompleteElId;
|
||||||
|
me = this;
|
||||||
|
uid = Math.floor(Math.random()*0x100000).toString(16);
|
||||||
|
autocompleteElId = 'Autocomplete_' + uid;
|
||||||
|
|
||||||
|
this.killerFn = function(e) {
|
||||||
|
if ($(e.target).parents('.autocomplete').size() === 0) {
|
||||||
|
me.killSuggestions();
|
||||||
|
me.disableKillerFn();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.options.width) { this.options.width = this.el.width(); }
|
||||||
|
this.mainContainerId = 'AutocompleteContainter_' + uid;
|
||||||
|
|
||||||
|
$('<div id="' + this.mainContainerId + '" style="position:absolute;z-index:9999;"><div class="autocomplete-w1"><div class="autocomplete" id="' + autocompleteElId + '" style="display:none; width:300px;"></div></div></div>').appendTo('body');
|
||||||
|
|
||||||
|
this.container = $('#' + autocompleteElId);
|
||||||
|
this.fixPosition();
|
||||||
|
if (window.opera) {
|
||||||
|
this.el.keypress(function(e) { me.onKeyPress(e); });
|
||||||
|
} else {
|
||||||
|
this.el.keydown(function(e) { me.onKeyPress(e); });
|
||||||
|
}
|
||||||
|
this.el.keyup(function(e) { me.onKeyUp(e); });
|
||||||
|
this.el.blur(function() { me.enableKillerFn(); });
|
||||||
|
this.el.focus(function() { me.fixPosition(); });
|
||||||
|
},
|
||||||
|
|
||||||
|
setOptions: function(options){
|
||||||
|
var o = this.options;
|
||||||
|
$.extend(o, options);
|
||||||
|
if(o.lookup){
|
||||||
|
this.isLocal = true;
|
||||||
|
if($.isArray(o.lookup)){ o.lookup = { suggestions:o.lookup, data:[] }; }
|
||||||
|
}
|
||||||
|
$('#'+this.mainContainerId).css({ zIndex:o.zIndex });
|
||||||
|
this.container.css({ maxHeight: o.maxHeight + 'px', width:o.width });
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCache: function(){
|
||||||
|
this.cachedResponse = [];
|
||||||
|
this.badQueries = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
disable: function(){
|
||||||
|
this.disabled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
enable: function(){
|
||||||
|
this.disabled = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
fixPosition: function() {
|
||||||
|
var offset = this.el.offset();
|
||||||
|
$('#' + this.mainContainerId).css({ top: (offset.top + this.el.innerHeight()) + 'px', left: offset.left + 'px' });
|
||||||
|
},
|
||||||
|
|
||||||
|
enableKillerFn: function() {
|
||||||
|
var me = this;
|
||||||
|
$(document).bind('click', me.killerFn);
|
||||||
|
},
|
||||||
|
|
||||||
|
disableKillerFn: function() {
|
||||||
|
var me = this;
|
||||||
|
$(document).unbind('click', me.killerFn);
|
||||||
|
},
|
||||||
|
|
||||||
|
killSuggestions: function() {
|
||||||
|
var me = this;
|
||||||
|
this.stopKillSuggestions();
|
||||||
|
this.intervalId = window.setInterval(function() { me.hide(); me.stopKillSuggestions(); }, 300);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopKillSuggestions: function() {
|
||||||
|
window.clearInterval(this.intervalId);
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyPress: function(e) {
|
||||||
|
if (this.disabled || !this.enabled) { return; }
|
||||||
|
// return will exit the function
|
||||||
|
// and event will not be prevented
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 27: //KEY_ESC:
|
||||||
|
this.el.val(this.currentValue);
|
||||||
|
this.hide();
|
||||||
|
break;
|
||||||
|
case 9: //KEY_TAB:
|
||||||
|
case 13: //KEY_RETURN:
|
||||||
|
if (this.selectedIndex === -1) {
|
||||||
|
this.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.select(this.selectedIndex);
|
||||||
|
if(e.keyCode === 9){ return; }
|
||||||
|
break;
|
||||||
|
case 38: //KEY_UP:
|
||||||
|
this.moveUp();
|
||||||
|
break;
|
||||||
|
case 40: //KEY_DOWN:
|
||||||
|
this.moveDown();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyUp: function(e) {
|
||||||
|
if(this.disabled){ return; }
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 38: //KEY_UP:
|
||||||
|
case 40: //KEY_DOWN:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearInterval(this.onChangeInterval);
|
||||||
|
if (this.currentValue !== this.el.val()) {
|
||||||
|
if (this.options.deferRequestBy > 0) {
|
||||||
|
// Defer lookup in case when value changes very quickly:
|
||||||
|
var me = this;
|
||||||
|
this.onChangeInterval = setInterval(function() { me.onValueChange(); }, this.options.deferRequestBy);
|
||||||
|
} else {
|
||||||
|
this.onValueChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onValueChange: function() {
|
||||||
|
clearInterval(this.onChangeInterval);
|
||||||
|
this.currentValue = this.el.val();
|
||||||
|
var q = this.getQuery(this.currentValue);
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
if (this.ignoreValueChange) {
|
||||||
|
this.ignoreValueChange = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (q === '' || q.length < this.options.minChars) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.getSuggestions(q);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getQuery: function(val) {
|
||||||
|
var d, arr;
|
||||||
|
d = this.options.delimiter;
|
||||||
|
if (!d) { return $.trim(val); }
|
||||||
|
arr = val.split(d);
|
||||||
|
return $.trim(arr[arr.length - 1]);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSuggestionsLocal: function(q) {
|
||||||
|
var ret, arr, len, val, i;
|
||||||
|
arr = this.options.lookup;
|
||||||
|
len = arr.suggestions.length;
|
||||||
|
ret = { suggestions:[], data:[] };
|
||||||
|
q = q.toLowerCase();
|
||||||
|
for(i=0; i< len; i++){
|
||||||
|
val = arr.suggestions[i];
|
||||||
|
if(val.toLowerCase().indexOf(q) === 0){
|
||||||
|
ret.suggestions.push(val);
|
||||||
|
ret.data.push(arr.data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSuggestions: function(q) {
|
||||||
|
var cr, me;
|
||||||
|
cr = this.isLocal ? this.getSuggestionsLocal(q) : this.cachedResponse[q];
|
||||||
|
if (cr && $.isArray(cr.suggestions)) {
|
||||||
|
this.suggestions = cr.suggestions;
|
||||||
|
this.data = cr.data;
|
||||||
|
this.suggest();
|
||||||
|
} else if (!this.isBadQuery(q)) {
|
||||||
|
me = this;
|
||||||
|
me.options.params.query = q;
|
||||||
|
$.get(this.serviceUrl, me.options.params, function(txt) { me.processResponse(txt); }, 'text');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isBadQuery: function(q) {
|
||||||
|
var i = this.badQueries.length;
|
||||||
|
while (i--) {
|
||||||
|
if (q.indexOf(this.badQueries[i]) === 0) { return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
hide: function() {
|
||||||
|
this.enabled = false;
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
this.container.hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
suggest: function() {
|
||||||
|
if (this.suggestions.length === 0) {
|
||||||
|
this.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var me, len, div, f, v, i, s, mOver, mClick;
|
||||||
|
me = this;
|
||||||
|
len = this.suggestions.length;
|
||||||
|
f = this.options.fnFormatResult;
|
||||||
|
v = this.getQuery(this.currentValue);
|
||||||
|
mOver = function(xi) { return function() { me.activate(xi); }; };
|
||||||
|
mClick = function(xi) { return function() { me.select(xi); }; };
|
||||||
|
this.container.hide().empty();
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
s = this.suggestions[i];
|
||||||
|
div = $((me.selectedIndex === i ? '<div class="selected"' : '<div') + ' title="' + s + '">' + f(s, this.data[i], v) + '</div>');
|
||||||
|
div.mouseover(mOver(i));
|
||||||
|
div.click(mClick(i));
|
||||||
|
this.container.append(div);
|
||||||
|
}
|
||||||
|
this.enabled = true;
|
||||||
|
this.container.show();
|
||||||
|
if (len) {
|
||||||
|
this.adjustScroll(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
processResponse: function(text) {
|
||||||
|
var response;
|
||||||
|
try {
|
||||||
|
response = eval('(' + text + ')');
|
||||||
|
} catch (err) { return; }
|
||||||
|
if (!$.isArray(response.data)) { response.data = []; }
|
||||||
|
if(!this.options.noCache){
|
||||||
|
this.cachedResponse[response.query] = response;
|
||||||
|
if (response.suggestions.length === 0) { this.badQueries.push(response.query); }
|
||||||
|
}
|
||||||
|
if (response.query === this.getQuery(this.currentValue)) {
|
||||||
|
this.suggestions = response.suggestions;
|
||||||
|
this.data = response.data;
|
||||||
|
this.suggest();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
activate: function(index) {
|
||||||
|
var divs, activeItem;
|
||||||
|
divs = this.container.children();
|
||||||
|
// Clear previous selection:
|
||||||
|
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
|
||||||
|
$(divs.get(this.selectedIndex)).removeClass();
|
||||||
|
}
|
||||||
|
this.selectedIndex = index;
|
||||||
|
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
|
||||||
|
activeItem = divs.get(this.selectedIndex);
|
||||||
|
$(activeItem).addClass('selected');
|
||||||
|
}
|
||||||
|
return activeItem;
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivate: function(div, index) {
|
||||||
|
div.className = '';
|
||||||
|
if (this.selectedIndex === index) { this.selectedIndex = -1; }
|
||||||
|
},
|
||||||
|
|
||||||
|
select: function(i) {
|
||||||
|
var selectedValue, f;
|
||||||
|
selectedValue = this.suggestions[i];
|
||||||
|
if (selectedValue) {
|
||||||
|
this.el.val(selectedValue);
|
||||||
|
if (this.options.autoSubmit) {
|
||||||
|
f = this.el.parents('form');
|
||||||
|
if (f.length > 0) { f.get(0).submit(); }
|
||||||
|
}
|
||||||
|
this.ignoreValueChange = true;
|
||||||
|
this.hide();
|
||||||
|
this.onSelect(i);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
moveUp: function() {
|
||||||
|
if (this.selectedIndex === -1) { return; }
|
||||||
|
if (this.selectedIndex === 0) {
|
||||||
|
this.container.children().get(0).className = '';
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
this.el.val(this.currentValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.adjustScroll(this.selectedIndex - 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
moveDown: function() {
|
||||||
|
if (this.selectedIndex === (this.suggestions.length - 1)) { return; }
|
||||||
|
this.adjustScroll(this.selectedIndex + 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
adjustScroll: function(i) {
|
||||||
|
var activeItem, offsetTop, upperBound, lowerBound;
|
||||||
|
activeItem = this.activate(i);
|
||||||
|
offsetTop = activeItem.offsetTop;
|
||||||
|
upperBound = this.container.scrollTop();
|
||||||
|
lowerBound = upperBound + this.options.maxHeight - 25;
|
||||||
|
if (offsetTop < upperBound) {
|
||||||
|
this.container.scrollTop(offsetTop);
|
||||||
|
} else if (offsetTop > lowerBound) {
|
||||||
|
this.container.scrollTop(offsetTop - this.options.maxHeight + 25);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onSelect: function(i) {
|
||||||
|
var me, fn, s, d;
|
||||||
|
me = this;
|
||||||
|
fn = me.options.onSelect;
|
||||||
|
s = me.suggestions[i];
|
||||||
|
d = me.data[i];
|
||||||
|
me.el.val(me.getValue(s));
|
||||||
|
if ($.isFunction(fn)) { fn(s, d, me.el); }
|
||||||
|
},
|
||||||
|
|
||||||
|
getValue: function(value){
|
||||||
|
var del, currVal, arr, me;
|
||||||
|
me = this;
|
||||||
|
del = me.options.delimiter;
|
||||||
|
if (!del) { return value; }
|
||||||
|
currVal = me.currentValue;
|
||||||
|
arr = currVal.split(del);
|
||||||
|
if (arr.length === 1) { return value; }
|
||||||
|
return currVal.substr(0, currVal.length - arr[arr.length - 1].length) + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}(jQuery));
|
4
rattail/pyramid/static/js/jquery.js
vendored
Normal file
13
rattail/pyramid/static/js/jquery.loading.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
;(function($){var L=$.loading=function(show,opts){return $('body').loading(show,opts,true);};$.fn.loading=function(show,opts,page){opts=toOpts(show,opts);var base=page?$.extend(true,{},L,L.pageOptions):L;return this.each(function(){var $el=$(this),l=$.extend(true,{},base,$.metadata?$el.metadata():null,opts);if(typeof l.onAjax=="boolean"){L.setAjax.call($el,l);}else{L.toggle.call($el,l);}});};var fixed={position:$.browser.msie?'absolute':'fixed'};$.extend(L,{version:"1.6.4",align:'top-left',pulse:'working error',mask:false,img:null,element:null,text:'Loading...',onAjax:undefined,delay:0,max:0,classname:'loading',imgClass:'loading-img',elementClass:'loading-element',maskClass:'loading-mask',css:{position:'absolute',whiteSpace:'nowrap',zIndex:1001},maskCss:{position:'absolute',opacity:.15,background:'#333',zIndex:101,display:'block',cursor:'wait'},cloneEvents:true,pageOptions:{page:true,align:'top-center',css:fixed,maskCss:fixed},html:'<div></div>',maskHtml:'<div></div>',maskedClass:'loading-masked',maskEvents:'mousedown mouseup keydown keypress',resizeEvents:'resize',working:{time:10000,text:'Still working...',run:function(l){var w=l.working,self=this;w.timeout=setTimeout(function(){self.height('auto').width('auto').text(l.text=w.text);l.place.call(self,l);},w.time);}},error:{time:100000,text:'Task may have failed...',classname:'loading-error',run:function(l){var e=l.error,self=this;e.timeout=setTimeout(function(){self.height('auto').width('auto').text(l.text=e.text).addClass(e.classname);l.place.call(self,l);},e.time);}},fade:{time:800,speed:'slow',run:function(l){var f=l.fade,s=f.speed,self=this;f.interval=setInterval(function(){self.fadeOut(s).fadeIn(s);},f.time);}},ellipsis:{time:300,run:function(l){var e=l.ellipsis,self=this;e.interval=setInterval(function(){var et=self.text(),t=l.text,i=dotIndex(t);self.text((et.length-i)<3?et+'.':t.substring(0,i));},e.time);function dotIndex(t){var x=t.indexOf('.');return x<0?t.length:x;}}},type:{time:100,run:function(l){var t=l.type,self=this;t.interval=setInterval(function(){var e=self.text(),el=e.length,txt=l.text;self.text(el==txt.length?txt.charAt(0):txt.substring(0,el+1));},t.time);}},toggle:function(l){var old=this.data('loading');if(old){if(l.show!==true)old.off.call(this,old,l);}else{if(l.show!==false)l.on.call(this,l);}},setAjax:function(l){if(l.onAjax){var self=this,count=0,A=l.ajax={start:function(){if(!count++)l.on.call(self,l);},stop:function(){if(!--count)l.off.call(self,l,l);}};this.bind('ajaxStart.loading',A.start).bind('ajaxStop.loading',A.stop);}else{this.unbind('ajaxStart.loading ajaxStop.loading');}},on:function(l,force){var p=l.parent=this.data('loading',l);if(l.max)l.maxout=setTimeout(function(){l.off.call(p,l,l);},l.max);if(l.delay&&!force){return l.timeout=setTimeout(function(){delete l.timeout;l.on.call(p,l,true);},l.delay);}
|
||||||
|
if(l.mask)l.mask=l.createMask.call(p,l);l.display=l.create.call(p,l);if(l.img){l.initImg.call(p,l);}else if(l.element){l.initElement.call(p,l);}else{l.init.call(p,l);}
|
||||||
|
p.trigger('loadingStart',[l]);},initImg:function(l){var self=this;l.imgElement=$('<img src="'+l.img+'"/>').bind('load',function(){l.init.call(self,l);});l.display.addClass(l.imgClass).append(l.imgElement);},initElement:function(l){l.element=$(l.element).clone(l.cloneEvents).show();l.display.addClass(l.elementClass).append(l.element);l.init.call(this,l);},init:function(l){l.place.call(l.display,l);if(l.pulse)l.initPulse.call(this,l);},initPulse:function(l){$.each(l.pulse.split(' '),function(){l[this].run.call(l.display,l);});},create:function(l){var el=$(l.html).addClass(l.classname).css(l.css).appendTo(this);if(l.text&&!l.img&&!l.element)el.text(l.originalText=l.text);$(window).bind(l.resizeEvents,l.resizer=function(){l.resize(l);});return el;},resize:function(l){l.parent.box=null;if(l.mask)l.mask.hide();l.place.call(l.display.hide(),l);if(l.mask)l.mask.show().css(l.parent.box);},createMask:function(l){var box=l.measure.call(this.addClass(l.maskedClass),l);l.handler=function(e){return l.maskHandler(e,l);};$(document).bind(l.maskEvents,l.handler);return $(l.maskHtml).addClass(l.maskClass).css(box).css(l.maskCss).appendTo(this);},maskHandler:function(e,l){var $els=$(e.target).parents().andSelf();if($els.filter('.'+l.classname).length!=0)return true;return!l.page&&$els.filter('.'+l.maskedClass).length==0;},place:function(l){var box=l.align,v='top',h='left';if(typeof box=="object"){box=$.extend(l.calc.call(this,v,h,l),box);}else{if(box!='top-left'){var s=box.split('-');if(s.length==1){v=h=s[0];}else{v=s[0];h=s[1];}}
|
||||||
|
if(!this.hasClass(v))this.addClass(v);if(!this.hasClass(h))this.addClass(h);box=l.calc.call(this,v,h,l);}
|
||||||
|
this.show().css(l.box=box);},calc:function(v,h,l){var box=$.extend({},l.measure.call(l.parent,l)),H=$.boxModel?this.height():this.innerHeight(),W=$.boxModel?this.width():this.innerWidth();if(v!='top'){var d=box.height-H;if(v=='center'){d/=2;}else if(v!='bottom'){d=0;}else if($.boxModel){d-=css(this,'paddingTop')+css(this,'paddingBottom');}
|
||||||
|
box.top+=d;}
|
||||||
|
if(h!='left'){var d=box.width-W;if(h=='center'){d/=2;}else if(h!='right'){d=0;}else if($.boxModel){d-=css(this,'paddingLeft')+css(this,'paddingRight');}
|
||||||
|
box.left+=d;}
|
||||||
|
box.height=H;box.width=W;return box;},measure:function(l){return this.box||(this.box=l.page?l.pageBox(l):l.elementBox(this,l));},elementBox:function(e,l){if(e.css('position')=='absolute'){var box={top:0,left:0};}else{var box=e.position();box.top+=css(e,'marginTop');box.left+=css(e,'marginLeft');}
|
||||||
|
box.height=e.outerHeight();box.width=e.outerWidth();return box;},pageBox:function(l){var full=$.boxModel&&l.css.position!='fixed';return{top:0,left:0,height:get(full,'Height'),width:get(full,'Width')};function get(full,side){var doc=document;if(full){var s=side.toLowerCase(),d=$(doc)[s](),w=$(window)[s]();return d-css($(doc.body),'marginTop')>w?d:w;}
|
||||||
|
var c='client'+side;return Math.max(doc.documentElement[c],doc.body[c]);}},off:function(old,l){this.data('loading',null);if(old.maxout)clearTimeout(old.maxout);if(old.timeout)return clearTimeout(old.timeout);if(old.pulse)old.stopPulse.call(this,old,l);if(old.originalText)old.text=old.originalText;if(old.mask)old.stopMask.call(this,old,l);$(window).unbind(old.resizeEvents,old.resizer);if(old.display)old.display.remove();if(old.parent)old.parent.trigger('loadingEnd',[old]);},stopPulse:function(old,l){$.each(old.pulse.split(' '),function(){var p=old[this];if(p.end)p.end.call(l.display,old,l);if(p.interval)clearInterval(p.interval);if(p.timeout)clearTimeout(p.timeout);});},stopMask:function(old,l){this.removeClass(l.maskedClass);$(document).unbind(old.maskEvents,old.handler);old.mask.remove();}});function toOpts(s,l){if(l===undefined){l=(typeof s=="boolean")?{show:s}:s;}else{l.show=s;}
|
||||||
|
if(l&&(l.img||l.element)&&!l.pulse)l.pulse=false;if(l&&l.onAjax!==undefined&&l.show===undefined)l.show=false;return l;}
|
||||||
|
function css(el,prop){var val=el.css(prop);return val=='auto'?0:parseFloat(val,10);}})(jQuery);
|
1012
rattail/pyramid/static/js/jquery.ui.js
Normal file
380
rattail/pyramid/static/js/rattail.js
Normal file
|
@ -0,0 +1,380 @@
|
||||||
|
|
||||||
|
/************************************************************
|
||||||
|
*
|
||||||
|
* rattail.js
|
||||||
|
*
|
||||||
|
* This library contains all of Javascript functionality
|
||||||
|
* provided directly by Rattail.
|
||||||
|
*
|
||||||
|
* It also attaches some jQuery event handlers for certain
|
||||||
|
* design patterns.
|
||||||
|
*
|
||||||
|
************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
var filters_to_disable = [];
|
||||||
|
|
||||||
|
|
||||||
|
function disable_button(button, text) {
|
||||||
|
if (text) {
|
||||||
|
$(button).html(text + ", please wait...");
|
||||||
|
}
|
||||||
|
$(button).attr('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function disable_filter_options() {
|
||||||
|
for (var i = 0; i <= filters_to_disable.length; ++i) {
|
||||||
|
var filter = filters_to_disable.pop();
|
||||||
|
var option = $('#add-filter option[value='+filter+']').attr('disabled', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* get_dialog(id, callback)
|
||||||
|
*
|
||||||
|
* Returns a <DIV> element suitable for use as a jQuery dialog.
|
||||||
|
*
|
||||||
|
* ``id`` is used to construct a proper ID for the element and allows the
|
||||||
|
* dialog to be resused if possible.
|
||||||
|
*
|
||||||
|
* ``callback``, if specified, should be a callback function for the dialog.
|
||||||
|
* This function will be called whenever the dialog has been closed
|
||||||
|
* "successfully" (i.e. data submitted) by the user, and should accept a single
|
||||||
|
* ``data`` object which is the JSON response returned by the server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function get_dialog(id, callback) {
|
||||||
|
var dialog = $('#'+id+'-dialog');
|
||||||
|
if (! dialog.length) {
|
||||||
|
dialog = $('<div class="dialog" id="'+id+'-dialog"></div>');
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
dialog.attr('callback', callback);
|
||||||
|
}
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* get_lookup_dialog(id, callback, textcol)
|
||||||
|
*
|
||||||
|
* TODO: Document this.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function get_lookup_dialog(id, callback, textcol) {
|
||||||
|
var dialog = get_dialog('lookup-'+id, callback);
|
||||||
|
dialog.addClass('lookup');
|
||||||
|
dialog.attr('textcol', textcol || 0);
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* get_uuid(obj)
|
||||||
|
*
|
||||||
|
* Returns the UUID associated with ``obj``, if any can be found. The object
|
||||||
|
* itself is checked, as well its most immediate <TR> parent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function get_uuid(obj) {
|
||||||
|
|
||||||
|
obj = $(obj);
|
||||||
|
if (obj.attr('uuid')) {
|
||||||
|
return obj.attr('uuid');
|
||||||
|
}
|
||||||
|
var tr = obj.parents('tr:first');
|
||||||
|
if (tr.attr('uuid')) {
|
||||||
|
return tr.attr('uuid');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* json_success(data)
|
||||||
|
*
|
||||||
|
* Returns a boolean indicating whether ``data`` represents a successful
|
||||||
|
* response from the server, or not.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function json_success(data) {
|
||||||
|
return typeof(data) == 'object' && data.ok == 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* loading(element)
|
||||||
|
*
|
||||||
|
* Used to indicate that data is being retrieved from the server. ``element``
|
||||||
|
* is typically a <div class="grid"> element, though it can be anything.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function loading(element) {
|
||||||
|
element.loading(true, {mask: true, text: ''});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Navigates to another page of results within a grid.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function grid_navigate_page(link, url) {
|
||||||
|
var div = $(link).parents('div.grid:first');
|
||||||
|
loading(div);
|
||||||
|
div.load(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* reload_grid_div(div)
|
||||||
|
*
|
||||||
|
* Reloads a grid's contents. ``div``, if provied, is assumed to be an element
|
||||||
|
* of type <div class="grid">, or else contain such an element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function reload_grid_div(div) {
|
||||||
|
if (! div) {
|
||||||
|
div = $('div.grid');
|
||||||
|
} else if (! div.hasClass('grid')) {
|
||||||
|
div = div.find('div.grid');
|
||||||
|
}
|
||||||
|
if (! div.length) {
|
||||||
|
alert('assert: div should have length');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading(div);
|
||||||
|
div.load(div.attr('url'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
$('div.filter label').live('click', function() {
|
||||||
|
var checkbox = $(this).prev();
|
||||||
|
if (checkbox.attr('checked')) {
|
||||||
|
checkbox.attr('checked', false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
checkbox.attr('checked', true);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#add-filter').live('change', function() {
|
||||||
|
var div = $(this).parents('div.filters:first');
|
||||||
|
var filter = div.find('#filter-'+$(this).val());
|
||||||
|
filter.find(':first-child').attr('checked', true);
|
||||||
|
filter.show();
|
||||||
|
var field = filter.find(':last-child');
|
||||||
|
field.select();
|
||||||
|
field.focus();
|
||||||
|
$(this).find('option:selected').attr('disabled', true);
|
||||||
|
$(this).val('add a filter');
|
||||||
|
if ($(this).find('option[disabled=false]').length == 1) {
|
||||||
|
$(this).hide();
|
||||||
|
}
|
||||||
|
div.find('input[type=submit]').show();
|
||||||
|
div.find('button[type=reset]').show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.filters form').live('submit', function() {
|
||||||
|
var div = $('div.grid:first');
|
||||||
|
var data = $(this).serialize() + '&partial=true';
|
||||||
|
loading(div);
|
||||||
|
$.post(div.attr('url'), data, function(data) {
|
||||||
|
div.replaceWith(data);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.filters form div.buttons button[type=reset]').click(function() {
|
||||||
|
var filters = $(this).parents('div.filters:first');
|
||||||
|
filters.find('div.filter').each(function() {
|
||||||
|
$(this).find('div.value input').val('');
|
||||||
|
});
|
||||||
|
var url = filters.attr('url');
|
||||||
|
var grid = $('div.grid[url='+url+']');
|
||||||
|
loading(grid);
|
||||||
|
var form = filters.find('form');
|
||||||
|
var data = form.serialize() + '&partial=true';
|
||||||
|
$.post(url, data, function(data) {
|
||||||
|
grid.replaceWith(data);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid table th.sortable a').live('click', function() {
|
||||||
|
var div = $(this).parents('div.grid:first');
|
||||||
|
var th = $(this).parents('th:first');
|
||||||
|
var dir = 'asc';
|
||||||
|
if (th.hasClass('sorted') && th.hasClass('asc')) {
|
||||||
|
dir = 'desc';
|
||||||
|
}
|
||||||
|
loading(div);
|
||||||
|
var url = div.attr('url');
|
||||||
|
url += url.match(/\?/) ? '&' : '?';
|
||||||
|
url += 'sort=' + th.attr('field') + '&dir=' + dir;
|
||||||
|
url += '&page=1';
|
||||||
|
url += '&partial=true';
|
||||||
|
div.load(url);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.hoverable table tbody tr').live('mouseenter', function() {
|
||||||
|
$(this).addClass('hovering');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.hoverable table tbody tr').live('mouseleave', function() {
|
||||||
|
$(this).removeClass('hovering');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.clickable table tbody tr').live('mouseenter', function() {
|
||||||
|
$(this).addClass('hovering');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.clickable table tbody tr').live('mouseleave', function() {
|
||||||
|
$(this).removeClass('hovering');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.selectable table tbody tr').live('mouseenter', function() {
|
||||||
|
$(this).addClass('hovering');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.selectable table tbody tr').live('mouseleave', function() {
|
||||||
|
$(this).removeClass('hovering');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.checkable table tbody tr').live('mouseenter', function() {
|
||||||
|
$(this).addClass('hovering');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.checkable table tbody tr').live('mouseleave', function() {
|
||||||
|
$(this).removeClass('hovering');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.clickable table tbody td').live('click', function() {
|
||||||
|
if (! $(this).hasClass('noclick')) {
|
||||||
|
var row = $(this).parents('tr:first');
|
||||||
|
var url = row.attr('url');
|
||||||
|
if (url) {
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid table thead th.checkbox input[type=checkbox]').live('click', function() {
|
||||||
|
var checked = $(this).is(':checked');
|
||||||
|
var table = $(this).parents('table:first');
|
||||||
|
table.find('tbody tr').each(function() {
|
||||||
|
$(this).find('td.checkbox input[type=checkbox]').attr('checked', checked);
|
||||||
|
// if (checked) {
|
||||||
|
// $(this).addClass('selected');
|
||||||
|
// } else {
|
||||||
|
// $(this).removeClass('selected');
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.selectable table tbody tr').live('click', function() {
|
||||||
|
var table = $(this).parents('table:first');
|
||||||
|
if (! table.hasClass('multiple')) {
|
||||||
|
table.find('tbody tr').removeClass('selected');
|
||||||
|
}
|
||||||
|
$(this).addClass('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid.checkable table tbody tr').live('click', function() {
|
||||||
|
var checkbox = $(this).find('td:first input[type=checkbox]');
|
||||||
|
checkbox.attr('checked', !checkbox.is(':checked'));
|
||||||
|
$(this).toggleClass('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid table tbody td.edit').live('click', function() {
|
||||||
|
var url = $(this).attr('url');
|
||||||
|
if (url) {
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.grid table tbody td.delete').live('click', function() {
|
||||||
|
var url = $(this).attr('url');
|
||||||
|
if (url) {
|
||||||
|
if (confirm("Do you really wish to delete this object?")) {
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#grid-page-count').live('change', function() {
|
||||||
|
var div = $(this).parents('div.grid:first');
|
||||||
|
loading(div);
|
||||||
|
url = div.attr('url');
|
||||||
|
url += url.match(/\?/) ? '&' : '?';
|
||||||
|
url += 'per_page=' + $(this).val();
|
||||||
|
url += '&partial=true';
|
||||||
|
div.load(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.autocomplete-container div.autocomplete-display button.autocomplete-change').live('click', function() {
|
||||||
|
var container = $(this).parents('div.autocomplete-container');
|
||||||
|
var display = $(this).parents('div.autocomplete-display');
|
||||||
|
var textbox = container.find('input.autocomplete-textbox');
|
||||||
|
var hidden = container.find('input[type=hidden]');
|
||||||
|
display.hide();
|
||||||
|
hidden.val('');
|
||||||
|
textbox.show();
|
||||||
|
textbox.select();
|
||||||
|
textbox.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.dialog form').live('submit', function() {
|
||||||
|
var form = $(this);
|
||||||
|
var dialog = form.parents('div.dialog:first');
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: form.attr('action'),
|
||||||
|
data: form.serialize(),
|
||||||
|
success: function(data) {
|
||||||
|
if (json_success(data)) {
|
||||||
|
if (dialog.attr('callback')) {
|
||||||
|
eval(dialog.attr('callback'))(data);
|
||||||
|
}
|
||||||
|
dialog.dialog('close');
|
||||||
|
} else if (typeof(data) == 'object') {
|
||||||
|
alert(data.message);
|
||||||
|
} else {
|
||||||
|
dialog.html(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert("Sorry, something went wrong...try again?");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.dialog button.close').live('click', function() {
|
||||||
|
var dialog = $(this).parents('div.dialog:first');
|
||||||
|
dialog.dialog('close');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.dialog button.cancel').live('click', function() {
|
||||||
|
var dialog = $(this).parents('div.dialog:first');
|
||||||
|
dialog.dialog('close');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('div.dialog.lookup button.ok').live('click', function() {
|
||||||
|
var dialog = $(this).parents('div.dialog.lookup:first');
|
||||||
|
var tr = dialog.find('div.grid table tbody tr.selected');
|
||||||
|
if (! tr.length) {
|
||||||
|
alert("You haven't selected anything.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var uuid = get_uuid(tr);
|
||||||
|
var col = parseInt(dialog.attr('textcol'));
|
||||||
|
var text = tr.find('td:eq('+col+')').html();
|
||||||
|
eval(dialog.attr('callback'))(uuid, text);
|
||||||
|
dialog.dialog('close');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -27,29 +27,77 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pyramid import threadlocal
|
from pyramid import threadlocal
|
||||||
|
from pyramid.security import authenticated_userid
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
|
from rattail.pyramid import helpers
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.model import User
|
||||||
|
from rattail.db.auth import has_permission
|
||||||
|
|
||||||
|
|
||||||
def before_render(event):
|
def before_render(event):
|
||||||
"""
|
"""
|
||||||
Adds goodies to the global template renderer context:
|
Adds goodies to the global template renderer context.
|
||||||
|
|
||||||
* ``rattail``
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Import labels module so it's available if/when needed.
|
|
||||||
import rattail.labels
|
|
||||||
|
|
||||||
# Import SIL module so it's available if/when needed.
|
|
||||||
import rattail.sil
|
|
||||||
|
|
||||||
request = event.get('request') or threadlocal.get_current_request()
|
request = event.get('request') or threadlocal.get_current_request()
|
||||||
|
|
||||||
renderer_globals = event
|
renderer_globals = event
|
||||||
|
renderer_globals['h'] = helpers
|
||||||
|
renderer_globals['url'] = request.route_url
|
||||||
renderer_globals['rattail'] = rattail
|
renderer_globals['rattail'] = rattail
|
||||||
|
renderer_globals['Session'] = Session
|
||||||
|
|
||||||
|
|
||||||
|
def context_found(event):
|
||||||
|
"""
|
||||||
|
This hook attaches various attributes and methods to the ``request``
|
||||||
|
object. Specifically:
|
||||||
|
|
||||||
|
The :class:`rattail.db.model.User` instance currently logged-in (if indeed
|
||||||
|
there is one) is attached as ``request.user``.
|
||||||
|
|
||||||
|
A ``request.has_perm()`` method is attached, which is a shortcut for
|
||||||
|
:func:`rattail.db.auth.has_permission()`.
|
||||||
|
|
||||||
|
A ``request.get_referrer()`` method is attached, which contains some
|
||||||
|
convenient logic for determining the referring URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = event.request
|
||||||
|
|
||||||
|
request.user = None
|
||||||
|
uuid = authenticated_userid(request)
|
||||||
|
if uuid:
|
||||||
|
request.user = Session.query(User).get(uuid)
|
||||||
|
|
||||||
|
def has_perm(perm):
|
||||||
|
return has_permission(Session(), request.user, perm)
|
||||||
|
request.has_perm = has_perm
|
||||||
|
|
||||||
|
def has_any_perm(perms):
|
||||||
|
for perm in perms:
|
||||||
|
if has_permission(Session(), request.user, perm):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
request.has_any_perm = has_any_perm
|
||||||
|
|
||||||
|
def get_referrer(default=None):
|
||||||
|
if request.params.get('referrer'):
|
||||||
|
return request.params['referrer']
|
||||||
|
if request.session.get('referrer'):
|
||||||
|
return request.session.pop('referrer')
|
||||||
|
referrer = request.referrer
|
||||||
|
if not referrer or referrer == request.current_route_url():
|
||||||
|
if default:
|
||||||
|
referrer = default
|
||||||
|
else:
|
||||||
|
referrer = request.route_url('home')
|
||||||
|
return referrer
|
||||||
|
request.get_referrer = get_referrer
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
config.add_subscriber('rattail.pyramid.subscribers:before_render',
|
config.add_subscriber(before_render, 'pyramid.events.BeforeRender')
|
||||||
'pyramid.events.BeforeRender')
|
config.add_subscriber(context_found, 'pyramid.events.ContextFound')
|
||||||
|
|
74
rattail/pyramid/templates/base.mako
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<%def name="global_title()">Rattail</%def>
|
||||||
|
<%def name="title()"></%def>
|
||||||
|
<%def name="head_tags()"></%def>
|
||||||
|
<%def name="home_link()"><h1 class="right">${h.link_to("Home", url('home'))}</h1></%def>
|
||||||
|
<%def name="menu()"></%def>
|
||||||
|
<%def name="footer()">
|
||||||
|
powered by ${h.link_to('Rattail', 'http://rattail.edbob.org', target='_blank')} v${rattail.__version__}
|
||||||
|
</%def>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>${self.global_title()}${' : ' + capture(self.title) if capture(self.title) else ''}</title>
|
||||||
|
|
||||||
|
${h.javascript_link(request.static_url('rattail.pyramid:static/js/jquery.js'))}
|
||||||
|
${h.javascript_link(request.static_url('rattail.pyramid:static/js/jquery.ui.js'))}
|
||||||
|
${h.javascript_link(request.static_url('rattail.pyramid:static/js/jquery.loading.js'))}
|
||||||
|
${h.javascript_link(request.static_url('rattail.pyramid:static/js/jquery.autocomplete.js'))}
|
||||||
|
${h.javascript_link(request.static_url('rattail.pyramid:static/js/rattail.js'))}
|
||||||
|
|
||||||
|
${h.stylesheet_link(request.static_url('rattail.pyramid:static/css/base.css'))}
|
||||||
|
${h.stylesheet_link(request.static_url('rattail.pyramid:static/css/layout.css'))}
|
||||||
|
${h.stylesheet_link(request.static_url('rattail.pyramid:static/css/grids.css'))}
|
||||||
|
${h.stylesheet_link(request.static_url('rattail.pyramid:static/css/filters.css'))}
|
||||||
|
${h.stylesheet_link(request.static_url('rattail.pyramid:static/css/forms.css'))}
|
||||||
|
${h.stylesheet_link(request.static_url('rattail.pyramid:static/css/autocomplete.css'))}
|
||||||
|
${h.stylesheet_link(request.static_url('rattail.pyramid:static/css/smoothness/jquery-ui-1.8.2.custom.css'))}
|
||||||
|
|
||||||
|
${self.head_tags()}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="container">
|
||||||
|
|
||||||
|
<div id="header">
|
||||||
|
${self.home_link()}
|
||||||
|
<h1 class="left">${self.title()}</h1>
|
||||||
|
<div id="login" class="right">
|
||||||
|
% if request.user:
|
||||||
|
${h.link_to(request.user.display_name, url('change_password'), class_='username')}
|
||||||
|
(${h.link_to("logout", url('logout'))})
|
||||||
|
% else:
|
||||||
|
${h.link_to("login", url('login'))}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div><!-- header -->
|
||||||
|
|
||||||
|
<div id="body">
|
||||||
|
% if request.session.peek_flash('error'):
|
||||||
|
<div id="error-messages">
|
||||||
|
% for error in request.session.pop_flash('error'):
|
||||||
|
<div class="error">${error}</div>
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% if request.session.peek_flash():
|
||||||
|
<div id="flash-messages">
|
||||||
|
% for msg in request.session.pop_flash():
|
||||||
|
<div class="flash-message">${msg|n}</div>
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
${self.body()}
|
||||||
|
</div><!-- body -->
|
||||||
|
|
||||||
|
</div><!-- container -->
|
||||||
|
|
||||||
|
<div id="footer">
|
||||||
|
${self.footer()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
18
rattail/pyramid/templates/crud.mako
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<%inherit file="/form.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+h.literal(str(form.fieldset.model))}</%def>
|
||||||
|
|
||||||
|
<%def name="head_tags()">
|
||||||
|
${parent.head_tags()}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$('a.delete').click(function() {
|
||||||
|
if (! confirm("Do you really wish to delete this object?")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
${parent.body()}
|
13
rattail/pyramid/templates/form.mako
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
|
<%def name="context_menu_items()"></%def>
|
||||||
|
|
||||||
|
<div class="form-wrapper">
|
||||||
|
|
||||||
|
<ul class="context-menu">
|
||||||
|
${self.context_menu_items()}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
${form.render()|n}
|
||||||
|
|
||||||
|
</div>
|
3
rattail/pyramid/templates/forms/field_autocomplete.mako
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<%namespace file="/autocomplete.mako" import="autocomplete" />
|
||||||
|
|
||||||
|
${autocomplete(field_name, service_url, field_value, field_display, width=width, callback=callback)}
|
39
rattail/pyramid/templates/forms/fieldset.mako
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<% _focus_rendered = False %>
|
||||||
|
|
||||||
|
% for error in fieldset.errors.get(None, []):
|
||||||
|
<div class="fieldset-error">${error}</div>
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
% for field in fieldset.render_fields.itervalues():
|
||||||
|
|
||||||
|
% if field.requires_label:
|
||||||
|
<div class="field-wrapper ${field.name}">
|
||||||
|
% for error in field.errors:
|
||||||
|
<div class="field-error">${error}</div>
|
||||||
|
% endfor
|
||||||
|
${field.label_tag()|n}
|
||||||
|
<div class="field">
|
||||||
|
${field.render()|n}
|
||||||
|
</div>
|
||||||
|
% if 'instructions' in field.metadata:
|
||||||
|
<span class="instructions">${field.metadata['instructions']}</span>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
% if not _focus_rendered and (fieldset.focus == field or fieldset.focus is True):
|
||||||
|
% if not field.is_readonly() and getattr(field.renderer, 'needs_focus', True):
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
% if hasattr(field.renderer, 'focus_name'):
|
||||||
|
$('#${field.renderer.focus_name}').focus();
|
||||||
|
% else:
|
||||||
|
$('#${field.renderer.name}').focus();
|
||||||
|
% endif
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<% _focus_rendered = True %>
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% endfor
|
12
rattail/pyramid/templates/forms/fieldset_readonly.mako
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<div class="fieldset">
|
||||||
|
% for field in fieldset.render_fields.itervalues():
|
||||||
|
% if field.requires_label:
|
||||||
|
<div class="field-wrapper ${field.name}">
|
||||||
|
${field.label_tag()|n}
|
||||||
|
<div class="field">
|
||||||
|
${field.render_readonly()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
37
rattail/pyramid/templates/forms/filterset.mako
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<div class="filterset">
|
||||||
|
${search.begin()}
|
||||||
|
${search.hidden('filters', True)}
|
||||||
|
<% visible = [] %>
|
||||||
|
% for f in search.sorted_filters():
|
||||||
|
<% f = search.filters[f] %>
|
||||||
|
<div class="filter" id="filter-${f.name}"${' style="display: none;"' if not search.config.get('include_filter_'+f.name) else ''|n}>
|
||||||
|
${search.checkbox('include_filter_'+f.name)}
|
||||||
|
<label for="${f.name}">${f.label}</label>
|
||||||
|
${f.types_select()}
|
||||||
|
${f.value_control()}
|
||||||
|
</div>
|
||||||
|
% if search.config.get('include_filter_'+f.name):
|
||||||
|
<% visible.append(f.name) %>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
<div class="buttons">
|
||||||
|
${search.add_filter(visible)}
|
||||||
|
${search.submit('submit', "Search", style='display: none;' if not visible else None)}
|
||||||
|
<button type="reset"${' style="display: none;"' if not visible else ''}>Reset</button>
|
||||||
|
</div>
|
||||||
|
${search.end()}
|
||||||
|
% if visible:
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
var filters_to_disable = [
|
||||||
|
% for field in visible:
|
||||||
|
'${field}',
|
||||||
|
% endfor
|
||||||
|
];
|
||||||
|
% if not dialog:
|
||||||
|
$(function() {
|
||||||
|
disable_filter_options();
|
||||||
|
});
|
||||||
|
% endif
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</div>
|
15
rattail/pyramid/templates/forms/form.mako
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<div class="form">
|
||||||
|
${h.form(form.action_url, enctype='multipart/form-data')}
|
||||||
|
|
||||||
|
${form.fieldset.render()|n}
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
${h.submit('create', form.create_label if form.creating else form.update_label)}
|
||||||
|
% if form.creating and form.allow_successive_creates:
|
||||||
|
${h.submit('create_and_continue', form.successive_create_label)}
|
||||||
|
% endif
|
||||||
|
<button type="button" onclick="location.href = '${form.cancel_url}';">Cancel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
3
rattail/pyramid/templates/forms/form_readonly.mako
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="form">
|
||||||
|
${form.fieldset.render()|n}
|
||||||
|
</div>
|
54
rattail/pyramid/templates/forms/grid_readonly.mako
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<div class="grid${' '+class_ if class_ else ''}" ${grid.url_attrs()|n}>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
% if checkboxes:
|
||||||
|
<th class="checkbox">${h.checkbox('check-all')}</th>
|
||||||
|
% endif
|
||||||
|
% for field in grid.iter_fields():
|
||||||
|
${grid.column_header(field)}
|
||||||
|
% endfor
|
||||||
|
% for col in grid.extra_columns:
|
||||||
|
<th>${col.label}</td>
|
||||||
|
% endfor
|
||||||
|
% if grid.deletable:
|
||||||
|
<th> </th>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
% for i, row in enumerate(grid.rows):
|
||||||
|
<% grid._set_active(row) %>
|
||||||
|
<tr ${grid.row_attrs(i)|n}>
|
||||||
|
% if checkboxes:
|
||||||
|
<td class="checkbox">${h.checkbox('check-'+grid.model.uuid, disabled=True)}</td>
|
||||||
|
% endif
|
||||||
|
% for field in grid.iter_fields():
|
||||||
|
<td class="${grid.field_name(field)}">${grid.render_field(field, True)|n}</td>
|
||||||
|
% endfor
|
||||||
|
% for col in grid.extra_columns:
|
||||||
|
<td class="${col.name}">${col.callback(row)|n}</td>
|
||||||
|
% endfor
|
||||||
|
% if grid.deletable:
|
||||||
|
<td class="delete"> </td>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
|
% endfor
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
% if hasattr(grid, 'pager') and grid.pager:
|
||||||
|
<div class="pager">
|
||||||
|
<p class="showing">
|
||||||
|
showing
|
||||||
|
${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count}
|
||||||
|
</p>
|
||||||
|
<p class="page-links">
|
||||||
|
${h.select('grid-page-count', grid.pager.items_per_page, (5, 10, 20, 50, 100))}
|
||||||
|
per page:
|
||||||
|
${grid.pager.pager('~3~', onclick='return grid_navigate_page($(this));')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</div>
|
37
rattail/pyramid/templates/grid.mako
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
|
<%def name="context_menu_items()"></%def>
|
||||||
|
|
||||||
|
<%def name="form()">
|
||||||
|
% if search:
|
||||||
|
${search.render()}
|
||||||
|
% else:
|
||||||
|
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="tools()"></%def>
|
||||||
|
|
||||||
|
<div class="grid-wrapper">
|
||||||
|
|
||||||
|
<table class="grid-header">
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2" class="form">
|
||||||
|
${self.form()}
|
||||||
|
</td>
|
||||||
|
<td class="context-menu">
|
||||||
|
<ul>
|
||||||
|
${self.context_menu_items()}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tools">
|
||||||
|
${self.tools()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table><!-- grid-header -->
|
||||||
|
|
||||||
|
${grid}
|
||||||
|
|
||||||
|
</div><!-- grid-wrapper -->
|
57
rattail/pyramid/templates/grids/grid.mako
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<div ${grid.div_attrs()}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
% if grid.checkboxes:
|
||||||
|
<th class="checkbox">${h.checkbox('check-all')}</th>
|
||||||
|
% endif
|
||||||
|
% for field in grid.iter_fields():
|
||||||
|
${grid.column_header(field)}
|
||||||
|
% endfor
|
||||||
|
% for col in grid.extra_columns:
|
||||||
|
<th>${col.label}</td>
|
||||||
|
% endfor
|
||||||
|
% if grid.editable:
|
||||||
|
<th> </th>
|
||||||
|
% endif
|
||||||
|
% if grid.deletable:
|
||||||
|
<th> </th>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
% for i, row in enumerate(grid.iter_rows(), 1):
|
||||||
|
<tr ${grid.get_row_attrs(row, i)}>
|
||||||
|
% if grid.checkboxes:
|
||||||
|
<td class="checkbox">${grid.checkbox(row)}</td>
|
||||||
|
% endif
|
||||||
|
% for field in grid.iter_fields():
|
||||||
|
<td class="${grid.cell_class(field)}">${grid.render_field(field)}</td>
|
||||||
|
% endfor
|
||||||
|
% for col in grid.extra_columns:
|
||||||
|
<td class="noclick ${col.name}">${col.callback(row)}</td>
|
||||||
|
% endfor
|
||||||
|
% if grid.editable:
|
||||||
|
<td class="noclick edit" url="${grid.get_edit_url(row)}"> </td>
|
||||||
|
% endif
|
||||||
|
% if grid.deletable:
|
||||||
|
<td class="noclick delete" url="${grid.get_delete_url(row)}"> </td>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
|
% endfor
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
% if grid.pager:
|
||||||
|
<div class="pager">
|
||||||
|
<p class="showing">
|
||||||
|
showing ${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count}
|
||||||
|
(page ${grid.pager.page} of ${grid.pager.page_count})
|
||||||
|
</p>
|
||||||
|
<p class="page-links">
|
||||||
|
${h.select('grid-page-count', grid.pager.items_per_page, grid.page_count_options())}
|
||||||
|
per page
|
||||||
|
${grid.page_links()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</div>
|
36
rattail/pyramid/templates/grids/search.mako
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<div class="filters" url="${search.request.current_route_url()}">
|
||||||
|
${search.begin()}
|
||||||
|
${search.hidden('filters', 'true')}
|
||||||
|
<% visible = [] %>
|
||||||
|
% for f in search.sorted_filters():
|
||||||
|
<div class="filter" id="filter-${f.name}"${' style="display: none;"' if not search.config.get('include_filter_'+f.name) else ''|n}>
|
||||||
|
${search.checkbox('include_filter_'+f.name)}
|
||||||
|
<label for="${f.name}">${f.label}</label>
|
||||||
|
${f.types_select()}
|
||||||
|
<div class="value">
|
||||||
|
${f.value_control()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% if search.config.get('include_filter_'+f.name):
|
||||||
|
<% visible.append(f.name) %>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
<div class="buttons">
|
||||||
|
${search.add_filter(visible)}
|
||||||
|
${search.submit('submit', "Search", style='display: none;' if not visible else None)}
|
||||||
|
<button type="reset"${' style="display: none;"' if not visible else ''|n}>Reset</button>
|
||||||
|
</div>
|
||||||
|
${search.end()}
|
||||||
|
% if visible:
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
filters_to_disable = [
|
||||||
|
% for field in visible:
|
||||||
|
'${field}',
|
||||||
|
% endfor
|
||||||
|
];
|
||||||
|
$(function() {
|
||||||
|
disable_filter_options();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</div>
|
80
rattail/pyramid/templates/login.mako
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">Login</%def>
|
||||||
|
|
||||||
|
<%def name="head_tags()">
|
||||||
|
${parent.head_tags()}
|
||||||
|
${h.stylesheet_link(request.static_url('rattail.pyramid:static/css/login.css'))}
|
||||||
|
${self.logo_styles()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="logo_styles()">
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
#login-logo {
|
||||||
|
background-image: url(${request.static_url('rattail.pyramid:static/img/logo.jpg')});
|
||||||
|
background-size: 350px auto;
|
||||||
|
height: 114.25px;
|
||||||
|
margin: 25px auto;
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<div id="login-logo"></div>
|
||||||
|
|
||||||
|
<div class="form">
|
||||||
|
${h.form('')}
|
||||||
|
## <input type="hidden" name="login" value="True" />
|
||||||
|
<input type="hidden" name="referrer" value="${referrer}" />
|
||||||
|
|
||||||
|
% if error:
|
||||||
|
<div class="error">${error}</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" name="username" id="username" value="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" name="password" id="password" value="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
${h.submit('submit', "Login")}
|
||||||
|
<input type="reset" value="Reset" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
$('form').submit(function() {
|
||||||
|
if (! $('#username').val()) {
|
||||||
|
with ($('#username').get(0)) {
|
||||||
|
select();
|
||||||
|
focus();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (! $('#password').val()) {
|
||||||
|
with ($('#password').get(0)) {
|
||||||
|
select();
|
||||||
|
focus();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#username').focus();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
9
rattail/pyramid/tests/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class SomeTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_something(self):
|
||||||
|
pass
|
59
rattail/pyramid/tweens.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.tweens`` -- Tween Factories
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy.exc
|
||||||
|
|
||||||
|
from transaction.interfaces import TransientError
|
||||||
|
|
||||||
|
|
||||||
|
def sqlerror_tween_factory(handler, registry):
|
||||||
|
"""
|
||||||
|
Produces a tween which will convert ``sqlalchemy.exc.OperationalError``
|
||||||
|
instances (caused by database server restart) into a retryable
|
||||||
|
``transaction.interfaces.TransientError`` instance, so that a second
|
||||||
|
attempt may be made to connect to the database before really giving up.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
This tween alone is not enough to cause the transaction to be retried;
|
||||||
|
it only marks the error as being *retryable*. If you wish more than one
|
||||||
|
attempt to be made, you must define the ``tm.attempts`` setting within
|
||||||
|
your Pyramid app configuration. See `Retrying
|
||||||
|
<http://docs.pylonsproject.org/projects/pyramid_tm/en/latest/#retrying>`_
|
||||||
|
for more information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def sqlerror_tween(request):
|
||||||
|
try:
|
||||||
|
response = handler(request)
|
||||||
|
except sqlalchemy.exc.OperationalError, error:
|
||||||
|
if error.connection_invalidated:
|
||||||
|
raise TransientError(str(error))
|
||||||
|
raise
|
||||||
|
return response
|
||||||
|
|
||||||
|
return sqlerror_tween
|
|
@ -26,8 +26,14 @@
|
||||||
``rattail.pyramid.views`` -- Pyramid Views
|
``rattail.pyramid.views`` -- Pyramid Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from rattail.pyramid.views.core import *
|
||||||
|
from rattail.pyramid.views.grids import *
|
||||||
|
from rattail.pyramid.views.crud import *
|
||||||
|
from rattail.pyramid.views.autocomplete import *
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
config.include('rattail.pyramid.views.auth')
|
||||||
config.include('rattail.pyramid.views.batches')
|
config.include('rattail.pyramid.views.batches')
|
||||||
# config.include('rattail.pyramid.views.categories')
|
# config.include('rattail.pyramid.views.categories')
|
||||||
config.include('rattail.pyramid.views.customer_groups')
|
config.include('rattail.pyramid.views.customer_groups')
|
||||||
|
@ -35,6 +41,7 @@ def includeme(config):
|
||||||
config.include('rattail.pyramid.views.departments')
|
config.include('rattail.pyramid.views.departments')
|
||||||
config.include('rattail.pyramid.views.employees')
|
config.include('rattail.pyramid.views.employees')
|
||||||
config.include('rattail.pyramid.views.labels')
|
config.include('rattail.pyramid.views.labels')
|
||||||
|
config.include('rattail.pyramid.views.people')
|
||||||
config.include('rattail.pyramid.views.products')
|
config.include('rattail.pyramid.views.products')
|
||||||
config.include('rattail.pyramid.views.stores')
|
config.include('rattail.pyramid.views.stores')
|
||||||
config.include('rattail.pyramid.views.subdepartments')
|
config.include('rattail.pyramid.views.subdepartments')
|
||||||
|
|
185
rattail/pyramid/views/auth.py
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.auth`` -- Authentication Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
from pyramid.security import remember, forget
|
||||||
|
|
||||||
|
import formencode
|
||||||
|
from pyramid_simpleform import Form
|
||||||
|
import pyramid_simpleform.renderers
|
||||||
|
|
||||||
|
from webhelpers.html import tags
|
||||||
|
from webhelpers.html.builder import HTML
|
||||||
|
|
||||||
|
import rattail
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.auth import authenticate_user, set_user_password
|
||||||
|
from rattail.time import local_time
|
||||||
|
from rattail.util import prettify
|
||||||
|
|
||||||
|
|
||||||
|
class FormRenderer(pyramid_simpleform.renderers.FormRenderer):
|
||||||
|
"""
|
||||||
|
Customized form renderer. Provides some extra methods for convenience.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Note that as of this writing, this renderer is used only by the
|
||||||
|
# ``change_password`` view. This should probably change, and this class
|
||||||
|
# definition should be moved elsewhere.
|
||||||
|
|
||||||
|
def field_div(self, name, field, label=None):
|
||||||
|
errors = self.errors_for(name)
|
||||||
|
if errors:
|
||||||
|
errors = [HTML.tag('div', class_='field-error', c=x) for x in errors]
|
||||||
|
errors = tags.literal('').join(errors)
|
||||||
|
|
||||||
|
label = HTML.tag('label', for_=name, c=label or prettify(name))
|
||||||
|
inner = HTML.tag('div', class_='field', c=field)
|
||||||
|
|
||||||
|
outer_class = 'field-wrapper'
|
||||||
|
if errors:
|
||||||
|
outer_class += ' error'
|
||||||
|
outer = HTML.tag('div', class_=outer_class, c=(errors or '') + label + inner)
|
||||||
|
return outer
|
||||||
|
|
||||||
|
def referrer_field(self):
|
||||||
|
return self.hidden('referrer', value=self.form.request.get_referrer())
|
||||||
|
|
||||||
|
|
||||||
|
class UserLogin(formencode.Schema):
|
||||||
|
allow_extra_fields = True
|
||||||
|
filter_extra_fields = True
|
||||||
|
username = formencode.validators.NotEmpty()
|
||||||
|
password = formencode.validators.NotEmpty()
|
||||||
|
|
||||||
|
|
||||||
|
def login(request):
|
||||||
|
"""
|
||||||
|
The login view, responsible for displaying and handling the login form.
|
||||||
|
"""
|
||||||
|
|
||||||
|
referrer = request.get_referrer()
|
||||||
|
|
||||||
|
# Redirect if already logged in.
|
||||||
|
if request.user:
|
||||||
|
return HTTPFound(location=referrer)
|
||||||
|
|
||||||
|
form = Form(request, schema=UserLogin)
|
||||||
|
if form.validate():
|
||||||
|
user = authenticate_user(Session(),
|
||||||
|
form.data['username'],
|
||||||
|
form.data['password'])
|
||||||
|
if user:
|
||||||
|
request.session.flash("%s logged in at %s" % (
|
||||||
|
user.display_name,
|
||||||
|
local_time().strftime('%I:%M %p')))
|
||||||
|
headers = remember(request, user.uuid)
|
||||||
|
return HTTPFound(location=referrer, headers=headers)
|
||||||
|
request.session.flash("Invalid username or password")
|
||||||
|
|
||||||
|
url = rattail.config.get(
|
||||||
|
'rattail.pyramid', 'login.logo_url',
|
||||||
|
default=request.static_url('rattail.pyramid:static/img/logo.jpg'))
|
||||||
|
kwargs = eval(rattail.config.get(
|
||||||
|
'rattail.pyramid', 'login.logo_kwargs', default='dict(width=500)'))
|
||||||
|
|
||||||
|
return {'form': FormRenderer(form), 'referrer': referrer,
|
||||||
|
'logo_url': url, 'logo_kwargs': kwargs}
|
||||||
|
|
||||||
|
|
||||||
|
def logout(request):
|
||||||
|
"""
|
||||||
|
View responsible for logging out the current user.
|
||||||
|
|
||||||
|
This deletes/invalidates the current session and then redirects to the
|
||||||
|
login page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
request.session.delete()
|
||||||
|
request.session.invalidate()
|
||||||
|
headers = forget(request)
|
||||||
|
referrer = request.get_referrer()
|
||||||
|
return HTTPFound(location=referrer, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentPasswordCorrect(formencode.validators.FancyValidator):
|
||||||
|
|
||||||
|
def _to_python(self, value, state):
|
||||||
|
user = state
|
||||||
|
if not authenticate_user(user.username, value, session=Session()):
|
||||||
|
raise formencode.Invalid("The password is incorrect.", value, state)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePassword(formencode.Schema):
|
||||||
|
|
||||||
|
allow_extra_fields = True
|
||||||
|
filter_extra_fields = True
|
||||||
|
|
||||||
|
current_password = formencode.All(
|
||||||
|
formencode.validators.NotEmpty(),
|
||||||
|
CurrentPasswordCorrect())
|
||||||
|
|
||||||
|
new_password = formencode.validators.NotEmpty()
|
||||||
|
confirm_password = formencode.validators.NotEmpty()
|
||||||
|
|
||||||
|
chained_validators = [formencode.validators.FieldsMatch(
|
||||||
|
'new_password', 'confirm_password')]
|
||||||
|
|
||||||
|
|
||||||
|
def change_password(request):
|
||||||
|
"""
|
||||||
|
Allows a user to change his or her password.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not request.user:
|
||||||
|
return HTTPFound(location=request.route_url('home'))
|
||||||
|
|
||||||
|
form = Form(request, schema=ChangePassword, state=request.user)
|
||||||
|
if form.validate():
|
||||||
|
set_user_password(request.user, form.data['new_password'])
|
||||||
|
return HTTPFound(location=request.get_referrer())
|
||||||
|
|
||||||
|
return {'form': FormRenderer(form)}
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
|
||||||
|
config.add_route('login', '/login')
|
||||||
|
config.add_view(login,
|
||||||
|
route_name='login',
|
||||||
|
renderer='/login.mako')
|
||||||
|
|
||||||
|
config.add_route('logout', '/logout')
|
||||||
|
config.add_view(logout,
|
||||||
|
route_name='logout')
|
||||||
|
|
||||||
|
config.add_route('change_password', '/change-password')
|
||||||
|
config.add_view(change_password,
|
||||||
|
route_name='change_password',
|
||||||
|
renderer='/change_password.mako')
|
70
rattail/pyramid/views/autocomplete.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.autocomplete`` -- Autocomplete View
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rattail.pyramid.views.core import View
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['AutocompleteView']
|
||||||
|
|
||||||
|
|
||||||
|
class AutocompleteView(View):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mapped_class(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fieldname(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def filter_query(self, q):
|
||||||
|
return q
|
||||||
|
|
||||||
|
def make_query(self, query):
|
||||||
|
q = Session.query(self.mapped_class)
|
||||||
|
q = self.filter_query(q)
|
||||||
|
q = q.filter(getattr(self.mapped_class, self.fieldname).ilike('%%%s%%' % query))
|
||||||
|
q = q.order_by(getattr(self.mapped_class, self.fieldname))
|
||||||
|
return q
|
||||||
|
|
||||||
|
def query(self, query):
|
||||||
|
return self.make_query(query)
|
||||||
|
|
||||||
|
def display(self, instance):
|
||||||
|
return getattr(instance, self.fieldname)
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
query = self.request.params['query']
|
||||||
|
objs = self.query(query).all()
|
||||||
|
data = dict(
|
||||||
|
query=query,
|
||||||
|
suggestions=[self.display(x) for x in objs],
|
||||||
|
data=[x.uuid for x in objs],
|
||||||
|
)
|
||||||
|
return data
|
|
@ -33,20 +33,19 @@ from pyramid.renderers import render_to_response
|
||||||
|
|
||||||
from webhelpers.html import tags
|
from webhelpers.html import tags
|
||||||
|
|
||||||
import edbob
|
|
||||||
from edbob.pyramid import Session
|
|
||||||
from edbob.pyramid.forms import EnumFieldRenderer
|
|
||||||
from edbob.pyramid.grids.search import BooleanSearchFilter
|
|
||||||
from edbob.pyramid.progress import SessionProgress
|
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView, View
|
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
from rattail import batches
|
from rattail import batches
|
||||||
|
from rattail.db.model import Batch
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.pyramid.forms import EnumFieldRenderer
|
||||||
|
from rattail.pyramid.grids.search import BooleanSearchFilter
|
||||||
|
from rattail.pyramid.progress import SessionProgress
|
||||||
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView, View
|
||||||
|
|
||||||
|
|
||||||
class BatchesGrid(SearchableAlchemyGridView):
|
class BatchesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Batch
|
mapped_class = Batch
|
||||||
config_prefix = 'batches'
|
config_prefix = 'batches'
|
||||||
sort = 'id'
|
sort = 'id'
|
||||||
|
|
||||||
|
@ -54,15 +53,15 @@ class BatchesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
def executed_is(q, v):
|
def executed_is(q, v):
|
||||||
if v == 'True':
|
if v == 'True':
|
||||||
return q.filter(rattail.Batch.executed != None)
|
return q.filter(Batch.executed != None)
|
||||||
else:
|
else:
|
||||||
return q.filter(rattail.Batch.executed == None)
|
return q.filter(Batch.executed == None)
|
||||||
|
|
||||||
def executed_isnot(q, v):
|
def executed_isnot(q, v):
|
||||||
if v == 'True':
|
if v == 'True':
|
||||||
return q.filter(rattail.Batch.executed == None)
|
return q.filter(Batch.executed == None)
|
||||||
else:
|
else:
|
||||||
return q.filter(rattail.Batch.executed != None)
|
return q.filter(Batch.executed != None)
|
||||||
|
|
||||||
return self.make_filter_map(
|
return self.make_filter_map(
|
||||||
exact=['id'],
|
exact=['id'],
|
||||||
|
@ -113,7 +112,7 @@ class BatchesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class BatchCrud(CrudView):
|
class BatchCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Batch
|
mapped_class = Batch
|
||||||
home_route = 'batches'
|
home_route = 'batches'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
@ -138,7 +137,8 @@ class BatchCrud(CrudView):
|
||||||
class ExecuteBatch(View):
|
class ExecuteBatch(View):
|
||||||
|
|
||||||
def execute_batch(self, batch, progress):
|
def execute_batch(self, batch, progress):
|
||||||
session = edbob.Session()
|
from rattail.db import Session
|
||||||
|
session = Session()
|
||||||
batch = session.merge(batch)
|
batch = session.merge(batch)
|
||||||
|
|
||||||
if not batch.execute(progress):
|
if not batch.execute(progress):
|
||||||
|
@ -158,7 +158,7 @@ class ExecuteBatch(View):
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
uuid = self.request.matchdict['uuid']
|
uuid = self.request.matchdict['uuid']
|
||||||
batch = Session.query(rattail.Batch).get(uuid) if uuid else None
|
batch = Session.query(Batch).get(uuid) if uuid else None
|
||||||
if not batch:
|
if not batch:
|
||||||
return HTTPFound(location=self.request.route_url('batches'))
|
return HTTPFound(location=self.request.route_url('batches'))
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,9 @@
|
||||||
``rattail.pyramid.views.batches.params`` -- Batch Parameter Views
|
``rattail.pyramid.views.batches.params`` -- Batch Parameter Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from edbob.pyramid.views import View
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
|
from rattail.pyramid.views import View
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['BatchParamsView']
|
__all__ = ['BatchParamsView']
|
||||||
|
|
|
@ -26,10 +26,9 @@
|
||||||
``rattail.pyramid.views.batches.params.printlabels`` -- Print Labels Batch
|
``rattail.pyramid.views.batches.params.printlabels`` -- Print Labels Batch
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from edbob.pyramid import Session
|
|
||||||
|
|
||||||
import rattail
|
|
||||||
from rattail.pyramid.views.batches.params import BatchParamsView
|
from rattail.pyramid.views.batches.params import BatchParamsView
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.model import LabelProfile
|
||||||
|
|
||||||
|
|
||||||
class PrintLabels(BatchParamsView):
|
class PrintLabels(BatchParamsView):
|
||||||
|
@ -37,8 +36,8 @@ class PrintLabels(BatchParamsView):
|
||||||
provider_name = 'print_labels'
|
provider_name = 'print_labels'
|
||||||
|
|
||||||
def render_kwargs(self):
|
def render_kwargs(self):
|
||||||
q = Session.query(rattail.LabelProfile)
|
q = Session.query(LabelProfile)
|
||||||
q = q.order_by(rattail.LabelProfile.ordinal)
|
q = q.order_by(LabelProfile.ordinal)
|
||||||
profiles = [(x.code, x.description) for x in q]
|
profiles = [(x.code, x.description) for x in q]
|
||||||
return {'label_profiles': profiles}
|
return {'label_profiles': profiles}
|
||||||
|
|
||||||
|
@ -46,6 +45,7 @@ class PrintLabels(BatchParamsView):
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
config.add_route('batch_params.print_labels', '/batches/params/print-labels')
|
config.add_route('batch_params.print_labels', '/batches/params/print-labels')
|
||||||
config.add_view(PrintLabels, route_name='batch_params.print_labels',
|
config.add_view(PrintLabels,
|
||||||
|
route_name='batch_params.print_labels',
|
||||||
renderer='/batches/params/print_labels.mako',
|
renderer='/batches/params/print_labels.mako',
|
||||||
permission='batches.print_labels')
|
permission='batches.print_labels')
|
||||||
|
|
|
@ -28,11 +28,10 @@
|
||||||
|
|
||||||
from pyramid.httpexceptions import HTTPFound
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
from edbob.pyramid import Session
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.model import LabelProfile, Batch
|
||||||
import rattail
|
from rattail.pyramid.forms.formalchemy.renderers import GPCFieldRenderer
|
||||||
from rattail.pyramid.forms import GPCFieldRenderer
|
|
||||||
|
|
||||||
|
|
||||||
def field_with_renderer(field, column):
|
def field_with_renderer(field, column):
|
||||||
|
@ -41,8 +40,8 @@ def field_with_renderer(field, column):
|
||||||
field = field.with_renderer(GPCFieldRenderer)
|
field = field.with_renderer(GPCFieldRenderer)
|
||||||
|
|
||||||
elif column.sil_name == 'F95': # Shelf Tag Type
|
elif column.sil_name == 'F95': # Shelf Tag Type
|
||||||
q = Session.query(rattail.LabelProfile)
|
q = Session.query(LabelProfile)
|
||||||
q = q.order_by(rattail.LabelProfile.ordinal)
|
q = q.order_by(LabelProfile.ordinal)
|
||||||
field = field.dropdown(options=[(x.description, x.code) for x in q])
|
field = field.dropdown(options=[(x.description, x.code) for x in q])
|
||||||
|
|
||||||
return field
|
return field
|
||||||
|
@ -50,7 +49,7 @@ def field_with_renderer(field, column):
|
||||||
|
|
||||||
def BatchRowsGrid(request):
|
def BatchRowsGrid(request):
|
||||||
uuid = request.matchdict['uuid']
|
uuid = request.matchdict['uuid']
|
||||||
batch = Session.query(rattail.Batch).get(uuid) if uuid else None
|
batch = Session.query(Batch).get(uuid) if uuid else None
|
||||||
if not batch:
|
if not batch:
|
||||||
return HTTPFound(location=request.route_url('batches'))
|
return HTTPFound(location=request.route_url('batches'))
|
||||||
|
|
||||||
|
@ -139,7 +138,7 @@ def batch_rows_delete(request):
|
||||||
|
|
||||||
def batch_row_crud(request, attr):
|
def batch_row_crud(request, attr):
|
||||||
batch_uuid = request.matchdict['batch_uuid']
|
batch_uuid = request.matchdict['batch_uuid']
|
||||||
batch = Session.query(rattail.Batch).get(batch_uuid)
|
batch = Session.query(Batch).get(batch_uuid)
|
||||||
if not batch:
|
if not batch:
|
||||||
return HTTPFound(location=request.route_url('batches'))
|
return HTTPFound(location=request.route_url('batches'))
|
||||||
|
|
||||||
|
|
|
@ -26,15 +26,14 @@
|
||||||
``rattail.pyramid.views.brands`` -- Brand Views
|
``rattail.pyramid.views.brands`` -- Brand Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from edbob.pyramid.views import (
|
from rattail.db.model import Brand
|
||||||
|
from rattail.pyramid.views import (
|
||||||
SearchableAlchemyGridView, CrudView, AutocompleteView)
|
SearchableAlchemyGridView, CrudView, AutocompleteView)
|
||||||
|
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class BrandsGrid(SearchableAlchemyGridView):
|
class BrandsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Brand
|
mapped_class = Brand
|
||||||
config_prefix = 'brands'
|
config_prefix = 'brands'
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
|
|
||||||
|
@ -70,7 +69,7 @@ class BrandsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class BrandCrud(CrudView):
|
class BrandCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Brand
|
mapped_class = Brand
|
||||||
home_route = 'brands'
|
home_route = 'brands'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
@ -84,7 +83,7 @@ class BrandCrud(CrudView):
|
||||||
|
|
||||||
class BrandsAutocomplete(AutocompleteView):
|
class BrandsAutocomplete(AutocompleteView):
|
||||||
|
|
||||||
mapped_class = rattail.Brand
|
mapped_class = Brand
|
||||||
fieldname = 'name'
|
fieldname = 'name'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,14 +26,13 @@
|
||||||
``rattail.pyramid.views.categories`` -- Category Views
|
``rattail.pyramid.views.categories`` -- Category Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from rattail.db.model import Category
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class CategoriesGrid(SearchableAlchemyGridView):
|
class CategoriesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Category
|
mapped_class = Category
|
||||||
config_prefix = 'categories'
|
config_prefix = 'categories'
|
||||||
sort = 'number'
|
sort = 'number'
|
||||||
|
|
||||||
|
@ -70,7 +69,7 @@ class CategoriesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class CategoryCrud(CrudView):
|
class CategoryCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Category
|
mapped_class = Category
|
||||||
home_route = 'categories'
|
home_route = 'categories'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
|
60
rattail/pyramid/views/core.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.core`` -- Core Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
from pyramid.security import authenticated_userid
|
||||||
|
|
||||||
|
from webhelpers.html import literal
|
||||||
|
from webhelpers.html.tags import link_to
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['View']
|
||||||
|
|
||||||
|
|
||||||
|
class View(object):
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
|
||||||
|
def forbidden(request):
|
||||||
|
"""
|
||||||
|
The forbidden view. This is triggered whenever access rights are denied
|
||||||
|
for an otherwise-appropriate view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg = literal("You do not have permission to do that.")
|
||||||
|
if not authenticated_userid(request):
|
||||||
|
msg += literal(" (Perhaps you should %s?)" %
|
||||||
|
link_to("log in", request.route_url('login')))
|
||||||
|
request.session.flash(msg, allow_duplicate=False)
|
||||||
|
|
||||||
|
url = request.referer
|
||||||
|
if not url or url == request.current_route_url():
|
||||||
|
url = request.route_url('home')
|
||||||
|
return HTTPFound(location=url)
|
201
rattail/pyramid/views/crud.py
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.crud`` -- CRUD View Function
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
|
import formalchemy
|
||||||
|
|
||||||
|
from rattail.pyramid.views.core import View
|
||||||
|
from rattail.pyramid.forms import AlchemyForm
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.util import prettify
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['CrudView']
|
||||||
|
|
||||||
|
|
||||||
|
class CrudView(View):
|
||||||
|
|
||||||
|
readonly = False
|
||||||
|
allow_successive_creates = False
|
||||||
|
update_cancel_route = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mapped_class(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pretty_name(self):
|
||||||
|
return self.mapped_class.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def home_route(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def home_url(self):
|
||||||
|
return self.request.route_url(self.home_route)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cancel_route(self):
|
||||||
|
return self.home_route
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cancel_url(self):
|
||||||
|
return self.request.route_url(self.cancel_route)
|
||||||
|
|
||||||
|
def make_fieldset(self, model, **kwargs):
|
||||||
|
kwargs.setdefault('session', Session())
|
||||||
|
fieldset = formalchemy.FieldSet(model, **kwargs)
|
||||||
|
fieldset.prettify = prettify
|
||||||
|
return fieldset
|
||||||
|
|
||||||
|
def fieldset(self, model):
|
||||||
|
return self.make_fieldset(model)
|
||||||
|
|
||||||
|
def make_form(self, model, **kwargs):
|
||||||
|
self.creating = model is self.mapped_class
|
||||||
|
self.updating = not self.creating
|
||||||
|
|
||||||
|
fieldset = self.fieldset(model)
|
||||||
|
kwargs.setdefault('pretty_name', self.pretty_name)
|
||||||
|
kwargs.setdefault('action_url', self.request.current_route_url())
|
||||||
|
if self.updating and self.update_cancel_route:
|
||||||
|
kwargs.setdefault('cancel_url', self.request.route_url(
|
||||||
|
self.update_cancel_route, uuid=model.uuid))
|
||||||
|
else:
|
||||||
|
kwargs.setdefault('cancel_url', self.cancel_url)
|
||||||
|
kwargs.setdefault('creating', self.creating)
|
||||||
|
kwargs.setdefault('updating', self.updating)
|
||||||
|
form = AlchemyForm(self.request, fieldset, **kwargs)
|
||||||
|
|
||||||
|
if form.creating:
|
||||||
|
if hasattr(self, 'create_label'):
|
||||||
|
form.create_label = self.create_label
|
||||||
|
if self.allow_successive_creates:
|
||||||
|
form.allow_successive_creates = True
|
||||||
|
if hasattr(self, 'successive_create_label'):
|
||||||
|
form.successive_create_label = self.successive_create_label
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
def form(self, model):
|
||||||
|
return self.make_form(model)
|
||||||
|
|
||||||
|
def crud(self, model, readonly=False):
|
||||||
|
|
||||||
|
if readonly:
|
||||||
|
self.readonly = True
|
||||||
|
|
||||||
|
form = self.form(model)
|
||||||
|
if readonly:
|
||||||
|
form.readonly = True
|
||||||
|
|
||||||
|
if not form.readonly and self.request.POST:
|
||||||
|
if form.validate():
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
result = self.post_save(form)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
if form.creating:
|
||||||
|
self.flash_create(form.fieldset.model)
|
||||||
|
else:
|
||||||
|
self.flash_update(form.fieldset.model)
|
||||||
|
|
||||||
|
if (form.creating and form.allow_successive_creates
|
||||||
|
and self.request.params.get('create_and_continue')):
|
||||||
|
return HTTPFound(location=self.request.current_route_url())
|
||||||
|
|
||||||
|
return HTTPFound(location=self.post_save_url(form))
|
||||||
|
|
||||||
|
self.validation_failed(form)
|
||||||
|
|
||||||
|
kwargs = self.template_kwargs(form)
|
||||||
|
kwargs['form'] = form
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def template_kwargs(self, form):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def post_save(self, form):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post_save_url(self, form):
|
||||||
|
return self.home_url
|
||||||
|
|
||||||
|
def validation_failed(self, form):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def flash_create(self, model):
|
||||||
|
self.request.session.flash("%s \"%s\" has been created." %
|
||||||
|
(self.pretty_name, model))
|
||||||
|
|
||||||
|
def flash_delete(self, model):
|
||||||
|
self.request.session.flash("%s \"%s\" has been deleted." %
|
||||||
|
(self.pretty_name, model))
|
||||||
|
|
||||||
|
def flash_update(self, model):
|
||||||
|
self.request.session.flash("%s \"%s\" has been updated." %
|
||||||
|
(self.pretty_name, model))
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
return self.crud(self.mapped_class)
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
uuid = self.request.matchdict['uuid']
|
||||||
|
model = Session.query(self.mapped_class).get(uuid) if uuid else None
|
||||||
|
if not model:
|
||||||
|
return HTTPFound(location=self.home_url)
|
||||||
|
return self.crud(model, readonly=True)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
uuid = self.request.matchdict['uuid']
|
||||||
|
model = Session.query(self.mapped_class).get(uuid) if uuid else None
|
||||||
|
assert model
|
||||||
|
return self.crud(model)
|
||||||
|
|
||||||
|
def pre_delete(self, model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post_delete(self, model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
uuid = self.request.matchdict['uuid']
|
||||||
|
model = Session.query(self.mapped_class).get(uuid) if uuid else None
|
||||||
|
assert model
|
||||||
|
result = self.pre_delete(model)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
Session.delete(model)
|
||||||
|
Session.flush() # Don't set flash message if delete fails.
|
||||||
|
self.post_delete(model)
|
||||||
|
self.flash_delete(model)
|
||||||
|
return HTTPFound(location=self.home_url)
|
|
@ -26,14 +26,13 @@
|
||||||
``rattail.pyramid.views.customergroups`` -- CustomerGroup Views
|
``rattail.pyramid.views.customergroups`` -- CustomerGroup Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from rattail.db.model import CustomerGroup
|
||||||
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerGroupsGrid(SearchableAlchemyGridView):
|
class CustomerGroupsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.CustomerGroup
|
mapped_class = CustomerGroup
|
||||||
config_prefix = 'customer_groups'
|
config_prefix = 'customer_groups'
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
|
|
||||||
|
@ -61,7 +60,7 @@ class CustomerGroupsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class CustomerGroupCrud(CrudView):
|
class CustomerGroupCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.CustomerGroup
|
mapped_class = CustomerGroup
|
||||||
home_route = 'customer_groups'
|
home_route = 'customer_groups'
|
||||||
pretty_name = "Customer Group"
|
pretty_name = "Customer Group"
|
||||||
|
|
||||||
|
|
|
@ -28,16 +28,15 @@
|
||||||
|
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
|
|
||||||
import edbob
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from rattail.db.model import Customer, CustomerPhoneNumber, CustomerEmailAddress
|
||||||
from edbob.pyramid.forms import EnumFieldRenderer
|
from rattail.pyramid.forms import EnumFieldRenderer
|
||||||
|
from rattail.enum import EMAIL_PREFERENCE
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class CustomersGrid(SearchableAlchemyGridView):
|
class CustomersGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Customer
|
mapped_class = Customer
|
||||||
config_prefix = 'customers'
|
config_prefix = 'customers'
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
clickable = True
|
clickable = True
|
||||||
|
@ -45,21 +44,21 @@ class CustomersGrid(SearchableAlchemyGridView):
|
||||||
def join_map(self):
|
def join_map(self):
|
||||||
return {
|
return {
|
||||||
'email':
|
'email':
|
||||||
lambda q: q.outerjoin(rattail.CustomerEmailAddress, and_(
|
lambda q: q.outerjoin(CustomerEmailAddress, and_(
|
||||||
rattail.CustomerEmailAddress.parent_uuid == rattail.Customer.uuid,
|
CustomerEmailAddress.parent_uuid == Customer.uuid,
|
||||||
rattail.CustomerEmailAddress.preference == 1)),
|
CustomerEmailAddress.preference == 1)),
|
||||||
'phone':
|
'phone':
|
||||||
lambda q: q.outerjoin(rattail.CustomerPhoneNumber, and_(
|
lambda q: q.outerjoin(CustomerPhoneNumber, and_(
|
||||||
rattail.CustomerPhoneNumber.parent_uuid == rattail.Customer.uuid,
|
CustomerPhoneNumber.parent_uuid == Customer.uuid,
|
||||||
rattail.CustomerPhoneNumber.preference == 1)),
|
CustomerPhoneNumber.preference == 1)),
|
||||||
}
|
}
|
||||||
|
|
||||||
def filter_map(self):
|
def filter_map(self):
|
||||||
return self.make_filter_map(
|
return self.make_filter_map(
|
||||||
exact=['id'],
|
exact=['id'],
|
||||||
ilike=['name'],
|
ilike=['name'],
|
||||||
email=self.filter_ilike(rattail.CustomerEmailAddress.address),
|
email=self.filter_ilike(CustomerEmailAddress.address),
|
||||||
phone=self.filter_ilike(rattail.CustomerPhoneNumber.number))
|
phone=self.filter_ilike(CustomerPhoneNumber.number))
|
||||||
|
|
||||||
def filter_config(self):
|
def filter_config(self):
|
||||||
return self.make_filter_config(
|
return self.make_filter_config(
|
||||||
|
@ -72,8 +71,8 @@ class CustomersGrid(SearchableAlchemyGridView):
|
||||||
def sort_map(self):
|
def sort_map(self):
|
||||||
return self.make_sort_map(
|
return self.make_sort_map(
|
||||||
'id', 'name',
|
'id', 'name',
|
||||||
email=self.sorter(rattail.CustomerEmailAddress.address),
|
email=self.sorter(CustomerEmailAddress.address),
|
||||||
phone=self.sorter(rattail.CustomerPhoneNumber.number))
|
phone=self.sorter(CustomerPhoneNumber.number))
|
||||||
|
|
||||||
def grid(self):
|
def grid(self):
|
||||||
g = self.make_grid()
|
g = self.make_grid()
|
||||||
|
@ -91,12 +90,12 @@ class CustomersGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class CustomerCrud(CrudView):
|
class CustomerCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Customer
|
mapped_class = Customer
|
||||||
home_route = 'customers'
|
home_route = 'customers'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
fs = self.make_fieldset(model)
|
fs = self.make_fieldset(model)
|
||||||
fs.email_preference.set(renderer=EnumFieldRenderer(edbob.EMAIL_PREFERENCE))
|
fs.email_preference.set(renderer=EnumFieldRenderer(EMAIL_PREFERENCE))
|
||||||
fs.configure(
|
fs.configure(
|
||||||
include=[
|
include=[
|
||||||
fs.id.label("ID"),
|
fs.id.label("ID"),
|
||||||
|
|
|
@ -27,15 +27,14 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from edbob.pyramid.views import (
|
from rattail.db.model import Department, Vendor, Product, ProductCost
|
||||||
|
from rattail.pyramid.views import (
|
||||||
SearchableAlchemyGridView, CrudView, AlchemyGridView, AutocompleteView)
|
SearchableAlchemyGridView, CrudView, AlchemyGridView, AutocompleteView)
|
||||||
|
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class DepartmentsGrid(SearchableAlchemyGridView):
|
class DepartmentsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Department
|
mapped_class = Department
|
||||||
config_prefix = 'departments'
|
config_prefix = 'departments'
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
|
|
||||||
|
@ -72,7 +71,7 @@ class DepartmentsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class DepartmentCrud(CrudView):
|
class DepartmentCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Department
|
mapped_class = Department
|
||||||
home_route = 'departments'
|
home_route = 'departments'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
@ -87,19 +86,19 @@ class DepartmentCrud(CrudView):
|
||||||
|
|
||||||
class DepartmentsByVendorGrid(AlchemyGridView):
|
class DepartmentsByVendorGrid(AlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Department
|
mapped_class = Department
|
||||||
config_prefix = 'departments.by_vendor'
|
config_prefix = 'departments.by_vendor'
|
||||||
checkboxes = True
|
checkboxes = True
|
||||||
partial_only = True
|
partial_only = True
|
||||||
|
|
||||||
def query(self):
|
def query(self):
|
||||||
q = self.make_query()
|
q = self.make_query()
|
||||||
q = q.outerjoin(rattail.Product)
|
q = q.outerjoin(Product)
|
||||||
q = q.join(rattail.ProductCost)
|
q = q.join(ProductCost)
|
||||||
q = q.join(rattail.Vendor)
|
q = q.join(Vendor)
|
||||||
q = q.filter(rattail.Vendor.uuid == self.request.params['uuid'])
|
q = q.filter(Vendor.uuid == self.request.params['uuid'])
|
||||||
q = q.distinct()
|
q = q.distinct()
|
||||||
q = q.order_by(rattail.Department.name)
|
q = q.order_by(Department.name)
|
||||||
return q
|
return q
|
||||||
|
|
||||||
def grid(self):
|
def grid(self):
|
||||||
|
@ -114,7 +113,7 @@ class DepartmentsByVendorGrid(AlchemyGridView):
|
||||||
|
|
||||||
class DepartmentsAutocomplete(AutocompleteView):
|
class DepartmentsAutocomplete(AutocompleteView):
|
||||||
|
|
||||||
mapped_class = rattail.Department
|
mapped_class = Department
|
||||||
fieldname = 'name'
|
fieldname = 'name'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,32 +28,31 @@
|
||||||
|
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
|
|
||||||
import edbob
|
from rattail.pyramid.views import SearchableAlchemyGridView
|
||||||
from edbob.pyramid.forms import AssociationProxyField
|
from rattail.db.model import Employee, Person
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView
|
from rattail.pyramid.forms import AssociationProxyField
|
||||||
|
from rattail.enum import EMPLOYEE_STATUS_CURRENT
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class EmployeesGrid(SearchableAlchemyGridView):
|
class EmployeesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Employee
|
mapped_class = Employee
|
||||||
config_prefix = 'employees'
|
config_prefix = 'employees'
|
||||||
sort = 'first_name'
|
sort = 'first_name'
|
||||||
|
|
||||||
def join_map(self):
|
def join_map(self):
|
||||||
return {
|
return {
|
||||||
'phone':
|
'phone':
|
||||||
lambda q: q.outerjoin(rattail.EmployeePhoneNumber, and_(
|
lambda q: q.outerjoin(EmployeePhoneNumber, and_(
|
||||||
rattail.EmployeePhoneNumber.parent_uuid == rattail.Employee.uuid,
|
EmployeePhoneNumber.parent_uuid == Employee.uuid,
|
||||||
rattail.EmployeePhoneNumber.preference == 1)),
|
EmployeePhoneNumber.preference == 1)),
|
||||||
}
|
}
|
||||||
|
|
||||||
def filter_map(self):
|
def filter_map(self):
|
||||||
return self.make_filter_map(
|
return self.make_filter_map(
|
||||||
first_name=self.filter_ilike(edbob.Person.first_name),
|
first_name=self.filter_ilike(Person.first_name),
|
||||||
last_name=self.filter_ilike(edbob.Person.last_name),
|
last_name=self.filter_ilike(Person.last_name),
|
||||||
phone=self.filter_ilike(rattail.EmployeePhoneNumber.number))
|
phone=self.filter_ilike(EmployeePhoneNumber.number))
|
||||||
|
|
||||||
def filter_config(self):
|
def filter_config(self):
|
||||||
return self.make_filter_config(
|
return self.make_filter_config(
|
||||||
|
@ -65,15 +64,15 @@ class EmployeesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
def sort_map(self):
|
def sort_map(self):
|
||||||
return self.make_sort_map(
|
return self.make_sort_map(
|
||||||
first_name=self.sorter(edbob.Person.first_name),
|
first_name=self.sorter(Person.first_name),
|
||||||
last_name=self.sorter(edbob.Person.last_name),
|
last_name=self.sorter(Person.last_name),
|
||||||
phone=self.sorter(rattail.EmployeePhoneNumber.number))
|
phone=self.sorter(EmployeePhoneNumber.number))
|
||||||
|
|
||||||
def query(self):
|
def query(self):
|
||||||
q = self.make_query()
|
q = self.make_query()
|
||||||
q = q.join(edbob.Person)
|
q = q.join(Person)
|
||||||
if not self.request.has_perm('employees.edit'):
|
if not self.request.has_perm('employees.edit'):
|
||||||
q = q.filter(rattail.Employee.status == rattail.EMPLOYEE_STATUS_CURRENT)
|
q = q.filter(Employee.status == EMPLOYEE_STATUS_CURRENT)
|
||||||
return q
|
return q
|
||||||
|
|
||||||
def grid(self):
|
def grid(self):
|
||||||
|
|
30
rattail/pyramid/views/grids/__init__.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.grids`` -- Grid Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rattail.pyramid.views.grids.core import *
|
||||||
|
from rattail.pyramid.views.grids.alchemy import *
|
184
rattail/pyramid/views/grids/alchemy.py
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.grids.alchemy`` -- FormAlchemy Grid Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from webhelpers import paginate
|
||||||
|
|
||||||
|
from rattail.pyramid import grids
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.pyramid.views.grids.core import GridView
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['AlchemyGridView', 'SortableAlchemyGridView',
|
||||||
|
'PagedAlchemyGridView', 'SearchableAlchemyGridView']
|
||||||
|
|
||||||
|
|
||||||
|
class AlchemyGridView(GridView):
|
||||||
|
|
||||||
|
def make_query(self):
|
||||||
|
q = Session.query(self.mapped_class)
|
||||||
|
return q
|
||||||
|
|
||||||
|
def query(self):
|
||||||
|
return self.make_query()
|
||||||
|
|
||||||
|
def make_grid(self, **kwargs):
|
||||||
|
self.update_grid_kwargs(kwargs)
|
||||||
|
return grids.AlchemyGrid(
|
||||||
|
self.request, self.mapped_class, self._data, **kwargs)
|
||||||
|
|
||||||
|
def grid(self):
|
||||||
|
return self.make_grid()
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self._data = self.query()
|
||||||
|
grid = self.grid()
|
||||||
|
return grids.util.render_grid(grid)
|
||||||
|
|
||||||
|
|
||||||
|
class SortableAlchemyGridView(AlchemyGridView):
|
||||||
|
|
||||||
|
sort = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config_prefix(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def join_map(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def make_sort_map(self, *args, **kwargs):
|
||||||
|
return grids.util.get_sort_map(
|
||||||
|
self.mapped_class, names=args or None, **kwargs)
|
||||||
|
|
||||||
|
def sorter(self, field):
|
||||||
|
return grids.util.sorter(field)
|
||||||
|
|
||||||
|
def sort_map(self):
|
||||||
|
return self.make_sort_map()
|
||||||
|
|
||||||
|
def make_sort_config(self, **kwargs):
|
||||||
|
return grids.util.get_sort_config(
|
||||||
|
self.config_prefix, self.request, **kwargs)
|
||||||
|
|
||||||
|
def sort_config(self):
|
||||||
|
return self.make_sort_config(sort=self.sort)
|
||||||
|
|
||||||
|
def make_query(self):
|
||||||
|
query = Session.query(self.mapped_class)
|
||||||
|
query = grids.util.sort_query(
|
||||||
|
query, self._sort_config, self.sort_map(), self.join_map())
|
||||||
|
return query
|
||||||
|
|
||||||
|
def query(self):
|
||||||
|
return self.make_query()
|
||||||
|
|
||||||
|
def make_grid(self, **kwargs):
|
||||||
|
self.update_grid_kwargs(kwargs)
|
||||||
|
return grids.AlchemyGrid(
|
||||||
|
self.request, self.mapped_class, self._data,
|
||||||
|
sort_map=self.sort_map(), config=self._sort_config, **kwargs)
|
||||||
|
|
||||||
|
def grid(self):
|
||||||
|
return self.make_grid()
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self._sort_config = self.sort_config()
|
||||||
|
self._data = self.query()
|
||||||
|
grid = self.grid()
|
||||||
|
return grids.util.render_grid(grid)
|
||||||
|
|
||||||
|
|
||||||
|
class PagedAlchemyGridView(SortableAlchemyGridView):
|
||||||
|
|
||||||
|
full = True
|
||||||
|
|
||||||
|
def make_pager(self):
|
||||||
|
config = self._sort_config
|
||||||
|
query = self.query()
|
||||||
|
return paginate.Page(
|
||||||
|
query, item_count=query.count(),
|
||||||
|
items_per_page=int(config['per_page']),
|
||||||
|
page=int(config['page']),
|
||||||
|
url=paginate.PageURL_WebOb(self.request))
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self._sort_config = self.sort_config()
|
||||||
|
self._data = self.make_pager()
|
||||||
|
grid = self.grid()
|
||||||
|
grid.pager = self._data
|
||||||
|
return grids.util.render_grid(grid)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchableAlchemyGridView(PagedAlchemyGridView):
|
||||||
|
|
||||||
|
def filter_exact(self, field):
|
||||||
|
return grids.search.filter_exact(field)
|
||||||
|
|
||||||
|
def filter_ilike(self, field):
|
||||||
|
return grids.search.filter_ilike(field)
|
||||||
|
|
||||||
|
def make_filter_map(self, **kwargs):
|
||||||
|
return grids.search.get_filter_map(self.mapped_class, **kwargs)
|
||||||
|
|
||||||
|
def filter_map(self):
|
||||||
|
return self.make_filter_map()
|
||||||
|
|
||||||
|
def make_filter_config(self, **kwargs):
|
||||||
|
return grids.search.get_filter_config(
|
||||||
|
self.config_prefix, self.request, self.filter_map(), **kwargs)
|
||||||
|
|
||||||
|
def filter_config(self):
|
||||||
|
return self.make_filter_config()
|
||||||
|
|
||||||
|
def make_search_form(self):
|
||||||
|
return grids.search.get_search_form(
|
||||||
|
self.request, self.filter_map(), self._filter_config)
|
||||||
|
|
||||||
|
def search_form(self):
|
||||||
|
return self.make_search_form()
|
||||||
|
|
||||||
|
def make_query(self, session=Session):
|
||||||
|
join_map = self.join_map()
|
||||||
|
query = session.query(self.mapped_class)
|
||||||
|
query = grids.search.filter_query(
|
||||||
|
query, self._filter_config, self.filter_map(), join_map)
|
||||||
|
if hasattr(self, '_sort_config'):
|
||||||
|
self._sort_config['joins'] = self._filter_config['joins']
|
||||||
|
query = grids.util.sort_query(
|
||||||
|
query, self._sort_config, self.sort_map(), join_map)
|
||||||
|
return query
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self._filter_config = self.filter_config()
|
||||||
|
search = self.search_form()
|
||||||
|
self._sort_config = self.sort_config()
|
||||||
|
self._data = self.make_pager()
|
||||||
|
grid = self.grid()
|
||||||
|
grid.pager = self._data
|
||||||
|
kwargs = self.render_kwargs()
|
||||||
|
return grids.util.render_grid(grid, search, **kwargs)
|
70
rattail/pyramid/views/grids/core.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.grids.core`` -- Core Grid View
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rattail.pyramid.views.core import View
|
||||||
|
from rattail.pyramid import grids
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['GridView']
|
||||||
|
|
||||||
|
|
||||||
|
class GridView(View):
|
||||||
|
|
||||||
|
route_name = None
|
||||||
|
route_url = None
|
||||||
|
renderer = None
|
||||||
|
permission = None
|
||||||
|
|
||||||
|
full = False
|
||||||
|
checkboxes = False
|
||||||
|
clickable = False
|
||||||
|
deletable = False
|
||||||
|
|
||||||
|
partial_only = False
|
||||||
|
|
||||||
|
def update_grid_kwargs(self, kwargs):
|
||||||
|
kwargs.setdefault('full', self.full)
|
||||||
|
kwargs.setdefault('checkboxes', self.checkboxes)
|
||||||
|
kwargs.setdefault('clickable', self.clickable)
|
||||||
|
kwargs.setdefault('deletable', self.deletable)
|
||||||
|
kwargs.setdefault('partial_only', self.partial_only)
|
||||||
|
|
||||||
|
def make_grid(self, **kwargs):
|
||||||
|
self.update_grid_kwargs(kwargs)
|
||||||
|
return grids.Grid(self.request, **kwargs)
|
||||||
|
|
||||||
|
def grid(self):
|
||||||
|
return self.make_grid()
|
||||||
|
|
||||||
|
def render_kwargs(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
grid = self.grid()
|
||||||
|
kwargs = self.render_kwargs()
|
||||||
|
return grids.util.render_grid(grid, **kwargs)
|
|
@ -32,17 +32,16 @@ import formalchemy
|
||||||
|
|
||||||
from webhelpers.html import HTML
|
from webhelpers.html import HTML
|
||||||
|
|
||||||
from edbob.pyramid import Session
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from rattail.pyramid import Session
|
||||||
from edbob.pyramid.grids.search import BooleanSearchFilter
|
from rattail.db.model import LabelProfile
|
||||||
from edbob.pyramid.forms import StrippingFieldRenderer
|
from rattail.pyramid.grids.search import BooleanSearchFilter
|
||||||
|
from rattail.pyramid.forms import StrippingFieldRenderer
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class ProfilesGrid(SearchableAlchemyGridView):
|
class ProfilesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.LabelProfile
|
mapped_class = LabelProfile
|
||||||
config_prefix = 'label_profiles'
|
config_prefix = 'label_profiles'
|
||||||
sort = 'ordinal'
|
sort = 'ordinal'
|
||||||
|
|
||||||
|
@ -82,7 +81,7 @@ class ProfilesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class ProfileCrud(CrudView):
|
class ProfileCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.LabelProfile
|
mapped_class = LabelProfile
|
||||||
home_route = 'label_profiles'
|
home_route = 'label_profiles'
|
||||||
pretty_name = "Label Profile"
|
pretty_name = "Label Profile"
|
||||||
update_cancel_route = 'label_profile.read'
|
update_cancel_route = 'label_profile.read'
|
||||||
|
@ -134,7 +133,7 @@ class ProfileCrud(CrudView):
|
||||||
|
|
||||||
def printer_settings(request):
|
def printer_settings(request):
|
||||||
uuid = request.matchdict['uuid']
|
uuid = request.matchdict['uuid']
|
||||||
profile = Session.query(rattail.LabelProfile).get(uuid) if uuid else None
|
profile = Session.query(LabelProfile).get(uuid) if uuid else None
|
||||||
if not profile:
|
if not profile:
|
||||||
return HTTPFound(location=request.route_url('label_profiles'))
|
return HTTPFound(location=request.route_url('label_profiles'))
|
||||||
|
|
||||||
|
|
118
rattail/pyramid/views/people.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.people`` -- Person Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from rattail.db.model import Person, PersonPhoneNumber, PersonEmailAddress
|
||||||
|
|
||||||
|
|
||||||
|
class PeopleGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
|
mapped_class = Person
|
||||||
|
config_prefix = 'people'
|
||||||
|
sort = 'first_name'
|
||||||
|
|
||||||
|
def join_map(self):
|
||||||
|
return {
|
||||||
|
'email':
|
||||||
|
lambda q: q.outerjoin(PersonEmailAddress, and_(
|
||||||
|
PersonEmailAddress.parent_uuid == Person.uuid,
|
||||||
|
PersonEmailAddress.preference == 1)),
|
||||||
|
'phone':
|
||||||
|
lambda q: q.outerjoin(PersonPhoneNumber, and_(
|
||||||
|
PersonPhoneNumber.parent_uuid == Person.uuid,
|
||||||
|
PersonPhoneNumber.preference == 1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
def filter_map(self):
|
||||||
|
return self.make_filter_map(
|
||||||
|
ilike=['first_name', 'last_name'],
|
||||||
|
email=self.filter_ilike(PersonEmailAddress.address),
|
||||||
|
phone=self.filter_ilike(PersonPhoneNumber.number))
|
||||||
|
|
||||||
|
def filter_config(self):
|
||||||
|
return self.make_filter_config(
|
||||||
|
include_filter_first_name=True,
|
||||||
|
filter_type_first_name='lk',
|
||||||
|
include_filter_last_name=True,
|
||||||
|
filter_type_last_name='lk',
|
||||||
|
filter_label_phone="Phone Number",
|
||||||
|
filter_label_email="Email Address")
|
||||||
|
|
||||||
|
def sort_map(self):
|
||||||
|
return self.make_sort_map(
|
||||||
|
'first_name', 'last_name',
|
||||||
|
email=self.sorter(PersonEmailAddress.address),
|
||||||
|
phone=self.sorter(PersonPhoneNumber.number))
|
||||||
|
|
||||||
|
def grid(self):
|
||||||
|
g = self.make_grid()
|
||||||
|
g.configure(
|
||||||
|
include=[
|
||||||
|
g.first_name,
|
||||||
|
g.last_name,
|
||||||
|
g.phone.label("Phone Number"),
|
||||||
|
g.email.label("Email Address"),
|
||||||
|
],
|
||||||
|
readonly=True)
|
||||||
|
g.clickable = True
|
||||||
|
g.click_route_name = 'person.read'
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
class PersonCrud(CrudView):
|
||||||
|
|
||||||
|
mapped_class = Person
|
||||||
|
home_route = 'people'
|
||||||
|
|
||||||
|
def fieldset(self, model):
|
||||||
|
fs = self.make_fieldset(model)
|
||||||
|
fs.configure(
|
||||||
|
include=[
|
||||||
|
fs.first_name,
|
||||||
|
fs.last_name,
|
||||||
|
fs.phone.label("Phone Number"),
|
||||||
|
fs.email.label("Email Address"),
|
||||||
|
])
|
||||||
|
return fs
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
|
||||||
|
config.add_route('people', '/people')
|
||||||
|
config.add_view(PeopleGrid,
|
||||||
|
route_name='people',
|
||||||
|
renderer='/people/index.mako',
|
||||||
|
permission='people.list')
|
||||||
|
|
||||||
|
config.add_route('person.read', '/people/{uuid}')
|
||||||
|
config.add_view(PersonCrud, attr='read',
|
||||||
|
route_name='person.read',
|
||||||
|
renderer='/people/crud.mako',
|
||||||
|
permission='people.read')
|
|
@ -36,22 +36,23 @@ from webhelpers.html.tags import link_to
|
||||||
from pyramid.httpexceptions import HTTPFound
|
from pyramid.httpexceptions import HTTPFound
|
||||||
from pyramid.renderers import render_to_response
|
from pyramid.renderers import render_to_response
|
||||||
|
|
||||||
import edbob
|
|
||||||
from edbob.pyramid import Session
|
|
||||||
from edbob.pyramid.progress import SessionProgress
|
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
import rattail.labels
|
|
||||||
from rattail import sil
|
from rattail import sil
|
||||||
from rattail import batches
|
from rattail import batches
|
||||||
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.model import (
|
||||||
|
Department, Subdepartment, Brand, Vendor,
|
||||||
|
Product, ProductCost, ProductPrice, LabelProfile)
|
||||||
|
from rattail.pyramid.forms.formalchemy.renderers import (
|
||||||
|
GPCFieldRenderer, PriceFieldRenderer)
|
||||||
|
from rattail.pyramid.progress import SessionProgress
|
||||||
from rattail.exceptions import LabelPrintingError
|
from rattail.exceptions import LabelPrintingError
|
||||||
from rattail.pyramid.forms import GPCFieldRenderer, PriceFieldRenderer
|
|
||||||
|
|
||||||
|
|
||||||
class ProductsGrid(SearchableAlchemyGridView):
|
class ProductsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Product
|
mapped_class = Product
|
||||||
config_prefix = 'products'
|
config_prefix = 'products'
|
||||||
sort = 'description'
|
sort = 'description'
|
||||||
|
|
||||||
|
@ -59,29 +60,29 @@ class ProductsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
def join_vendor(q):
|
def join_vendor(q):
|
||||||
q = q.outerjoin(
|
q = q.outerjoin(
|
||||||
rattail.ProductCost,
|
ProductCost,
|
||||||
and_(
|
and_(
|
||||||
rattail.ProductCost.product_uuid == rattail.Product.uuid,
|
ProductCost.product_uuid == Product.uuid,
|
||||||
rattail.ProductCost.preference == 1,
|
ProductCost.preference == 1,
|
||||||
))
|
))
|
||||||
q = q.outerjoin(rattail.Vendor)
|
q = q.outerjoin(Vendor)
|
||||||
return q
|
return q
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'brand':
|
'brand':
|
||||||
lambda q: q.outerjoin(rattail.Brand),
|
lambda q: q.outerjoin(Brand),
|
||||||
'department':
|
'department':
|
||||||
lambda q: q.outerjoin(rattail.Department,
|
lambda q: q.outerjoin(Department,
|
||||||
rattail.Department.uuid == rattail.Product.department_uuid),
|
Department.uuid == Product.department_uuid),
|
||||||
'subdepartment':
|
'subdepartment':
|
||||||
lambda q: q.outerjoin(rattail.Subdepartment,
|
lambda q: q.outerjoin(Subdepartment,
|
||||||
rattail.Subdepartment.uuid == rattail.Product.subdepartment_uuid),
|
Subdepartment.uuid == Product.subdepartment_uuid),
|
||||||
'regular_price':
|
'regular_price':
|
||||||
lambda q: q.outerjoin(rattail.ProductPrice,
|
lambda q: q.outerjoin(ProductPrice,
|
||||||
rattail.ProductPrice.uuid == rattail.Product.regular_price_uuid),
|
ProductPrice.uuid == Product.regular_price_uuid),
|
||||||
'current_price':
|
'current_price':
|
||||||
lambda q: q.outerjoin(rattail.ProductPrice,
|
lambda q: q.outerjoin(ProductPrice,
|
||||||
rattail.ProductPrice.uuid == rattail.Product.current_price_uuid),
|
ProductPrice.uuid == Product.current_price_uuid),
|
||||||
'vendor':
|
'vendor':
|
||||||
join_vendor,
|
join_vendor,
|
||||||
}
|
}
|
||||||
|
@ -96,7 +97,7 @@ class ProductsGrid(SearchableAlchemyGridView):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return q
|
return q
|
||||||
else:
|
else:
|
||||||
return q.filter(rattail.Product.upc == v) if v else q
|
return q.filter(Product.upc == v) if v else q
|
||||||
|
|
||||||
def filter_not(q, v):
|
def filter_not(q, v):
|
||||||
try:
|
try:
|
||||||
|
@ -104,17 +105,17 @@ class ProductsGrid(SearchableAlchemyGridView):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return q
|
return q
|
||||||
else:
|
else:
|
||||||
return q.filter(rattail.Product.upc != v) if v else q
|
return q.filter(Product.upc != v) if v else q
|
||||||
|
|
||||||
return {'is': filter_is, 'nt': filter_not}
|
return {'is': filter_is, 'nt': filter_not}
|
||||||
|
|
||||||
return self.make_filter_map(
|
return self.make_filter_map(
|
||||||
ilike=['description', 'size'],
|
ilike=['description', 'size'],
|
||||||
upc=filter_upc(),
|
upc=filter_upc(),
|
||||||
brand=self.filter_ilike(rattail.Brand.name),
|
brand=self.filter_ilike(Brand.name),
|
||||||
department=self.filter_ilike(rattail.Department.name),
|
department=self.filter_ilike(Department.name),
|
||||||
subdepartment=self.filter_ilike(rattail.Subdepartment.name),
|
subdepartment=self.filter_ilike(Subdepartment.name),
|
||||||
vendor=self.filter_ilike(rattail.Vendor.name))
|
vendor=self.filter_ilike(Vendor.name))
|
||||||
|
|
||||||
def filter_config(self):
|
def filter_config(self):
|
||||||
return self.make_filter_config(
|
return self.make_filter_config(
|
||||||
|
@ -133,21 +134,21 @@ class ProductsGrid(SearchableAlchemyGridView):
|
||||||
def sort_map(self):
|
def sort_map(self):
|
||||||
return self.make_sort_map(
|
return self.make_sort_map(
|
||||||
'upc', 'description', 'size',
|
'upc', 'description', 'size',
|
||||||
brand=self.sorter(rattail.Brand.name),
|
brand=self.sorter(Brand.name),
|
||||||
department=self.sorter(rattail.Department.name),
|
department=self.sorter(Department.name),
|
||||||
subdepartment=self.sorter(rattail.Subdepartment.name),
|
subdepartment=self.sorter(Subdepartment.name),
|
||||||
regular_price=self.sorter(rattail.ProductPrice.price),
|
regular_price=self.sorter(ProductPrice.price),
|
||||||
current_price=self.sorter(rattail.ProductPrice.price),
|
current_price=self.sorter(ProductPrice.price),
|
||||||
vendor=self.sorter(rattail.Vendor.name))
|
vendor=self.sorter(Vendor.name))
|
||||||
|
|
||||||
def query(self):
|
def query(self):
|
||||||
q = self.make_query()
|
q = self.make_query()
|
||||||
q = q.options(joinedload(rattail.Product.brand))
|
q = q.options(joinedload(Product.brand))
|
||||||
q = q.options(joinedload(rattail.Product.department))
|
q = q.options(joinedload(Product.department))
|
||||||
q = q.options(joinedload(rattail.Product.subdepartment))
|
q = q.options(joinedload(Product.subdepartment))
|
||||||
q = q.options(joinedload(rattail.Product.regular_price))
|
q = q.options(joinedload(Product.regular_price))
|
||||||
q = q.options(joinedload(rattail.Product.current_price))
|
q = q.options(joinedload(Product.current_price))
|
||||||
q = q.options(joinedload(rattail.Product.vendor))
|
q = q.options(joinedload(Product.vendor))
|
||||||
return q
|
return q
|
||||||
|
|
||||||
def grid(self):
|
def grid(self):
|
||||||
|
@ -178,7 +179,7 @@ class ProductsGrid(SearchableAlchemyGridView):
|
||||||
g.deletable = True
|
g.deletable = True
|
||||||
g.delete_route_name = 'product.delete'
|
g.delete_route_name = 'product.delete'
|
||||||
|
|
||||||
q = Session.query(rattail.LabelProfile)
|
q = Session.query(LabelProfile)
|
||||||
if q.count():
|
if q.count():
|
||||||
def labels(row):
|
def labels(row):
|
||||||
return link_to("Print", '#', class_='print-label')
|
return link_to("Print", '#', class_='print-label')
|
||||||
|
@ -187,15 +188,15 @@ class ProductsGrid(SearchableAlchemyGridView):
|
||||||
return g
|
return g
|
||||||
|
|
||||||
def render_kwargs(self):
|
def render_kwargs(self):
|
||||||
q = Session.query(rattail.LabelProfile)
|
q = Session.query(LabelProfile)
|
||||||
q = q.filter(rattail.LabelProfile.visible == True)
|
q = q.filter(LabelProfile.visible == True)
|
||||||
q = q.order_by(rattail.LabelProfile.ordinal)
|
q = q.order_by(LabelProfile.ordinal)
|
||||||
return {'label_profiles': q.all()}
|
return {'label_profiles': q.all()}
|
||||||
|
|
||||||
|
|
||||||
class ProductCrud(CrudView):
|
class ProductCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Product
|
mapped_class = Product
|
||||||
home_route = 'products'
|
home_route = 'products'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
@ -222,12 +223,12 @@ class ProductCrud(CrudView):
|
||||||
|
|
||||||
def print_labels(request):
|
def print_labels(request):
|
||||||
profile = request.params.get('profile')
|
profile = request.params.get('profile')
|
||||||
profile = Session.query(rattail.LabelProfile).get(profile) if profile else None
|
profile = Session.query(LabelProfile).get(profile) if profile else None
|
||||||
if not profile:
|
if not profile:
|
||||||
return {'error': "Label profile not found"}
|
return {'error': "Label profile not found"}
|
||||||
|
|
||||||
product = request.params.get('product')
|
product = request.params.get('product')
|
||||||
product = Session.query(rattail.Product).get(product) if product else None
|
product = Session.query(Product).get(product) if product else None
|
||||||
if not product:
|
if not product:
|
||||||
return {'error': "Product not found"}
|
return {'error': "Product not found"}
|
||||||
|
|
||||||
|
@ -250,7 +251,8 @@ def print_labels(request):
|
||||||
class CreateProductsBatch(ProductsGrid):
|
class CreateProductsBatch(ProductsGrid):
|
||||||
|
|
||||||
def make_batch(self, provider, progress):
|
def make_batch(self, provider, progress):
|
||||||
session = edbob.Session()
|
from rattail.db import Session
|
||||||
|
session = Session()
|
||||||
|
|
||||||
self._filter_config = self.filter_config()
|
self._filter_config = self.filter_config()
|
||||||
self._sort_config = self.sort_config()
|
self._sort_config = self.sort_config()
|
||||||
|
@ -301,7 +303,7 @@ class CreateProductsBatch(ProductsGrid):
|
||||||
}
|
}
|
||||||
return render_to_response('/progress.mako', kwargs, request=self.request)
|
return render_to_response('/progress.mako', kwargs, request=self.request)
|
||||||
|
|
||||||
enabled = edbob.config.get('rattail.pyramid', 'batches.providers')
|
enabled = rattail.config.get('rattail.pyramid', 'batches.providers')
|
||||||
if enabled:
|
if enabled:
|
||||||
enabled = enabled.split()
|
enabled = enabled.split()
|
||||||
|
|
||||||
|
|
62
rattail/pyramid/views/progress.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.progress`` -- Progress Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rattail.pyramid.progress import get_progress_session
|
||||||
|
|
||||||
|
|
||||||
|
def progress(request):
|
||||||
|
key = request.matchdict['key']
|
||||||
|
session = get_progress_session(request.session, key)
|
||||||
|
if session.get('complete'):
|
||||||
|
request.session.flash(session.get('success_msg', "The process has completed successfully."))
|
||||||
|
elif session.get('error'):
|
||||||
|
request.session.flash(session.get('error_msg', "An unspecified error occurred."), 'error')
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def cancel(request):
|
||||||
|
key = request.matchdict['key']
|
||||||
|
session = get_progress_session(request.session, key)
|
||||||
|
session.clear()
|
||||||
|
session['canceled'] = True
|
||||||
|
session.save()
|
||||||
|
msg = request.params.get('cancel_msg', "The operation was canceled.")
|
||||||
|
request.session.flash(msg)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
config.add_route('progress', '/progress/{key}')
|
||||||
|
config.add_view(progress,
|
||||||
|
route_name='progress',
|
||||||
|
renderer='json')
|
||||||
|
|
||||||
|
config.add_route('progress.cancel', '/progress/{key}/cancel')
|
||||||
|
config.add_view(cancel,
|
||||||
|
route_name='progress.cancel',
|
||||||
|
renderer='json')
|
|
@ -12,11 +12,12 @@ from mako.template import Template
|
||||||
|
|
||||||
from pyramid.response import Response
|
from pyramid.response import Response
|
||||||
|
|
||||||
import edbob
|
|
||||||
from edbob.pyramid import Session
|
|
||||||
from edbob.files import resource_path
|
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.model import Department, Brand, Vendor, Product, ProductCost
|
||||||
|
from rattail.files import resource_path
|
||||||
|
from rattail.time import local_time
|
||||||
|
from rattail.enum import UNIT_OF_MEASURE_POUND
|
||||||
|
|
||||||
|
|
||||||
plu_upc_pattern = re.compile(r'^000000000(\d{5})$')
|
plu_upc_pattern = re.compile(r'^000000000(\d{5})$')
|
||||||
|
@ -38,7 +39,7 @@ def inventory_report(request):
|
||||||
This is the "Inventory Worksheet" report.
|
This is the "Inventory Worksheet" report.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
departments = Session.query(rattail.Department)
|
departments = Session.query(Department)
|
||||||
|
|
||||||
if request.params.get('department'):
|
if request.params.get('department'):
|
||||||
department = departments.get(request.params['department'])
|
department = departments.get(request.params['department'])
|
||||||
|
@ -50,7 +51,7 @@ def inventory_report(request):
|
||||||
response.text = body
|
response.text = body
|
||||||
return response
|
return response
|
||||||
|
|
||||||
departments = departments.order_by(rattail.Department.name)
|
departments = departments.order_by(Department.name)
|
||||||
departments = departments.all()
|
departments = departments.all()
|
||||||
return{'departments': departments}
|
return{'departments': departments}
|
||||||
|
|
||||||
|
@ -61,15 +62,15 @@ def write_inventory_worksheet(request, department):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_products(subdepartment):
|
def get_products(subdepartment):
|
||||||
q = Session.query(rattail.Product)
|
q = Session.query(Product)
|
||||||
q = q.outerjoin(rattail.Brand)
|
q = q.outerjoin(Brand)
|
||||||
q = q.filter(rattail.Product.subdepartment == subdepartment)
|
q = q.filter(Product.subdepartment == subdepartment)
|
||||||
if request.params.get('weighted-only'):
|
if request.params.get('weighted-only'):
|
||||||
q = q.filter(rattail.Product.unit_of_measure == rattail.UNIT_OF_MEASURE_POUND)
|
q = q.filter(Product.unit_of_measure == UNIT_OF_MEASURE_POUND)
|
||||||
q = q.order_by(rattail.Brand.name, rattail.Product.description)
|
q = q.order_by(Brand.name, Product.description)
|
||||||
return q.all()
|
return q.all()
|
||||||
|
|
||||||
now = edbob.local_time()
|
now = local_time()
|
||||||
data = dict(
|
data = dict(
|
||||||
date=now.strftime('%a %d %b %Y'),
|
date=now.strftime('%a %d %b %Y'),
|
||||||
time=now.strftime('%I:%M %p'),
|
time=now.strftime('%I:%M %p'),
|
||||||
|
@ -89,13 +90,13 @@ def ordering_report(request):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if request.params.get('vendor'):
|
if request.params.get('vendor'):
|
||||||
vendor = Session.query(rattail.Vendor).get(request.params['vendor'])
|
vendor = Session.query(Vendor).get(request.params['vendor'])
|
||||||
if vendor:
|
if vendor:
|
||||||
departments = []
|
departments = []
|
||||||
uuids = request.params.get('departments')
|
uuids = request.params.get('departments')
|
||||||
if uuids:
|
if uuids:
|
||||||
for uuid in uuids.split(','):
|
for uuid in uuids.split(','):
|
||||||
dept = Session.query(rattail.Department).get(uuid)
|
dept = Session.query(Department).get(uuid)
|
||||||
if dept:
|
if dept:
|
||||||
departments.append(dept)
|
departments.append(dept)
|
||||||
body = write_ordering_worksheet(vendor, departments)
|
body = write_ordering_worksheet(vendor, departments)
|
||||||
|
@ -112,10 +113,10 @@ def write_ordering_worksheet(vendor, departments):
|
||||||
Rendering engine for the ordering worksheet report.
|
Rendering engine for the ordering worksheet report.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
q = Session.query(rattail.ProductCost)
|
q = Session.query(ProductCost)
|
||||||
q = q.join(rattail.Product)
|
q = q.join(Product)
|
||||||
q = q.filter(rattail.ProductCost.vendor == vendor)
|
q = q.filter(ProductCost.vendor == vendor)
|
||||||
q = q.filter(rattail.Product.department_uuid.in_([x.uuid for x in departments]))
|
q = q.filter(Product.department_uuid.in_([x.uuid for x in departments]))
|
||||||
|
|
||||||
costs = {}
|
costs = {}
|
||||||
for cost in q:
|
for cost in q:
|
||||||
|
@ -125,7 +126,7 @@ def write_ordering_worksheet(vendor, departments):
|
||||||
costs[dept].setdefault(subdept, [])
|
costs[dept].setdefault(subdept, [])
|
||||||
costs[dept][subdept].append(cost)
|
costs[dept][subdept].append(cost)
|
||||||
|
|
||||||
now = edbob.local_time()
|
now = local_time()
|
||||||
data = dict(
|
data = dict(
|
||||||
vendor=vendor,
|
vendor=vendor,
|
||||||
costs=costs,
|
costs=costs,
|
||||||
|
|
217
rattail/pyramid/views/roles.py
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.roles`` -- Role Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
|
import formalchemy
|
||||||
|
from webhelpers.html import tags
|
||||||
|
from webhelpers.html.builder import HTML
|
||||||
|
|
||||||
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.model import Role
|
||||||
|
from rattail.db.auth import has_permission, administrator_role, guest_role
|
||||||
|
|
||||||
|
|
||||||
|
default_permissions = [
|
||||||
|
|
||||||
|
("People", [
|
||||||
|
('people.list', "List People"),
|
||||||
|
('people.read', "View Person"),
|
||||||
|
('people.create', "Create Person"),
|
||||||
|
('people.update', "Edit Person"),
|
||||||
|
('people.delete', "Delete Person"),
|
||||||
|
]),
|
||||||
|
|
||||||
|
("Roles", [
|
||||||
|
('roles.list', "List Roles"),
|
||||||
|
('roles.read', "View Role"),
|
||||||
|
('roles.create', "Create Role"),
|
||||||
|
('roles.update', "Edit Role"),
|
||||||
|
('roles.delete', "Delete Role"),
|
||||||
|
]),
|
||||||
|
|
||||||
|
("Users", [
|
||||||
|
('users.list', "List Users"),
|
||||||
|
('users.read', "View User"),
|
||||||
|
('users.create', "Create User"),
|
||||||
|
('users.update', "Edit User"),
|
||||||
|
('users.delete', "Delete User"),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RolesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
|
mapped_class = Role
|
||||||
|
config_prefix = 'roles'
|
||||||
|
sort = 'name'
|
||||||
|
|
||||||
|
def filter_map(self):
|
||||||
|
return self.make_filter_map(ilike=['name'])
|
||||||
|
|
||||||
|
def filter_config(self):
|
||||||
|
return self.make_filter_config(
|
||||||
|
include_filter_name=True,
|
||||||
|
filter_type_name='lk')
|
||||||
|
|
||||||
|
def sort_map(self):
|
||||||
|
return self.make_sort_map('name')
|
||||||
|
|
||||||
|
def grid(self):
|
||||||
|
g = self.make_grid()
|
||||||
|
g.configure(
|
||||||
|
include=[
|
||||||
|
g.name,
|
||||||
|
],
|
||||||
|
readonly=True)
|
||||||
|
if self.request.has_perm('roles.read'):
|
||||||
|
g.clickable = True
|
||||||
|
g.click_route_name = 'role.read'
|
||||||
|
if self.request.has_perm('roles.update'):
|
||||||
|
g.editable = True
|
||||||
|
g.edit_route_name = 'role.update'
|
||||||
|
if self.request.has_perm('roles.delete'):
|
||||||
|
g.deletable = True
|
||||||
|
g.delete_route_name = 'role.delete'
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsField(formalchemy.Field):
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
if not self.is_readonly():
|
||||||
|
role = self.model
|
||||||
|
role.permissions = self.renderer.deserialize()
|
||||||
|
|
||||||
|
|
||||||
|
def PermissionsFieldRenderer(permissions, *args, **kwargs):
|
||||||
|
|
||||||
|
perms = permissions
|
||||||
|
|
||||||
|
class PermissionsFieldRenderer(formalchemy.FieldRenderer):
|
||||||
|
|
||||||
|
permissions = perms
|
||||||
|
|
||||||
|
def deserialize(self):
|
||||||
|
perms = []
|
||||||
|
i = len(self.name) + 1
|
||||||
|
for key in self.params:
|
||||||
|
if key.startswith(self.name):
|
||||||
|
perms.append(key[i:])
|
||||||
|
return perms
|
||||||
|
|
||||||
|
def _render(self, readonly=False, **kwargs):
|
||||||
|
role = self.field.model
|
||||||
|
admin = administrator_role(Session())
|
||||||
|
if role is admin:
|
||||||
|
html = HTML.tag('p', c="This is the administrative role; "
|
||||||
|
"it has full access to the entire system.")
|
||||||
|
if not readonly:
|
||||||
|
html += tags.hidden(self.name, value='') # ugly hack..or good idea?
|
||||||
|
else:
|
||||||
|
html = ''
|
||||||
|
for group, perms in self.permissions:
|
||||||
|
inner = HTML.tag('p', c=group)
|
||||||
|
for perm, title in perms:
|
||||||
|
checked = has_permission(
|
||||||
|
role, perm, include_guest=False, session=Session())
|
||||||
|
if readonly:
|
||||||
|
span = HTML.tag('span', c="[X]" if checked else "[ ]")
|
||||||
|
inner += HTML.tag('p', class_='perm', c=span + ' ' + title)
|
||||||
|
else:
|
||||||
|
inner += tags.checkbox(self.name + '-' + perm,
|
||||||
|
checked=checked, label=title)
|
||||||
|
html += HTML.tag('div', class_='group', c=inner)
|
||||||
|
return html
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
return self._render(**kwargs)
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
return self._render(readonly=True, **kwargs)
|
||||||
|
|
||||||
|
return PermissionsFieldRenderer
|
||||||
|
|
||||||
|
|
||||||
|
class RoleCrud(CrudView):
|
||||||
|
|
||||||
|
mapped_class = Role
|
||||||
|
home_route = 'roles'
|
||||||
|
permissions = default_permissions
|
||||||
|
|
||||||
|
def fieldset(self, role):
|
||||||
|
fs = self.make_fieldset(role)
|
||||||
|
fs.append(PermissionsField(
|
||||||
|
'permissions',
|
||||||
|
renderer=PermissionsFieldRenderer(self.permissions)))
|
||||||
|
fs.configure(
|
||||||
|
include=[
|
||||||
|
fs.name,
|
||||||
|
fs.permissions,
|
||||||
|
])
|
||||||
|
return fs
|
||||||
|
|
||||||
|
def pre_delete(self, model):
|
||||||
|
admin = administrator_role(Session())
|
||||||
|
guest = guest_role(Session())
|
||||||
|
if model in (admin, guest):
|
||||||
|
self.request.session.flash("You may not delete the %s role." % str(model), 'error')
|
||||||
|
return HTTPFound(location=self.request.get_referrer())
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
|
||||||
|
config.add_route('roles', '/roles')
|
||||||
|
config.add_view(RolesGrid, route_name='roles',
|
||||||
|
renderer='/roles/index.mako',
|
||||||
|
permission='roles.list')
|
||||||
|
|
||||||
|
settings = config.get_settings()
|
||||||
|
perms = settings.get('rattail.permissions')
|
||||||
|
if perms:
|
||||||
|
RoleCrud.permissions = perms
|
||||||
|
|
||||||
|
config.add_route('role.create', '/roles/new')
|
||||||
|
config.add_view(RoleCrud, attr='create', route_name='role.create',
|
||||||
|
renderer='/roles/crud.mako',
|
||||||
|
permission='roles.create')
|
||||||
|
|
||||||
|
config.add_route('role.read', '/roles/{uuid}')
|
||||||
|
config.add_view(RoleCrud, attr='read', route_name='role.read',
|
||||||
|
renderer='/roles/crud.mako',
|
||||||
|
permission='roles.read')
|
||||||
|
|
||||||
|
config.add_route('role.update', '/roles/{uuid}/edit')
|
||||||
|
config.add_view(RoleCrud, attr='update', route_name='role.update',
|
||||||
|
renderer='/roles/crud.mako',
|
||||||
|
permission='roles.update')
|
||||||
|
|
||||||
|
config.add_route('role.delete', '/roles/{uuid}/delete')
|
||||||
|
config.add_view(RoleCrud, attr='delete', route_name='role.delete',
|
||||||
|
permission='roles.delete')
|
|
@ -28,35 +28,34 @@
|
||||||
|
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
|
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from rattail.db.model import Store, StorePhoneNumber, StoreEmailAddress
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class StoresGrid(SearchableAlchemyGridView):
|
class StoresGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Store
|
mapped_class = Store
|
||||||
config_prefix = 'stores'
|
config_prefix = 'stores'
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
|
|
||||||
def join_map(self):
|
def join_map(self):
|
||||||
return {
|
return {
|
||||||
'email':
|
'email':
|
||||||
lambda q: q.outerjoin(rattail.StoreEmailAddress, and_(
|
lambda q: q.outerjoin(StoreEmailAddress, and_(
|
||||||
rattail.StoreEmailAddress.parent_uuid == rattail.Store.uuid,
|
StoreEmailAddress.parent_uuid == Store.uuid,
|
||||||
rattail.StoreEmailAddress.preference == 1)),
|
StoreEmailAddress.preference == 1)),
|
||||||
'phone':
|
'phone':
|
||||||
lambda q: q.outerjoin(rattail.StorePhoneNumber, and_(
|
lambda q: q.outerjoin(StorePhoneNumber, and_(
|
||||||
rattail.StorePhoneNumber.parent_uuid == rattail.Store.uuid,
|
StorePhoneNumber.parent_uuid == Store.uuid,
|
||||||
rattail.StorePhoneNumber.preference == 1)),
|
StorePhoneNumber.preference == 1)),
|
||||||
}
|
}
|
||||||
|
|
||||||
def filter_map(self):
|
def filter_map(self):
|
||||||
return self.make_filter_map(
|
return self.make_filter_map(
|
||||||
exact=['id'],
|
exact=['id'],
|
||||||
ilike=['name'],
|
ilike=['name'],
|
||||||
email=self.filter_ilike(rattail.StoreEmailAddress.address),
|
email=self.filter_ilike(StoreEmailAddress.address),
|
||||||
phone=self.filter_ilike(rattail.StorePhoneNumber.number))
|
phone=self.filter_ilike(StorePhoneNumber.number))
|
||||||
|
|
||||||
def filter_config(self):
|
def filter_config(self):
|
||||||
return self.make_filter_config(
|
return self.make_filter_config(
|
||||||
|
@ -67,8 +66,8 @@ class StoresGrid(SearchableAlchemyGridView):
|
||||||
def sort_map(self):
|
def sort_map(self):
|
||||||
return self.make_sort_map(
|
return self.make_sort_map(
|
||||||
'id', 'name',
|
'id', 'name',
|
||||||
email=self.sorter(rattail.StoreEmailAddress.address),
|
email=self.sorter(StoreEmailAddress.address),
|
||||||
phone=self.sorter(rattail.StorePhoneNumber.number))
|
phone=self.sorter(StorePhoneNumber.number))
|
||||||
|
|
||||||
def grid(self):
|
def grid(self):
|
||||||
g = self.make_grid()
|
g = self.make_grid()
|
||||||
|
@ -93,7 +92,7 @@ class StoresGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class StoreCrud(CrudView):
|
class StoreCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Store
|
mapped_class = Store
|
||||||
home_route = 'stores'
|
home_route = 'stores'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
|
|
@ -26,14 +26,13 @@
|
||||||
``rattail.pyramid.views.subdepartments`` -- Subdepartment Views
|
``rattail.pyramid.views.subdepartments`` -- Subdepartment Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from rattail.db.model import Subdepartment
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class SubdepartmentsGrid(SearchableAlchemyGridView):
|
class SubdepartmentsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Subdepartment
|
mapped_class = Subdepartment
|
||||||
config_prefix = 'subdepartments'
|
config_prefix = 'subdepartments'
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
|
|
||||||
|
@ -71,7 +70,7 @@ class SubdepartmentsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class SubdepartmentCrud(CrudView):
|
class SubdepartmentCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Subdepartment
|
mapped_class = Subdepartment
|
||||||
home_route = 'subdepartments'
|
home_route = 'subdepartments'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
|
252
rattail/pyramid/views/users.py
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2012 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``rattail.pyramid.views.users`` -- User Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from webhelpers.html import tags
|
||||||
|
from webhelpers.html.builder import HTML
|
||||||
|
|
||||||
|
import formalchemy
|
||||||
|
from formalchemy.fields import SelectFieldRenderer
|
||||||
|
|
||||||
|
from rattail.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from rattail.pyramid import Session
|
||||||
|
from rattail.db.model import User, Person, Role
|
||||||
|
from rattail.db.auth import guest_role, set_user_password
|
||||||
|
|
||||||
|
|
||||||
|
class UsersGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
|
mapped_class = User
|
||||||
|
config_prefix = 'users'
|
||||||
|
sort = 'username'
|
||||||
|
|
||||||
|
def join_map(self):
|
||||||
|
return {
|
||||||
|
'person':
|
||||||
|
lambda q: q.outerjoin(Person),
|
||||||
|
}
|
||||||
|
|
||||||
|
def filter_map(self):
|
||||||
|
return self.make_filter_map(
|
||||||
|
ilike=['username'],
|
||||||
|
person=self.filter_ilike(Person.display_name))
|
||||||
|
|
||||||
|
def filter_config(self):
|
||||||
|
return self.make_filter_config(
|
||||||
|
include_filter_username=True,
|
||||||
|
filter_type_username='lk',
|
||||||
|
include_filter_person=True,
|
||||||
|
filter_type_person='lk')
|
||||||
|
|
||||||
|
def sort_map(self):
|
||||||
|
return self.make_sort_map(
|
||||||
|
'username',
|
||||||
|
person=self.sorter(Person.display_name))
|
||||||
|
|
||||||
|
def grid(self):
|
||||||
|
g = self.make_grid()
|
||||||
|
g.configure(
|
||||||
|
include=[
|
||||||
|
g.username,
|
||||||
|
g.person,
|
||||||
|
],
|
||||||
|
readonly=True)
|
||||||
|
if self.request.has_perm('users.read'):
|
||||||
|
g.clickable = True
|
||||||
|
g.click_route_name = 'user.read'
|
||||||
|
if self.request.has_perm('users.update'):
|
||||||
|
g.editable = True
|
||||||
|
g.edit_route_name = 'user.update'
|
||||||
|
if self.request.has_perm('users.delete'):
|
||||||
|
g.deletable = True
|
||||||
|
g.delete_route_name = 'user.delete'
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def RolesFieldRenderer(request):
|
||||||
|
|
||||||
|
class RolesFieldRenderer(SelectFieldRenderer):
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
roles = Session.query(Role)
|
||||||
|
html = ''
|
||||||
|
for uuid in self.value:
|
||||||
|
role = roles.get(uuid)
|
||||||
|
link = tags.link_to(
|
||||||
|
role.name, request.route_url('role.read', uuid=role.uuid))
|
||||||
|
html += HTML.tag('li', c=link)
|
||||||
|
html = HTML.tag('ul', c=html)
|
||||||
|
return html
|
||||||
|
|
||||||
|
return RolesFieldRenderer
|
||||||
|
|
||||||
|
|
||||||
|
class RolesField(formalchemy.Field):
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
kwargs.setdefault('value', self.get_value)
|
||||||
|
kwargs.setdefault('options', self.get_options())
|
||||||
|
kwargs.setdefault('multiple', True)
|
||||||
|
super(RolesField, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
|
def get_value(self, user):
|
||||||
|
return [x.uuid for x in user.roles]
|
||||||
|
|
||||||
|
def get_options(self):
|
||||||
|
q = Session.query(Role.name, Role.uuid)
|
||||||
|
q = q.filter(Role.uuid != guest_role(Session()).uuid)
|
||||||
|
q = q.order_by(Role.name)
|
||||||
|
return q.all()
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
if not self.is_readonly():
|
||||||
|
user = self.model
|
||||||
|
roles = Session.query(Role)
|
||||||
|
data = self.renderer.deserialize()
|
||||||
|
user.roles = [roles.get(x) for x in data]
|
||||||
|
|
||||||
|
|
||||||
|
class _ProtectedPersonRenderer(formalchemy.FieldRenderer):
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
res = str(self.person)
|
||||||
|
res += tags.hidden('User--person_uuid',
|
||||||
|
value=self.field.parent.person_uuid.value)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def ProtectedPersonRenderer(uuid):
|
||||||
|
person = Session.query(Person).get(uuid)
|
||||||
|
assert person
|
||||||
|
return type('ProtectedPersonRenderer', (_ProtectedPersonRenderer,),
|
||||||
|
{'person': person})
|
||||||
|
|
||||||
|
|
||||||
|
class _LinkedPersonRenderer(formalchemy.FieldRenderer):
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
return tags.link_to(str(self.raw_value),
|
||||||
|
self.request.route_url('person.edit', uuid=self.value))
|
||||||
|
|
||||||
|
|
||||||
|
def LinkedPersonRenderer(request):
|
||||||
|
return type('LinkedPersonRenderer', (_LinkedPersonRenderer,),
|
||||||
|
{'request': request})
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordFieldRenderer(formalchemy.PasswordFieldRenderer):
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
return tags.password(self.name, value='', maxlength=self.length, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def passwords_match(value, field):
|
||||||
|
if field.parent.confirm_password.value != value:
|
||||||
|
raise formalchemy.ValidationError("Passwords do not match")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordField(formalchemy.Field):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault('value', lambda x: x.password)
|
||||||
|
kwargs.setdefault('renderer', PasswordFieldRenderer)
|
||||||
|
kwargs.setdefault('validate', passwords_match)
|
||||||
|
super(PasswordField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
if not self.is_readonly():
|
||||||
|
password = self.renderer.deserialize()
|
||||||
|
if password:
|
||||||
|
set_user_password(self.model, password)
|
||||||
|
|
||||||
|
|
||||||
|
class UserCrud(CrudView):
|
||||||
|
|
||||||
|
mapped_class = User
|
||||||
|
home_route = 'users'
|
||||||
|
|
||||||
|
def fieldset(self, user):
|
||||||
|
fs = self.make_fieldset(user)
|
||||||
|
|
||||||
|
fs.append(PasswordField('password'))
|
||||||
|
fs.append(formalchemy.Field('confirm_password',
|
||||||
|
renderer=PasswordFieldRenderer))
|
||||||
|
fs.append(RolesField('roles',
|
||||||
|
renderer=RolesFieldRenderer(self.request)))
|
||||||
|
|
||||||
|
fs.configure(
|
||||||
|
include=[
|
||||||
|
fs.username,
|
||||||
|
fs.person,
|
||||||
|
fs.password.label("Set Password"),
|
||||||
|
fs.confirm_password,
|
||||||
|
fs.roles,
|
||||||
|
])
|
||||||
|
|
||||||
|
if self.readonly:
|
||||||
|
del fs.password
|
||||||
|
del fs.confirm_password
|
||||||
|
|
||||||
|
# if fs.edit and user.person:
|
||||||
|
if isinstance(user, User) and user.person:
|
||||||
|
fs.person.set(readonly=True,
|
||||||
|
renderer=LinkedPersonRenderer(self.request))
|
||||||
|
|
||||||
|
return fs
|
||||||
|
|
||||||
|
def validation_failed(self, fs):
|
||||||
|
if not fs.edit and fs.person_uuid.value:
|
||||||
|
fs.person.set(readonly=True,
|
||||||
|
renderer=ProtectedPersonRenderer(fs.person_uuid.value))
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
|
||||||
|
config.add_route('users', '/users')
|
||||||
|
config.add_view(UsersGrid, route_name='users',
|
||||||
|
renderer='/users/index.mako',
|
||||||
|
permission='users.list')
|
||||||
|
|
||||||
|
config.add_route('user.create', '/users/new')
|
||||||
|
config.add_view(UserCrud, attr='create', route_name='user.create',
|
||||||
|
renderer='/users/crud.mako',
|
||||||
|
permission='users.create')
|
||||||
|
|
||||||
|
config.add_route('user.read', '/users/{uuid}')
|
||||||
|
config.add_view(UserCrud, attr='read', route_name='user.read',
|
||||||
|
renderer='/users/crud.mako',
|
||||||
|
permission='users.read')
|
||||||
|
|
||||||
|
config.add_route('user.update', '/users/{uuid}/edit')
|
||||||
|
config.add_view(UserCrud, attr='update', route_name='user.update',
|
||||||
|
renderer='/users/crud.mako',
|
||||||
|
permission='users.update')
|
||||||
|
|
||||||
|
config.add_route('user.delete', '/users/{uuid}/delete')
|
||||||
|
config.add_view(UserCrud, attr='delete', route_name='user.delete',
|
||||||
|
permission='users.delete')
|
|
@ -26,15 +26,14 @@
|
||||||
``rattail.pyramid.views.vendors`` -- Vendor Views
|
``rattail.pyramid.views.vendors`` -- Vendor Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from edbob.pyramid.views import (SearchableAlchemyGridView, CrudView,
|
from rattail.pyramid.views import (
|
||||||
AutocompleteView)
|
SearchableAlchemyGridView, CrudView, AutocompleteView)
|
||||||
|
from rattail.db.model import Vendor
|
||||||
import rattail
|
|
||||||
|
|
||||||
|
|
||||||
class VendorsGrid(SearchableAlchemyGridView):
|
class VendorsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = rattail.Vendor
|
mapped_class = Vendor
|
||||||
config_prefix = 'vendors'
|
config_prefix = 'vendors'
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
|
|
||||||
|
@ -74,7 +73,7 @@ class VendorsGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class VendorCrud(CrudView):
|
class VendorCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = rattail.Vendor
|
mapped_class = Vendor
|
||||||
home_route = 'vendors'
|
home_route = 'vendors'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
@ -90,7 +89,7 @@ class VendorCrud(CrudView):
|
||||||
|
|
||||||
class VendorsAutocomplete(AutocompleteView):
|
class VendorsAutocomplete(AutocompleteView):
|
||||||
|
|
||||||
mapped_class = rattail.Vendor
|
mapped_class = Vendor
|
||||||
fieldname = 'name'
|
fieldname = 'name'
|
||||||
|
|
||||||
|
|
||||||
|
|
42
runtests.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import coverage
|
||||||
|
|
||||||
|
|
||||||
|
def runtests():
|
||||||
|
"""
|
||||||
|
Run all tests for the ``rattail.pyramid`` package.
|
||||||
|
|
||||||
|
Unfortunately we can't just run ``nosetests --with-cover``, because there
|
||||||
|
doesn't seem to be any way to prevent the ``pkg_resources`` API from
|
||||||
|
initializing. When this happens, ``rattail`` is imported because of its
|
||||||
|
being a namespace package. But...it gets imported before ``coverage``
|
||||||
|
starts, which means that we can't get a true measure of coverage.
|
||||||
|
|
||||||
|
Oh, the humanity...
|
||||||
|
"""
|
||||||
|
|
||||||
|
cov = coverage.coverage(source=['rattail.pyramid'])
|
||||||
|
cov.start()
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
cov.exclude(r'# pragma: win32 no cover$')
|
||||||
|
|
||||||
|
# Must import this *after* coverage has started.
|
||||||
|
import nose
|
||||||
|
nose.run(argv=[''])
|
||||||
|
|
||||||
|
cov.stop()
|
||||||
|
cov.save()
|
||||||
|
|
||||||
|
modules = [
|
||||||
|
'rattail.pyramid',
|
||||||
|
]
|
||||||
|
modules = [sys.modules[x] for x in modules]
|
||||||
|
|
||||||
|
cov.html_report(modules, directory='htmlcov')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
runtests()
|