diff --git a/tailbone/app.py b/tailbone/app.py
index 4ac508a4..55f85c7b 100644
--- a/tailbone/app.py
+++ b/tailbone/app.py
@@ -31,7 +31,7 @@ from pyramid.config import Configurator
import os.path
import edbob
-import edbob.db
+from sqlalchemy import engine_from_config
from .db import Session
from zope.sqlalchemy import ZopeTransactionExtension
@@ -55,12 +55,11 @@ def main(global_config, **settings):
# Initialize edbob, dammit.
edbob.init('rattail', os.path.abspath(settings['edbob.config']))
- edbob.init_modules(['edbob.time', 'edbob.db', 'rattail.db'])
+ edbob.init_modules(['edbob.time'])
- # 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)
+ # Configure the primary database session.
+ engine = engine_from_config(settings)
+ Session.configure(bind=engine)
Session.configure(extension=ZopeTransactionExtension())
# Configure user authentication / authorization.
diff --git a/tailbone/auth.py b/tailbone/auth.py
index 84bc9489..fe7c06d6 100644
--- a/tailbone/auth.py
+++ b/tailbone/auth.py
@@ -32,7 +32,7 @@ from pyramid.security import Everyone, Authenticated
from .db import Session
from rattail.db.model import User
-from edbob.db.auth import has_permission
+from rattail.db.auth import has_permission
@implementer(IAuthorizationPolicy)
@@ -43,9 +43,9 @@ class TailboneAuthorizationPolicy(object):
if userid not in (Everyone, Authenticated):
user = Session.query(User).get(userid)
assert user
- return has_permission(user, permission)
+ return has_permission(Session(), user, permission)
if Everyone in principals:
- return has_permission(None, permission, session=Session())
+ return has_permission(Session(), None, permission)
return False
def principals_allowed_by_permission(self, context, permission):
diff --git a/tailbone/static/css/login.css b/tailbone/static/css/login.css
new file mode 100644
index 00000000..19efe6b2
--- /dev/null
+++ b/tailbone/static/css/login.css
@@ -0,0 +1,34 @@
+
+/******************************
+ * login.css
+ ******************************/
+
+#logo {
+ margin: auto;
+}
+
+div.form {
+ margin: auto;
+ float: none;
+ text-align: center;
+}
+
+div.field-wrapper {
+ margin: 10px auto;
+ width: 300px;
+}
+
+div.field-wrapper label {
+ margin: 0px;
+ padding-top: 3px;
+ text-align: right;
+ width: 100px;
+}
+
+div.field-wrapper input {
+ width: 150px;
+}
+
+div.buttons input {
+ margin: auto 5px;
+}
diff --git a/tailbone/static/js/login.js b/tailbone/static/js/login.js
new file mode 100644
index 00000000..ec3bcd8e
--- /dev/null
+++ b/tailbone/static/js/login.js
@@ -0,0 +1,24 @@
+
+$(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();
+
+});
diff --git a/tailbone/static/js/tailbone.js b/tailbone/static/js/tailbone.js
index 74c7cece..5ac3c049 100644
--- a/tailbone/static/js/tailbone.js
+++ b/tailbone/static/js/tailbone.js
@@ -105,6 +105,13 @@ $(function() {
autoExpand: true
});
+ /*
+ * Fix buttons.
+ */
+ $('button').button();
+ $('input[type=submit]').button();
+ $('input[type=reset]').button();
+
/*
* When filter labels are clicked, (un)check the associated checkbox.
*/
diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py
index b02800ad..b9f6557f 100644
--- a/tailbone/subscribers.py
+++ b/tailbone/subscribers.py
@@ -33,7 +33,7 @@ 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
+from rattail.db.auth import has_permission
def before_render(event):
@@ -70,12 +70,12 @@ def context_found(event):
request.user = Session.query(User).get(uuid)
def has_perm(perm):
- return has_permission(request.user, perm, session=Session())
+ return has_permission(Session(), request.user, perm)
request.has_perm = has_perm
def has_any_perm(perms):
for perm in perms:
- if has_permission(request.user, perm, session=Session()):
+ if has_permission(Session(), request.user, perm):
return True
return False
request.has_any_perm = has_any_perm
diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako
new file mode 100644
index 00000000..095d6384
--- /dev/null
+++ b/tailbone/templates/form.mako
@@ -0,0 +1,13 @@
+<%inherit file="/base.mako" />
+
+<%def name="context_menu_items()">%def>
+
+
+
+
+
+ ${form.render()|n}
+
+
diff --git a/tailbone/templates/forms/fieldset.mako b/tailbone/templates/forms/fieldset.mako
new file mode 100644
index 00000000..0e5a6d4a
--- /dev/null
+++ b/tailbone/templates/forms/fieldset.mako
@@ -0,0 +1,39 @@
+<% _focus_rendered = False %>
+
+% for error in fieldset.errors.get(None, []):
+ ${error}
+% endfor
+
+% for field in fieldset.render_fields.itervalues():
+
+ % if field.requires_label:
+
+ % for error in field.errors:
+
${error}
+ % endfor
+ ${field.label_tag()|n}
+
+ ${field.render()|n}
+
+ % if 'instructions' in field.metadata:
+
${field.metadata['instructions']}
+ % endif
+
+
+ % 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):
+
+ <% _focus_rendered = True %>
+ % endif
+ % endif
+ % endif
+
+% endfor
diff --git a/tailbone/templates/forms/fieldset_readonly.mako b/tailbone/templates/forms/fieldset_readonly.mako
new file mode 100644
index 00000000..350a3151
--- /dev/null
+++ b/tailbone/templates/forms/fieldset_readonly.mako
@@ -0,0 +1,12 @@
+
+ % for field in fieldset.render_fields.itervalues():
+ % if field.requires_label:
+
+ ${field.label_tag()|n}
+
+ ${field.render_readonly()}
+
+
+ % endif
+ % endfor
+
diff --git a/tailbone/templates/forms/form.mako b/tailbone/templates/forms/form.mako
new file mode 100644
index 00000000..ea25d01b
--- /dev/null
+++ b/tailbone/templates/forms/form.mako
@@ -0,0 +1,15 @@
+
diff --git a/tailbone/templates/forms/form_readonly.mako b/tailbone/templates/forms/form_readonly.mako
new file mode 100644
index 00000000..96920421
--- /dev/null
+++ b/tailbone/templates/forms/form_readonly.mako
@@ -0,0 +1,3 @@
+
+ ${form.fieldset.render()|n}
+
diff --git a/tailbone/templates/grid.mako b/tailbone/templates/grid.mako
new file mode 100644
index 00000000..9c78db07
--- /dev/null
+++ b/tailbone/templates/grid.mako
@@ -0,0 +1,37 @@
+<%inherit file="/base.mako" />
+
+<%def name="context_menu_items()">%def>
+
+<%def name="form()">
+ % if search:
+ ${search.render()}
+ % else:
+
+ % endif
+%def>
+
+<%def name="tools()">%def>
+
+
+
+
+
+ ${grid}
+
+
diff --git a/tailbone/templates/grids/search.mako b/tailbone/templates/grids/search.mako
new file mode 100644
index 00000000..17fbabd2
--- /dev/null
+++ b/tailbone/templates/grids/search.mako
@@ -0,0 +1,36 @@
+
+ ${search.begin()}
+ ${search.hidden('filters', 'true')}
+ <% visible = [] %>
+ % for f in search.sorted_filters():
+
+ ${search.checkbox('include_filter_'+f.name)}
+
+ ${f.types_select()}
+
+ ${f.value_control()}
+
+
+ % if search.config.get('include_filter_'+f.name):
+ <% visible.append(f.name) %>
+ % endif
+ % endfor
+
+ ${search.add_filter(visible)}
+ ${search.submit('submit', "Search", style='display: none;' if not visible else None)}
+
+
+ ${search.end()}
+ % if visible:
+
+ % endif
+
diff --git a/tailbone/templates/login.mako b/tailbone/templates/login.mako
new file mode 100644
index 00000000..b895255a
--- /dev/null
+++ b/tailbone/templates/login.mako
@@ -0,0 +1,37 @@
+<%inherit file="/base.mako" />
+
+<%def name="title()">Login%def>
+
+<%def name="head_tags()">
+ ${parent.head_tags()}
+ ${h.javascript_link(request.static_url('tailbone:static/js/login.js'))}
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/login.css'))}
+%def>
+
+${h.image(request.static_url('tailbone:static/img/home_logo.png'), "Rattail Logo", id='logo')}
+
+
diff --git a/tailbone/templates/people/index.mako b/tailbone/templates/people/index.mako
new file mode 100644
index 00000000..77b7badf
--- /dev/null
+++ b/tailbone/templates/people/index.mako
@@ -0,0 +1,11 @@
+<%inherit file="/grid.mako" />
+
+<%def name="title()">People%def>
+
+<%def name="context_menu_items()">
+## % if request.has_perm('people.create'):
+## ${h.link_to("Create a new Person", url('person.new'))}
+## % endif
+%def>
+
+${parent.body()}
diff --git a/tailbone/templates/roles/crud.mako b/tailbone/templates/roles/crud.mako
new file mode 100644
index 00000000..863a773b
--- /dev/null
+++ b/tailbone/templates/roles/crud.mako
@@ -0,0 +1,18 @@
+<%inherit file="edbob.pyramid:templates/crud.mako" />
+
+<%def name="head_tags()">
+ ${parent.head_tags()}
+ ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/perms.css'))}
+%def>
+
+<%def name="context_menu_items()">
+ ${h.link_to("Back to Roles", url('roles'))}
+ % if form.readonly:
+ ${h.link_to("Edit this Role", url('role.update', uuid=form.fieldset.model.uuid))}
+ % elif form.updating:
+ ${h.link_to("View this Role", url('role.read', uuid=form.fieldset.model.uuid))}
+ % endif
+ ${h.link_to("Delete this Role", url('role.delete', uuid=form.fieldset.model.uuid), class_='delete')}
+%def>
+
+${parent.body()}
diff --git a/tailbone/templates/roles/index.mako b/tailbone/templates/roles/index.mako
new file mode 100644
index 00000000..49deacbd
--- /dev/null
+++ b/tailbone/templates/roles/index.mako
@@ -0,0 +1,11 @@
+<%inherit file="/grid.mako" />
+
+<%def name="title()">Roles%def>
+
+<%def name="context_menu_items()">
+ % if request.has_perm('roles.create'):
+ ${h.link_to("Create a new Role", url('role.create'))}
+ % endif
+%def>
+
+${parent.body()}
diff --git a/tailbone/templates/users/index.mako b/tailbone/templates/users/index.mako
new file mode 100644
index 00000000..c9f9918d
--- /dev/null
+++ b/tailbone/templates/users/index.mako
@@ -0,0 +1,11 @@
+<%inherit file="/grid.mako" />
+
+<%def name="title()">Users%def>
+
+<%def name="context_menu_items()">
+ % if request.has_perm('users.create'):
+ ${h.link_to("Create a new User", url('user.create'))}
+ % endif
+%def>
+
+${parent.body()}
diff --git a/tailbone/views/auth.py b/tailbone/views/auth.py
index d7d1a088..3e5acd1c 100644
--- a/tailbone/views/auth.py
+++ b/tailbone/views/auth.py
@@ -35,7 +35,7 @@ from ..forms.simpleform import FormRenderer
import edbob
from ..db import Session
-from edbob.db.auth import authenticate_user, set_user_password
+from rattail.db.auth import authenticate_user, set_user_password
class UserLogin(formencode.Schema):
@@ -58,9 +58,9 @@ def login(request):
form = Form(request, schema=UserLogin)
if form.validate():
- user = authenticate_user(form.data['username'],
- form.data['password'],
- session=Session())
+ user = authenticate_user(Session(),
+ form.data['username'],
+ form.data['password'])
if user:
request.session.flash("%s logged in at %s" % (
user.display_name,
diff --git a/tailbone/views/batches/rows.py b/tailbone/views/batches/rows.py
index eae3961d..1d5dece0 100644
--- a/tailbone/views/batches/rows.py
+++ b/tailbone/views/batches/rows.py
@@ -26,12 +26,11 @@
Batch Row Views
"""
+from .. import SearchableAlchemyGridView, CrudView
from pyramid.httpexceptions import HTTPFound
from ...db import Session
-from .. import SearchableAlchemyGridView, CrudView
-
-import rattail
+from rattail.db.model import Batch, LabelProfile
from ...forms import GPCFieldRenderer
@@ -41,8 +40,8 @@ def field_with_renderer(field, column):
field = field.with_renderer(GPCFieldRenderer)
elif column.sil_name == 'F95': # Shelf Tag Type
- q = Session.query(rattail.LabelProfile)
- q = q.order_by(rattail.LabelProfile.ordinal)
+ q = Session.query(LabelProfile)
+ q = q.order_by(LabelProfile.ordinal)
field = field.dropdown(options=[(x.description, x.code) for x in q])
return field
@@ -50,7 +49,7 @@ def field_with_renderer(field, column):
def BatchRowsGrid(request):
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:
return HTTPFound(location=request.route_url('batches'))
@@ -141,7 +140,7 @@ def batch_rows_delete(request):
def batch_row_crud(request, attr):
batch_uuid = request.matchdict['batch_uuid']
- batch = Session.query(rattail.Batch).get(batch_uuid)
+ batch = Session.query(Batch).get(batch_uuid)
if not batch:
return HTTPFound(location=request.route_url('batches'))
diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py
index edd6aaa3..ce6520e2 100644
--- a/tailbone/views/roles.py
+++ b/tailbone/views/roles.py
@@ -26,43 +26,121 @@
Role Views
"""
+from . import SearchableAlchemyGridView, CrudView
from pyramid.httpexceptions import HTTPFound
+from ..db import Session
+from rattail.db.model import Role
+from rattail.db.auth import has_permission, administrator_role, guest_role
+
import formalchemy
from webhelpers.html import tags
-from webhelpers.html.builder import HTML
-
-from edbob.db import auth
-
-from ..db import Session
-from . import SearchableAlchemyGridView, CrudView
-from rattail.db.model import Role
+from webhelpers.html import HTML
default_permissions = [
-
+ ("Batches", [
+ ('batches.list', "List Batches"),
+ ('batches.read', "View Batches"),
+ ('batches.create', "Create Batches"),
+ ('batches.update', "Edit Batches"),
+ ('batches.delete', "Delete Batches"),
+ ('batches.execute', "Execute Batches"),
+ ('batch_rows.read', "View Batch Rows"),
+ ('batch_rows.update', "Edit Batch Rows"),
+ ('batch_rows.delete', "Delete Batch Rows"),
+ ]),
+ ("Brands", [
+ ('brands.list', "List Brands"),
+ ('brands.read', "View Brands"),
+ ('brands.create', "Create Brands"),
+ ('brands.update', "Edit Brands"),
+ ('brands.delete', "Delete Brands"),
+ ('brands.force_sync', "Forcibly Sync Brands"),
+ ]),
+ ("Customers", [
+ ('customers.list', "List Customers"),
+ ('customers.read', "View Customers"),
+ ('customers.force_sync', "Forcibly Sync Customers"),
+ ('customer_groups.list', "List Customer Groups"),
+ ('customer_groups.read', "View Customer Groups"),
+ ('customer_groups.force_sync', "Forcibly Sync Customer Groups"),
+ ]),
+ ("Departments", [
+ ('departments.list', "List Departments"),
+ ('departments.read', "View Departments"),
+ ('departments.create', "Create Departments"),
+ ('departments.update', "Edit Departments"),
+ ('departments.delete', "Delete Departments"),
+ ('departments.force_sync', "Forcibly Sync Departments"),
+ ]),
+ ("Employees", [
+ ('employees.list', "List Employees"),
+ ('employees.force_sync', "Forcibly Sync Employees"),
+ ]),
+ ("Label Profiles", [
+ ('label_profiles.list', "List Label Profiles"),
+ ('label_profiles.view', "View Label Profiles"),
+ ('label_profiles.create', "Create Label Profiles"),
+ ('label_profiles.update', "Edit Label Profiles"),
+ ('label_profiles.delete', "Delete Label Profiles"),
+ ]),
("People", [
- ('people.list', "List People"),
- ('people.read', "View Person"),
- ('people.create', "Create Person"),
- ('people.update', "Edit Person"),
- ('people.delete', "Delete Person"),
+ ('people.list', "List People"),
+ ('people.read', "View People"),
+ ('people.create', "Create People"),
+ ('people.update', "Edit People"),
+ ('people.delete', "Delete People"),
+ ('people.force_sync', "Forcibly Sync People"),
+ ]),
+ ("Products", [
+ ('products.list', "List Products"),
+ ('products.read', "View Products"),
+ ('products.create', "Create Products"),
+ ('products.update', "Edit Products"),
+ ('products.delete', "Delete Products"),
+ ('products.print_labels', "Print Product Labels"),
+ ('products.force_sync', "Forcibly Sync Products"),
]),
-
("Roles", [
- ('roles.list', "List Roles"),
- ('roles.read', "View Role"),
- ('roles.create', "Create Role"),
- ('roles.update', "Edit Role"),
- ('roles.delete', "Delete Role"),
+ ('roles.list', "List Roles"),
+ ('roles.read', "View Roles"),
+ ('roles.create', "Create Roles"),
+ ('roles.update', "Edit Roles"),
+ ('roles.delete', "Delete Roles"),
+ ]),
+ ("Stores", [
+ ('stores.list', "List Stores"),
+ ('stores.read', "View Stores"),
+ ('stores.create', "Create Stores"),
+ ('stores.update', "Edit Stores"),
+ ('stores.delete', "Delete Stores"),
+ ('stores.force_sync', "Forcibly Sync Stores"),
+ ]),
+ ("Subdepartments", [
+ ('subdepartments.list', "List Subdepartments"),
+ ('subdepartments.read', "View Subdepartments"),
+ ('subdepartments.create', "Create Subdepartments"),
+ ('subdepartments.update', "Edit Subdepartments"),
+ ('subdepartments.delete', "Delete Subdepartments"),
+ ('subdepartments.force_sync', "Forcibly Sync Subdepartments"),
]),
-
("Users", [
- ('users.list', "List Users"),
- ('users.read', "View User"),
- ('users.create', "Create User"),
- ('users.update', "Edit User"),
- ('users.delete', "Delete User"),
+ ('users.list', "List Users"),
+ ('users.read', "View Users"),
+ ('users.create', "Create Users"),
+ ('users.update', "Edit Users"),
+ ('users.delete', "Delete Users"),
+ ('users.force_sync', "Forcibly Sync Users"),
+ ]),
+ ("Vendors", [
+ ('vendors.list', "List Vendors"),
+ ('vendors.read', "View Vendors"),
+ ('vendors.create', "Create Vendors"),
+ ('vendors.update', "Edit Vendors"),
+ ('vendors.delete', "Delete Vendors"),
+ ('vendors.import_catalog', "Import Vendor Catalogs"),
+ ('vendors.force_sync', "Forcibly Sync Vendors"),
]),
]
@@ -129,7 +207,7 @@ def PermissionsFieldRenderer(permissions, *args, **kwargs):
def _render(self, readonly=False, **kwargs):
role = self.field.model
- admin = auth.administrator_role(Session())
+ 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.")
@@ -140,8 +218,8 @@ def PermissionsFieldRenderer(permissions, *args, **kwargs):
for group, perms in self.permissions:
inner = HTML.tag('p', c=group)
for perm, title in perms:
- checked = auth.has_permission(
- role, perm, include_guest=False, session=Session())
+ checked = has_permission(
+ Session(), role, perm, include_guest=False)
if readonly:
span = HTML.tag('span', c="[X]" if checked else "[ ]")
inner += HTML.tag('p', class_='perm', c=span + ' ' + title)
@@ -179,8 +257,8 @@ class RoleCrud(CrudView):
return fs
def pre_delete(self, model):
- admin = auth.administrator_role(Session())
- guest = auth.guest_role(Session())
+ 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())
diff --git a/tailbone/views/users.py b/tailbone/views/users.py
index 6d537aed..e23dbb01 100644
--- a/tailbone/views/users.py
+++ b/tailbone/views/users.py
@@ -35,7 +35,7 @@ from . import SearchableAlchemyGridView, CrudView
from ..forms import PersonFieldRenderer
from ..db import Session
from rattail.db.model import User, Person, Role
-from edbob.db.auth import guest_role
+from rattail.db.auth import guest_role
from webhelpers.html import tags
from webhelpers.html import HTML