more batch improvements (save point)

This commit is contained in:
Lance Edgar 2012-04-16 00:51:12 -05:00
parent 523d7de3e7
commit 675c4deb63
12 changed files with 327 additions and 162 deletions

View file

@ -5,8 +5,9 @@
<%def name="menu()">
<p>${h.link_to("Back to Batches", url('batch.list'))}</p>
% if fieldset.edit and fieldset.model.rowcount:
% if fieldset.edit:
<p>${h.link_to("View Batch Details", url('batch.details', uuid=fieldset.model.uuid))}</p>
<p>${h.link_to("Execute this Batch", url('batch.execute', uuid=fieldset.model.uuid))}</p>
% endif
</%def>

View file

@ -4,7 +4,7 @@
<%def name="title()">Batch Dictionaries</%def>
<%def name="menu()">
<p>${h.link_to("Back to Batches", url('batches'))}</p>
<p>${h.link_to("Back to Batches", url('batch.list'))}</p>
## <p>${h.link_to("Create a New Dictionary", url('batch_dictionary'))}</p>
</%def>

View file

@ -2,10 +2,8 @@
<%inherit file="/crud.mako" />
<%def name="menu()">
<p>${h.link_to("Back to Batches", url('batches'))}</p>
<p>${h.link_to("Back to Batches", url('batch.list'))}</p>
<p>${h.link_to("Back to Dictionaries", url('batch_dictionaries'))}</p>
</%def>
${parent.body()}
${columns|n}

View file

@ -3,9 +3,34 @@
<%def name="title()">Batches</%def>
<%def name="head_tags()">
<style type="text/css">
div.grid table tbody td.rowcount {
text-align: right;
}
</style>
<script language="javascript" type="text/javascript">
$(function() {
$('div.grid table tbody td.action.execute a').live('click', function() {
var tr = $(this).parents('tr:first');
var desc = tr.find('td.description').text();
if (confirm("Do you really wish to execute this batch?\n\n" + desc)) {
var url = '${url('batch.execute', uuid='{uuid}')}';
location.href = url.replace(/%7Buuid%7D/, get_uuid(this));
}
return false;
});
});
</script>
</%def>
<%def name="menu()">
## <p>${h.link_to("Create a New Batch", url('batch.new'))}</p>
<p>${h.link_to("Manage Terminals", url('batch_terminal.list'))}</p>
<p>${h.link_to("Manage Terminals", url('batch_terminals'))}</p>
<p>${h.link_to("View Dictionaries", url('batch_dictionaries'))}</p>
<p>${h.link_to("SIL Columns", url('sil_columns'))}</p>
</%def>

View file

@ -2,7 +2,7 @@
<%inherit file="/crud.mako" />
<%def name="menu()">
<p>${h.link_to("Back to Batches", url('batches'))}</p>
<p>${h.link_to("Back to Batches", url('batch.list'))}</p>
<p>${h.link_to("Back to SIL Columns", url('sil_columns'))}</p>
</%def>

View file

@ -4,7 +4,7 @@
<%def name="title()">SIL Columns</%def>
<%def name="menu()">
<p>${h.link_to("Back to Batches", url('batches'))}</p>
<p>${h.link_to("Back to Batches", url('batch.list'))}</p>
</%def>
${parent.body()}

View file

@ -3,7 +3,7 @@
<%def name="menu()">
<p>${h.link_to("Back to Batches", url('batch.list'))}</p>
<p>${h.link_to("Back to Batch Terminals", url('batch_terminal.list'))}</p>
<p>${h.link_to("Back to Batch Terminals", url('batch_terminals'))}</p>
</%def>
${parent.body()}

View file

@ -1,9 +1,7 @@
<%inherit file="/products/base.mako" />
<%inherit file="/index.mako" />
<%def name="title()">Products</%def>
<%def name="menu()">
<p>${h.link_to("New Batch from Results", url('products.batch'))}</p>
</%def>
<%def name="title()">Products</%def>
${parent.body()}

96
rattail/pyramid/util.py Normal file
View file

@ -0,0 +1,96 @@
#!/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.util`` -- Utilities
"""
from pyramid import threadlocal
from webhelpers.html import tags
import edbob
from edbob.pyramid import Session
import rattail
def get_column(sil_name):
"""
Returns the :class:`rattail.SilColumn` instance with the given SIL name.
"""
q = Session.query(rattail.SilColumn)
q = q.filter(rattail.SilColumn.sil_name == sil_name)
if q.count() == 1:
return q.one()
def get_dictionary(name, flash=False):
"""
Returns the :class:`rattail.BatchDictionary` instance with the given name.
"""
q = Session.query(rattail.BatchDictionary)
q = q.filter(rattail.BatchDictionary.name == name)
if q.count() == 1:
return q.one()
if flash:
request = threadlocal.get_current_request()
dct = tags.link_to("Batch Dictionary", request.route_url('batch_dictionaries'))
request.session.flash("Hm, I couldn't find the '%s' %s." % (name, dct))
def get_terminal(key=None, default='rattail', title="Rattail"):
"""
Returns the :class:`rattail.BatchTerminal` instance with the given SIL ID.
If ``key`` is specified, it will be used to obtain the SIL ID from config.
If no key is given, or config contains no appropriate value, then
``default`` will be used as the SIL ID.
``title`` is used for a flash message, should no such terminal be found.
.. highlight:: ini
Given a ``key`` value of ``'products'``, a SIL ID of ``'rattail.locsms'``
should be configured like this::
[rattail.pyramid]
batch_terminal.products = rattail.locsms
"""
if key:
sil_id = edbob.config.get('rattail.pyramid', 'batch_terminal.%s' % key,
default=default)
assert sil_id
q = Session.query(rattail.BatchTerminal)
q = q.filter(rattail.BatchTerminal.sil_id == sil_id)
if q.count() == 1:
return q.one()
request = threadlocal.get_current_request()
terminal = tags.link_to("Batch Terminal", request.route_url('batch_terminals'))
request.session.flash("Hm, I couldn't find a %s for %s." % (terminal, title))

View file

@ -47,6 +47,7 @@ from edbob.util import prettify
import rattail
from rattail import sil
from rattail.batches import next_batch_id
from rattail.pyramid import util
from rattail.pyramid.forms import (
BatchIdFieldRenderer,
unique_batch_terminal_id,
@ -91,9 +92,8 @@ def sil_columns(context, request):
columns = grids.get_pager(query, config)
g = forms.AlchemyGrid(
rattail.SilColumn, columns, config,
request.route_url('sil_columns'),
url_object=request.route_url('sil_column'),
url_delete=request.route_url('sil_column'))
gridurl=request.route_url('sil_columns'),
objurl='sil_column', delurl='sil_column')
g.configure(
include=[
@ -171,9 +171,8 @@ def batch_dictionaries(context, request):
dictionaries = grids.get_pager(query, config)
g = forms.AlchemyGrid(
rattail.BatchDictionary, dictionaries, config,
request.route_url('batch_dictionaries'),
url_object=request.route_url('batch_dictionary'),
url_delete=request.route_url('batch_dictionary'))
gridurl=request.route_url('batch_dictionaries'),
objurl='batch_dictionary', delurl='batch_dictionary')
g.configure(
include=[
@ -302,7 +301,6 @@ def _terminal_columns(request, terminal):
def query(config):
q = Session.query(rattail.BatchTerminalColumn)
# q = q.join(rattail.BatchDictionary)
q = q.options(joinedload(rattail.BatchTerminalColumn.dictionary))
q = q.options(joinedload(rattail.BatchTerminalColumn.sil_column))
q = q.filter(rattail.BatchTerminalColumn.terminal_uuid == terminal.uuid)
@ -343,78 +341,63 @@ def terminal_columns(context, request):
def update_terminal_info(terminal):
"""
Updates the terminal's list of supported source and target columns, based
on feedback from the actual :class:`rattail.batch.BatchTerminal` instance.
Also sets the terminal's ``functional`` flag (et al.) to reflect reality.
This function updates a :class:`rattail.BatchTerminal` instance to reflect
the reality found by way of inspecting its underlying
:class:`rattail.batches.BatchTerminal` instance.
"""
terminal.functional = True
# Assume the worst.
terminal.functional = False
del terminal.columns[:]
# Fetch "real" terminal instance, if we can.
try:
true_terminal = terminal.get_terminal()
except LoadSpecError, err:
terminal.functional = False
return err
else:
if true_terminal:
# for column_type in ('source', 'target'):
# declared_columns = getattr(true_terminal, '%s_columns' % column_type)
# stored_columns = getattr(terminal, '%s_columns' % column_type)
# cls = getattr(rattail, 'BatchTerminal%sColumn' % column_type.title())
# for dictionary, columns in declared_columns.iteritems():
# q = Session.query(rattail.BatchDictionary)
# q = q.filter_by(name=dictionary)
# dictionary = q.one()
# for col in columns:
# q = Session.query(rattail.SilColumn)
# q = q.filter_by(sil_name=col)
# col = q.one()
# stored_columns.append(cls(
# dictionary=dictionary,
# column=col))
# setattr(terminal, column_type, bool(declared_columns))
stored_columns = {}
for col in terminal.columns:
stored_columns[col.sil_column.sil_name] = col
for column_type in ('source', 'target'):
declared_columns = getattr(true_terminal, '%s_columns' % column_type)
for dictionary, columns in declared_columns.iteritems():
q = Session.query(rattail.BatchDictionary)
q = q.filter_by(name=dictionary)
dictionary = q.one()
for col in columns:
q = Session.query(rattail.SilColumn)
q = q.filter_by(sil_name=col)
col = q.one()
if col.sil_name in stored_columns:
setattr(stored_columns[col.sil_name], column_type, True)
else:
col = rattail.BatchTerminalColumn(
dictionary=dictionary,
sil_column=col)
setattr(col, column_type, True)
terminal.columns.append(col)
stored_columns[col.sil_column.sil_name] = col
# Session.add(col)
Session.flush()
# stored_columns.append(rattail.BatchTerminalColumn(
# dictionary=dictionary,
# sil_column=col))
setattr(terminal, column_type, bool(declared_columns))
for col in terminal.columns:
if not col.source:
col.source = False
if not col.target:
col.target = False
return true_terminal
else:
terminal.functional = False
# Spec was fine but still no terminal? (This may not be needed?)
if not true_terminal:
return
# Okay, we have *something*.
terminal.functional = True
if true_terminal.source_columns:
terminal.source = True
for dct, cols in true_terminal.source_columns.iteritems():
dct = util.get_dictionary(dct)
for col in cols:
col = rattail.BatchTerminalColumn(
dictionary=dct,
sil_column=util.get_column(col),
source=True,
target=False)
terminal.columns.append(col)
Session.flush()
if true_terminal.target_columns:
terminal.target = True
for dct, cols in true_terminal.target_columns.iteritems():
dct = util.get_dictionary(dct)
for col in cols:
col = rattail.BatchTerminalColumn(
dictionary=dct,
sil_column=util.get_column(col),
source=False,
target=True)
terminal.columns.append(col)
Session.flush()
# Really, why?
return true_terminal
def terminal_fieldset(terminal, request):
fs = forms.make_fieldset(
terminal, crud_title="Batch Terminal",
url_action=request.current_route_url(),
url_cancel=request.route_url('batch_terminal.list'))
url_cancel=request.route_url('batch_terminals'))
fs.configure(
include=[
@ -422,13 +405,11 @@ def terminal_fieldset(terminal, request):
fs.description,
fs.class_spec.validate(forms.required),
fs.functional.readonly(),
# fs.source_kwargs,
# fs.target_kwargs,
])
# if not fs.edit:
# del fs.source_kwargs
# del fs.target_kwargs
if not fs.edit:
del fs.functional
return fs
@ -508,7 +489,7 @@ def delete_terminal(context, request):
assert terminal
with transaction.manager:
Session.delete(terminal)
return HTTPFound(location=request.route_url('batch_terminal.list'))
return HTTPFound(location=request.route_url('batch_terminals'))
# @view_config(route_name='batch_terminal', renderer='/batches/terminal.mako')
@ -563,7 +544,7 @@ def delete_terminal(context, request):
# pre_render=pre_render, post_sync=post_sync)
@view_config(route_name='batch_terminal.list', renderer='/batches/terminals.mako')
@view_config(route_name='batch_terminals', renderer='/batches/terminals.mako')
def terminals(context, request):
fmap = filters.get_filter_map(
@ -572,7 +553,7 @@ def terminals(context, request):
ilike=['description'])
config = filters.get_search_config(
'batch_terminal.list', request, fmap,
'batch_terminals', request, fmap,
include_filter_description=True,
filter_type_description='lk')
@ -580,7 +561,7 @@ def terminals(context, request):
config, sil_id="SIL ID")
config = grids.get_grid_config(
'batch_terminal.list', request, search,
'batch_terminals', request, search,
filter_map=fmap, sort='description', deletable=True)
smap = grids.get_sort_map(
@ -596,7 +577,7 @@ def terminals(context, request):
terminals = grids.get_pager(query, config)
g = forms.AlchemyGrid(
rattail.BatchTerminal, terminals, config,
gridurl=request.route_url('batch_terminal.list'),
gridurl=request.route_url('batch_terminals'),
objurl='batch_terminal.edit',
delurl='batch_terminal.delete')
@ -633,12 +614,14 @@ def batches(context, request):
config = grids.get_grid_config(
'batches', request, search,
filter_map=fmap, deletable=True, sort='target')
filter_map=fmap, deletable=True, sort='target',
actions=['Execute'])
smap = grids.get_sort_map(
rattail.Batch,
['source_description', 'batch_id', 'action_type',
'description', 'rowcount', 'effective'],
# 'description', 'rowcount', 'effective'],
'description', 'rowcount'],
target=grids.sorter(rattail.BatchTerminal.description))
def query(config):
@ -672,7 +655,7 @@ def batches(context, request):
g.action_type.with_renderer(forms.EnumFieldRenderer(rattail.BATCH_ACTION_TYPE)).label("Action"),
g.description,
g.rowcount.label("Rows"),
g.effective.with_renderer(forms.PrettyDateTimeFieldRenderer),
# g.effective.with_renderer(forms.PrettyDateTimeFieldRenderer),
])
g.readonly = True
@ -692,7 +675,7 @@ def _batch_columns(request, batch):
smap = grids.get_sort_map(
rattail.BatchColumn,
['ordinal', 'targeted'])
smap['column'] = grids.sorter(rattail.SilColumn.sil_name)
smap['sil_column'] = grids.sorter(rattail.SilColumn.sil_name)
smap['display'] = grids.sorter(rattail.SilColumn.display)
smap['source'] = grids.sorter(rattail.BatchTerminal.description)
@ -741,10 +724,7 @@ def batch_fieldset(batch, request):
# Remove unsupported action types...for now.
enum = rattail.BATCH_ACTION_TYPE.copy()
del enum[rattail.BATCH_ADD_REPLACE]
del enum[rattail.BATCH_CHANGE]
del enum[rattail.BATCH_LOAD]
del enum[rattail.BATCH_REMOVE]
fs.configure(
include=[
@ -754,20 +734,21 @@ def batch_fieldset(batch, request):
fs.dictionary.validate(forms.required),
fs.action_type.with_renderer(forms.EnumFieldRenderer(enum)).validate(forms.required).label("Action"),
fs.description,
fs.effective.with_renderer(forms.PrettyDateTimeFieldRenderer),
# fs.effective.with_renderer(forms.PrettyDateTimeFieldRenderer),
fs.rowcount.label("Rows").readonly(),
])
if fs.edit:
fs.source.set(readonly=True)
fs.dictionary.set(readonly=True)
if fs.model.target:
fs.target.set(readonly=True)
fs.action_type.set(readonly=True)
fs.effective.set(readonly=True)
# if fs.model.target:
# fs.target.set(readonly=True)
# fs.action_type.set(readonly=True)
# fs.effective.set(readonly=True)
fs.append(forms.ChildGridField('columns', _batch_columns(request, fs.model)))
else:
del fs.batch_id
del fs.effective
# del fs.effective
del fs.rowcount
return fs
@ -995,38 +976,13 @@ def execute(context, request):
Executes a batch.
"""
home = HTTPFound(location=request.route_url('batch.list'))
# batch = self.request.params.get('uuid')
# batch = self.Session.query(rattail.Batch).get(batch) if batch else None
# if not batch:
# return home
# print 'got a batch'
uuid = request.matchdict['uuid']
home = HTTPFound(location=request.route_url('batch.list'))
with transaction.manager:
batch = Session.query(rattail.Batch).get(uuid) if uuid else None
if not batch:
return home
# print 'got a batch'
# jct = batch.target_junction
# if not jct:
# self.request.session.flash("Batch does not have a valid target")
# return home
# print 'got a target'
# if not batch.target:
# request.session.flash("Batch does not have a valid target")
# return home
# jct = jct()
# if batch.dictionary == rattail.BATCH_MAIN_ITEM:
# print 'exporting main item'
# jct.export_main_item(batch, self.Session)
# print 'exported main item'
batch.execute()
# table = batch.table
@ -1053,10 +1009,10 @@ def includeme(config):
config.add_route('batch_dictionary', '/batches/dictionary')
config.add_route('batch_dictionary.columns', '/batches/dictionary-columns')
config.add_route('batch_terminal.list', '/batches/terminals')
config.add_route('batch_terminal.new', '/batches/terminal')
config.add_route('batch_terminal.delete', '/batches/terminal/{uuid}/delete')
config.add_route('batch_terminals', '/batches/terminals')
config.add_route('batch_terminal.new', '/batches/terminal/new')
config.add_route('batch_terminal.edit', '/batches/terminal/{uuid}/edit')
config.add_route('batch_terminal.delete', '/batches/terminal/{uuid}/delete')
config.add_route('batch_terminal.columns', '/batches/terminal/{uuid}/columns')
# config.add_route('batch_terminal', '/batches/terminal/{uuid}')

View file

@ -26,6 +26,8 @@
``rattail.pyramid.views.departments`` -- Department Views
"""
import transaction
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from edbob.pyramid import filters
@ -36,7 +38,7 @@ from edbob.pyramid import Session
import rattail
@view_config(route_name='department.list', renderer='/departments/index.mako')
@view_config(route_name='departments.list', renderer='/departments/index.mako')
def list_departments(context, request):
fmap = filters.get_filter_map(
@ -45,15 +47,15 @@ def list_departments(context, request):
ilike=['name'])
config = filters.get_search_config(
'department.list', request, fmap,
'departments.list', request, fmap,
include_filter_name=True,
filter_type_name='lk')
search = filters.get_search_form(config)
config = grids.get_grid_config(
'department.list', request, search,
filter_map=fmap, sort='name')
'departments.list', request, search,
filter_map=fmap, sort='name', deletable=True)
smap = grids.get_sort_map(
rattail.Department,
@ -68,7 +70,9 @@ def list_departments(context, request):
departments = grids.get_pager(query, config)
g = forms.AlchemyGrid(
rattail.Department, departments, config,
gridurl=request.route_url('department.list'))
gridurl=request.route_url('departments.list'),
delurl='department.delete',
)
g.configure(
include=[
@ -77,10 +81,25 @@ def list_departments(context, request):
],
readonly=True)
grid = g.render(class_='clickable departments')
grid = g.render(class_='hoverable departments')
return grids.render_grid(request, grid, search)
@view_config(route_name='department.delete')
def delete_department(context, request):
uuid = request.matchdict['uuid']
dept = Session.query(rattail.Department).get(uuid) if uuid else None
assert dept
with transaction.manager:
q = Session.query(rattail.Product)
q = q.filter(rattail.Product.department_uuid == dept.uuid)
if q.count():
q.update({'department_uuid': None}, synchronize_session=False)
Session.delete(dept)
return HTTPFound(location=request.route_url('departments.list'))
def includeme(config):
config.add_route('department.list', '/departments')
config.add_route('departments.list', '/departments')
config.add_route('department.delete', '/department/{uuid}/delete')
config.scan(__name__)

View file

@ -28,6 +28,8 @@
from sqlalchemy.orm import joinedload
import transaction
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from edbob.pyramid import filters
@ -36,59 +38,79 @@ from edbob.pyramid import grids
from edbob.pyramid import Session
import rattail
from rattail.batches import next_batch_id
from rattail.pyramid import util
from rattail.pyramid.forms import UpcFieldRenderer
@view_config(route_name='products', renderer='/products/index.mako')
def products(context, request):
fmap = filters.get_filter_map(
def filter_map():
return filters.get_filter_map(
rattail.Product,
exact=['upc'],
ilike=['description'],
ilike=['description', 'size'],
department=filters.filter_ilike(rattail.Department.name),
brand=filters.filter_ilike(rattail.Brand.name))
config = filters.get_search_config(
'products', request, fmap,
def search_config(request, fmap):
return filters.get_search_config(
'products.list', request, fmap,
include_filter_brand=True,
filter_type_brand='lk',
include_filter_description=True,
filter_type_description='lk')
filter_type_description='lk',
include_filter_department=True,
filter_type_department='lk')
search = filters.get_search_form(
def search_form(config):
return filters.get_search_form(
config, upc="UPC")
config = grids.get_grid_config(
'products', request, search,
def grid_config(request, search, fmap):
return grids.get_grid_config(
'products.list', request, search,
filter_map=fmap, sort='description')
smap = grids.get_sort_map(
def sort_map():
return grids.get_sort_map(
rattail.Product,
['upc', 'description'],
['upc', 'description', 'size'],
department=grids.sorter(rattail.Department.name),
brand=grids.sorter(rattail.Brand.name))
def query(config):
jmap = {
'brand':
lambda q: q.join(rattail.Brand),
'department': lambda q: q.outerjoin(rattail.Department),
'brand': lambda q: q.outerjoin(rattail.Brand),
}
smap = sort_map()
q = Session.query(rattail.Product)
q = q.options(joinedload(rattail.Product.department))
q = q.options(joinedload(rattail.Product.brand))
q = filters.filter_query(q, config, jmap)
q = grids.sort_query(q, config, smap, jmap)
return q
def query(config):
q = Session.query(rattail.Product)
q = q.options(joinedload(rattail.Product.brand))
q = filters.filter_query(q, config, jmap)
q = grids.sort_query(q, config, smap, jmap)
return q
@view_config(route_name='products.list', renderer='/products/index.mako')
def products(context, request):
fmap = filter_map()
config = search_config(request, fmap)
search = search_form(config)
config = grid_config(request, search, fmap)
products = grids.get_pager(query, config)
g = forms.AlchemyGrid(
rattail.Product, products, config,
request.route_url('products'))
gridurl=request.route_url('products.list'))
g.configure(
include=[
g.upc.with_renderer(UpcFieldRenderer).label("UPC"),
g.brand,
g.description,
g.size,
g.department,
],
readonly=True)
@ -96,6 +118,56 @@ def products(context, request):
return grids.render_grid(request, grid, search)
@view_config(route_name='products.batch')
def batch(context, request):
fmap = filter_map()
config = search_config(request, fmap)
search = search_form(config)
config = grid_config(request, search, fmap)
products = query(config)
home = HTTPFound(location=request.route_url('products.list'))
source = util.get_terminal('rattail')
if not source:
return home
dct = util.get_dictionary('ITEM_DCT')
if not dct:
return home
with transaction.manager:
batch = rattail.Batch()
Session.add(batch)
batch.source = source
batch.source_description = source.description
batch.batch_id = next_batch_id(source.sil_id, consume=True,
session=Session())
batch.name = '%s.%08u' % (source.sil_id, batch.batch_id)
batch.dictionary = dct
batch.action_type = rattail.BATCH_ADD
batch.description = "products from Rattail"
for i, col in enumerate(source.source_columns(dct), 1):
batch.columns.append(rattail.BatchColumn(
ordinal=i,
sil_column=col.sil_column,
source=source,
targeted=True,
))
batch.create_table()
batch.add_rows(source, dct, query=products)
batch.rowcount = products.count()
url = request.route_url('batch.edit', uuid=batch.uuid)
return HTTPFound(location=url)
def includeme(config):
config.add_route('products', '/products')
config.add_route('products.list', '/products')
config.add_route('products.batch', '/products/batch')
config.scan(__name__)