initial rattail v0.4 port (savepoint)

This commit is contained in:
Lance Edgar 2012-12-14 09:26:15 -08:00
parent 4cd598f33e
commit 79e6a46b94
97 changed files with 6729 additions and 294 deletions

View file

@ -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
View 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

View file

@ -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&nbsp; ($ %0.2f / %u)' % (
price.price, price.multiple,
price.pack_price, price.pack_multiple))
return literal('$ %0.2f&nbsp; ($ %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 += '&nbsp; (%s)' % pretty_datetime(price.ends, from_='utc')
return res

View 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 *

View 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)))

View 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 *

View 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)

View 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)

View 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)

View 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&nbsp; ($ %0.2f / %u)' % (
price.price, price.multiple,
price.pack_price, price.pack_multiple))
return literal('$ %0.2f&nbsp; ($ %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 += '&nbsp; (%s)' % pretty_datetime(price.ends, from_='utc')
return res

View 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)

View 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

View 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

View 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

View 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

View 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)())

View 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 *

View 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

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View 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%; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

View 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

File diff suppressed because one or more lines are too long

View 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);

File diff suppressed because it is too large Load diff

View 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');
});
});

View file

@ -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')

View 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>

View 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()}

View 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>

View file

@ -0,0 +1,3 @@
<%namespace file="/autocomplete.mako" import="autocomplete" />
${autocomplete(field_name, service_url, field_value, field_display, width=width, callback=callback)}

View 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

View 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>

View 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>

View 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>

View file

@ -0,0 +1,3 @@
<div class="form">
${form.fieldset.render()|n}
</div>

View 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>&nbsp;</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">&nbsp;</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:&nbsp;
${grid.pager.pager('~3~', onclick='return grid_navigate_page($(this));')}
</p>
</div>
% endif
</div>

View file

@ -0,0 +1,37 @@
<%inherit file="/base.mako" />
<%def name="context_menu_items()"></%def>
<%def name="form()">
% if search:
${search.render()}
% else:
&nbsp;
% 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 -->

View 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>&nbsp;</th>
% endif
% if grid.deletable:
<th>&nbsp;</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)}">&nbsp;</td>
% endif
% if grid.deletable:
<td class="noclick delete" url="${grid.get_delete_url(row)}">&nbsp;</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&nbsp;
${grid.page_links()}
</p>
</div>
% endif
</div>

View 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>

View 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>

View 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
View 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

View file

@ -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')

View 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')

View 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

View file

@ -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'))

View file

@ -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']

View file

@ -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')

View file

@ -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'))

View file

@ -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'

View file

@ -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):

View 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("&nbsp; (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)

View 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)

View file

@ -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"

View file

@ -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"),

View file

@ -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'

View file

@ -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):

View 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 *

View 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)

View 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)

View file

@ -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'))

View 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')

View file

@ -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()

View 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')

View file

@ -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,

View 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')

View file

@ -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):

View file

@ -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):

View 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')

View file

@ -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
View 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()