Major overhaul for standalone operation.
This removes some of the `edbob` reliance, as well as borrowing some templates and styling etc. from Dtail.
42
setup.py
|
@ -62,11 +62,40 @@ requires = [
|
|||
#
|
||||
# package # low high
|
||||
|
||||
'edbob[db,pyramid]>=0.1a29', # 0.1a29
|
||||
'rattail>=0.3a40', # 0.3a40
|
||||
# Pyramid 1.3 introduced 'pcreate' command (and friends) to replace
|
||||
# deprecated 'paster create' (and friends).
|
||||
'pyramid>=1.3a1', # 1.3b2
|
||||
|
||||
'FormAlchemy', # 1.4.2
|
||||
'FormEncode', # 1.2.4
|
||||
'Mako', # 0.6.2
|
||||
'pyramid_beaker>=0.6', # 0.6.1
|
||||
'pyramid_debugtoolbar', # 1.0
|
||||
'pyramid_exclog', # 0.6
|
||||
'pyramid_simpleform', # 0.6.1
|
||||
'pyramid_tm', # 0.3
|
||||
'rattail[db]>=0.3.4', # 0.3.4
|
||||
'transaction', # 1.2.0
|
||||
'waitress', # 0.8.1
|
||||
'WebHelpers', # 1.3
|
||||
'zope.sqlalchemy', # 0.7
|
||||
]
|
||||
|
||||
|
||||
extras = {
|
||||
|
||||
'tests': [
|
||||
#
|
||||
# package # low high
|
||||
|
||||
'coverage', # 3.6
|
||||
'fixture', # 1.5
|
||||
'mock', # 1.0.1
|
||||
'nose', # 1.3.0
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
setup(
|
||||
name = "Tailbone",
|
||||
version = __version__,
|
||||
|
@ -94,10 +123,17 @@ setup(
|
|||
],
|
||||
|
||||
install_requires = requires,
|
||||
tests_require = requires + ['mock', 'nose', 'coverage', 'fixture'],
|
||||
extras_require = extras,
|
||||
tests_require = ['Tailbone[tests]'],
|
||||
test_suite = 'nose.collector',
|
||||
|
||||
packages = find_packages(),
|
||||
include_package_data = True,
|
||||
zip_safe = False,
|
||||
|
||||
entry_points = {
|
||||
'paste.app_factory': [
|
||||
'main = tailbone.app:main',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -23,13 +23,11 @@
|
|||
################################################################################
|
||||
|
||||
"""
|
||||
Rattail's Pyramid Framework
|
||||
Backoffice Web Application for Rattail
|
||||
"""
|
||||
|
||||
from ._version import __version__
|
||||
|
||||
from edbob.pyramid import Session
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.include('tailbone.static')
|
||||
|
|
81
tailbone/app.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
#!/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/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Application Entry Point
|
||||
"""
|
||||
|
||||
from pyramid.config import Configurator
|
||||
|
||||
import os.path
|
||||
import edbob
|
||||
|
||||
import edbob.db
|
||||
from .db import Session
|
||||
from zope.sqlalchemy import ZopeTransactionExtension
|
||||
|
||||
from pyramid.authentication import SessionAuthenticationPolicy
|
||||
from .auth import TailboneAuthorizationPolicy
|
||||
|
||||
|
||||
def main(global_config, **settings):
|
||||
"""
|
||||
This function returns a Pyramid WSGI application.
|
||||
"""
|
||||
|
||||
# Use Tailbone templates by default.
|
||||
settings.setdefault('mako.directories', ['tailbone:templates'])
|
||||
|
||||
# Make two attempts when "retryable" errors happen during transactions.
|
||||
# This is intended to gracefully handle database restarts.
|
||||
settings.setdefault('tm.attempts', 2)
|
||||
|
||||
config = Configurator(settings=settings)
|
||||
|
||||
# Initialize edbob, dammit.
|
||||
edbob.init('rattail', os.path.abspath(settings['edbob.config']))
|
||||
edbob.init_modules(['edbob.time', 'edbob.db', 'rattail.db'])
|
||||
|
||||
# Configure the primary database session. For now, this leverages edbob's
|
||||
# initialization to define the engine connection.
|
||||
assert edbob.db.engine
|
||||
Session.configure(bind=edbob.db.engine)
|
||||
Session.configure(extension=ZopeTransactionExtension())
|
||||
|
||||
# Configure user authentication / authorization.
|
||||
config.set_authentication_policy(SessionAuthenticationPolicy())
|
||||
config.set_authorization_policy(TailboneAuthorizationPolicy())
|
||||
|
||||
# Bring in some Pyramid goodies.
|
||||
config.include('pyramid_beaker')
|
||||
config.include('pyramid_tm')
|
||||
|
||||
# Bring in the rest of Tailbone.
|
||||
config.include('tailbone')
|
||||
|
||||
# Consider PostgreSQL server restart errors to be "retryable."
|
||||
config.add_tween('edbob.pyramid.tweens.sqlerror_tween_factory',
|
||||
under='pyramid_tm.tm_tween_factory')
|
||||
|
||||
return config.make_wsgi_app()
|
52
tailbone/auth.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Authentication & Authorization
|
||||
"""
|
||||
|
||||
from zope.interface import implementer
|
||||
from pyramid.interfaces import IAuthorizationPolicy
|
||||
from pyramid.security import Everyone, Authenticated
|
||||
|
||||
from .db import Session
|
||||
from rattail.db.model import User
|
||||
from edbob.db.auth import has_permission
|
||||
|
||||
|
||||
@implementer(IAuthorizationPolicy)
|
||||
class TailboneAuthorizationPolicy(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(user, permission)
|
||||
if Everyone in principals:
|
||||
return has_permission(None, permission, session=Session())
|
||||
return False
|
||||
|
||||
def principals_allowed_by_permission(self, context, permission):
|
||||
raise NotImplementedError
|
35
tailbone/db.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
#!/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/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Database Stuff
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
|
||||
__all__ = ['Session']
|
||||
|
||||
|
||||
Session = scoped_session(sessionmaker())
|
|
@ -27,6 +27,7 @@ Common Field Renderers
|
|||
"""
|
||||
|
||||
from formalchemy.fields import FieldRenderer, SelectFieldRenderer
|
||||
from pyramid.renderers import render
|
||||
|
||||
|
||||
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer']
|
||||
|
|
|
@ -35,7 +35,7 @@ import edbob
|
|||
from edbob.util import prettify
|
||||
|
||||
from .core import Grid
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
from sqlalchemy.orm import object_session
|
||||
|
||||
__all__ = ['AlchemyGrid']
|
||||
|
|
|
@ -28,4 +28,6 @@ Static Assets
|
|||
|
||||
|
||||
def includeme(config):
|
||||
# TODO: Remove edbob.
|
||||
config.include('edbob.pyramid.static')
|
||||
config.add_static_view('tailbone', 'tailbone:static')
|
||||
|
|
28
tailbone/static/css/base.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
/******************************
|
||||
* General
|
||||
******************************/
|
||||
|
||||
body {
|
||||
font-family: Verdana, Arial, sans-serif;
|
||||
font-size: 82%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3D6E1C;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.flash-messages div.ui-state-highlight {
|
||||
padding: .3em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
div.error-messages div.ui-state-error {
|
||||
padding: .3em;
|
||||
margin-bottom: 8px;
|
||||
}
|
28
tailbone/static/css/filters.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
/******************************
|
||||
* Filters
|
||||
******************************/
|
||||
|
||||
div.filters form {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
121
tailbone/static/css/forms.css
Normal file
|
@ -0,0 +1,121 @@
|
|||
|
||||
/******************************
|
||||
* 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;
|
||||
}
|
||||
|
||||
div.form-wrapper ul.context-menu li {
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* 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: 160px;
|
||||
font-weight: bold;
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
div.field-wrapper div.field-error {
|
||||
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"],
|
||||
label input[type="radio"] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
div.field ul {
|
||||
margin: 0px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Buttons
|
||||
******************************/
|
||||
|
||||
div.buttons {
|
||||
clear: both;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Employee Login Form
|
||||
******************************/
|
||||
|
||||
#employee-login-dialog label {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
#employee-login-dialog input[type="text"],
|
||||
#employee-login-dialog input[type="password"] {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
#employee-login-dialog div.buttons {
|
||||
margin: 10px 0px 0px 0px;
|
||||
text-align: center;
|
||||
}
|
40
tailbone/static/css/jquery.loadmask.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.loadmask {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
-moz-opacity: 0.5;
|
||||
opacity: .50;
|
||||
filter: alpha(opacity=50);
|
||||
background-color: #CCC;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
zoom: 1;
|
||||
}
|
||||
.loadmask-msg {
|
||||
z-index: 20001;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border:1px solid #6593cf;
|
||||
background: #c3daf9;
|
||||
padding:2px;
|
||||
}
|
||||
.loadmask-msg div {
|
||||
padding:5px 10px 5px 25px;
|
||||
background: #fbfbfb url('../img/loading.gif') no-repeat 5px 5px;
|
||||
line-height: 16px;
|
||||
border:1px solid #a3bad9;
|
||||
color:#222;
|
||||
font:normal 11px tahoma, arial, helvetica, sans-serif;
|
||||
cursor:wait;
|
||||
}
|
||||
.masked {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.masked-relative {
|
||||
position: relative !important;
|
||||
}
|
||||
.masked-hidden {
|
||||
visibility: hidden !important;
|
||||
}
|
15
tailbone/static/css/jquery.ui.menubar.css
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* jQuery UI Menubar @VERSION
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*/
|
||||
.ui-menubar { list-style: none; margin: 0; padding-left: 0; }
|
||||
|
||||
.ui-menubar-item { float: left; }
|
||||
|
||||
.ui-menubar .ui-button { float: left; font-weight: normal; border-top-width: 0 !important; border-bottom-width: 0 !important; margin: 0; outline: none; }
|
||||
.ui-menubar .ui-menubar-link { border-right: 1px dashed transparent; border-left: 1px dashed transparent; }
|
||||
|
||||
.ui-menubar .ui-menu { width: 200px; position: absolute; z-index: 9999; font-weight: normal; }
|
64
tailbone/static/css/layout.css
Normal file
|
@ -0,0 +1,64 @@
|
|||
|
||||
/******************************
|
||||
* Main Layout
|
||||
******************************/
|
||||
|
||||
html, body, #body-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body > #body-wrapper {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
#body-wrapper {
|
||||
margin: 0px auto;
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
#header {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
#body {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 5em;
|
||||
}
|
||||
|
||||
#footer {
|
||||
clear: both;
|
||||
margin-top: -4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Header
|
||||
******************************/
|
||||
|
||||
#header h1 {
|
||||
float: left;
|
||||
font-size: 25px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#header h1.title {
|
||||
font-size: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#header div.login {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Logo
|
||||
******************************/
|
||||
|
||||
#logo {
|
||||
display: block;
|
||||
margin: 40px auto;
|
||||
}
|
396
tailbone/static/css/normalize.css
vendored
Normal file
|
@ -0,0 +1,396 @@
|
|||
/*! normalize.css v2.1.0 | MIT License | git.io/normalize */
|
||||
|
||||
/* ==========================================================================
|
||||
HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined in IE 8/9.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct `inline-block` display not defined in IE 8/9.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
video {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Base
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address `outline` inconsistency between Chrome and other browsers.
|
||||
*/
|
||||
|
||||
a:focus {
|
||||
outline: thin dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Typography
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari 5 and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct font family set oddly in Safari 5 and Chrome.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability of pre-formatted text in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set consistent quote types.
|
||||
*/
|
||||
|
||||
q {
|
||||
quotes: "\201C" "\201D" "\2018" "\2019";
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow displayed oddly in IE 9.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Figures
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari 5.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct font family not being inherited in all browsers.
|
||||
* 2. Correct font size not being inherited in all browsers.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
|
||||
* Correct `select` style inheritance in Firefox 4+ and Opera.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9.
|
||||
* 2. Remove excess padding in IE 8/9.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari 5 and Chrome
|
||||
* on OS X.
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove default vertical scrollbar in IE 8/9.
|
||||
* 2. Improve readability and alignment in all browsers.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto; /* 1 */
|
||||
vertical-align: top; /* 2 */
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
BIN
tailbone/static/css/smoothness/images/animated-overlay.gif
Normal file
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 274 B |
After Width: | Height: | Size: 271 B |
After Width: | Height: | Size: 387 B |
After Width: | Height: | Size: 272 B |
After Width: | Height: | Size: 375 B |
After Width: | Height: | Size: 368 B |
After Width: | Height: | Size: 384 B |
After Width: | Height: | Size: 360 B |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 4.3 KiB |
5
tailbone/static/css/smoothness/jquery-ui-1.10.0.custom.min.css
vendored
Normal file
BIN
tailbone/static/img/Hymenocephalus_italicus.jpg
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
tailbone/static/img/home_logo.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
tailbone/static/img/loading.gif
Normal file
After Width: | Height: | Size: 771 B |
BIN
tailbone/static/img/rattail.ico
Normal file
After Width: | Height: | Size: 5.6 KiB |
5
tailbone/static/js/lib/jquery-1.9.1.min.js
vendored
Normal file
6
tailbone/static/js/lib/jquery-ui-1.10.0.custom.min.js
vendored
Normal file
10
tailbone/static/js/lib/jquery.loadmask.min.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* Copyright (c) 2009 Sergiy Kovalchuk (serg472@gmail.com)
|
||||
*
|
||||
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
|
||||
*
|
||||
* Following code is based on Element.mask() implementation from ExtJS framework (http://extjs.com/)
|
||||
*
|
||||
*/
|
||||
(function(a){a.fn.mask=function(c,b){a(this).each(function(){if(b!==undefined&&b>0){var d=a(this);d.data("_mask_timeout",setTimeout(function(){a.maskElement(d,c)},b))}else{a.maskElement(a(this),c)}})};a.fn.unmask=function(){a(this).each(function(){a.unmaskElement(a(this))})};a.fn.isMasked=function(){return this.hasClass("masked")};a.maskElement=function(d,c){if(d.data("_mask_timeout")!==undefined){clearTimeout(d.data("_mask_timeout"));d.removeData("_mask_timeout")}if(d.isMasked()){a.unmaskElement(d)}if(d.css("position")=="static"){d.addClass("masked-relative")}d.addClass("masked");var e=a('<div class="loadmask"></div>');if(navigator.userAgent.toLowerCase().indexOf("msie")>-1){e.height(d.height()+parseInt(d.css("padding-top"))+parseInt(d.css("padding-bottom")));e.width(d.width()+parseInt(d.css("padding-left"))+parseInt(d.css("padding-right")))}if(navigator.userAgent.toLowerCase().indexOf("msie 6")>-1){d.find("select").addClass("masked-hidden")}d.append(e);if(c!==undefined){var b=a('<div class="loadmask-msg" style="display:none;"></div>');b.append("<div>"+c+"</div>");d.append(b);b.css("top",Math.round(d.height()/2-(b.height()-parseInt(b.css("padding-top"))-parseInt(b.css("padding-bottom")))/2)+"px");b.css("left",Math.round(d.width()/2-(b.width()-parseInt(b.css("padding-left"))-parseInt(b.css("padding-right")))/2)+"px");b.show()}};a.unmaskElement=function(b){if(b.data("_mask_timeout")!==undefined){clearTimeout(b.data("_mask_timeout"));b.removeData("_mask_timeout")}b.find(".loadmask-msg,.loadmask").remove();b.removeClass("masked");b.removeClass("masked-relative");b.find("select").removeClass("masked-hidden")}})(jQuery);
|
327
tailbone/static/js/lib/jquery.ui.menubar.js
vendored
Normal file
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
* jQuery UI Menubar @VERSION
|
||||
*
|
||||
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Menubar
|
||||
*
|
||||
* Depends:
|
||||
* jquery.ui.core.js
|
||||
* jquery.ui.widget.js
|
||||
* jquery.ui.position.js
|
||||
* jquery.ui.menu.js
|
||||
*/
|
||||
(function( $ ) {
|
||||
|
||||
// TODO when mixing clicking menus and keyboard navigation, focus handling is broken
|
||||
// there has to be just one item that has tabindex
|
||||
$.widget( "ui.menubar", {
|
||||
version: "@VERSION",
|
||||
options: {
|
||||
autoExpand: false,
|
||||
buttons: false,
|
||||
items: "li",
|
||||
menuElement: "ul",
|
||||
menuIcon: false,
|
||||
position: {
|
||||
my: "left top",
|
||||
at: "left bottom"
|
||||
}
|
||||
},
|
||||
_create: function() {
|
||||
var that = this;
|
||||
this.menuItems = this.element.children( this.options.items );
|
||||
this.items = this.menuItems.children( "button, a" );
|
||||
|
||||
this.menuItems
|
||||
.addClass( "ui-menubar-item" )
|
||||
.attr( "role", "presentation" );
|
||||
// let only the first item receive focus
|
||||
this.items.slice(1).attr( "tabIndex", -1 );
|
||||
|
||||
this.element
|
||||
.addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
|
||||
.attr( "role", "menubar" );
|
||||
this._focusable( this.items );
|
||||
this._hoverable( this.items );
|
||||
this.items.siblings( this.options.menuElement )
|
||||
.menu({
|
||||
position: {
|
||||
within: this.options.position.within
|
||||
},
|
||||
select: function( event, ui ) {
|
||||
ui.item.parents( "ul.ui-menu:last" ).hide();
|
||||
that._close();
|
||||
// TODO what is this targetting? there's probably a better way to access it
|
||||
$(event.target).prev().focus();
|
||||
that._trigger( "select", event, ui );
|
||||
},
|
||||
menus: that.options.menuElement
|
||||
})
|
||||
.hide()
|
||||
.attr({
|
||||
"aria-hidden": "true",
|
||||
"aria-expanded": "false"
|
||||
})
|
||||
// TODO use _on
|
||||
.bind( "keydown.menubar", function( event ) {
|
||||
var menu = $( this );
|
||||
if ( menu.is( ":hidden" ) ) {
|
||||
return;
|
||||
}
|
||||
switch ( event.keyCode ) {
|
||||
case $.ui.keyCode.LEFT:
|
||||
that.previous( event );
|
||||
event.preventDefault();
|
||||
break;
|
||||
case $.ui.keyCode.RIGHT:
|
||||
that.next( event );
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.items.each(function() {
|
||||
var input = $(this),
|
||||
// TODO menu var is only used on two places, doesn't quite justify the .each
|
||||
menu = input.next( that.options.menuElement );
|
||||
|
||||
// might be a non-menu button
|
||||
if ( menu.length ) {
|
||||
// TODO use _on
|
||||
input.bind( "click.menubar focus.menubar mouseenter.menubar", function( event ) {
|
||||
// ignore triggered focus event
|
||||
if ( event.type === "focus" && !event.originalEvent ) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
// TODO can we simplify or extractthis check? especially the last two expressions
|
||||
// there's a similar active[0] == menu[0] check in _open
|
||||
if ( event.type === "click" && menu.is( ":visible" ) && that.active && that.active[0] === menu[0] ) {
|
||||
that._close();
|
||||
return;
|
||||
}
|
||||
if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" || that.options.autoExpand ) {
|
||||
if( that.options.autoExpand ) {
|
||||
clearTimeout( that.closeTimer );
|
||||
}
|
||||
|
||||
that._open( event, menu );
|
||||
}
|
||||
})
|
||||
// TODO use _on
|
||||
.bind( "keydown", function( event ) {
|
||||
switch ( event.keyCode ) {
|
||||
case $.ui.keyCode.SPACE:
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.DOWN:
|
||||
that._open( event, $( this ).next() );
|
||||
event.preventDefault();
|
||||
break;
|
||||
case $.ui.keyCode.LEFT:
|
||||
that.previous( event );
|
||||
event.preventDefault();
|
||||
break;
|
||||
case $.ui.keyCode.RIGHT:
|
||||
that.next( event );
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
})
|
||||
.attr( "aria-haspopup", "true" );
|
||||
|
||||
// TODO review if these options (menuIcon and buttons) are a good choice, maybe they can be merged
|
||||
if ( that.options.menuIcon ) {
|
||||
input.addClass( "ui-state-default" ).append( "<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>" );
|
||||
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
|
||||
}
|
||||
} else {
|
||||
// TODO use _on
|
||||
input.bind( "click.menubar mouseenter.menubar", function( event ) {
|
||||
if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
|
||||
that._close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
input
|
||||
.addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
|
||||
.attr( "role", "menuitem" )
|
||||
.wrapInner( "<span class='ui-button-text'></span>" );
|
||||
|
||||
if ( that.options.buttons ) {
|
||||
input.removeClass( "ui-menubar-link" ).addClass( "ui-state-default" );
|
||||
}
|
||||
});
|
||||
that._on( {
|
||||
keydown: function( event ) {
|
||||
if ( event.keyCode === $.ui.keyCode.ESCAPE && that.active && that.active.menu( "collapse", event ) !== true ) {
|
||||
var active = that.active;
|
||||
that.active.blur();
|
||||
that._close( event );
|
||||
active.prev().focus();
|
||||
}
|
||||
},
|
||||
focusin: function( event ) {
|
||||
clearTimeout( that.closeTimer );
|
||||
},
|
||||
focusout: function( event ) {
|
||||
that.closeTimer = setTimeout( function() {
|
||||
that._close( event );
|
||||
}, 150);
|
||||
},
|
||||
"mouseleave .ui-menubar-item": function( event ) {
|
||||
if ( that.options.autoExpand ) {
|
||||
that.closeTimer = setTimeout( function() {
|
||||
that._close( event );
|
||||
}, 150);
|
||||
}
|
||||
},
|
||||
"mouseenter .ui-menubar-item": function( event ) {
|
||||
clearTimeout( that.closeTimer );
|
||||
}
|
||||
});
|
||||
|
||||
// Keep track of open submenus
|
||||
this.openSubmenus = 0;
|
||||
},
|
||||
|
||||
_destroy : function() {
|
||||
this.menuItems
|
||||
.removeClass( "ui-menubar-item" )
|
||||
.removeAttr( "role" );
|
||||
|
||||
this.element
|
||||
.removeClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
|
||||
.removeAttr( "role" )
|
||||
.unbind( ".menubar" );
|
||||
|
||||
this.items
|
||||
.unbind( ".menubar" )
|
||||
.removeClass( "ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default" )
|
||||
.removeAttr( "role" )
|
||||
.removeAttr( "aria-haspopup" )
|
||||
// TODO unwrap?
|
||||
.children( "span.ui-button-text" ).each(function( i, e ) {
|
||||
var item = $( this );
|
||||
item.parent().html( item.html() );
|
||||
})
|
||||
.end()
|
||||
.children( ".ui-icon" ).remove();
|
||||
|
||||
this.element.find( ":ui-menu" )
|
||||
.menu( "destroy" )
|
||||
.show()
|
||||
.removeAttr( "aria-hidden" )
|
||||
.removeAttr( "aria-expanded" )
|
||||
.removeAttr( "tabindex" )
|
||||
.unbind( ".menubar" );
|
||||
},
|
||||
|
||||
_close: function() {
|
||||
if ( !this.active || !this.active.length ) {
|
||||
return;
|
||||
}
|
||||
this.active
|
||||
.menu( "collapseAll" )
|
||||
.hide()
|
||||
.attr({
|
||||
"aria-hidden": "true",
|
||||
"aria-expanded": "false"
|
||||
});
|
||||
this.active
|
||||
.prev()
|
||||
.removeClass( "ui-state-active" )
|
||||
.removeAttr( "tabIndex" );
|
||||
this.active = null;
|
||||
this.open = false;
|
||||
this.openSubmenus = 0;
|
||||
},
|
||||
|
||||
_open: function( event, menu ) {
|
||||
// on a single-button menubar, ignore reopening the same menu
|
||||
if ( this.active && this.active[0] === menu[0] ) {
|
||||
return;
|
||||
}
|
||||
// TODO refactor, almost the same as _close above, but don't remove tabIndex
|
||||
if ( this.active ) {
|
||||
this.active
|
||||
.menu( "collapseAll" )
|
||||
.hide()
|
||||
.attr({
|
||||
"aria-hidden": "true",
|
||||
"aria-expanded": "false"
|
||||
});
|
||||
this.active
|
||||
.prev()
|
||||
.removeClass( "ui-state-active" );
|
||||
}
|
||||
// set tabIndex -1 to have the button skipped on shift-tab when menu is open (it gets focus)
|
||||
var button = menu.prev().addClass( "ui-state-active" ).attr( "tabIndex", -1 );
|
||||
this.active = menu
|
||||
.show()
|
||||
.position( $.extend({
|
||||
of: button
|
||||
}, this.options.position ) )
|
||||
.removeAttr( "aria-hidden" )
|
||||
.attr( "aria-expanded", "true" )
|
||||
.menu("focus", event, menu.children( ".ui-menu-item" ).first() )
|
||||
// TODO need a comment here why both events are triggered
|
||||
.focus()
|
||||
.focusin();
|
||||
this.open = true;
|
||||
},
|
||||
|
||||
next: function( event ) {
|
||||
if ( this.open && this.active.data( "menu" ).active.has( ".ui-menu" ).length ) {
|
||||
// Track number of open submenus and prevent moving to next menubar item
|
||||
this.openSubmenus++;
|
||||
return;
|
||||
}
|
||||
this.openSubmenus = 0;
|
||||
this._move( "next", "first", event );
|
||||
},
|
||||
|
||||
previous: function( event ) {
|
||||
if ( this.open && this.openSubmenus ) {
|
||||
// Track number of open submenus and prevent moving to previous menubar item
|
||||
this.openSubmenus--;
|
||||
return;
|
||||
}
|
||||
this.openSubmenus = 0;
|
||||
this._move( "prev", "last", event );
|
||||
},
|
||||
|
||||
_move: function( direction, filter, event ) {
|
||||
var next,
|
||||
wrapItem;
|
||||
if ( this.open ) {
|
||||
next = this.active.closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).first().children( ".ui-menu" ).eq( 0 );
|
||||
wrapItem = this.menuItems[ filter ]().children( ".ui-menu" ).eq( 0 );
|
||||
} else {
|
||||
if ( event ) {
|
||||
next = $( event.target ).closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).children( ".ui-menubar-link" ).eq( 0 );
|
||||
wrapItem = this.menuItems[ filter ]().children( ".ui-menubar-link" ).eq( 0 );
|
||||
} else {
|
||||
next = wrapItem = this.menuItems.children( "a" ).eq( 0 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( next.length ) {
|
||||
if ( this.open ) {
|
||||
this._open( event, next );
|
||||
} else {
|
||||
next.removeAttr( "tabIndex")[0].focus();
|
||||
}
|
||||
} else {
|
||||
if ( this.open ) {
|
||||
this._open( event, wrapItem );
|
||||
} else {
|
||||
wrapItem.removeAttr( "tabIndex")[0].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}( jQuery ));
|
260
tailbone/static/js/tailbone.js
Normal file
|
@ -0,0 +1,260 @@
|
|||
|
||||
/************************************************************
|
||||
*
|
||||
* tailbone.js
|
||||
*
|
||||
************************************************************/
|
||||
|
||||
|
||||
/*
|
||||
* Initialize the disabled filters array. This is populated from within the
|
||||
* /grids/search.mako template.
|
||||
*/
|
||||
var filters_to_disable = [];
|
||||
|
||||
|
||||
/*
|
||||
* Disables options within the "add filter" dropdown which correspond to those
|
||||
* filters already being displayed. Called from /grids/search.mako template.
|
||||
*/
|
||||
function disable_filter_options() {
|
||||
while (filters_to_disable.length) {
|
||||
var filter = filters_to_disable.shift();
|
||||
var option = $('#add-filter option[value="' + filter + '"]');
|
||||
option.attr('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Convenience function to disable a form button.
|
||||
*/
|
||||
function disable_button(button, label) {
|
||||
if (label) {
|
||||
$(button).html(label + ", please wait...");
|
||||
}
|
||||
$(button).attr('disabled', 'disabled');
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load next / previous page of results to grid. This function is called on
|
||||
* the click event from the pager links, via inline script code.
|
||||
*/
|
||||
function grid_navigate_page(link, url) {
|
||||
var wrapper = $(link).parents('div.grid-wrapper');
|
||||
var grid = wrapper.find('div.grid');
|
||||
wrapper.mask("Loading...");
|
||||
$.get(url, function(data) {
|
||||
wrapper.unmask();
|
||||
grid.replaceWith(data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Fetch the UUID value associated with a table row.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
$(function() {
|
||||
|
||||
/*
|
||||
* Initialize the menu bar.
|
||||
*/
|
||||
$('ul.menubar').menubar({
|
||||
buttons: true,
|
||||
menuIcon: true,
|
||||
autoExpand: true
|
||||
});
|
||||
|
||||
/*
|
||||
* When filter labels are clicked, (un)check the associated checkbox.
|
||||
*/
|
||||
$('div.grid-wrapper div.filter label').on('click', function() {
|
||||
var checkbox = $(this).prev('input[type="checkbox"]');
|
||||
if (checkbox.prop('checked')) {
|
||||
checkbox.prop('checked', false);
|
||||
return false;
|
||||
}
|
||||
checkbox.prop('checked', true);
|
||||
});
|
||||
|
||||
/*
|
||||
* When a new filter is selected in the "add filter" dropdown, show it in
|
||||
* the UI. This selects the filter's checkbox and puts focus to its input
|
||||
* element. If all available filters have been displayed, the "add filter"
|
||||
* dropdown will be hidden.
|
||||
*/
|
||||
$('#add-filter').on('change', function() {
|
||||
var select = $(this);
|
||||
var filters = select.parents('div.filters:first');
|
||||
var filter = filters.find('#filter-' + select.val());
|
||||
var checkbox = filter.find('input[type="checkbox"]:first');
|
||||
var input = filter.find(':last-child');
|
||||
|
||||
checkbox.prop('checked', true);
|
||||
filter.show();
|
||||
input.select();
|
||||
input.focus();
|
||||
|
||||
filters.find('input[type="submit"]').show();
|
||||
filters.find('button[type="reset"]').show();
|
||||
|
||||
select.find('option:selected').attr('disabled', true);
|
||||
select.val('add a filter');
|
||||
if (select.find('option:enabled').length == 1) {
|
||||
select.hide();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* When user clicks the grid filters search button, perform the search in
|
||||
* the background and reload the grid in-place.
|
||||
*/
|
||||
$('div.filters form').submit(function() {
|
||||
var form = $(this);
|
||||
var wrapper = form.parents('div.grid-wrapper');
|
||||
var grid = wrapper.find('div.grid');
|
||||
var data = form.serializeArray();
|
||||
data.push({name: 'partial', value: true});
|
||||
wrapper.mask("Loading...");
|
||||
$.get(grid.attr('url'), data, function(data) {
|
||||
wrapper.unmask();
|
||||
grid.replaceWith(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
/*
|
||||
* When user clicks the grid filters reset button, manually clear all
|
||||
* filter input elements, and submit a new search.
|
||||
*/
|
||||
$('div.filters form button[type="reset"]').click(function() {
|
||||
var form = $(this).parents('form');
|
||||
form.find('div.filter').each(function() {
|
||||
$(this).find('div.value input').val('');
|
||||
});
|
||||
form.submit();
|
||||
return false;
|
||||
});
|
||||
|
||||
$('div.grid-wrapper').on('click', 'div.grid th.sortable a', function() {
|
||||
var th = $(this).parent();
|
||||
var wrapper = th.parents('div.grid-wrapper');
|
||||
var grid = wrapper.find('div.grid');
|
||||
var data = {
|
||||
sort: th.attr('field'),
|
||||
dir: (th.hasClass('sorted') && th.hasClass('asc')) ? 'desc' : 'asc',
|
||||
page: 1,
|
||||
partial: true
|
||||
};
|
||||
wrapper.mask("Loading...");
|
||||
$.get(grid.attr('url'), data, function(data) {
|
||||
wrapper.unmask();
|
||||
grid.replaceWith(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#body').on('mouseenter', 'div.grid.hoverable table tbody tr', function() {
|
||||
$(this).addClass('hovering');
|
||||
});
|
||||
|
||||
$('#body').on('mouseleave', 'div.grid.hoverable table tbody tr', function() {
|
||||
$(this).removeClass('hovering');
|
||||
});
|
||||
|
||||
$('div.grid-wrapper').on('click', 'div.grid table tbody td.view', function() {
|
||||
var url = $(this).attr('url');
|
||||
if (url) {
|
||||
location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
$('div.grid-wrapper').on('click', 'div.grid table tbody td.edit', function() {
|
||||
var url = $(this).attr('url');
|
||||
if (url) {
|
||||
location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
$('div.grid-wrapper').on('click', 'div.grid table tbody td.delete', function() {
|
||||
var url = $(this).attr('url');
|
||||
if (url) {
|
||||
if (confirm("Do you really wish to delete this object?")) {
|
||||
location.href = url;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('div.grid-wrapper').on('change', 'div.grid div.pager select#grid-page-count', function() {
|
||||
var select = $(this);
|
||||
var wrapper = select.parents('div.grid-wrapper');
|
||||
var grid = wrapper.find('div.grid');
|
||||
var data = {
|
||||
per_page: select.val(),
|
||||
partial: true
|
||||
};
|
||||
wrapper.mask("Loading...");
|
||||
$.get(grid.attr('url'), data, function(data) {
|
||||
wrapper.unmask();
|
||||
grid.replaceWith(data);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
* Add "check all" functionality to tables with checkboxes.
|
||||
*/
|
||||
$('body').on('click', 'div.grid table thead th.checkbox input[type="checkbox"]', function() {
|
||||
var table = $(this).parents('table:first');
|
||||
var checked = $(this).prop('checked');
|
||||
table.find('tbody tr').each(function() {
|
||||
$(this).find('td.checkbox input[type="checkbox"]').prop('checked', checked);
|
||||
});
|
||||
});
|
||||
|
||||
$('body').on('click', 'div.dialog button.close', function() {
|
||||
var dialog = $(this).parents('div.dialog:first');
|
||||
dialog.dialog('close');
|
||||
});
|
||||
|
||||
});
|
|
@ -27,31 +27,74 @@ Event Subscribers
|
|||
"""
|
||||
|
||||
from pyramid import threadlocal
|
||||
|
||||
import rattail
|
||||
from . import helpers
|
||||
|
||||
from pyramid.security import authenticated_userid
|
||||
from .db import Session
|
||||
from rattail.db.model import User
|
||||
from edbob.db.auth import has_permission
|
||||
|
||||
|
||||
def before_render(event):
|
||||
"""
|
||||
Adds goodies to the global template renderer context:
|
||||
|
||||
* ``rattail``
|
||||
Adds goodies to the global template renderer context.
|
||||
"""
|
||||
|
||||
# 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()
|
||||
|
||||
renderer_globals = event
|
||||
renderer_globals['h'] = helpers
|
||||
renderer_globals['url'] = request.route_url
|
||||
renderer_globals['rattail'] = rattail
|
||||
|
||||
|
||||
def context_found(event):
|
||||
"""
|
||||
Attach some goodies to the request object.
|
||||
|
||||
The following is attached to the request:
|
||||
|
||||
* The currently logged-in user instance (if any), as ``user``.
|
||||
|
||||
* A shortcut method for permission checking, as ``has_perm()``.
|
||||
|
||||
* A shortcut method for fetching the referrer, as ``get_referrer()``.
|
||||
"""
|
||||
|
||||
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(request.user, perm, session=Session())
|
||||
request.has_perm = has_perm
|
||||
|
||||
def has_any_perm(perms):
|
||||
for perm in perms:
|
||||
if has_permission(request.user, perm, session=Session()):
|
||||
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):
|
||||
config.add_subscriber('tailbone.subscribers:before_render',
|
||||
'pyramid.events.BeforeRender')
|
||||
config.add_subscriber(before_render, 'pyramid.events.BeforeRender')
|
||||
config.add_subscriber(context_found, 'pyramid.events.ContextFound')
|
||||
|
|
141
tailbone/templates/base.mako
Normal file
|
@ -0,0 +1,141 @@
|
|||
<!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>Tailbone » ${capture(self.title)}</title>
|
||||
<link rel="icon" type="image/x-icon" href="${request.static_url('tailbone:static/img/rattail.ico')}" />
|
||||
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery-1.9.1.min.js'))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery-ui-1.10.0.custom.min.js'))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.ui.menubar.js'))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.loadmask.min.js'))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.js'))}
|
||||
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/normalize.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/smoothness/jquery-ui-1.10.0.custom.min.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.ui.menubar.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.loadmask.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/base.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/layout.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/forms.css'))}
|
||||
|
||||
${self.head_tags()}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="body-wrapper">
|
||||
|
||||
<div id="header">
|
||||
<h1>${h.link_to("Tailbone", url('home'))}</h1>
|
||||
<h1 class="title">» ${self.title()}</h1>
|
||||
<div class="login">
|
||||
% if request.user:
|
||||
${h.link_to(request.user.display_name, url('change_password'))}
|
||||
(${h.link_to("logout", url('logout'))})
|
||||
% else:
|
||||
${h.link_to("login", url('login'))}
|
||||
% endif
|
||||
</div>
|
||||
</div><!-- header -->
|
||||
|
||||
<ul class="menubar">
|
||||
<li>
|
||||
<a>Products</a>
|
||||
<ul>
|
||||
<li>${h.link_to("Products", url('products'))}</li>
|
||||
<li>${h.link_to("Brands", url('brands'))}</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a>Customers</a>
|
||||
<ul>
|
||||
<li>${h.link_to("Customers", url('customers'))}</li>
|
||||
<li>${h.link_to("Customer Groups", url('customer_groups'))}</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a>Employees</a>
|
||||
<ul>
|
||||
<li>${h.link_to("Employees", url('employees'))}</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a>Vendors</a>
|
||||
<ul>
|
||||
<li>${h.link_to("Vendors", url('vendors'))}</li>
|
||||
</ul>
|
||||
</li>
|
||||
% if request.has_perm('batches.list'):
|
||||
<li>
|
||||
<a>Batches</a>
|
||||
<ul>
|
||||
<li>${h.link_to("Batches", url('batches'))}</li>
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
<li>
|
||||
<a>Stores</a>
|
||||
<ul>
|
||||
<li>${h.link_to("Stores", url('stores'))}</li>
|
||||
<li>${h.link_to("Departments", url('departments'))}</li>
|
||||
<li>${h.link_to("Subdepartments", url('subdepartments'))}</li>
|
||||
</ul>
|
||||
</li>
|
||||
% if request.has_perm('users.list') or request.has_perm('roles.list'):
|
||||
<li>
|
||||
<a>Auth</a>
|
||||
<ul>
|
||||
% if request.has_perm('users.list'):
|
||||
<li>${h.link_to("Users", url('users'))}</li>
|
||||
% endif
|
||||
% if request.has_perm('roles.list'):
|
||||
<li>${h.link_to("Roles", url('roles'))}</li>
|
||||
% endif
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
</ul>
|
||||
|
||||
<div id="body">
|
||||
|
||||
% if request.session.peek_flash('error'):
|
||||
<div class="error-messages">
|
||||
% for error in request.session.pop_flash('error'):
|
||||
<div class="ui-state-error ui-corner-all">
|
||||
<span style="float: left; margin-right: .3em;" class="ui-icon ui-icon-alert"></span>
|
||||
${error}
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if request.session.peek_flash():
|
||||
<div class="flash-messages">
|
||||
% for msg in request.session.pop_flash():
|
||||
<div class="ui-state-highlight ui-corner-all">
|
||||
<span style="float: left; margin-right: .3em;" class="ui-icon ui-icon-info"></span>
|
||||
${msg|n}
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
% endif
|
||||
|
||||
${self.body()}
|
||||
|
||||
</div><!-- body -->
|
||||
|
||||
</div><!-- body-wrapper -->
|
||||
|
||||
<div id="footer">
|
||||
powered by ${h.link_to("Rattail", 'http://rattail.edbob.org/', target='_blank')}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<%def name="title()"></%def>
|
||||
|
||||
<%def name="head_tags()"></%def>
|
8
tailbone/templates/home.mako
Normal file
|
@ -0,0 +1,8 @@
|
|||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="title()">Home</%def>
|
||||
|
||||
<div style="text-align: center;">
|
||||
${h.image(request.static_url('tailbone:static/img/home_logo.png'), "Rattail Logo")}
|
||||
<h1>Welcome to Tailbone</h1>
|
||||
</div>
|
|
@ -32,16 +32,39 @@ from .crud import *
|
|||
from .autocomplete import *
|
||||
|
||||
|
||||
def home(request):
|
||||
"""
|
||||
Default home view.
|
||||
"""
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def add_routes(config):
|
||||
config.add_route('home', '/')
|
||||
|
||||
|
||||
def includeme(config):
|
||||
add_routes(config)
|
||||
|
||||
config.add_forbidden_view('edbob.pyramid.views.forbidden')
|
||||
|
||||
config.add_view(home, route_name='home',
|
||||
renderer='/home.mako')
|
||||
|
||||
config.include('tailbone.views.auth')
|
||||
config.include('tailbone.views.batches')
|
||||
# config.include('tailbone.views.categories')
|
||||
config.include('tailbone.views.brands')
|
||||
config.include('tailbone.views.categories')
|
||||
config.include('tailbone.views.customergroups')
|
||||
config.include('tailbone.views.customers')
|
||||
config.include('tailbone.views.departments')
|
||||
config.include('tailbone.views.employees')
|
||||
config.include('tailbone.views.labels')
|
||||
config.include('tailbone.views.people')
|
||||
config.include('tailbone.views.products')
|
||||
config.include('tailbone.views.roles')
|
||||
config.include('tailbone.views.stores')
|
||||
config.include('tailbone.views.subdepartments')
|
||||
config.include('tailbone.views.users')
|
||||
config.include('tailbone.views.vendors')
|
||||
|
|
152
tailbone/views/auth.py
Normal file
|
@ -0,0 +1,152 @@
|
|||
#!/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/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Auth Views
|
||||
"""
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
from pyramid.security import remember, forget
|
||||
|
||||
import formencode
|
||||
from pyramid_simpleform import Form
|
||||
from ..forms.simpleform import FormRenderer
|
||||
|
||||
import edbob
|
||||
from ..db import Session
|
||||
from edbob.db.auth import authenticate_user, set_user_password
|
||||
|
||||
|
||||
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(form.data['username'],
|
||||
form.data['password'],
|
||||
session=Session())
|
||||
if user:
|
||||
request.session.flash("%s logged in at %s" % (
|
||||
user.display_name,
|
||||
edbob.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 = edbob.config.get('edbob.pyramid', 'login.logo_url',
|
||||
default=request.static_url('edbob.pyramid:static/img/logo.jpg'))
|
||||
kwargs = eval(edbob.config.get('edbob.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 add_routes(config):
|
||||
config.add_route('login', '/login')
|
||||
config.add_route('logout', '/logout')
|
||||
config.add_route('change_password', '/change-password')
|
||||
|
||||
|
||||
def includeme(config):
|
||||
add_routes(config)
|
||||
|
||||
config.add_view(login, route_name='login',
|
||||
renderer='/login.mako')
|
||||
|
||||
config.add_view(logout, route_name='logout')
|
||||
|
||||
config.add_view(change_password, route_name='change_password',
|
||||
renderer='/change_password.mako')
|
|
@ -27,7 +27,7 @@ Autocomplete View
|
|||
"""
|
||||
|
||||
from .core import View
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
|
||||
|
||||
__all__ = ['AutocompleteView']
|
||||
|
|
|
@ -39,7 +39,7 @@ from .. import SearchableAlchemyGridView, CrudView, View
|
|||
|
||||
import rattail
|
||||
from rattail import batches
|
||||
from ... import Session
|
||||
from ...db import Session
|
||||
from rattail.db.model import Batch
|
||||
from rattail.threads import Thread
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
Print Labels Batch
|
||||
"""
|
||||
|
||||
from .... import Session
|
||||
from ....db import Session
|
||||
|
||||
import rattail
|
||||
from . import BatchParamsView
|
||||
|
|
|
@ -28,7 +28,7 @@ Batch Row Views
|
|||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from ... import Session
|
||||
from ...db import Session
|
||||
from .. import SearchableAlchemyGridView, CrudView
|
||||
|
||||
import rattail
|
||||
|
|
|
@ -26,6 +26,10 @@
|
|||
Core View
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ['View']
|
||||
|
||||
|
||||
class View(object):
|
||||
"""
|
||||
Base for all class-based views.
|
||||
|
|
|
@ -30,10 +30,10 @@ from pyramid.httpexceptions import HTTPFound
|
|||
|
||||
import formalchemy
|
||||
|
||||
from .. import Session
|
||||
from edbob.pyramid.forms.formalchemy import AlchemyForm
|
||||
from .core import View
|
||||
from edbob.util import requires_impl, prettify
|
||||
from ..db import Session
|
||||
|
||||
|
||||
__all__ = ['CrudView']
|
||||
|
|
|
@ -28,7 +28,7 @@ CustomerGroup Views
|
|||
|
||||
from . import SearchableAlchemyGridView, CrudView
|
||||
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
from rattail.db.model import CustomerGroup, CustomerGroupAssignment
|
||||
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ from . import SearchableAlchemyGridView
|
|||
from ..forms import EnumFieldRenderer
|
||||
|
||||
import rattail
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
from rattail.db.model import (
|
||||
Customer, CustomerPerson, CustomerGroupAssignment,
|
||||
CustomerEmailAddress, CustomerPhoneNumber)
|
||||
|
|
|
@ -30,7 +30,7 @@ from webhelpers import paginate
|
|||
|
||||
from .core import GridView
|
||||
from ... import grids
|
||||
from ... import Session
|
||||
from ...db import Session
|
||||
|
||||
|
||||
__all__ = ['AlchemyGridView', 'SortableAlchemyGridView',
|
||||
|
|
|
@ -32,7 +32,7 @@ import formalchemy
|
|||
|
||||
from webhelpers.html import HTML
|
||||
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
from . import SearchableAlchemyGridView, CrudView
|
||||
from ..grids.search import BooleanSearchFilter
|
||||
from edbob.pyramid.forms import StrippingFieldRenderer
|
||||
|
|
|
@ -30,7 +30,7 @@ from sqlalchemy import and_
|
|||
|
||||
from . import SearchableAlchemyGridView, CrudView, AutocompleteView
|
||||
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
from rattail.db.model import (Person, PersonEmailAddress, PersonPhoneNumber,
|
||||
VendorContact)
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ from rattail.db.model import (
|
|||
Brand, Vendor, Department, Subdepartment, LabelProfile)
|
||||
from rattail.gpc import GPC
|
||||
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
from ..forms import AutocompleteFieldRenderer, GPCFieldRenderer, PriceFieldRenderer
|
||||
from . import CrudView
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ from .core import View
|
|||
from mako.template import Template
|
||||
from pyramid.response import Response
|
||||
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
from rattail.db.model import Vendor, Department, Product, ProductCost
|
||||
|
||||
import re
|
||||
|
|
|
@ -34,7 +34,7 @@ from webhelpers.html.builder import HTML
|
|||
|
||||
from edbob.db import auth
|
||||
|
||||
from .. import Session
|
||||
from ..db import Session
|
||||
from . import SearchableAlchemyGridView, CrudView
|
||||
from rattail.db.model import Role
|
||||
|
||||
|
|
|
@ -26,13 +26,19 @@
|
|||
User Views
|
||||
"""
|
||||
|
||||
import formalchemy
|
||||
from formalchemy import Field
|
||||
from formalchemy.fields import SelectFieldRenderer
|
||||
|
||||
from edbob.pyramid.views import users
|
||||
|
||||
from . import SearchableAlchemyGridView, CrudView
|
||||
from ..forms import PersonFieldRenderer
|
||||
from rattail.db.model import User, Person
|
||||
from ..db import Session
|
||||
from rattail.db.model import User, Person, Role
|
||||
from edbob.db.auth import guest_role
|
||||
|
||||
from webhelpers.html import tags
|
||||
from webhelpers.html import HTML
|
||||
|
||||
|
||||
class UsersGrid(SearchableAlchemyGridView):
|
||||
|
@ -84,6 +90,49 @@ class UsersGrid(SearchableAlchemyGridView):
|
|||
return g
|
||||
|
||||
|
||||
class RolesField(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):
|
||||
return Session.query(Role.name, Role.uuid)\
|
||||
.filter(Role.uuid != guest_role(Session()).uuid)\
|
||||
.order_by(Role.name)\
|
||||
.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]
|
||||
|
||||
|
||||
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 UserCrud(CrudView):
|
||||
|
||||
mapped_class = User
|
||||
|
@ -98,10 +147,10 @@ class UserCrud(CrudView):
|
|||
self.request.route_url('people.autocomplete')))
|
||||
|
||||
fs.append(users.PasswordField('password'))
|
||||
fs.append(formalchemy.Field(
|
||||
'confirm_password', renderer=users.PasswordFieldRenderer))
|
||||
fs.append(users.RolesField(
|
||||
'roles', renderer=users.RolesFieldRenderer(self.request)))
|
||||
fs.append(Field('confirm_password',
|
||||
renderer=users.PasswordFieldRenderer))
|
||||
fs.append(RolesField(
|
||||
'roles', renderer=RolesFieldRenderer(self.request)))
|
||||
|
||||
fs.configure(
|
||||
include=[
|
||||
|
|