commit 81f6b57a63d0bca1073ae554705776e8e891b34c Author: Lance Edgar Date: Tue Apr 10 12:39:30 2012 -0500 initial commit (save point) diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 00000000..c58e4b8d --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,5 @@ + +0.3a1 +----- + +- Initial port to Rattail v0.3. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..a5c09028 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include rattail/pyramid *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..c71ce1e0 --- /dev/null +++ b/README.txt @@ -0,0 +1,11 @@ + +rattail.pyramid +=============== + +Rattail is a retail software framework based on `edbob `_, +and released under the GNU Affero General Public License. + +This package contains Pyramid views, etc., for managing a Rattail system. + +Please see Rattail's `home page `_ for more +information. diff --git a/rattail/__init__.py b/rattail/__init__.py new file mode 100644 index 00000000..3ad9513f --- /dev/null +++ b/rattail/__init__.py @@ -0,0 +1,2 @@ +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) diff --git a/rattail/pyramid/__init__.py b/rattail/pyramid/__init__.py new file mode 100644 index 00000000..a87d0d10 --- /dev/null +++ b/rattail/pyramid/__init__.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.pyramid`` -- Rattail's Pyramid Framework +""" + +# import os.path + +# import pyramid_beaker +# from pyramid.config import Configurator + +# import edbob + +from rattail.pyramid._version import __version__ + + +# def main(global_config, **settings): +# """ +# This function returns a Pyramid WSGI application. +# """ + +# # Here you can insert any code to modify the ``settings`` dict. +# # You can: +# # * Add additional keys to serve as constants or "global variables" in the +# # application. +# # * Set default values for settings that may have been omitted. +# # * Override settings that you don't want the user to change. +# # * Raise an exception if a setting is missing or invalid. +# # * Convert values from strings to their intended type. + +# settings['mako.directories'] = [ +# 'something:templates', +# 'edbob.pyramid:templates', +# ] + +# # Configure Pyramid +# config = Configurator(settings=settings) +# config.include('edbob.pyramid') +# config.include('something.subscribers') +# config.scan() + +# # Configure Beaker +# session_factory = pyramid_beaker.session_factory_from_settings(settings) +# config.set_session_factory(session_factory) +# pyramid_beaker.set_cache_regions_from_settings(settings) + +# # Configure edbob +# edbob.basic_logging() +# edbob.init('something', os.path.abspath(settings['edbob.config'])) + +# # Add static views +# # config.add_static_view('css', 'static/css', cache_max_age=3600) +# # config.add_static_view('img', 'static/img', cache_max_age=3600) +# # config.add_static_view('js', 'static/js', cache_max_age=3600) + +# return config.make_wsgi_app() diff --git a/rattail/pyramid/_version.py b/rattail/pyramid/_version.py new file mode 100644 index 00000000..7ca1d174 --- /dev/null +++ b/rattail/pyramid/_version.py @@ -0,0 +1 @@ +__version__ = '0.3a1' diff --git a/rattail/pyramid/forms.py b/rattail/pyramid/forms.py new file mode 100644 index 00000000..f0e8bb27 --- /dev/null +++ b/rattail/pyramid/forms.py @@ -0,0 +1,98 @@ +#!/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 . +# +################################################################################ + +""" +``rattail.pyramid.forms`` -- Rattail Forms +""" + +import formalchemy +# from formalchemy.fields import SelectFieldRenderer + +from edbob.pyramid import Session + +import rattail + + +__all__ = ['UpcFieldRenderer'] + + +class BatchIdFieldRenderer(formalchemy.FieldRenderer): + """ + Renders the :attr:`rattail.Batch.batch_id` field. + """ + + def render_readonly(self, **kwargs): + value = self.raw_value + if value is None: + return '' + return '%08u' % int(value) + + +# class BatchTerminalFieldRenderer(SelectFieldRenderer): +# """ +# Renders a field whose value is a relationship to a +# :class:`rattail.BatchTerminal` instance. +# """ + +# def render(self, options, **kwargs): + + + +class UpcFieldRenderer(formalchemy.TextFieldRenderer): + """ + Handles rendering for the product UPC field. + """ + + def render_readonly(self, **kwargs): + value = self.raw_value + if not value: + return '' + if isinstance(value, basestring): + if value.isdigit(): + value = int(value) + if isinstance(value, int): + return '%013u' % value + return self.stringify_value(value, as_html=True) + + +def unique_batch_terminal_id(value, field=None): + """ + .. highlight:: python + + Validator for the :class:`rattail.BatchTerminal` class to ensure that SIL + IDs are not duplicated. For example:: + + from rattail.pyramid.forms import unique_batch_terminal_id + + # fieldset = some_batch_terminal_fieldset_factory() + fieldset.sil_id.set(validate=unique_batch_terminal_id) + """ + + if value: + q = Session.query(rattail.BatchTerminal) + q = q.filter(rattail.BatchTerminal.sil_id == value) + if field.parent.edit: + q = q.filter(rattail.BatchTerminal.uuid != field.parent.model.uuid) + if q.count(): + raise formalchemy.ValidationError("SIL ID value must be unique within the system") diff --git a/rattail/pyramid/subscribers.py b/rattail/pyramid/subscribers.py new file mode 100644 index 00000000..25fa04a3 --- /dev/null +++ b/rattail/pyramid/subscribers.py @@ -0,0 +1,49 @@ +#!/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 . +# +################################################################################ + +""" +``rattail.pyramid.subscribers`` -- Event Subscribers +""" + +from pyramid import threadlocal + +import rattail + + +def before_render(event): + """ + Adds goodies to the global template renderer context: + + * ``rattail`` + """ + + request = event.get('request') or threadlocal.get_current_request() + + renderer_globals = event + renderer_globals['rattail'] = rattail + + +def includeme(config): + config.add_subscriber('rattail.pyramid.subscribers:before_render', + 'pyramid.events.BeforeRender') diff --git a/rattail/pyramid/templates/batches/base.mako b/rattail/pyramid/templates/batches/base.mako new file mode 100644 index 00000000..27f7dd90 --- /dev/null +++ b/rattail/pyramid/templates/batches/base.mako @@ -0,0 +1,2 @@ +<%inherit file="/base.mako" /> +${parent.body()} diff --git a/rattail/pyramid/templates/batches/batch.mako b/rattail/pyramid/templates/batches/batch.mako new file mode 100644 index 00000000..ba7f54e8 --- /dev/null +++ b/rattail/pyramid/templates/batches/batch.mako @@ -0,0 +1,13 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/crud.mako" /> + +<%def name="crud_name()">Batch + +<%def name="menu()"> +

${h.link_to("Back to Batches", url('batches'))}

+ % if fieldset.edit and fieldset.model.rowcount: +

${h.link_to("View Batch Details", url('batch.details', uuid=fieldset.model.uuid))}

+ % endif + + +${parent.body()} diff --git a/rattail/pyramid/templates/batches/columns.mako b/rattail/pyramid/templates/batches/columns.mako new file mode 100644 index 00000000..332298ca --- /dev/null +++ b/rattail/pyramid/templates/batches/columns.mako @@ -0,0 +1,9 @@ + + + + + + +
Columns + ${columns|n} +
diff --git a/rattail/pyramid/templates/batches/details.mako b/rattail/pyramid/templates/batches/details.mako new file mode 100644 index 00000000..b2763197 --- /dev/null +++ b/rattail/pyramid/templates/batches/details.mako @@ -0,0 +1,12 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">Batch : ${batch.name} + +<%def name="menu()"> +

${h.link_to("Back to Batches", url('batches'))}

+

${h.link_to("View Batch Properties", url('batch') + '?uuid=' + batch.uuid)}

+

${h.link_to("Execute this Batch", url('batch.execute', uuid=batch.uuid))}

+ + +${parent.body()} diff --git a/rattail/pyramid/templates/batches/dictionaries.mako b/rattail/pyramid/templates/batches/dictionaries.mako new file mode 100644 index 00000000..b4c0ac24 --- /dev/null +++ b/rattail/pyramid/templates/batches/dictionaries.mako @@ -0,0 +1,11 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">Batch Dictionaries + +<%def name="menu()"> +

${h.link_to("Back to Batches", url('batches'))}

+##

${h.link_to("Create a New Dictionary", url('batch_dictionary'))}

+ + +${parent.body()} diff --git a/rattail/pyramid/templates/batches/dictionary.mako b/rattail/pyramid/templates/batches/dictionary.mako new file mode 100644 index 00000000..9b2be47f --- /dev/null +++ b/rattail/pyramid/templates/batches/dictionary.mako @@ -0,0 +1,11 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/crud.mako" /> + +<%def name="menu()"> +

${h.link_to("Back to Batches", url('batches'))}

+

${h.link_to("Back to Dictionaries", url('batch_dictionaries'))}

+ + +${parent.body()} + +${columns|n} diff --git a/rattail/pyramid/templates/batches/dictionary_columns.mako b/rattail/pyramid/templates/batches/dictionary_columns.mako new file mode 100644 index 00000000..78f353e5 --- /dev/null +++ b/rattail/pyramid/templates/batches/dictionary_columns.mako @@ -0,0 +1,8 @@ + + + + + +
Supported Columns + ${grid|n} +
\ No newline at end of file diff --git a/rattail/pyramid/templates/batches/index.mako b/rattail/pyramid/templates/batches/index.mako new file mode 100644 index 00000000..93376d11 --- /dev/null +++ b/rattail/pyramid/templates/batches/index.mako @@ -0,0 +1,13 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">Batches + +<%def name="menu()"> +

${h.link_to("Create a New Batch", url('batch'))}

+

${h.link_to("Manage Terminals", url('batch_terminals'))}

+

${h.link_to("View Dictionaries", url('batch_dictionaries'))}

+

${h.link_to("SIL Columns", url('sil_columns'))}

+ + +${parent.body()} diff --git a/rattail/pyramid/templates/batches/sil_column.mako b/rattail/pyramid/templates/batches/sil_column.mako new file mode 100644 index 00000000..eb634d78 --- /dev/null +++ b/rattail/pyramid/templates/batches/sil_column.mako @@ -0,0 +1,9 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/crud.mako" /> + +<%def name="menu()"> +

${h.link_to("Back to Batches", url('batches'))}

+

${h.link_to("Back to SIL Columns", url('sil_columns'))}

+ + +${parent.body()} diff --git a/rattail/pyramid/templates/batches/sil_columns.mako b/rattail/pyramid/templates/batches/sil_columns.mako new file mode 100644 index 00000000..78164b6f --- /dev/null +++ b/rattail/pyramid/templates/batches/sil_columns.mako @@ -0,0 +1,10 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">SIL Columns + +<%def name="menu()"> +

${h.link_to("Back to Batches", url('batches'))}

+ + +${parent.body()} diff --git a/rattail/pyramid/templates/batches/supported_fields.mako b/rattail/pyramid/templates/batches/supported_fields.mako new file mode 100644 index 00000000..aba8b077 --- /dev/null +++ b/rattail/pyramid/templates/batches/supported_fields.mako @@ -0,0 +1,24 @@ + + + + + +## + + +

Supported Fields

${h.link_to("Update Field List", '#', id='update-fields')}
+ +${grid|n} + +## diff --git a/rattail/pyramid/templates/batches/terminal.mako b/rattail/pyramid/templates/batches/terminal.mako new file mode 100644 index 00000000..0c170461 --- /dev/null +++ b/rattail/pyramid/templates/batches/terminal.mako @@ -0,0 +1,13 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/crud.mako" /> + +<%def name="menu()"> +

${h.link_to("Back to Batches", url('batches'))}

+

${h.link_to("Back to Batch Terminals", url('batch_terminals'))}

+ + +${parent.body()} + +% if fieldset.edit: + ${terminal_columns|n} +% endif diff --git a/rattail/pyramid/templates/batches/terminal_columns.mako b/rattail/pyramid/templates/batches/terminal_columns.mako new file mode 100644 index 00000000..94d0bdae --- /dev/null +++ b/rattail/pyramid/templates/batches/terminal_columns.mako @@ -0,0 +1,17 @@ + +

Supported Columns

+ + + + + + + + + + +
Source + ${source|n} +
Target + ${target|n} +
diff --git a/rattail/pyramid/templates/batches/terminals.mako b/rattail/pyramid/templates/batches/terminals.mako new file mode 100644 index 00000000..96904409 --- /dev/null +++ b/rattail/pyramid/templates/batches/terminals.mako @@ -0,0 +1,11 @@ +<%inherit file="/batches/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">Batch Terminals + +<%def name="menu()"> +

${h.link_to("Back to Batches", url('batches'))}

+

${h.link_to("Create a New Terminal", url('batch_terminal'))}

+ + +${parent.body()} diff --git a/rattail/pyramid/templates/products/base.mako b/rattail/pyramid/templates/products/base.mako new file mode 100644 index 00000000..27f7dd90 --- /dev/null +++ b/rattail/pyramid/templates/products/base.mako @@ -0,0 +1,2 @@ +<%inherit file="/base.mako" /> +${parent.body()} diff --git a/rattail/pyramid/templates/products/index.mako b/rattail/pyramid/templates/products/index.mako new file mode 100644 index 00000000..f7b77f7a --- /dev/null +++ b/rattail/pyramid/templates/products/index.mako @@ -0,0 +1,9 @@ +<%inherit file="/products/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">Products + +<%def name="menu()"> + + +${parent.body()} diff --git a/rattail/pyramid/views/__init__.py b/rattail/pyramid/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rattail/pyramid/views/batches.py b/rattail/pyramid/views/batches.py new file mode 100644 index 00000000..187b11a2 --- /dev/null +++ b/rattail/pyramid/views/batches.py @@ -0,0 +1,858 @@ +#!/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 . +# +################################################################################ + +""" +``rattail.pyramid.views.batches`` -- Batch (et al.) Views +""" + +import formalchemy +from sqlalchemy import and_, or_ + +from pyramid.httpexceptions import HTTPFound +from pyramid.renderers import render +from pyramid.response import Response +from pyramid.view import view_config + +from edbob.exceptions import LoadSpecError +from edbob.pyramid import filters +from edbob.pyramid import forms +from edbob.pyramid import grids +from edbob.pyramid import Session +from edbob.pyramid.views.crud import crud +from edbob.util import prettify + +import rattail +from rattail import sil +from rattail.batches import next_batch_id +from rattail.pyramid.forms import ( + BatchIdFieldRenderer, + unique_batch_terminal_id, + ) + + +@view_config(route_name='sil_columns', renderer='/batches/sil_columns.mako') +def sil_columns(context, request): + """ + Index page for SIL columns (:class:`rattail.SilColumn`). + """ + + fmap = filters.get_filter_map( + rattail.SilColumn, + exact=['sil_name'], + ilike=['display']) + + config = filters.get_search_config( + 'sil_columns', request, fmap, + include_filter_sil_name=True, + filter_type_sil_name='eq', + include_filter_display=True, + filter_type_display='lk') + + search = filters.get_search_form( + config, sil_name="SIL Name") + + config = grids.get_grid_config( + 'sil_columns', request, search, + filter_map=fmap, sort='sil_name') + + smap = grids.get_sort_map( + rattail.SilColumn, + ['sil_name', 'display', 'data_type']) + + def query(config): + q = Session.query(rattail.SilColumn) + q = filters.filter_query(q, config) + q = grids.sort_query(q, config, smap) + return q + + 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')) + + g.configure( + include=[ + g.sil_name.label("SIL Name"), + g.display, + g.data_type, + ]) + + g.readonly = True + grid = g.render(class_='clickable batch-columns') + return grids.render_grid(request, grid, search) + + +@view_config(route_name='sil_column', renderer='/batches/sil_column.mako') +def sil_column(context, request): + + def fieldset(column): + fs = forms.make_fieldset( + column, crud_title="Batch Column", + url_action=request.route_url('sil_column'), + url_cancel=request.route_url('sil_columns')) + + fs.configure( + include=[ + fs.sil_name.label("SIL Name"), + fs.display, + fs.data_type, + ]) + + if fs.edit: + fs.sil_name.set(readonly=True) + return fs + + + return crud( + request, rattail.SilColumn, fieldset, + home=request.route_url('sil_columns'), + delete=request.route_url('sil_columns')) + + +@view_config(route_name='batch_dictionaries', renderer='/batches/dictionaries.mako') +def batch_dictionaries(context, request): + """ + Index page for batch dictionaries (:class:`rattail.BatchDictionary`). + """ + + fmap = filters.get_filter_map( + rattail.BatchDictionary, + exact=['name'], + ilike=['description']) + + config = filters.get_search_config( + 'batch_dictionaries', request, fmap, + include_filter_name=True, + filter_type_name='eq', + include_filter_description=True, + filter_type_description='lk') + + search = filters.get_search_form(config) + + config = grids.get_grid_config( + 'batch_dictionaries', request, search, + filter_map=fmap, sort='description') + + smap = grids.get_sort_map( + rattail.BatchDictionary, + ['name', 'description']) + + def query(config): + q = Session.query(rattail.BatchDictionary) + q = filters.filter_query(q, config) + q = grids.sort_query(q, config, smap) + return q + + 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')) + + g.configure( + include=[ + g.name, + g.description, + ]) + + g.readonly = True + grid = g.render(class_='clickable batch-dictionaries') + return grids.render_grid(request, grid, search) + + +def _dictionary_columns(request, uuid=None): + """ + Returns a rendered grid of columns for a batch dictionary. + """ + + if not uuid: + uuid = request.params.get('uuid') + dictionary = Session.query(rattail.BatchDictionary).get(uuid) if uuid else None + assert dictionary + + fmap = filters.get_filter_map( + rattail.BatchDictionaryColumn, + exact=['key']) + fmap['column'] = filters.filter_exact(rattail.SilColumn.sil_name) + fmap['display'] = filters.filter_ilike(rattail.SilColumn.display) + + config = grids.get_grid_config( + 'batch_dictionary.columns', request, + filter_map=fmap, sort='column') + + smap = grids.get_sort_map( + rattail.BatchDictionaryColumn, + ['key']) + smap['column'] = grids.sorter(rattail.SilColumn.sil_name) + smap['display'] = grids.sorter(rattail.SilColumn.display) + + def query(config): + q = Session.query(rattail.BatchDictionaryColumn) + q = q.join(rattail.SilColumn) + q = filters.filter_query(q, config) + q = grids.sort_query(q, config, smap) + return q + + columns = query(config) + url = (request.route_url('batch_dictionary.columns') + + '?uuid=' + dictionary.uuid) + g = forms.AlchemyGrid( + rattail.BatchDictionaryColumn, columns, config, url) + + g.append( + formalchemy.Field( + 'display', + value=lambda x: x.column.display)) + + g.configure( + include=[ + g.column, + g.display, + g.key, + ]) + + g.readonly = True + return g.render(class_='hoverable batch-dictionary-columns') + + +@view_config(route_name='batch_dictionary.columns') +def batch_dictionary_columns(context, request): + return Response(body=_dictionary_columns(request), + content_type='text/html') + + +@view_config(route_name='batch_dictionary', renderer='/batches/dictionary.mako') +def batch_dictionary(context, request): + + def fieldset(dictionary): + fs = forms.make_fieldset( + dictionary, crud_title="Batch Dictionary", + url_action=request.route_url('batch_dictionary'), + url_cancel=request.route_url('batch_dictionaries')) + + fs.configure( + include=[ + fs.name, + fs.description, + ]) + + fs.readonly = True + return fs + + + def pre_render(fs): + if fs.edit: + g = _dictionary_columns(request, fs.model.uuid) + return {'columns': + render('/batches/dictionary_columns.mako', {'grid':g})} + + return crud( + request, rattail.BatchDictionary, fieldset, + home=request.route_url('batch_dictionaries'), + delete=request.route_url('batch_dictionaries'), + pre_render=pre_render) + + +def _terminal_columns(request, terminal, column_type): + """ + Returns a rendered grid of either + :class:`rattail.BatchTerminalSourceColumn` or + :class:`rattail.BatchTerminalTargetColumn` instances, depending on + ``column_type`` (and of cource ``terminal``). + """ + + cls = getattr(rattail, 'BatchTerminal%sColumn' % column_type.title()) + + config = grids.get_grid_config( + 'batch_terminal.columns', request, sort='dictionary') + + smap = { + 'dictionary': grids.sorter(rattail.BatchDictionary.name), + 'column': grids.sorter(rattail.SilColumn.sil_name), + 'display': grids.sorter(rattail.SilColumn.display), + } + + def query(config): + q = Session.query(cls) + q = q.join(rattail.BatchDictionary) + q = q.join(rattail.SilColumn) + q = q.filter(cls.terminal == terminal) + q = grids.sort_query(q, config, smap) + return q + + url = (request.route_url('batch_terminal.columns') + + '?uuid=' + terminal.uuid + '&type=' + column_type) + + columns = query(config) + g = forms.AlchemyGrid(cls, columns, config, url) + + g.append( + formalchemy.Field( + 'display', + value=lambda x: x.column.display)) + + g.configure( + include=[ + g.dictionary, + g.column, + g.display, + ]) + + g.readonly = True + return g.render(class_='hoverable batch-terminal-columns') + + +@view_config(route_name='batch_terminal.columns') +def batch_terminal_columns(context, request): + uuid = request.params.get('uuid') + terminal = Session.query(rattail.BatchTerminal).get(uuid) if uuid else None + assert terminal + return Response( + body=_terminal_columns(request, terminal, request.params.get('type')), + content_type='text/html') + + +# @view_config(route_name='batch_terminal.source_columns') +# def batch_terminal_source_columns(context, request): +# return Response(body=_terminal_columns(request, 'source'), +# content_type='text/html') + + +# @view_config(route_name='batch_terminal.target_columns') +# def batch_terminal_target_columns(context, request): +# return Response(body=_terminal_columns(request, 'target'), +# content_type='text/html') + + +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. + """ + + terminal.functional = True + 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)) + return true_terminal + else: + terminal.functional = False + + +@view_config(route_name='batch_terminal', renderer='/batches/terminal.mako') +def batch_terminal(context, request): + + def fieldset(terminal): + fs = forms.make_fieldset( + terminal, crud_title="Batch Terminal", + url_action=request.route_url('batch_terminal'), + url_cancel=request.route_url('batch_terminals')) + + fs.configure( + include=[ + fs.sil_id.label("SIL ID").validate(unique_batch_terminal_id), + fs.description, + fs.class_spec.validate(forms.required), + fs.functional.readonly(), + fs.source_kwargs, + fs.target_kwargs, + ]) + + if fs.edit: + fs.sil_id.set(readonly=True) + else: + del fs.source_kwargs + del fs.target_kwargs + return fs + + def pre_render(fs): + if fs.edit: + data = { + 'source': _terminal_columns(request, fs.model, 'source'), + 'target': _terminal_columns(request, fs.model, 'target'), + } + return {'terminal_columns': + render('/batches/terminal_columns.mako', data)} + + def post_sync(fs): + terminal = update_terminal_info(fs.model) + if isinstance(terminal, LoadSpecError): + request.session.flash(str(terminal)) + elif terminal: + if not fs.model.sil_id: + fs.model.sil_id = terminal.name + if not fs.model.description: + fs.model.description = terminal.description + + return crud( + request, rattail.BatchTerminal, fieldset, + delete=request.route_url('batch_terminals'), + pre_render=pre_render, post_sync=post_sync) + + +@view_config(route_name='batch_terminals', renderer='/batches/terminals.mako') +def batch_terminals(context, request): + + fmap = filters.get_filter_map( + rattail.BatchTerminal, + exact=['sil_id'], + ilike=['description']) + + config = filters.get_search_config( + 'batch_terminals', request, fmap, + include_filter_description=True, + filter_type_description='lk') + + search = filters.get_search_form( + config, sil_id="SIL ID") + + config = grids.get_grid_config( + 'batch_terminals', request, search, + filter_map=fmap, sort='description', deletable=True) + + smap = grids.get_sort_map( + rattail.BatchTerminal, + ['sil_id', 'description']) + + def query(config): + q = Session.query(rattail.BatchTerminal) + q = filters.filter_query(q, config) + q = grids.sort_query(q, config, smap) + return q + + terminals = grids.get_pager(query, config) + g = forms.AlchemyGrid( + rattail.BatchTerminal, terminals, config, + request.route_url('batch_terminals'), + url_object=request.route_url('batch_terminal'), + url_delete=request.route_url('batch_terminal')) + + g.configure( + include=[ + g.sil_id.label("SIL ID"), + g.description, + ]) + + g.readonly = True + grid = g.render(class_='clickable batch-terminals') + return grids.render_grid(request, grid, search) + + +@view_config(route_name='batches', renderer='/batches/index.mako') +def batches(context, request): + + fmap = filters.get_filter_map( + rattail.Batch, + ilike=['description', 'source_description'], + target=filters.filter_ilike(rattail.BatchTerminal.description)) + + config = filters.get_search_config( + 'batches', request, fmap, + include_filter_description=True, + filter_type_description='lk', + include_filter_source_description=True, + filter_type_source_description='lk', + include_filter_target=True, + filter_type_target='lk') + + search = filters.get_search_form( + config, source_description="Source") + + config = grids.get_grid_config( + 'batches', request, search, + filter_map=fmap, deletable=True, sort='target') + + smap = grids.get_sort_map( + rattail.Batch, + ['source_description', 'batch_id', 'action_type', + 'description', 'rowcount', 'effective'], + target=grids.sorter(rattail.BatchTerminal.description)) + + def query(config): + q = Session.query(rattail.Batch) + q = q.outerjoin((rattail.BatchTerminal, + rattail.BatchTerminal.uuid == rattail.Batch.target_uuid)) + q = q.filter(or_( + rattail.Batch.deleted == None, + and_( + rattail.Batch.deleted != None, + rattail.Batch.deleted != True, + ), + )) + q = filters.filter_query(q, config) + q = grids.sort_query(q, config, smap) + return q + + batches = grids.get_pager(query, config) + g = forms.AlchemyGrid( + rattail.Batch, batches, config, + request.route_url('batches'), + url_object=request.route_url('batch'), + url_delete=request.route_url('batch')) + + g.configure( + include=[ + g.source_description.label("Source"), + g.batch_id.with_renderer(BatchIdFieldRenderer).label("Batch ID"), + g.target, + 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.readonly = True + grid = g.render(class_='clickable batches') + return grids.render_grid(request, grid, search) + + +def _batch_columns(request, batch): + """ + Returns a rendered grid of :class:`rattail.BatchColumn` instances for a + given :class:`rattail.Batch` instance. + """ + + config = grids.get_grid_config( + 'batch.columns', request, sort='ordinal') + + smap = grids.get_sort_map( + rattail.BatchColumn, + ['ordinal', 'targeted']) + smap['column'] = grids.sorter(rattail.SilColumn.sil_name) + smap['display'] = grids.sorter(rattail.SilColumn.display) + smap['source'] = grids.sorter(rattail.BatchTerminal.description) + + def query(config): + q = Session.query(rattail.BatchColumn) + q = q.join(rattail.SilColumn) + q = q.join(rattail.BatchTerminal) + q = q.filter(rattail.BatchColumn.batch == batch) + q = grids.sort_query(q, config, smap) + return q + + url = request.route_url('batch.columns', uuid=batch.uuid) + + columns = query(config) + g = forms.AlchemyGrid(rattail.BatchColumn, columns, config, url) + + g.append(formalchemy.Field('display', value=lambda x: x.column.display)) + + g.configure( + include=[ + g.ordinal, + g.column.label("SIL Name"), + g.display, + g.source, + g.targeted, + ]) + + g.readonly = True + return g.render(class_='hoverable batch-columns') + + +@view_config(route_name='batch.columns') +def batch_columns(context, request): + uuid = request.matchdict['uuid'] + batch = Session.query(rattail.Batch).get(uuid) if uuid else None + assert batch + return Response( + body=_batch_columns(request, batch), + content_type='text/html') + + +@view_config(route_name='batch', renderer='/batches/batch.mako') +def batch(context, request): + + def fieldset(batch): + fs = forms.make_fieldset(batch, url=request.route_url, + url_action=request.route_url('batch'), + route_name='batches') + + # 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=[ + fs.source.validate(forms.required), + fs.batch_id.with_renderer(BatchIdFieldRenderer).label("Batch ID").readonly(), + fs.target.validate(forms.required), + 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.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) + fs.append(forms.ChildGridField('columns', _batch_columns(request, fs.model))) + else: + del fs.batch_id + del fs.effective + del fs.rowcount + return fs + + def post_sync(fs): + if not fs.edit: + source = Session.query(rattail.BatchTerminal).get(fs.model.source_uuid) + target = Session.query(rattail.BatchTerminal).get(fs.model.target_uuid) + assert source and target + batch = fs.model + batch.batch_id = next_batch_id(source.sil_id, consume=True, + session=Session()) + batch.name = '%s.%08u' % (source.sil_id, batch.batch_id) + batch.source_description = source.description + + for i, col in enumerate(source.source_columns, 1): + batch.columns.append(rattail.BatchColumn( + ordinal=i, + column=col.column, + source=source, + targeted=True, + )) + + def pre_render(fs): + if not fs.edit: + + for field in ('source', 'target'): + if not fs.render_fields[field].value: + q = Session.query(rattail.BatchTerminal) + q = q.filter(getattr(rattail.BatchTerminal, field) == True) + count = q.count() + if count == 1: + setattr(fs.model, field, q.first()) + fs.render_fields[field].set(required=True) # hack to avoid null option + elif not count: + request.session.flash("You must create at least one %s Batch Terminal " + "before you may create a Batch." % prettify(field)) + return HTTPFound(location=request.route_url('batches')) + + # else: + # data = {'columns': _batch_columns(request, fs.model)} + # return {'columns': render('/batches/columns.mako', data)} + + # def delete(batch): + # # jct = batch.target_junction.display if batch.target_junction else batch.target + # # batch.table.drop() + # # self.request.session.flash("Batch was deleted: %s" % self.batch_display(batch)) + # self.Session.delete(batch) + # # self.Session.flush() + + return crud(request, rattail.Batch, fieldset, + delete=request.route_url('batches'), + post_sync=post_sync, pre_render=pre_render) + +@view_config(route_name='batch.details', renderer='/batches/details.mako') +def batch_details(context, request): + uuid = request.matchdict['uuid'] + batch = Session.query(rattail.Batch).get(uuid) if uuid else None + assert batch + request.session['batches.uuid'] = batch.uuid + name = 'batches.%s' % batch.name + + # def batch_search_form(batch, name): + # """ + # Returns a dynamically-assembled search form, specific to the current + # batch. + # """ + + # config = filters.get_search_config(name, request, batch_filter_map(batch)) + # labels = {} + # form = filters.get_search_form(config, **labels) + # return form + + # def batch_query(batch): + + # def query(config): + # q = Session.query(batch.rowclass) + # q = filters.filter_query(q, config, batch_filter_map(batch)) + # q = grids.sort_query(q, config, batch_sort_map(batch)) + # return q + + # return query + + # def batch_grid(batch): + + # cls = batch.rowclass + + # def grid(query, config): + # rows = grids.get_pager(query, config) + # g = grids.get_grid(cls, rows, config) + # include = [getattr(g, x.column.sil_name) for x in batch.columns] + # # if batch.target_junction: + # # fnmap = batch.target_junction.fieldname_map_ui + # # for name in fnmap: + # # getattr(g, name).set(label=fnmap[name]) + # g.configure(include=include) + # return g + + # return grid + + cls = batch.rowclass + + fmap = filters.get_filter_map(cls, exact=['F01']) + for col in batch.columns: + fmap.setdefault(col.column.sil_name, + filters.filter_ilike(getattr(cls, col.column.sil_name))) + + config = filters.get_search_config('batch.%s' % batch.name, request, fmap) + + search = filters.get_search_form(config) + + config = grids.get_grid_config( + 'batch.%s' % batch.name, request, search, filter_map=fmap, + url=request.route_url('batch.details', uuid=batch.uuid), + sort=batch.columns[0].column.sil_name) + + # url = request.route_url('batch.details') + # grid = forms.AlchemyGrid(name, batch_grid(batch), batch_query(batch), + # search, url=url, sort='F01', per_page=20) + # query = batch_query(batch) + + smap = grids.get_sort_map(cls) + # cls = batch.rowclass + # # return { + # # 'F01': util.sorter(cls.F01), + # # } + # sort_map = {} + # for name in batch.elements.split(','): + # sort_map[name] = util.sorter(getattr(cls, name)) + + def query(config): + q = Session.query(batch.rowclass) + q = filters.filter_query(q, config) + q = grids.sort_query(q, config, smap) + return q + + rows = grids.get_pager(query, config) + grid = forms.AlchemyGrid( + cls, rows, config, + request.route_url('batch.details', uuid=batch.uuid)) + grid.readonly = True + grid = grid.render(class_='clickable batch-details') + return grids.render_grid(request, grid, search, batch=batch, + pretty_datetime=forms.pretty_datetime) + +@view_config(route_name='batch.execute') +def execute(context, request): + """ + Executes a batch. + """ + + home = HTTPFound(location=request.route_url('batches')) + + # 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'] + 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 + # rowclass = batch.rowclass + # # self.Session.delete(batch) + # batch.deleted = True + # self.Session.flush() + # print 'deleted batch' + # q = self.Session.query(rowclass) + # q.delete(synchronize_session=False) + # print 'deleted rows' + # # table.drop(bind=self.Session().bind, checkfirst=False) + # # print 'dropped table' + + request.session.flash("The batch was executed: %s, %s" % ( + batch.target, rattail.BATCH_ACTION_TYPE[batch.action_type])) + return home + + +def includeme(config): + config.add_route('sil_columns', '/batches/sil_columns') + config.add_route('sil_column', '/batches/sil_column') + config.add_route('batch_dictionaries', '/batches/dictionaries') + config.add_route('batch_dictionary', '/batches/dictionary') + config.add_route('batch_dictionary.columns', '/batches/dictionary-columns') + config.add_route('batch_terminals', '/batches/terminals') + config.add_route('batch_terminal', '/batches/terminal') + config.add_route('batch_terminal.columns', '/batches/terminal-columns') + config.add_route('batches', '/batches') + config.add_route('batch', '/batch') + config.add_route('batch.columns', '/batch/{uuid}/columns') + config.add_route('batch.details', '/batch/{uuid}/details') + config.add_route('batch.execute', '/batch/{uuid}/execute') + config.scan(__name__) diff --git a/rattail/pyramid/views/products.py b/rattail/pyramid/views/products.py new file mode 100644 index 00000000..576fee07 --- /dev/null +++ b/rattail/pyramid/views/products.py @@ -0,0 +1,88 @@ +#!/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 . +# +################################################################################ + +""" +``rattail.pyramid.views.products`` -- Product Views +""" + +from pyramid.view import view_config + +from edbob.pyramid import filters +from edbob.pyramid import forms +from edbob.pyramid import grids +from edbob.pyramid import Session + +import rattail +from rattail.pyramid.forms import UpcFieldRenderer + + +@view_config(route_name='products', renderer='/products/index.mako') +def products(context, request): + + fmap = filters.get_filter_map( + rattail.Product, + exact=['upc'], + ilike=['description']) + + config = filters.get_search_config( + 'products', request, fmap, + include_filter_description=True, + filter_type_description='lk') + + search = filters.get_search_form( + config, upc="UPC") + + config = grids.get_grid_config( + 'products', request, search, + filter_map=fmap, sort='description') + + smap = grids.get_sort_map( + rattail.Product, + ['upc', 'description']) + + def query(config): + q = Session.query(rattail.Product) + q = filters.filter_query(q, config) + q = grids.sort_query(q, config, smap) + return q + + products = grids.get_pager(query, config) + g = forms.AlchemyGrid( + rattail.Product, products, config, + request.route_url('products')) + + g.configure( + include=[ + g.upc.with_renderer(UpcFieldRenderer).label("UPC"), + g.description, + ]) + + g.readonly = True + grid = g.render(class_='clickable products') + return grids.render_grid(request, grid, search) + + +def includeme(config): + config.add_route('products', '/products') + config.scan(__name__) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..018c3b43 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[egg_info] +tag_build = .dev diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..eef784e2 --- /dev/null +++ b/setup.py @@ -0,0 +1,120 @@ +#!/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 . +# +################################################################################ + + +import os.path +from setuptools import setup, find_packages + + +here = os.path.abspath(os.path.dirname(__file__)) +execfile(os.path.join(here, 'rattail', 'pyramid', '_version.py')) +README = open(os.path.join(here, 'README.txt')).read() +CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() + + +requires = [ + # + # Version numbers within comments below have specific meanings. + # Basically the 'low' value is a "soft low," and 'high' a "soft high." + # In other words: + # + # If either a 'low' or 'high' value exists, the primary point to be + # made about the value is that it represents the most current (stable) + # version available for the package (assuming typical public access + # methods) whenever this project was started and/or documented. + # Therefore: + # + # If a 'low' version is present, you should know that attempts to use + # versions of the package significantly older than the 'low' version + # may not yield happy results. (A "hard" high limit may or may not be + # indicated by a true version requirement.) + # + # Similarly, if a 'high' version is present, and especially if this + # project has laid dormant for a while, you may need to refactor a bit + # when attempting to support a more recent version of the package. (A + # "hard" low limit should be indicated by a true version requirement + # when a 'high' version is present.) + # + # In any case, developers and other users are encouraged to play + # outside the lines with regard to these soft limits. If bugs are + # encountered then they should be filed as such. + # + # package # low high + + # Beaker dependency included here because 'pyramid_beaker' uses incorrect + # case in its requirement declaration. + 'Beaker', # 1.6.3 + + # Pyramid 1.3 introduced 'pcreate' command (and friends) to replace + # deprecated 'paster create' (and friends). + 'pyramid>=1.3a1', # 1.3b2 + + 'decorator', # 3.3.2 + 'Mako', # 0.6.2 + 'pyramid_beaker', # 0.6.1 + 'pyramid_debugtoolbar', # 1.0 + 'pyramid_tm', # 0.3 + 'rattail>=0.3a1.dev', # 0.3a1.dev + 'SQLAlchemy', # 0.7.6 + 'Tempita', # 0.5.1 + 'transaction', # 1.2.0 + 'waitress', # 0.8.1 + 'WebHelpers', # 1.3 + 'zope.sqlalchemy', # 0.7 + ] + + +setup( + name = "rattail.pyramid", + version = __version__, + author = "Lance Edgar", + author_email = "lance@edbob.org", + url = "http://rattail.edbob.org/", + license = "GNU Affero GPL v3", + description = "Rattail Pyramid Framework", + long_description = README + '\n\n' + CHANGES, + + classifiers = [ + 'Development Status :: 3 - Alpha', + 'Environment :: Web Environment', + 'Framework :: Pylons', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Office/Business', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + install_requires = requires, + + namespace_packages = ['rattail'], + packages = find_packages(), + include_package_data = True, + zip_safe = False, + )