diff --git a/docs/api/wutta_corepos.web.db.rst b/docs/api/wutta_corepos.web.db.rst new file mode 100644 index 0000000..11c37ca --- /dev/null +++ b/docs/api/wutta_corepos.web.db.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.web.db`` +======================== + +.. automodule:: wutta_corepos.web.db + :members: diff --git a/docs/api/wutta_corepos.web.rst b/docs/api/wutta_corepos.web.rst new file mode 100644 index 0000000..56d6604 --- /dev/null +++ b/docs/api/wutta_corepos.web.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.web`` +===================== + +.. automodule:: wutta_corepos.web + :members: diff --git a/docs/api/wutta_corepos.web.views.corepos.master.rst b/docs/api/wutta_corepos.web.views.corepos.master.rst new file mode 100644 index 0000000..f9ebea7 --- /dev/null +++ b/docs/api/wutta_corepos.web.views.corepos.master.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.web.views.corepos.master`` +========================================== + +.. automodule:: wutta_corepos.web.views.corepos.master + :members: diff --git a/docs/api/wutta_corepos.web.views.corepos.members.rst b/docs/api/wutta_corepos.web.views.corepos.members.rst new file mode 100644 index 0000000..d5012c1 --- /dev/null +++ b/docs/api/wutta_corepos.web.views.corepos.members.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.web.views.corepos.members`` +=========================================== + +.. automodule:: wutta_corepos.web.views.corepos.members + :members: diff --git a/docs/api/wutta_corepos.web.views.corepos.products.rst b/docs/api/wutta_corepos.web.views.corepos.products.rst new file mode 100644 index 0000000..00db4c4 --- /dev/null +++ b/docs/api/wutta_corepos.web.views.corepos.products.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.web.views.corepos.products`` +============================================ + +.. automodule:: wutta_corepos.web.views.corepos.products + :members: diff --git a/docs/api/wutta_corepos.web.views.corepos.rst b/docs/api/wutta_corepos.web.views.corepos.rst new file mode 100644 index 0000000..d2703d5 --- /dev/null +++ b/docs/api/wutta_corepos.web.views.corepos.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.web.views.corepos`` +=================================== + +.. automodule:: wutta_corepos.web.views.corepos + :members: diff --git a/docs/api/wutta_corepos.web.views.rst b/docs/api/wutta_corepos.web.views.rst new file mode 100644 index 0000000..aecedcb --- /dev/null +++ b/docs/api/wutta_corepos.web.views.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.web.views`` +=========================== + +.. automodule:: wutta_corepos.web.views + :members: diff --git a/docs/conf.py b/docs/conf.py index 301c604..4759410 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,6 +27,7 @@ templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] intersphinx_mapping = { + 'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None), 'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None), } diff --git a/docs/index.rst b/docs/index.rst index 44e43b7..4f5d57b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,8 @@ Wutta-COREPOS This package adds basic integration with `CORE-POS`_, using `pyCOREPOS`_. -Its main purpose is to setup DB connections for CORE Office. +Its main purpose is to setup DB connections for CORE Office, but it +also contains basic readonly web views for some CORE tables. .. _CORE-POS: https://www.core-pos.com/ @@ -26,3 +27,10 @@ Its main purpose is to setup DB connections for CORE Office. api/wutta_corepos.app api/wutta_corepos.conf api/wutta_corepos.handler + api/wutta_corepos.web + api/wutta_corepos.web.db + api/wutta_corepos.web.views + api/wutta_corepos.web.views.corepos + api/wutta_corepos.web.views.corepos.master + api/wutta_corepos.web.views.corepos.members + api/wutta_corepos.web.views.corepos.products diff --git a/pyproject.toml b/pyproject.toml index a088123..a12611b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ [project.optional-dependencies] +web = ["WuttaWeb"] docs = ["Sphinx", "furo"] tests = ["pytest-cov", "tox"] diff --git a/src/wutta_corepos/handler.py b/src/wutta_corepos/handler.py index 9a66367..8b39795 100644 --- a/src/wutta_corepos/handler.py +++ b/src/wutta_corepos/handler.py @@ -21,7 +21,7 @@ # ################################################################################ """ -CORE-POS Handler +CORE-POS Integration Handler """ from wuttjamaican.app import GenericHandler @@ -33,6 +33,72 @@ class CoreposHandler(GenericHandler): :term:`handler`. """ + def get_model_office_op(self): + """ + Returns the :term:`data model` module for CORE Office 'op' DB, + i.e. :mod:`pycorepos:corepos.db.office_op.model`. + """ + from corepos.db.office_op import model + + return model + + def get_model_office_trans(self): + """ + Returns the :term:`data model` module for CORE Office 'trans' + DB, i.e. :mod:`pycorepos:corepos.db.office_trans.model`. + """ + from corepos.db.office_trans import model + + return model + + def get_model_office_arch(self): + """ + Returns the :term:`data model` module for CORE Office 'arch' + DB, i.e. :mod:`pycorepos:corepos.db.office_arch.model`. + """ + from corepos.db.office_arch import model + + return model + + def make_session_office_op(self, dbkey='default', **kwargs): + """ + Make a new :term:`db session` for the CORE Office 'op' DB. + + :returns: Instance of + :class:`pycorepos:corepos.db.office_op.Session`. + """ + from corepos.db.office_op import Session + + if 'bind' not in kwargs: + kwargs['bind'] = self.config.core_office_op_engines[dbkey] + return Session(**kwargs) + + def make_session_office_trans(self, dbkey='default', **kwargs): + """ + Make a new :term:`db session` for the CORE Office 'trans' DB. + + :returns: Instance of + :class:`pycorepos:corepos.db.office_trans.Session`. + """ + from corepos.db.office_trans import Session + + if 'bind' not in kwargs: + kwargs['bind'] = self.config.core_office_trans_engines[dbkey] + return Session(**kwargs) + + def make_session_office_arch(self, dbkey='default', **kwargs): + """ + Make a new :term:`db session` for the CORE Office 'arch' DB. + + :returns: Instance of + :class:`pycorepos:corepos.db.office_arch.Session`. + """ + from corepos.db.office_arch import Session + + if 'bind' not in kwargs: + kwargs['bind'] = self.config.core_office_arch_engines[dbkey] + return Session(**kwargs) + def get_office_url(self, require=False): """ Returns the base URL for the CORE Office web app. diff --git a/src/wutta_corepos/web/__init__.py b/src/wutta_corepos/web/__init__.py new file mode 100644 index 0000000..71a9477 --- /dev/null +++ b/src/wutta_corepos/web/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Wutta-COREPOS -- Wutta Framework integration for CORE-POS +# Copyright © 2025 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Wutta-COREPOS -- wuttaweb features +""" + + +def includeme(config): + config.include('wutta_corepos.web.views') diff --git a/src/wutta_corepos/web/db.py b/src/wutta_corepos/web/db.py new file mode 100644 index 0000000..7faeff2 --- /dev/null +++ b/src/wutta_corepos/web/db.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Wutta-COREPOS -- Wutta Framework integration for CORE-POS +# Copyright © 2025 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Wutta-COREPOS -- wuttaweb DB sessions + +See :mod:`wuttaweb:wuttaweb.db.sess` for more info on web app sessions +in general. + +.. class:: CoreOpSession + + Primary web app :term:`db session` for CORE Office 'op' DB. + +.. class:: CoreTransSession + + Primary web app :term:`db session` for CORE Office 'trans' DB. + +.. class:: CoreArchSession + + Primary web app :term:`db session` for CORE Office 'arch' DB. + +.. class:: ExtraCoreOpSessions + + Dict of secondary CORE Office 'op' DB sessions, if applicable. + +.. class:: ExtraCoreTransSessions + + Dict of secondary CORE Office 'trans' DB sessions, if applicable. + +.. class:: ExtraCoreArchSessions + + Dict of secondary CORE Office 'arch' DB sessions, if applicable. +""" + +from sqlalchemy.orm import sessionmaker, scoped_session +from zope.sqlalchemy import register + + +CoreOpSession = scoped_session(sessionmaker()) +register(CoreOpSession) + +CoreTransSession = scoped_session(sessionmaker()) +register(CoreTransSession) + +CoreArchSession = scoped_session(sessionmaker()) +register(CoreArchSession) + +# nb. these start out empty but may be populated on app startup +ExtraCoreOpSessions = {} +ExtraCoreTransSessions = {} +ExtraCoreArchSessions = {} diff --git a/src/wutta_corepos/web/views/__init__.py b/src/wutta_corepos/web/views/__init__.py new file mode 100644 index 0000000..d4112a2 --- /dev/null +++ b/src/wutta_corepos/web/views/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Wutta-COREPOS -- Wutta Framework integration for CORE-POS +# Copyright © 2025 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Wutta-COREPOS -- wuttaweb views +""" + + +def includeme(config): + config.include('wutta_corepos.web.views.corepos') diff --git a/src/wutta_corepos/web/views/corepos/__init__.py b/src/wutta_corepos/web/views/corepos/__init__.py new file mode 100644 index 0000000..a78e4ef --- /dev/null +++ b/src/wutta_corepos/web/views/corepos/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Wutta-COREPOS -- Wutta Framework integration for CORE-POS +# Copyright © 2025 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Views for CORE-POS +""" + +from .master import CoreOpMasterView + + +def includeme(config): + config.include('wutta_corepos.web.views.corepos.members') + config.include('wutta_corepos.web.views.corepos.products') diff --git a/src/wutta_corepos/web/views/corepos/master.py b/src/wutta_corepos/web/views/corepos/master.py new file mode 100644 index 0000000..817a443 --- /dev/null +++ b/src/wutta_corepos/web/views/corepos/master.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Wutta-COREPOS -- Wutta Framework integration for CORE-POS +# Copyright © 2025 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +CORE-POS master view base class +""" + +from wuttaweb.views import MasterView + +from wutta_corepos.web.db import CoreOpSession + + +class CoreOpMasterView(MasterView): + """ + Base class for master views which use the CORE Office 'op' DB. + """ + Session = CoreOpSession + + def __init__(self, request, context=None): + super().__init__(request, context=context) + self.corepos_handler = self.app.get_corepos_handler() diff --git a/src/wutta_corepos/web/views/corepos/members.py b/src/wutta_corepos/web/views/corepos/members.py new file mode 100644 index 0000000..66c967e --- /dev/null +++ b/src/wutta_corepos/web/views/corepos/members.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Wutta-COREPOS -- Wutta Framework integration for CORE-POS +# Copyright © 2025 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Views for CORE-POS Members +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from corepos.db.office_op.model import MemberInfo + +from wutta_corepos.web.views.corepos import CoreOpMasterView + + +class MemberView(CoreOpMasterView): + """ + Master view for + :class:`~pycorepos:corepos.db.office_op.model.MemberInfo`; route + prefix is ``corepos_members``. + + Notable URLs provided by this class: + + * ``/corepos/members/`` + * ``/corepos/members/XXX`` + """ + model_class = MemberInfo + model_title = "CORE-POS Member" + route_prefix = 'corepos_members' + url_prefix = '/corepos/members' + + # nb. this is just for readonly lookup + creatable = False + editable = False + deletable = False + + grid_columns = [ + 'card_number', + 'first_name', + 'last_name', + 'street', + 'city', + 'state', + 'zip', + 'phone', + 'email', + ] + + filter_defaults = { + 'card_number': {'active': True, 'verb': 'equal'}, + 'first_name': {'active': True, 'verb': 'contains'}, + 'last_name': {'active': True, 'verb': 'contains'}, + } + + sort_defaults = 'card_number' + + def get_query(self, session=None): + """ """ + query = super().get_query(session=session) + + op_model = self.corepos_handler.get_model_office_op() + query = query.outerjoin(op_model.CustomerClassic, + sa.and_( + op_model.CustomerClassic.card_number == op_model.MemberInfo.card_number, + op_model.CustomerClassic.person_number == 1, + ))\ + .options(orm.joinedload(op_model.MemberInfo.customers)) + + return query + + def configure_grid(self, g): + """ """ + super().configure_grid(g) + op_model = self.corepos_handler.get_model_office_op() + + # first_name + g.set_renderer('first_name', self.render_customer_attr) + g.set_sorter('first_name', op_model.CustomerClassic.first_name) + + # last_name + g.set_renderer('last_name', self.render_customer_attr) + g.set_sorter('last_name', op_model.CustomerClassic.last_name) + + def render_customer_attr(self, member, key, value): + """ """ + customer = member.customers[0] + return getattr(customer, key) + + +def defaults(config, **kwargs): + base = globals() + + MemberView = kwargs.get('MemberView', base['MemberView']) + MemberView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/src/wutta_corepos/web/views/corepos/products.py b/src/wutta_corepos/web/views/corepos/products.py new file mode 100644 index 0000000..e8f33c4 --- /dev/null +++ b/src/wutta_corepos/web/views/corepos/products.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Wutta-COREPOS -- Wutta Framework integration for CORE-POS +# Copyright © 2025 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Views for CORE-POS Products +""" + +from corepos.db.office_op.model import Product + +from wutta_corepos.web.views.corepos import CoreOpMasterView + + +class ProductView(CoreOpMasterView): + """ + Master view for + :class:`~pycorepos:corepos.db.office_op.model.Product`; route + prefix is ``corepos_products``. + + Notable URLs provided by this class: + + * ``/corepos/products/`` + * ``/corepos/products/XXX`` + """ + model_class = Product + model_title = "CORE-POS Product" + route_prefix = 'corepos_products' + url_prefix = '/corepos/products' + + # nb. this is just for readonly lookup + creatable = False + editable = False + deletable = False + + labels = { + 'upc': "UPC", + } + + grid_columns = [ + 'upc', + 'brand', + 'description', + 'size', + 'department', + 'vendor', + 'normal_price', + ] + + filter_defaults = { + 'upc': {'active': True, 'verb': 'contains'}, + 'brand': {'active': True, 'verb': 'contains'}, + 'description': {'active': True, 'verb': 'contains'}, + } + + sort_defaults = 'upc' + + def configure_grid(self, g): + """ """ + super().configure_grid(g) + + # normal_price + g.set_renderer('normal_price', 'currency') + + # links + g.set_link('upc') + g.set_link('brand') + g.set_link('description') + g.set_link('size') + + +def defaults(config, **kwargs): + base = globals() + + ProductView = kwargs.get('ProductView', base['ProductView']) + ProductView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/tests/test_handler.py b/tests/test_handler.py index e29835b..6dc5077 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -1,5 +1,10 @@ # -*- coding: utf-8; -*- +from unittest.mock import patch + +import sqlalchemy as sa +from sqlalchemy import orm + from wuttjamaican.testing import ConfigTestCase from wuttjamaican.exc import ConfigurationError @@ -11,6 +16,51 @@ class TestCoreposHandler(ConfigTestCase): def make_handler(self): return mod.CoreposHandler(self.config) + def test_get_model_office_op(self): + from corepos.db.office_op import model + handler = self.make_handler() + op_model = handler.get_model_office_op() + self.assertIs(op_model, model) + + def test_get_model_office_trans(self): + from corepos.db.office_trans import model + handler = self.make_handler() + trans_model = handler.get_model_office_trans() + self.assertIs(trans_model, model) + + def test_get_model_office_arch(self): + from corepos.db.office_arch import model + handler = self.make_handler() + arch_model = handler.get_model_office_arch() + self.assertIs(arch_model, model) + + def test_make_session_office_op(self): + handler = self.make_handler() + engine = sa.create_engine('sqlite://') + with patch.object(self.config, 'core_office_op_engines', create=True, + new={'default': engine}): + op_session = handler.make_session_office_op() + self.assertIsInstance(op_session, orm.Session) + self.assertIs(op_session.bind, engine) + + def test_make_session_office_trans(self): + handler = self.make_handler() + engine = sa.create_engine('sqlite://') + with patch.object(self.config, 'core_office_trans_engines', create=True, + new={'default': engine}): + trans_session = handler.make_session_office_trans() + self.assertIsInstance(trans_session, orm.Session) + self.assertIs(trans_session.bind, engine) + + def test_make_session_office_arch(self): + handler = self.make_handler() + engine = sa.create_engine('sqlite://') + with patch.object(self.config, 'core_office_arch_engines', create=True, + new={'default': engine}): + arch_session = handler.make_session_office_arch() + self.assertIsInstance(arch_session, orm.Session) + self.assertIs(arch_session.bind, engine) + def test_get_office_url(self): handler = self.make_handler() diff --git a/tests/web/test_init.py b/tests/web/test_init.py new file mode 100644 index 0000000..6c9d367 --- /dev/null +++ b/tests/web/test_init.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8; -*- + +from wuttaweb.testing import WebTestCase + +from wutta_corepos import web as mod + + +class TestIncludeme(WebTestCase): + + def test_coverage(self): + return mod.includeme(self.pyramid_config) diff --git a/tests/web/views/corepos/test_members.py b/tests/web/views/corepos/test_members.py new file mode 100644 index 0000000..6055ef9 --- /dev/null +++ b/tests/web/views/corepos/test_members.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8; -*- + +from sqlalchemy import orm + +from corepos.db.office_op import model as op_model + +from wuttaweb.testing import WebTestCase + +from wutta_corepos.web.views.corepos import members as mod + + +class TestProductView(WebTestCase): + + def make_view(self): + return mod.MemberView(self.request) + + def test_includeme(self): + return mod.includeme(self.pyramid_config) + + def test_get_query(self): + view = self.make_view() + query = view.get_query() + # TODO: not sure how to test the join other than doing data + # setup and full runn-thru...and i'm feeling lazy + self.assertIsInstance(query, orm.Query) + + def test_configure_grid(self): + view = self.make_view() + grid = view.make_grid(model_class=view.model_class) + self.assertNotIn('first_name', grid.renderers) + view.configure_grid(grid) + self.assertIn('first_name', grid.renderers) + + def test_render_customer_attr(self): + view = self.make_view() + member = op_model.MemberInfo() + customer = op_model.CustomerClassic(first_name="Fred") + member.customers.append(customer) + self.assertEqual(view.render_customer_attr(member, 'first_name', 'nope'), "Fred") diff --git a/tests/web/views/corepos/test_products.py b/tests/web/views/corepos/test_products.py new file mode 100644 index 0000000..8870208 --- /dev/null +++ b/tests/web/views/corepos/test_products.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8; -*- + +from wuttaweb.testing import WebTestCase + +from wutta_corepos.web.views.corepos import products as mod + + +class TestProductView(WebTestCase): + + def make_view(self): + return mod.ProductView(self.request) + + def test_includeme(self): + return mod.includeme(self.pyramid_config) + + def test_configure_grid(self): + view = self.make_view() + grid = view.make_grid(model_class=view.model_class) + self.assertNotIn('upc', grid.linked_columns) + view.configure_grid(grid) + self.assertIn('upc', grid.linked_columns)