From 34e9528f4b8cd43247740a8c21adad0643864cb0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 11:06:15 -0600 Subject: [PATCH 01/13] fix: bump min version for pycorepos dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index afb21c9..62e0817 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ ] requires-python = ">= 3.8" dependencies = [ - "pyCOREPOS>=0.3.3", + "pyCOREPOS>=0.3.5", "WuttJamaican>=0.20.1", ] From d1de2389a589d7338e366ac171264745474a5b18 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 14:57:45 -0600 Subject: [PATCH 02/13] fix: add grid links for Members --- src/wutta_corepos/web/views/corepos/members.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wutta_corepos/web/views/corepos/members.py b/src/wutta_corepos/web/views/corepos/members.py index 66c967e..19a6268 100644 --- a/src/wutta_corepos/web/views/corepos/members.py +++ b/src/wutta_corepos/web/views/corepos/members.py @@ -100,6 +100,12 @@ class MemberView(CoreOpMasterView): g.set_renderer('last_name', self.render_customer_attr) g.set_sorter('last_name', op_model.CustomerClassic.last_name) + # links + if self.has_perm('view'): + g.set_link('card_number') + g.set_link('first_name') + g.set_link('last_name') + def render_customer_attr(self, member, key, value): """ """ customer = member.customers[0] From bf6bf63e6888bc2235f368b6e835aac90af70ee3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 17:19:59 -0600 Subject: [PATCH 03/13] tests: fix coverage for members view --- tests/web/views/corepos/test_members.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/web/views/corepos/test_members.py b/tests/web/views/corepos/test_members.py index 6055ef9..b3da7b0 100644 --- a/tests/web/views/corepos/test_members.py +++ b/tests/web/views/corepos/test_members.py @@ -1,5 +1,7 @@ # -*- coding: utf-8; -*- +from unittest.mock import patch + from sqlalchemy import orm from corepos.db.office_op import model as op_model @@ -9,7 +11,7 @@ from wuttaweb.testing import WebTestCase from wutta_corepos.web.views.corepos import members as mod -class TestProductView(WebTestCase): +class TestMemberView(WebTestCase): def make_view(self): return mod.MemberView(self.request) @@ -28,8 +30,11 @@ class TestProductView(WebTestCase): view = self.make_view() grid = view.make_grid(model_class=view.model_class) self.assertNotIn('first_name', grid.renderers) - view.configure_grid(grid) + self.assertNotIn('first_name', grid.linked_columns) + with patch.object(self.request, 'is_root', new=True): + view.configure_grid(grid) self.assertIn('first_name', grid.renderers) + self.assertIn('first_name', grid.linked_columns) def test_render_customer_attr(self): view = self.make_view() From e6921c853338069c583202a8fa47c9178e7db9ff Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 24 Jan 2025 19:33:34 -0600 Subject: [PATCH 04/13] feat: add support for `lane_op` and `lane_trans` DB sessions, models --- docs/narr/install.rst | 13 +++++++++-- src/wutta_corepos/conf.py | 42 ++++++++++++++++++++++++++++++++++ src/wutta_corepos/handler.py | 44 ++++++++++++++++++++++++++++++++++++ src/wutta_corepos/web/db.py | 24 ++++++++++++++++++++ tests/test_conf.py | 7 ++++++ tests/test_handler.py | 30 ++++++++++++++++++++++++ 6 files changed, 158 insertions(+), 2 deletions(-) diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 33e7cd2..bceda1c 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -12,8 +12,7 @@ Install the Wutta-COREPOS package to your virtual environment: pip install Wutta-COREPOS Edit your :term:`config file` to add CORE-POS DB connection info, and -related settings. Note that so far, only CORE Office DB connections -are supported. +related settings. .. code-block:: ini @@ -29,4 +28,14 @@ are supported. [corepos.db.office_arch] default.url = mysql+mysqlconnector://localhost/trans_archive + [corepos.db.lane_op] + keys = 01, 02 + 01.url = mysql+mysqlconnector://lane01/opdata + 02.url = mysql+mysqlconnector://lane02/opdata + + [corepos.db.lane_trans] + keys = 01, 02 + 01.url = mysql+mysqlconnector://lane01/translog + 02.url = mysql+mysqlconnector://lane02/translog + And that's it, the CORE-POS integration is configured. diff --git a/src/wutta_corepos/conf.py b/src/wutta_corepos/conf.py index 77bb6cc..7c3affb 100644 --- a/src/wutta_corepos/conf.py +++ b/src/wutta_corepos/conf.py @@ -74,6 +74,34 @@ class WuttaCoreposConfigExtension(WuttaConfigExtension): Dict of ``office_arch`` DB engines. May be empty if no config is found; otherwise there should at least be a ``default`` key defined, corresonding to :data:`core_office_arch_engine`. + + .. data:: core_lane_op_engine + + Primary engine for the ``lane_op`` DB. May be null if no + "default" engine is configured - which is *typical* for a + multi-lane environment. See :data:`core_lane_op_engines` for + the full set. + + .. data:: core_lane_op_engines + + Dict of ``lane_op`` DB engines. May be empty if no config is + found; otherwise keys are typically like ``01`` and ``02`` etc. + If present, the ``default`` key will correspond to + :data:`core_lane_op_engine`. + + .. data:: core_lane_trans_engine + + Primary engine for the ``lane_trans`` DB. May be null if no + "default" engine is configured - which is *typical* for a + multi-lane environment. See :data:`core_lane_trans_engines` + for the full set. + + .. data:: core_lane_trans_engines + + Dict of ``lane_trans`` DB engines. May be empty if no config + is found; otherwise keys are typically like ``01`` and ``02`` + etc. If present, the ``default`` key will correspond to + :data:`core_lane_trans_engine`. """ key = 'wutta_corepos' @@ -101,6 +129,20 @@ class WuttaCoreposConfigExtension(WuttaConfigExtension): config.core_office_arch_engine = engines.get('default') Session.configure(bind=config.core_office_arch_engine) + # lane_op + from corepos.db.lane_op import Session + engines = get_engines(config, 'corepos.db.lane_op') + config.core_lane_op_engines = engines + config.core_lane_op_engine = engines.get('default') + Session.configure(bind=config.core_lane_op_engine) + + # lane_trans + from corepos.db.lane_trans import Session + engines = get_engines(config, 'corepos.db.lane_trans') + config.core_lane_trans_engines = engines + config.core_lane_trans_engine = engines.get('default') + Session.configure(bind=config.core_lane_trans_engine) + # define some schema columns "late" unless not supported if config.get_bool('corepos.db.office_op.use_latest_columns', default=True, usedb=False): diff --git a/src/wutta_corepos/handler.py b/src/wutta_corepos/handler.py index 8b39795..6422f66 100644 --- a/src/wutta_corepos/handler.py +++ b/src/wutta_corepos/handler.py @@ -60,6 +60,24 @@ class CoreposHandler(GenericHandler): return model + def get_model_lane_op(self): + """ + Returns the :term:`data model` module for CORE Lane 'op' DB, + i.e. :mod:`pycorepos:corepos.db.lane_op.model`. + """ + from corepos.db.lane_op import model + + return model + + def get_model_lane_trans(self): + """ + Returns the :term:`data model` module for CORE Lane 'trans' + DB, i.e. :mod:`pycorepos:corepos.db.lane_trans.model`. + """ + from corepos.db.lane_trans 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. @@ -99,6 +117,32 @@ class CoreposHandler(GenericHandler): kwargs['bind'] = self.config.core_office_arch_engines[dbkey] return Session(**kwargs) + def make_session_lane_op(self, dbkey='default', **kwargs): + """ + Make a new :term:`db session` for the CORE Lane 'op' DB. + + :returns: Instance of + :class:`pycorepos:corepos.db.lane_op.Session`. + """ + from corepos.db.lane_op import Session + + if 'bind' not in kwargs: + kwargs['bind'] = self.config.core_lane_op_engines[dbkey] + return Session(**kwargs) + + def make_session_lane_trans(self, dbkey='default', **kwargs): + """ + Make a new :term:`db session` for the CORE Lane 'trans' DB. + + :returns: Instance of + :class:`pycorepos:corepos.db.lane_trans.Session`. + """ + from corepos.db.lane_trans import Session + + if 'bind' not in kwargs: + kwargs['bind'] = self.config.core_lane_trans_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/db.py b/src/wutta_corepos/web/db.py index 7faeff2..4c9e292 100644 --- a/src/wutta_corepos/web/db.py +++ b/src/wutta_corepos/web/db.py @@ -49,6 +49,22 @@ in general. .. class:: ExtraCoreArchSessions Dict of secondary CORE Office 'arch' DB sessions, if applicable. + +.. class:: CoreLaneOpSession + + Primary web app :term:`db session` for CORE Lane 'op' DB. + +.. class:: CoreLaneTransSession + + Primary web app :term:`db session` for CORE Lane 'trans' DB. + +.. class:: ExtraCoreLaneOpSessions + + Dict of secondary CORE Lane 'op' DB sessions, if applicable. + +.. class:: ExtraCoreLaneTransSessions + + Dict of secondary CORE Lane 'trans' DB sessions, if applicable. """ from sqlalchemy.orm import sessionmaker, scoped_session @@ -64,7 +80,15 @@ register(CoreTransSession) CoreArchSession = scoped_session(sessionmaker()) register(CoreArchSession) +CoreLaneOpSession = scoped_session(sessionmaker()) +register(CoreLaneOpSession) + +CoreLaneTransSession = scoped_session(sessionmaker()) +register(CoreLaneTransSession) + # nb. these start out empty but may be populated on app startup ExtraCoreOpSessions = {} ExtraCoreTransSessions = {} ExtraCoreArchSessions = {} +ExtraCoreLaneOpSessions = {} +ExtraCoreLaneTransSessions = {} diff --git a/tests/test_conf.py b/tests/test_conf.py index c957dce..10f97a0 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -16,14 +16,21 @@ class TestWuttaCoreposConfigExtension(TestCase): self.assertFalse(hasattr(config, 'core_office_op_engine')) self.assertFalse(hasattr(config, 'core_office_trans_engine')) self.assertFalse(hasattr(config, 'core_office_arch_engine')) + self.assertFalse(hasattr(config, 'core_lane_op_engine')) + self.assertFalse(hasattr(config, 'core_lane_trans_engine')) ext = mod.WuttaCoreposConfigExtension() ext.configure(config) self.assertIsNone(config.core_office_op_engine) self.assertIsNone(config.core_office_trans_engine) self.assertIsNone(config.core_office_arch_engine) + self.assertIsNone(config.core_lane_op_engine) + self.assertIsNone(config.core_lane_trans_engine) # but config can change that config.setdefault('corepos.db.office_op.default.url', 'sqlite://') + config.setdefault('corepos.db.lane_trans.default.url', 'sqlite://') ext.configure(config) self.assertIsNotNone(config.core_office_op_engine) self.assertEqual(str(config.core_office_op_engine.url), 'sqlite://') + self.assertIsNotNone(config.core_lane_trans_engine) + self.assertEqual(str(config.core_lane_trans_engine.url), 'sqlite://') diff --git a/tests/test_handler.py b/tests/test_handler.py index 6dc5077..74f6f7b 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -34,6 +34,18 @@ class TestCoreposHandler(ConfigTestCase): arch_model = handler.get_model_office_arch() self.assertIs(arch_model, model) + def test_get_model_lane_op(self): + from corepos.db.lane_op import model + handler = self.make_handler() + op_model = handler.get_model_lane_op() + self.assertIs(op_model, model) + + def test_get_model_lane_trans(self): + from corepos.db.lane_trans import model + handler = self.make_handler() + trans_model = handler.get_model_lane_trans() + self.assertIs(trans_model, model) + def test_make_session_office_op(self): handler = self.make_handler() engine = sa.create_engine('sqlite://') @@ -61,6 +73,24 @@ class TestCoreposHandler(ConfigTestCase): self.assertIsInstance(arch_session, orm.Session) self.assertIs(arch_session.bind, engine) + def test_make_session_lane_op(self): + handler = self.make_handler() + engine = sa.create_engine('sqlite://') + with patch.object(self.config, 'core_lane_op_engines', create=True, + new={'default': engine}): + op_session = handler.make_session_lane_op() + self.assertIsInstance(op_session, orm.Session) + self.assertIs(op_session.bind, engine) + + def test_make_session_lane_trans(self): + handler = self.make_handler() + engine = sa.create_engine('sqlite://') + with patch.object(self.config, 'core_lane_trans_engines', create=True, + new={'default': engine}): + trans_session = handler.make_session_lane_trans() + self.assertIsInstance(trans_session, orm.Session) + self.assertIs(trans_session.bind, engine) + def test_get_office_url(self): handler = self.make_handler() From 1a9929c73401628039244e564790ed868deb4f93 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 24 Jan 2025 19:35:14 -0600 Subject: [PATCH 05/13] fix: add `get_office_employee_url()` method for corepos handler --- src/wutta_corepos/handler.py | 22 ++++++++++++++++++++++ tests/test_handler.py | 10 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/wutta_corepos/handler.py b/src/wutta_corepos/handler.py index 6422f66..f52ef8e 100644 --- a/src/wutta_corepos/handler.py +++ b/src/wutta_corepos/handler.py @@ -180,6 +180,28 @@ class CoreposHandler(GenericHandler): if office_url: return f'{office_url}/item/departments/DepartmentEditor.php?did={dept_id}' + def get_office_employee_url( + self, + employee_id, + office_url=None, + require=False): + """ + Returns the CORE Office URL for an Employee. + + :param employee_id: Employee ID for the URL. + + :param office_url: Root URL from :meth:`get_office_url()`. + + :param require: If true, an error is raised when URL cannot be + determined. + + :returns: URL as string. + """ + if not office_url: + office_url = self.get_office_url(require=require) + if office_url: + return f'{office_url}/admin/Cashiers/CashierEditor.php?emp_no={employee_id}' + def get_office_likecode_url( self, likecode_id, diff --git a/tests/test_handler.py b/tests/test_handler.py index 74f6f7b..f751f22 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -115,6 +115,16 @@ class TestCoreposHandler(ConfigTestCase): self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') self.assertEqual(handler.get_office_department_url(7), 'http://localhost/fannie/item/departments/DepartmentEditor.php?did=7') + def test_get_office_employee_url(self): + handler = self.make_handler() + + # null + self.assertIsNone(handler.get_office_employee_url(7)) + + # typical + self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') + self.assertEqual(handler.get_office_employee_url(7), 'http://localhost/fannie/admin/Cashiers/CashierEditor.php?emp_no=7') + def test_get_office_likecode_url(self): handler = self.make_handler() From 73192a162d5011ef5bb949f2134889134b7e5ee2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 24 Jan 2025 19:59:46 -0600 Subject: [PATCH 06/13] fix: bump version requirement for pyCOREPOS --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 62e0817..76af27c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ ] requires-python = ">= 3.8" dependencies = [ - "pyCOREPOS>=0.3.5", + "pyCOREPOS>=0.4.0", "WuttJamaican>=0.20.1", ] From 4ee5aa537280ecbb50d5b4821e4f9b27d2cb076b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 25 Jan 2025 17:20:55 -0600 Subject: [PATCH 07/13] feat: add app db schema extension, for CoreUser need a way to map Wutta User to CORE Employee for auth purposes --- docs/api/wutta_corepos.db.model.rst | 6 ++ docs/api/wutta_corepos.db.rst | 6 ++ docs/index.rst | 13 +++- docs/narr/install.rst | 44 ++++++++++++ src/wutta_corepos/db/__init__.py | 0 .../0f94089f1af1_initial_user_extension.py | 37 ++++++++++ src/wutta_corepos/db/model.py | 67 +++++++++++++++++++ tests/db/test_model.py | 16 +++++ 8 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 docs/api/wutta_corepos.db.model.rst create mode 100644 docs/api/wutta_corepos.db.rst create mode 100644 src/wutta_corepos/db/__init__.py create mode 100644 src/wutta_corepos/db/alembic/versions/0f94089f1af1_initial_user_extension.py create mode 100644 src/wutta_corepos/db/model.py create mode 100644 tests/db/test_model.py diff --git a/docs/api/wutta_corepos.db.model.rst b/docs/api/wutta_corepos.db.model.rst new file mode 100644 index 0000000..9954c44 --- /dev/null +++ b/docs/api/wutta_corepos.db.model.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.db.model`` +========================== + +.. automodule:: wutta_corepos.db.model + :members: diff --git a/docs/api/wutta_corepos.db.rst b/docs/api/wutta_corepos.db.rst new file mode 100644 index 0000000..c04bb3e --- /dev/null +++ b/docs/api/wutta_corepos.db.rst @@ -0,0 +1,6 @@ + +``wutta_corepos.db`` +==================== + +.. automodule:: wutta_corepos.db + :members: diff --git a/docs/index.rst b/docs/index.rst index 4f5d57b..a9beddd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,8 +5,15 @@ Wutta-COREPOS This package adds basic integration with `CORE-POS`_, using `pyCOREPOS`_. -Its main purpose is to setup DB connections for CORE Office, but it -also contains basic readonly web views for some CORE tables. +It provides the following: + +* standard configuration for CORE Office + Lane databases +* special :term:`handler` for CORE integration + (:class:`~wutta_corepos.handler.CoreposHandler`) +* readonly web views for primary CORE Office DB tables +* :term:`data model` extension to map + :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` to CORE + Employee .. _CORE-POS: https://www.core-pos.com/ @@ -26,6 +33,8 @@ also contains basic readonly web views for some CORE tables. api/wutta_corepos api/wutta_corepos.app api/wutta_corepos.conf + api/wutta_corepos.db + api/wutta_corepos.db.model api/wutta_corepos.handler api/wutta_corepos.web api/wutta_corepos.web.db diff --git a/docs/narr/install.rst b/docs/narr/install.rst index bceda1c..fdff505 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -39,3 +39,47 @@ related settings. 02.url = mysql+mysqlconnector://lane02/translog And that's it, the CORE-POS integration is configured. + + +Schema Extension +---------------- + +As of writing the only reason to add the schema extension is if you +need to map Wutta Users to CORE Employees, for auth (login) purposes. +So this section can be skipped if you do not need that. + +This will effectively add the +:attr:`~wutta_corepos.db.model.CoreUser.corepos_employee_number` +attribute on the +:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` model. + +First you must override the :term:`app model` with your own. To do +this, create your own module (e.g. ``poser.db.model``) to contain:: + + from wuttjamaican.db.model import * + from wutta_corepos.db.model import * + +Then configure your app model to override the default: + +.. code-block:: ini + + [wutta] + model_spec = poser.db.model + +Then configure the Alembic section for schema migrations: + +.. code-block:: ini + + [alembic] + script_location = wuttjamaican.db:alembic + version_locations = wutta_corepos.db:alembic/versions wuttjamaican.db:alembic/versions + +And finally run the Alembic command to migrate: + +.. code-block:: sh + + cd /path/to/env + bin/alembic -c app/wutta.conf upgrade heads + +That should do it, from then on any changes will be migrated +automatically during upgrade. diff --git a/src/wutta_corepos/db/__init__.py b/src/wutta_corepos/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wutta_corepos/db/alembic/versions/0f94089f1af1_initial_user_extension.py b/src/wutta_corepos/db/alembic/versions/0f94089f1af1_initial_user_extension.py new file mode 100644 index 0000000..92ed240 --- /dev/null +++ b/src/wutta_corepos/db/alembic/versions/0f94089f1af1_initial_user_extension.py @@ -0,0 +1,37 @@ +"""initial user extension + +Revision ID: 0f94089f1af1 +Revises: +Create Date: 2025-01-24 21:13:14.359200 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = '0f94089f1af1' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = ('wutta_corepos',) +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # corepos_user + op.create_table('corepos_user', + sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), + sa.Column('corepos_employee_number', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['uuid'], ['user.uuid'], name=op.f('fk_corepos_user_uuid_user')), + sa.PrimaryKeyConstraint('uuid', name=op.f('pk_corepos_user')), + sa.UniqueConstraint('corepos_employee_number', name=op.f('uq_corepos_user_corepos_employee_number')) + ) + + +def downgrade() -> None: + + # corepos_user + op.drop_table('corepos_user') diff --git a/src/wutta_corepos/db/model.py b/src/wutta_corepos/db/model.py new file mode 100644 index 0000000..89cf90d --- /dev/null +++ b/src/wutta_corepos/db/model.py @@ -0,0 +1,67 @@ +# -*- 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 . +# +################################################################################ +""" +Data models for CORE-POS integration +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from wuttjamaican.db import model + + +class CoreUser(model.Base): + """ + CORE-POS extension for + :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`. + """ + + __tablename__ = 'corepos_user' + + uuid = model.uuid_column(sa.ForeignKey('user.uuid'), default=None) + user = orm.relationship( + model.User, + cascade_backrefs=False, + doc=""" + Reference to the + :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` which + this record extends. + """, + backref=orm.backref( + '_corepos', + uselist=False, + cascade='all, delete-orphan', + cascade_backrefs=False, + doc=""" + Reference to the CORE-POS extension record for the user. + """) + ) + + corepos_employee_number = sa.Column(sa.Integer(), nullable=False, unique=True, doc=""" + ``employees.emp_no`` value for the user within CORE-POS. + """) + + def __str__(self): + return str(self.user) + +CoreUser.make_proxy(model.User, '_corepos', 'corepos_employee_number') diff --git a/tests/db/test_model.py b/tests/db/test_model.py new file mode 100644 index 0000000..19135d3 --- /dev/null +++ b/tests/db/test_model.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8; -*- + +from wuttjamaican.testing import ConfigTestCase +from wuttjamaican.db.model import User + +from wutta_corepos.db import model as mod + + +class TestCoreUser(ConfigTestCase): + + def test_str(self): + user = User(username='barney') + self.assertEqual(str(user), 'barney') + + ext = mod.CoreUser(user=user, corepos_employee_number=42) + self.assertEqual(str(ext), 'barney') From 4df38318e38182d6901df87584da180de0d0ffa9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 25 Jan 2025 19:11:46 -0600 Subject: [PATCH 08/13] fix: bump min version for wuttjamaican need Base.make_proxy() --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 76af27c..ffd2ca7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ requires-python = ">= 3.8" dependencies = [ "pyCOREPOS>=0.4.0", - "WuttJamaican>=0.20.1", + "WuttJamaican>=0.20.3", ] From f0ac1d9bd46c9d0359b6aa6f31e4fb822de5b8f0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 18 Feb 2025 12:16:32 -0600 Subject: [PATCH 09/13] docs: update intersphinx doc links per server migration --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4759410..2f4f53f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,8 +27,8 @@ 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), + 'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None), + 'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None), } From eb9291fce7f4d5bd87053266b9e0d5444c0f7656 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 20 Feb 2025 09:32:26 -0600 Subject: [PATCH 10/13] =?UTF-8?q?bump:=20version=200.2.1=20=E2=86=92=200.3?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24f220b..9b25e7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to Wutta-COREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.3.0 (2025-02-20) + +### Feat + +- add app db schema extension, for CoreUser +- add support for `lane_op` and `lane_trans` DB sessions, models + +### Fix + +- bump min version for wuttjamaican +- bump version requirement for pyCOREPOS +- add `get_office_employee_url()` method for corepos handler +- add grid links for Members +- bump min version for pycorepos dependency + ## v0.2.1 (2025-01-15) ### Fix diff --git a/pyproject.toml b/pyproject.toml index ffd2ca7..ba60abb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Wutta-COREPOS" -version = "0.2.1" +version = "0.3.0" description = "Wutta Framework integration for CORE-POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] @@ -28,7 +28,7 @@ classifiers = [ ] requires-python = ">= 3.8" dependencies = [ - "pyCOREPOS>=0.4.0", + "pyCOREPOS>=0.5.1", "WuttJamaican>=0.20.3", ] From 1b4ba2089ac0a5faf1c2784777b06abb7c8f8dfd Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 31 Aug 2025 13:05:00 -0500 Subject: [PATCH 11/13] fix: format all code with black and from now on should not deviate from that... --- docs/conf.py | 28 +++--- src/wutta_corepos/_version.py | 2 +- src/wutta_corepos/app.py | 8 +- src/wutta_corepos/conf.py | 34 ++++--- .../0f94089f1af1_initial_user_extension.py | 29 +++--- src/wutta_corepos/db/model.py | 23 +++-- src/wutta_corepos/handler.py | 74 ++++++--------- src/wutta_corepos/web/__init__.py | 2 +- src/wutta_corepos/web/views/__init__.py | 2 +- .../web/views/corepos/__init__.py | 4 +- src/wutta_corepos/web/views/corepos/master.py | 1 + .../web/views/corepos/members.py | 62 +++++++------ .../web/views/corepos/products.py | 41 +++++---- tasks.py | 10 +- tests/db/test_model.py | 6 +- tests/test_conf.py | 18 ++-- tests/test_handler.py | 91 +++++++++++++------ tests/web/views/corepos/test_members.py | 14 +-- tests/web/views/corepos/test_products.py | 4 +- 19 files changed, 248 insertions(+), 205 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2f4f53f..f27fe59 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,32 +8,32 @@ from importlib.metadata import version as get_version -project = 'Wutta-COREPOS' -copyright = '2025, Lance Edgar' -author = 'Lance Edgar' -release = get_version('Wutta-COREPOS') +project = "Wutta-COREPOS" +copyright = "2025, Lance Edgar" +author = "Lance Edgar" +release = get_version("Wutta-COREPOS") # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - 'sphinx.ext.todo', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx.ext.todo", ] -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] intersphinx_mapping = { - 'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None), - 'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None), + "wuttaweb": ("https://docs.wuttaproject.org/wuttaweb/", None), + "wuttjamaican": ("https://docs.wuttaproject.org/wuttjamaican/", None), } # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'furo' -html_static_path = ['_static'] +html_theme = "furo" +html_static_path = ["_static"] diff --git a/src/wutta_corepos/_version.py b/src/wutta_corepos/_version.py index 7601e62..dfdbb08 100644 --- a/src/wutta_corepos/_version.py +++ b/src/wutta_corepos/_version.py @@ -3,4 +3,4 @@ from importlib.metadata import version -__version__ = version('Wutta-COREPOS') +__version__ = version("Wutta-COREPOS") diff --git a/src/wutta_corepos/app.py b/src/wutta_corepos/app.py index 7949339..fdc310f 100644 --- a/src/wutta_corepos/app.py +++ b/src/wutta_corepos/app.py @@ -41,9 +41,11 @@ class WuttaCoreposAppProvider(AppProvider): :rtype: :class:`~wutta_corepos.handler.CoreposHandler` """ - if not hasattr(self, 'corepos_handler'): - spec = self.config.get(f'{self.appname}.corepos_handler', - default='wutta_corepos.handler:CoreposHandler') + if not hasattr(self, "corepos_handler"): + spec = self.config.get( + f"{self.appname}.corepos_handler", + default="wutta_corepos.handler:CoreposHandler", + ) factory = self.app.load_object(spec) self.corepos_handler = factory(self.config, **kwargs) return self.corepos_handler diff --git a/src/wutta_corepos/conf.py b/src/wutta_corepos/conf.py index 7c3affb..70add3c 100644 --- a/src/wutta_corepos/conf.py +++ b/src/wutta_corepos/conf.py @@ -103,48 +103,56 @@ class WuttaCoreposConfigExtension(WuttaConfigExtension): etc. If present, the ``default`` key will correspond to :data:`core_lane_trans_engine`. """ - key = 'wutta_corepos' + + key = "wutta_corepos" def configure(self, config): """ """ # office_op from corepos.db.office_op import Session - engines = get_engines(config, 'corepos.db.office_op') + + engines = get_engines(config, "corepos.db.office_op") config.core_office_op_engines = engines - config.core_office_op_engine = engines.get('default') + config.core_office_op_engine = engines.get("default") Session.configure(bind=config.core_office_op_engine) # office_trans from corepos.db.office_trans import Session - engines = get_engines(config, 'corepos.db.office_trans') + + engines = get_engines(config, "corepos.db.office_trans") config.core_office_trans_engines = engines - config.core_office_trans_engine = engines.get('default') + config.core_office_trans_engine = engines.get("default") Session.configure(bind=config.core_office_trans_engine) # office_arch from corepos.db.office_arch import Session - engines = get_engines(config, 'corepos.db.office_arch') + + engines = get_engines(config, "corepos.db.office_arch") config.core_office_arch_engines = engines - config.core_office_arch_engine = engines.get('default') + config.core_office_arch_engine = engines.get("default") Session.configure(bind=config.core_office_arch_engine) # lane_op from corepos.db.lane_op import Session - engines = get_engines(config, 'corepos.db.lane_op') + + engines = get_engines(config, "corepos.db.lane_op") config.core_lane_op_engines = engines - config.core_lane_op_engine = engines.get('default') + config.core_lane_op_engine = engines.get("default") Session.configure(bind=config.core_lane_op_engine) # lane_trans from corepos.db.lane_trans import Session - engines = get_engines(config, 'corepos.db.lane_trans') + + engines = get_engines(config, "corepos.db.lane_trans") config.core_lane_trans_engines = engines - config.core_lane_trans_engine = engines.get('default') + config.core_lane_trans_engine = engines.get("default") Session.configure(bind=config.core_lane_trans_engine) # define some schema columns "late" unless not supported - if config.get_bool('corepos.db.office_op.use_latest_columns', - default=True, usedb=False): + if config.get_bool( + "corepos.db.office_op.use_latest_columns", default=True, usedb=False + ): from corepos.db.office_op.model import use_latest_columns + use_latest_columns() diff --git a/src/wutta_corepos/db/alembic/versions/0f94089f1af1_initial_user_extension.py b/src/wutta_corepos/db/alembic/versions/0f94089f1af1_initial_user_extension.py index 92ed240..cc5ffa5 100644 --- a/src/wutta_corepos/db/alembic/versions/0f94089f1af1_initial_user_extension.py +++ b/src/wutta_corepos/db/alembic/versions/0f94089f1af1_initial_user_extension.py @@ -1,10 +1,11 @@ """initial user extension Revision ID: 0f94089f1af1 -Revises: +Revises: Create Date: 2025-01-24 21:13:14.359200 """ + from typing import Sequence, Union from alembic import op @@ -13,25 +14,31 @@ import wuttjamaican.db.util # revision identifiers, used by Alembic. -revision: str = '0f94089f1af1' +revision: str = "0f94089f1af1" down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = ('wutta_corepos',) +branch_labels: Union[str, Sequence[str], None] = ("wutta_corepos",) depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # corepos_user - op.create_table('corepos_user', - sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), - sa.Column('corepos_employee_number', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['uuid'], ['user.uuid'], name=op.f('fk_corepos_user_uuid_user')), - sa.PrimaryKeyConstraint('uuid', name=op.f('pk_corepos_user')), - sa.UniqueConstraint('corepos_employee_number', name=op.f('uq_corepos_user_corepos_employee_number')) - ) + op.create_table( + "corepos_user", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("corepos_employee_number", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["uuid"], ["user.uuid"], name=op.f("fk_corepos_user_uuid_user") + ), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_corepos_user")), + sa.UniqueConstraint( + "corepos_employee_number", + name=op.f("uq_corepos_user_corepos_employee_number"), + ), + ) def downgrade() -> None: # corepos_user - op.drop_table('corepos_user') + op.drop_table("corepos_user") diff --git a/src/wutta_corepos/db/model.py b/src/wutta_corepos/db/model.py index 89cf90d..6b1781b 100644 --- a/src/wutta_corepos/db/model.py +++ b/src/wutta_corepos/db/model.py @@ -36,9 +36,9 @@ class CoreUser(model.Base): :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`. """ - __tablename__ = 'corepos_user' + __tablename__ = "corepos_user" - uuid = model.uuid_column(sa.ForeignKey('user.uuid'), default=None) + uuid = model.uuid_column(sa.ForeignKey("user.uuid"), default=None) user = orm.relationship( model.User, cascade_backrefs=False, @@ -48,20 +48,27 @@ class CoreUser(model.Base): this record extends. """, backref=orm.backref( - '_corepos', + "_corepos", uselist=False, - cascade='all, delete-orphan', + cascade="all, delete-orphan", cascade_backrefs=False, doc=""" Reference to the CORE-POS extension record for the user. - """) + """, + ), ) - corepos_employee_number = sa.Column(sa.Integer(), nullable=False, unique=True, doc=""" + corepos_employee_number = sa.Column( + sa.Integer(), + nullable=False, + unique=True, + doc=""" ``employees.emp_no`` value for the user within CORE-POS. - """) + """, + ) def __str__(self): return str(self.user) -CoreUser.make_proxy(model.User, '_corepos', 'corepos_employee_number') + +CoreUser.make_proxy(model.User, "_corepos", "corepos_employee_number") diff --git a/src/wutta_corepos/handler.py b/src/wutta_corepos/handler.py index f52ef8e..cfbae46 100644 --- a/src/wutta_corepos/handler.py +++ b/src/wutta_corepos/handler.py @@ -78,7 +78,7 @@ class CoreposHandler(GenericHandler): return model - def make_session_office_op(self, dbkey='default', **kwargs): + def make_session_office_op(self, dbkey="default", **kwargs): """ Make a new :term:`db session` for the CORE Office 'op' DB. @@ -87,11 +87,11 @@ class CoreposHandler(GenericHandler): """ from corepos.db.office_op import Session - if 'bind' not in kwargs: - kwargs['bind'] = self.config.core_office_op_engines[dbkey] + 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): + def make_session_office_trans(self, dbkey="default", **kwargs): """ Make a new :term:`db session` for the CORE Office 'trans' DB. @@ -100,11 +100,11 @@ class CoreposHandler(GenericHandler): """ from corepos.db.office_trans import Session - if 'bind' not in kwargs: - kwargs['bind'] = self.config.core_office_trans_engines[dbkey] + 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): + def make_session_office_arch(self, dbkey="default", **kwargs): """ Make a new :term:`db session` for the CORE Office 'arch' DB. @@ -113,11 +113,11 @@ class CoreposHandler(GenericHandler): """ from corepos.db.office_arch import Session - if 'bind' not in kwargs: - kwargs['bind'] = self.config.core_office_arch_engines[dbkey] + if "bind" not in kwargs: + kwargs["bind"] = self.config.core_office_arch_engines[dbkey] return Session(**kwargs) - def make_session_lane_op(self, dbkey='default', **kwargs): + def make_session_lane_op(self, dbkey="default", **kwargs): """ Make a new :term:`db session` for the CORE Lane 'op' DB. @@ -126,11 +126,11 @@ class CoreposHandler(GenericHandler): """ from corepos.db.lane_op import Session - if 'bind' not in kwargs: - kwargs['bind'] = self.config.core_lane_op_engines[dbkey] + if "bind" not in kwargs: + kwargs["bind"] = self.config.core_lane_op_engines[dbkey] return Session(**kwargs) - def make_session_lane_trans(self, dbkey='default', **kwargs): + def make_session_lane_trans(self, dbkey="default", **kwargs): """ Make a new :term:`db session` for the CORE Lane 'trans' DB. @@ -139,8 +139,8 @@ class CoreposHandler(GenericHandler): """ from corepos.db.lane_trans import Session - if 'bind' not in kwargs: - kwargs['bind'] = self.config.core_lane_trans_engines[dbkey] + if "bind" not in kwargs: + kwargs["bind"] = self.config.core_lane_trans_engines[dbkey] return Session(**kwargs) def get_office_url(self, require=False): @@ -154,15 +154,11 @@ class CoreposHandler(GenericHandler): :returns: URL as string. """ - url = self.config.get('corepos.office.url', require=require) + url = self.config.get("corepos.office.url", require=require) if url: - return url.rstrip('/') + return url.rstrip("/") - def get_office_department_url( - self, - dept_id, - office_url=None, - require=False): + def get_office_department_url(self, dept_id, office_url=None, require=False): """ Returns the CORE Office URL for a Department. @@ -178,13 +174,9 @@ class CoreposHandler(GenericHandler): if not office_url: office_url = self.get_office_url(require=require) if office_url: - return f'{office_url}/item/departments/DepartmentEditor.php?did={dept_id}' + return f"{office_url}/item/departments/DepartmentEditor.php?did={dept_id}" - def get_office_employee_url( - self, - employee_id, - office_url=None, - require=False): + def get_office_employee_url(self, employee_id, office_url=None, require=False): """ Returns the CORE Office URL for an Employee. @@ -200,13 +192,9 @@ class CoreposHandler(GenericHandler): if not office_url: office_url = self.get_office_url(require=require) if office_url: - return f'{office_url}/admin/Cashiers/CashierEditor.php?emp_no={employee_id}' + return f"{office_url}/admin/Cashiers/CashierEditor.php?emp_no={employee_id}" - def get_office_likecode_url( - self, - likecode_id, - office_url=None, - require=False): + def get_office_likecode_url(self, likecode_id, office_url=None, require=False): """ Returns the CORE Office URL for a Like Code. @@ -222,13 +210,9 @@ class CoreposHandler(GenericHandler): if not office_url: office_url = self.get_office_url(require=require) if office_url: - return f'{office_url}/item/likecodes/LikeCodeEditor.php?start={likecode_id}' + return f"{office_url}/item/likecodes/LikeCodeEditor.php?start={likecode_id}" - def get_office_product_url( - self, - upc, - office_url=None, - require=False): + def get_office_product_url(self, upc, office_url=None, require=False): """ Returns the CORE Office URL for a Product. @@ -244,13 +228,9 @@ class CoreposHandler(GenericHandler): if not office_url: office_url = self.get_office_url(require=require) if office_url: - return f'{office_url}/item/ItemEditorPage.php?searchupc={upc}' + return f"{office_url}/item/ItemEditorPage.php?searchupc={upc}" - def get_office_vendor_url( - self, - vend_id, - office_url=None, - require=False): + def get_office_vendor_url(self, vend_id, office_url=None, require=False): """ Returns the CORE Office URL for a Vendor. @@ -266,4 +246,4 @@ class CoreposHandler(GenericHandler): if not office_url: office_url = self.get_office_url(require=require) if office_url: - return f'{office_url}/item/vendors/VendorIndexPage.php?vid={vend_id}' + return f"{office_url}/item/vendors/VendorIndexPage.php?vid={vend_id}" diff --git a/src/wutta_corepos/web/__init__.py b/src/wutta_corepos/web/__init__.py index 71a9477..27a88a5 100644 --- a/src/wutta_corepos/web/__init__.py +++ b/src/wutta_corepos/web/__init__.py @@ -26,4 +26,4 @@ Wutta-COREPOS -- wuttaweb features def includeme(config): - config.include('wutta_corepos.web.views') + config.include("wutta_corepos.web.views") diff --git a/src/wutta_corepos/web/views/__init__.py b/src/wutta_corepos/web/views/__init__.py index d4112a2..78797ab 100644 --- a/src/wutta_corepos/web/views/__init__.py +++ b/src/wutta_corepos/web/views/__init__.py @@ -26,4 +26,4 @@ Wutta-COREPOS -- wuttaweb views def includeme(config): - config.include('wutta_corepos.web.views.corepos') + 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 index a78e4ef..436e8d5 100644 --- a/src/wutta_corepos/web/views/corepos/__init__.py +++ b/src/wutta_corepos/web/views/corepos/__init__.py @@ -28,5 +28,5 @@ from .master import CoreOpMasterView def includeme(config): - config.include('wutta_corepos.web.views.corepos.members') - config.include('wutta_corepos.web.views.corepos.products') + 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 index 817a443..92c7f9a 100644 --- a/src/wutta_corepos/web/views/corepos/master.py +++ b/src/wutta_corepos/web/views/corepos/master.py @@ -33,6 +33,7 @@ class CoreOpMasterView(MasterView): """ Base class for master views which use the CORE Office 'op' DB. """ + Session = CoreOpSession def __init__(self, request, context=None): diff --git a/src/wutta_corepos/web/views/corepos/members.py b/src/wutta_corepos/web/views/corepos/members.py index 19a6268..ffa106c 100644 --- a/src/wutta_corepos/web/views/corepos/members.py +++ b/src/wutta_corepos/web/views/corepos/members.py @@ -43,10 +43,11 @@ class MemberView(CoreOpMasterView): * ``/corepos/members/`` * ``/corepos/members/XXX`` """ + model_class = MemberInfo model_title = "CORE-POS Member" - route_prefix = 'corepos_members' - url_prefix = '/corepos/members' + route_prefix = "corepos_members" + url_prefix = "/corepos/members" # nb. this is just for readonly lookup creatable = False @@ -54,36 +55,37 @@ class MemberView(CoreOpMasterView): deletable = False grid_columns = [ - 'card_number', - 'first_name', - 'last_name', - 'street', - 'city', - 'state', - 'zip', - 'phone', - 'email', + "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'}, + "card_number": {"active": True, "verb": "equal"}, + "first_name": {"active": True, "verb": "contains"}, + "last_name": {"active": True, "verb": "contains"}, } - sort_defaults = 'card_number' + 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)) + 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 @@ -93,18 +95,18 @@ class MemberView(CoreOpMasterView): 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) + 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) + g.set_renderer("last_name", self.render_customer_attr) + g.set_sorter("last_name", op_model.CustomerClassic.last_name) # links - if self.has_perm('view'): - g.set_link('card_number') - g.set_link('first_name') - g.set_link('last_name') + if self.has_perm("view"): + g.set_link("card_number") + g.set_link("first_name") + g.set_link("last_name") def render_customer_attr(self, member, key, value): """ """ @@ -115,7 +117,7 @@ class MemberView(CoreOpMasterView): def defaults(config, **kwargs): base = globals() - MemberView = kwargs.get('MemberView', base['MemberView']) + MemberView = kwargs.get("MemberView", base["MemberView"]) MemberView.defaults(config) diff --git a/src/wutta_corepos/web/views/corepos/products.py b/src/wutta_corepos/web/views/corepos/products.py index e8f33c4..24e94af 100644 --- a/src/wutta_corepos/web/views/corepos/products.py +++ b/src/wutta_corepos/web/views/corepos/products.py @@ -40,10 +40,11 @@ class ProductView(CoreOpMasterView): * ``/corepos/products/`` * ``/corepos/products/XXX`` """ + model_class = Product model_title = "CORE-POS Product" - route_prefix = 'corepos_products' - url_prefix = '/corepos/products' + route_prefix = "corepos_products" + url_prefix = "/corepos/products" # nb. this is just for readonly lookup creatable = False @@ -51,45 +52,45 @@ class ProductView(CoreOpMasterView): deletable = False labels = { - 'upc': "UPC", + "upc": "UPC", } grid_columns = [ - 'upc', - 'brand', - 'description', - 'size', - 'department', - 'vendor', - 'normal_price', + "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'}, + "upc": {"active": True, "verb": "contains"}, + "brand": {"active": True, "verb": "contains"}, + "description": {"active": True, "verb": "contains"}, } - sort_defaults = 'upc' + sort_defaults = "upc" def configure_grid(self, g): """ """ super().configure_grid(g) # normal_price - g.set_renderer('normal_price', 'currency') + g.set_renderer("normal_price", "currency") # links - g.set_link('upc') - g.set_link('brand') - g.set_link('description') - g.set_link('size') + 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 = kwargs.get("ProductView", base["ProductView"]) ProductView.defaults(config) diff --git a/tasks.py b/tasks.py index 101278d..1eaed3b 100644 --- a/tasks.py +++ b/tasks.py @@ -15,10 +15,10 @@ def release(c, skip_tests=False): Release a new version of Wutta-COREPOS """ if not skip_tests: - c.run('pytest') + c.run("pytest") - if os.path.exists('dist'): - shutil.rmtree('dist') + if os.path.exists("dist"): + shutil.rmtree("dist") - c.run('python -m build --sdist') - c.run('twine upload dist/*') + c.run("python -m build --sdist") + c.run("twine upload dist/*") diff --git a/tests/db/test_model.py b/tests/db/test_model.py index 19135d3..8d76745 100644 --- a/tests/db/test_model.py +++ b/tests/db/test_model.py @@ -9,8 +9,8 @@ from wutta_corepos.db import model as mod class TestCoreUser(ConfigTestCase): def test_str(self): - user = User(username='barney') - self.assertEqual(str(user), 'barney') + user = User(username="barney") + self.assertEqual(str(user), "barney") ext = mod.CoreUser(user=user, corepos_employee_number=42) - self.assertEqual(str(ext), 'barney') + self.assertEqual(str(ext), "barney") diff --git a/tests/test_conf.py b/tests/test_conf.py index 10f97a0..db26ce3 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -13,11 +13,11 @@ class TestWuttaCoreposConfigExtension(TestCase): config = WuttaConfig() # no engines by default - self.assertFalse(hasattr(config, 'core_office_op_engine')) - self.assertFalse(hasattr(config, 'core_office_trans_engine')) - self.assertFalse(hasattr(config, 'core_office_arch_engine')) - self.assertFalse(hasattr(config, 'core_lane_op_engine')) - self.assertFalse(hasattr(config, 'core_lane_trans_engine')) + self.assertFalse(hasattr(config, "core_office_op_engine")) + self.assertFalse(hasattr(config, "core_office_trans_engine")) + self.assertFalse(hasattr(config, "core_office_arch_engine")) + self.assertFalse(hasattr(config, "core_lane_op_engine")) + self.assertFalse(hasattr(config, "core_lane_trans_engine")) ext = mod.WuttaCoreposConfigExtension() ext.configure(config) self.assertIsNone(config.core_office_op_engine) @@ -27,10 +27,10 @@ class TestWuttaCoreposConfigExtension(TestCase): self.assertIsNone(config.core_lane_trans_engine) # but config can change that - config.setdefault('corepos.db.office_op.default.url', 'sqlite://') - config.setdefault('corepos.db.lane_trans.default.url', 'sqlite://') + config.setdefault("corepos.db.office_op.default.url", "sqlite://") + config.setdefault("corepos.db.lane_trans.default.url", "sqlite://") ext.configure(config) self.assertIsNotNone(config.core_office_op_engine) - self.assertEqual(str(config.core_office_op_engine.url), 'sqlite://') + self.assertEqual(str(config.core_office_op_engine.url), "sqlite://") self.assertIsNotNone(config.core_lane_trans_engine) - self.assertEqual(str(config.core_lane_trans_engine.url), 'sqlite://') + self.assertEqual(str(config.core_lane_trans_engine.url), "sqlite://") diff --git a/tests/test_handler.py b/tests/test_handler.py index f751f22..b597044 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -18,75 +18,91 @@ class TestCoreposHandler(ConfigTestCase): 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_get_model_lane_op(self): from corepos.db.lane_op import model + handler = self.make_handler() op_model = handler.get_model_lane_op() self.assertIs(op_model, model) def test_get_model_lane_trans(self): from corepos.db.lane_trans import model + handler = self.make_handler() trans_model = handler.get_model_lane_trans() self.assertIs(trans_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}): + 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}): + 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}): + 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_make_session_lane_op(self): handler = self.make_handler() - engine = sa.create_engine('sqlite://') - with patch.object(self.config, 'core_lane_op_engines', create=True, - new={'default': engine}): + engine = sa.create_engine("sqlite://") + with patch.object( + self.config, "core_lane_op_engines", create=True, new={"default": engine} + ): op_session = handler.make_session_lane_op() self.assertIsInstance(op_session, orm.Session) self.assertIs(op_session.bind, engine) def test_make_session_lane_trans(self): handler = self.make_handler() - engine = sa.create_engine('sqlite://') - with patch.object(self.config, 'core_lane_trans_engines', create=True, - new={'default': engine}): + engine = sa.create_engine("sqlite://") + with patch.object( + self.config, "core_lane_trans_engines", create=True, new={"default": engine} + ): trans_session = handler.make_session_lane_trans() self.assertIsInstance(trans_session, orm.Session) self.assertIs(trans_session.bind, engine) @@ -101,9 +117,11 @@ class TestCoreposHandler(ConfigTestCase): self.assertRaises(ConfigurationError, handler.get_office_url, require=True) # config can specify (traliing slash is stripped) - self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') - self.assertEqual(handler.get_office_url(), 'http://localhost/fannie') - self.assertEqual(handler.get_office_url(require=True), 'http://localhost/fannie') + self.config.setdefault("corepos.office.url", "http://localhost/fannie/") + self.assertEqual(handler.get_office_url(), "http://localhost/fannie") + self.assertEqual( + handler.get_office_url(require=True), "http://localhost/fannie" + ) def test_get_office_department_url(self): handler = self.make_handler() @@ -112,8 +130,11 @@ class TestCoreposHandler(ConfigTestCase): self.assertIsNone(handler.get_office_department_url(7)) # typical - self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') - self.assertEqual(handler.get_office_department_url(7), 'http://localhost/fannie/item/departments/DepartmentEditor.php?did=7') + self.config.setdefault("corepos.office.url", "http://localhost/fannie/") + self.assertEqual( + handler.get_office_department_url(7), + "http://localhost/fannie/item/departments/DepartmentEditor.php?did=7", + ) def test_get_office_employee_url(self): handler = self.make_handler() @@ -122,8 +143,11 @@ class TestCoreposHandler(ConfigTestCase): self.assertIsNone(handler.get_office_employee_url(7)) # typical - self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') - self.assertEqual(handler.get_office_employee_url(7), 'http://localhost/fannie/admin/Cashiers/CashierEditor.php?emp_no=7') + self.config.setdefault("corepos.office.url", "http://localhost/fannie/") + self.assertEqual( + handler.get_office_employee_url(7), + "http://localhost/fannie/admin/Cashiers/CashierEditor.php?emp_no=7", + ) def test_get_office_likecode_url(self): handler = self.make_handler() @@ -132,18 +156,24 @@ class TestCoreposHandler(ConfigTestCase): self.assertIsNone(handler.get_office_likecode_url(7)) # typical - self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') - self.assertEqual(handler.get_office_likecode_url(7), 'http://localhost/fannie/item/likecodes/LikeCodeEditor.php?start=7') + self.config.setdefault("corepos.office.url", "http://localhost/fannie/") + self.assertEqual( + handler.get_office_likecode_url(7), + "http://localhost/fannie/item/likecodes/LikeCodeEditor.php?start=7", + ) def test_get_office_product_url(self): handler = self.make_handler() # null - self.assertIsNone(handler.get_office_product_url('07430500132')) + self.assertIsNone(handler.get_office_product_url("07430500132")) # typical - self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') - self.assertEqual(handler.get_office_product_url('07430500132'), 'http://localhost/fannie/item/ItemEditorPage.php?searchupc=07430500132') + self.config.setdefault("corepos.office.url", "http://localhost/fannie/") + self.assertEqual( + handler.get_office_product_url("07430500132"), + "http://localhost/fannie/item/ItemEditorPage.php?searchupc=07430500132", + ) def test_get_office_vendor_url(self): handler = self.make_handler() @@ -152,5 +182,8 @@ class TestCoreposHandler(ConfigTestCase): self.assertIsNone(handler.get_office_vendor_url(7)) # typical - self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') - self.assertEqual(handler.get_office_vendor_url(7), 'http://localhost/fannie/item/vendors/VendorIndexPage.php?vid=7') + self.config.setdefault("corepos.office.url", "http://localhost/fannie/") + self.assertEqual( + handler.get_office_vendor_url(7), + "http://localhost/fannie/item/vendors/VendorIndexPage.php?vid=7", + ) diff --git a/tests/web/views/corepos/test_members.py b/tests/web/views/corepos/test_members.py index b3da7b0..9c4f557 100644 --- a/tests/web/views/corepos/test_members.py +++ b/tests/web/views/corepos/test_members.py @@ -29,16 +29,18 @@ class TestMemberView(WebTestCase): def test_configure_grid(self): view = self.make_view() grid = view.make_grid(model_class=view.model_class) - self.assertNotIn('first_name', grid.renderers) - self.assertNotIn('first_name', grid.linked_columns) - with patch.object(self.request, 'is_root', new=True): + self.assertNotIn("first_name", grid.renderers) + self.assertNotIn("first_name", grid.linked_columns) + with patch.object(self.request, "is_root", new=True): view.configure_grid(grid) - self.assertIn('first_name', grid.renderers) - self.assertIn('first_name', grid.linked_columns) + self.assertIn("first_name", grid.renderers) + self.assertIn("first_name", grid.linked_columns) 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") + 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 index 8870208..e2e988e 100644 --- a/tests/web/views/corepos/test_products.py +++ b/tests/web/views/corepos/test_products.py @@ -16,6 +16,6 @@ class TestProductView(WebTestCase): def test_configure_grid(self): view = self.make_view() grid = view.make_grid(model_class=view.model_class) - self.assertNotIn('upc', grid.linked_columns) + self.assertNotIn("upc", grid.linked_columns) view.configure_grid(grid) - self.assertIn('upc', grid.linked_columns) + self.assertIn("upc", grid.linked_columns) From 25b5ca127db77cd290ff6de987d37cca9ef63e2e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 31 Aug 2025 13:29:46 -0500 Subject: [PATCH 12/13] docs: add badge for black code style --- docs/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index a9beddd..c51685e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,6 +19,9 @@ It provides the following: .. _pyCOREPOS: https://pypi.org/project/pyCOREPOS/ +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + .. toctree:: :maxdepth: 2 From 988ebe29d01d8976823681462a69e585286da94f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 1 Sep 2025 20:52:34 -0500 Subject: [PATCH 13/13] fix: address all warnings from pylint --- .pylintrc | 4 ++ docs/index.rst | 3 ++ pyproject.toml | 2 +- src/wutta_corepos/_version.py | 3 ++ src/wutta_corepos/app.py | 6 +-- src/wutta_corepos/conf.py | 36 ++++++++++----- src/wutta_corepos/db/model.py | 2 +- src/wutta_corepos/handler.py | 44 ++++++++++++++----- src/wutta_corepos/web/__init__.py | 2 +- src/wutta_corepos/web/views/__init__.py | 2 +- .../web/views/corepos/__init__.py | 2 +- src/wutta_corepos/web/views/corepos/master.py | 2 +- .../web/views/corepos/members.py | 19 +++++--- .../web/views/corepos/products.py | 13 +++--- tox.ini | 4 ++ 15 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..7eb5e2c --- /dev/null +++ b/.pylintrc @@ -0,0 +1,4 @@ +# -*- mode: conf; -*- + +[MESSAGES CONTROL] +disable=fixme diff --git a/docs/index.rst b/docs/index.rst index c51685e..0effd08 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,6 +19,9 @@ It provides the following: .. _pyCOREPOS: https://pypi.org/project/pyCOREPOS/ +.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen + :target: https://github.com/pylint-dev/pylint + .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black diff --git a/pyproject.toml b/pyproject.toml index ba60abb..c252e30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ [project.optional-dependencies] web = ["WuttaWeb"] docs = ["Sphinx", "furo"] -tests = ["pytest-cov", "tox"] +tests = ["pylint", "pytest", "pytest-cov", "tox"] [project.entry-points."wutta.app.providers"] diff --git a/src/wutta_corepos/_version.py b/src/wutta_corepos/_version.py index dfdbb08..c78b835 100644 --- a/src/wutta_corepos/_version.py +++ b/src/wutta_corepos/_version.py @@ -1,4 +1,7 @@ # -*- coding: utf-8; -*- +""" +Package Version +""" from importlib.metadata import version diff --git a/src/wutta_corepos/app.py b/src/wutta_corepos/app.py index fdc310f..a937686 100644 --- a/src/wutta_corepos/app.py +++ b/src/wutta_corepos/app.py @@ -41,11 +41,11 @@ class WuttaCoreposAppProvider(AppProvider): :rtype: :class:`~wutta_corepos.handler.CoreposHandler` """ - if not hasattr(self, "corepos_handler"): + if "corepos" not in self.app.handlers: spec = self.config.get( f"{self.appname}.corepos_handler", default="wutta_corepos.handler:CoreposHandler", ) factory = self.app.load_object(spec) - self.corepos_handler = factory(self.config, **kwargs) - return self.corepos_handler + self.app.handlers["corepos"] = factory(self.config, **kwargs) + return self.app.handlers["corepos"] diff --git a/src/wutta_corepos/conf.py b/src/wutta_corepos/conf.py index 70add3c..2c2a309 100644 --- a/src/wutta_corepos/conf.py +++ b/src/wutta_corepos/conf.py @@ -106,53 +106,65 @@ class WuttaCoreposConfigExtension(WuttaConfigExtension): key = "wutta_corepos" - def configure(self, config): + def configure(self, config): # pylint: disable=empty-docstring """ """ # office_op - from corepos.db.office_op import Session + from corepos.db.office_op import ( # pylint: disable=import-outside-toplevel + Session as OfficeOpSession, + ) engines = get_engines(config, "corepos.db.office_op") config.core_office_op_engines = engines config.core_office_op_engine = engines.get("default") - Session.configure(bind=config.core_office_op_engine) + OfficeOpSession.configure(bind=config.core_office_op_engine) # office_trans - from corepos.db.office_trans import Session + from corepos.db.office_trans import ( # pylint: disable=import-outside-toplevel + Session as OfficeTransSession, + ) engines = get_engines(config, "corepos.db.office_trans") config.core_office_trans_engines = engines config.core_office_trans_engine = engines.get("default") - Session.configure(bind=config.core_office_trans_engine) + OfficeTransSession.configure(bind=config.core_office_trans_engine) # office_arch - from corepos.db.office_arch import Session + from corepos.db.office_arch import ( # pylint: disable=import-outside-toplevel + Session as OfficeArchSession, + ) engines = get_engines(config, "corepos.db.office_arch") config.core_office_arch_engines = engines config.core_office_arch_engine = engines.get("default") - Session.configure(bind=config.core_office_arch_engine) + OfficeArchSession.configure(bind=config.core_office_arch_engine) # lane_op - from corepos.db.lane_op import Session + from corepos.db.lane_op import ( # pylint: disable=import-outside-toplevel + Session as LaneOpSession, + ) engines = get_engines(config, "corepos.db.lane_op") config.core_lane_op_engines = engines config.core_lane_op_engine = engines.get("default") - Session.configure(bind=config.core_lane_op_engine) + LaneOpSession.configure(bind=config.core_lane_op_engine) # lane_trans - from corepos.db.lane_trans import Session + from corepos.db.lane_trans import ( # pylint: disable=import-outside-toplevel + Session as LaneTransSession, + ) engines = get_engines(config, "corepos.db.lane_trans") config.core_lane_trans_engines = engines config.core_lane_trans_engine = engines.get("default") - Session.configure(bind=config.core_lane_trans_engine) + LaneTransSession.configure(bind=config.core_lane_trans_engine) # define some schema columns "late" unless not supported if config.get_bool( "corepos.db.office_op.use_latest_columns", default=True, usedb=False ): - from corepos.db.office_op.model import use_latest_columns + from corepos.db.office_op.model import ( # pylint: disable=import-outside-toplevel + use_latest_columns, + ) use_latest_columns() diff --git a/src/wutta_corepos/db/model.py b/src/wutta_corepos/db/model.py index 6b1781b..9dba587 100644 --- a/src/wutta_corepos/db/model.py +++ b/src/wutta_corepos/db/model.py @@ -30,7 +30,7 @@ from sqlalchemy import orm from wuttjamaican.db import model -class CoreUser(model.Base): +class CoreUser(model.Base): # pylint: disable=too-few-public-methods """ CORE-POS extension for :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`. diff --git a/src/wutta_corepos/handler.py b/src/wutta_corepos/handler.py index cfbae46..76f0ddc 100644 --- a/src/wutta_corepos/handler.py +++ b/src/wutta_corepos/handler.py @@ -38,7 +38,9 @@ class CoreposHandler(GenericHandler): 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 + from corepos.db.office_op import ( # pylint: disable=import-outside-toplevel + model, + ) return model @@ -47,7 +49,9 @@ class CoreposHandler(GenericHandler): 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 + from corepos.db.office_trans import ( # pylint: disable=import-outside-toplevel + model, + ) return model @@ -56,7 +60,9 @@ class CoreposHandler(GenericHandler): 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 + from corepos.db.office_arch import ( # pylint: disable=import-outside-toplevel + model, + ) return model @@ -65,7 +71,7 @@ class CoreposHandler(GenericHandler): Returns the :term:`data model` module for CORE Lane 'op' DB, i.e. :mod:`pycorepos:corepos.db.lane_op.model`. """ - from corepos.db.lane_op import model + from corepos.db.lane_op import model # pylint: disable=import-outside-toplevel return model @@ -74,7 +80,9 @@ class CoreposHandler(GenericHandler): Returns the :term:`data model` module for CORE Lane 'trans' DB, i.e. :mod:`pycorepos:corepos.db.lane_trans.model`. """ - from corepos.db.lane_trans import model + from corepos.db.lane_trans import ( # pylint: disable=import-outside-toplevel + model, + ) return model @@ -85,7 +93,9 @@ class CoreposHandler(GenericHandler): :returns: Instance of :class:`pycorepos:corepos.db.office_op.Session`. """ - from corepos.db.office_op import Session + from corepos.db.office_op import ( # pylint: disable=import-outside-toplevel + Session, + ) if "bind" not in kwargs: kwargs["bind"] = self.config.core_office_op_engines[dbkey] @@ -98,7 +108,9 @@ class CoreposHandler(GenericHandler): :returns: Instance of :class:`pycorepos:corepos.db.office_trans.Session`. """ - from corepos.db.office_trans import Session + from corepos.db.office_trans import ( # pylint: disable=import-outside-toplevel + Session, + ) if "bind" not in kwargs: kwargs["bind"] = self.config.core_office_trans_engines[dbkey] @@ -111,7 +123,9 @@ class CoreposHandler(GenericHandler): :returns: Instance of :class:`pycorepos:corepos.db.office_arch.Session`. """ - from corepos.db.office_arch import Session + from corepos.db.office_arch import ( # pylint: disable=import-outside-toplevel + Session, + ) if "bind" not in kwargs: kwargs["bind"] = self.config.core_office_arch_engines[dbkey] @@ -124,7 +138,9 @@ class CoreposHandler(GenericHandler): :returns: Instance of :class:`pycorepos:corepos.db.lane_op.Session`. """ - from corepos.db.lane_op import Session + from corepos.db.lane_op import ( # pylint: disable=import-outside-toplevel + Session, + ) if "bind" not in kwargs: kwargs["bind"] = self.config.core_lane_op_engines[dbkey] @@ -137,7 +153,9 @@ class CoreposHandler(GenericHandler): :returns: Instance of :class:`pycorepos:corepos.db.lane_trans.Session`. """ - from corepos.db.lane_trans import Session + from corepos.db.lane_trans import ( # pylint: disable=import-outside-toplevel + Session, + ) if "bind" not in kwargs: kwargs["bind"] = self.config.core_lane_trans_engines[dbkey] @@ -157,6 +175,7 @@ class CoreposHandler(GenericHandler): url = self.config.get("corepos.office.url", require=require) if url: return url.rstrip("/") + return None def get_office_department_url(self, dept_id, office_url=None, require=False): """ @@ -175,6 +194,7 @@ class CoreposHandler(GenericHandler): office_url = self.get_office_url(require=require) if office_url: return f"{office_url}/item/departments/DepartmentEditor.php?did={dept_id}" + return None def get_office_employee_url(self, employee_id, office_url=None, require=False): """ @@ -193,6 +213,7 @@ class CoreposHandler(GenericHandler): office_url = self.get_office_url(require=require) if office_url: return f"{office_url}/admin/Cashiers/CashierEditor.php?emp_no={employee_id}" + return None def get_office_likecode_url(self, likecode_id, office_url=None, require=False): """ @@ -211,6 +232,7 @@ class CoreposHandler(GenericHandler): office_url = self.get_office_url(require=require) if office_url: return f"{office_url}/item/likecodes/LikeCodeEditor.php?start={likecode_id}" + return None def get_office_product_url(self, upc, office_url=None, require=False): """ @@ -229,6 +251,7 @@ class CoreposHandler(GenericHandler): office_url = self.get_office_url(require=require) if office_url: return f"{office_url}/item/ItemEditorPage.php?searchupc={upc}" + return None def get_office_vendor_url(self, vend_id, office_url=None, require=False): """ @@ -247,3 +270,4 @@ class CoreposHandler(GenericHandler): office_url = self.get_office_url(require=require) if office_url: return f"{office_url}/item/vendors/VendorIndexPage.php?vid={vend_id}" + return None diff --git a/src/wutta_corepos/web/__init__.py b/src/wutta_corepos/web/__init__.py index 27a88a5..d3a2ac9 100644 --- a/src/wutta_corepos/web/__init__.py +++ b/src/wutta_corepos/web/__init__.py @@ -25,5 +25,5 @@ Wutta-COREPOS -- wuttaweb features """ -def includeme(config): +def includeme(config): # pylint: disable=missing-function-docstring config.include("wutta_corepos.web.views") diff --git a/src/wutta_corepos/web/views/__init__.py b/src/wutta_corepos/web/views/__init__.py index 78797ab..c36452d 100644 --- a/src/wutta_corepos/web/views/__init__.py +++ b/src/wutta_corepos/web/views/__init__.py @@ -25,5 +25,5 @@ Wutta-COREPOS -- wuttaweb views """ -def includeme(config): +def includeme(config): # pylint: disable=missing-function-docstring 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 index 436e8d5..fff81f8 100644 --- a/src/wutta_corepos/web/views/corepos/__init__.py +++ b/src/wutta_corepos/web/views/corepos/__init__.py @@ -27,6 +27,6 @@ Views for CORE-POS from .master import CoreOpMasterView -def includeme(config): +def includeme(config): # pylint: disable=missing-function-docstring 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 index 92c7f9a..b0d9ab2 100644 --- a/src/wutta_corepos/web/views/corepos/master.py +++ b/src/wutta_corepos/web/views/corepos/master.py @@ -29,7 +29,7 @@ from wuttaweb.views import MasterView from wutta_corepos.web.db import CoreOpSession -class CoreOpMasterView(MasterView): +class CoreOpMasterView(MasterView): # pylint: disable=abstract-method """ Base class for master views which use the CORE Office 'op' DB. """ diff --git a/src/wutta_corepos/web/views/corepos/members.py b/src/wutta_corepos/web/views/corepos/members.py index ffa106c..ffaffd3 100644 --- a/src/wutta_corepos/web/views/corepos/members.py +++ b/src/wutta_corepos/web/views/corepos/members.py @@ -32,7 +32,7 @@ from corepos.db.office_op.model import MemberInfo from wutta_corepos.web.views.corepos import CoreOpMasterView -class MemberView(CoreOpMasterView): +class MemberView(CoreOpMasterView): # pylint: disable=abstract-method """ Master view for :class:`~pycorepos:corepos.db.office_op.model.MemberInfo`; route @@ -74,7 +74,7 @@ class MemberView(CoreOpMasterView): sort_defaults = "card_number" - def get_query(self, session=None): + def get_query(self, session=None): # pylint: disable=empty-docstring """ """ query = super().get_query(session=session) @@ -89,8 +89,9 @@ class MemberView(CoreOpMasterView): return query - def configure_grid(self, g): + def configure_grid(self, grid): # pylint: disable=empty-docstring """ """ + g = grid super().configure_grid(g) op_model = self.corepos_handler.get_model_office_op() @@ -108,18 +109,22 @@ class MemberView(CoreOpMasterView): g.set_link("first_name") g.set_link("last_name") - def render_customer_attr(self, member, key, value): + def render_customer_attr( # pylint: disable=unused-argument,empty-docstring + self, member, key, value + ): """ """ customer = member.customers[0] return getattr(customer, key) -def defaults(config, **kwargs): +def defaults(config, **kwargs): # pylint: disable=missing-function-docstring base = globals() - MemberView = kwargs.get("MemberView", base["MemberView"]) + MemberView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name + "MemberView", base["MemberView"] + ) MemberView.defaults(config) -def includeme(config): +def includeme(config): # pylint: disable=missing-function-docstring defaults(config) diff --git a/src/wutta_corepos/web/views/corepos/products.py b/src/wutta_corepos/web/views/corepos/products.py index 24e94af..19b016d 100644 --- a/src/wutta_corepos/web/views/corepos/products.py +++ b/src/wutta_corepos/web/views/corepos/products.py @@ -29,7 +29,7 @@ from corepos.db.office_op.model import Product from wutta_corepos.web.views.corepos import CoreOpMasterView -class ProductView(CoreOpMasterView): +class ProductView(CoreOpMasterView): # pylint: disable=abstract-method """ Master view for :class:`~pycorepos:corepos.db.office_op.model.Product`; route @@ -73,8 +73,9 @@ class ProductView(CoreOpMasterView): sort_defaults = "upc" - def configure_grid(self, g): + def configure_grid(self, grid): # pylint: disable=empty-docstring """ """ + g = grid super().configure_grid(g) # normal_price @@ -87,12 +88,14 @@ class ProductView(CoreOpMasterView): g.set_link("size") -def defaults(config, **kwargs): +def defaults(config, **kwargs): # pylint: disable=missing-function-docstring base = globals() - ProductView = kwargs.get("ProductView", base["ProductView"]) + ProductView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name + "ProductView", base["ProductView"] + ) ProductView.defaults(config) -def includeme(config): +def includeme(config): # pylint: disable=missing-function-docstring defaults(config) diff --git a/tox.ini b/tox.ini index 49b89cb..20021b1 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,10 @@ envlist = py38, py39, py310, py311 extras = web,tests commands = pytest {posargs} +[testenv:pylint] +basepython = python3.11 +commands = pylint wutta_corepos + [testenv:coverage] basepython = python3.11 commands = pytest --cov=wutta_corepos --cov-report=html --cov-fail-under=100