diff --git a/CHANGELOG.md b/CHANGELOG.md index 024d9ea..1a39299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,6 @@ All notable changes to WuttJamaican 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.19.2 (2025-01-06) - -### Fix - -- add `cascade_backrefs=False` for all ORM relationships -- add `get_effective_rows()` method for batch handler -- add `make_full_name()` function, app handler method -- add batch handler logic to remove row -- add `render_boolean`, `render_quantity` app handler methods -- update post-install webapp command suggestion - ## v0.19.1 (2024-12-28) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 9f0f2fe..9cd1790 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttJamaican" -version = "0.19.2" +version = "0.19.1" description = "Base package for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index c7e0b37..69ba3a9 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -33,7 +33,7 @@ import warnings import humanize from wuttjamaican.util import (load_entry_points, load_object, - make_title, make_full_name, make_uuid, make_true_uuid, + make_title, make_uuid, make_true_uuid, progress_loop, resource_path, simple_error) @@ -495,15 +495,6 @@ class AppHandler: """ return make_title(text) - def make_full_name(self, *parts): - """ - Make a "full name" from the given parts. - - This is a convenience wrapper around - :func:`~wuttjamaican.util.make_full_name()`. - """ - return make_full_name(*parts) - def make_true_uuid(self): """ Generate a new UUID value. @@ -685,20 +676,7 @@ class AppHandler: # common value renderers ############################## - def render_boolean(self, value): - """ - Render a boolean value for display. - - :param value: A boolean, or ``None``. - - :returns: Display string for the value. - """ - if value is None: - return '' - - return "Yes" if value else "No" - - def render_currency(self, value, scale=2): + def render_currency(self, value, scale=2, **kwargs): """ Return a human-friendly display string for the given currency value, e.g. ``Decimal('4.20')`` becomes ``"$4.20"``. @@ -771,28 +749,6 @@ class AppHandler: """ return simple_error(error) - def render_quantity(self, value, empty_zero=False): - """ - Return a human-friendly display string for the given quantity - value, e.g. ``1.000`` becomes ``"1"``. - - :param value: The quantity to be rendered. - - :param empty_zero: Affects the display when value equals zero. - If false (the default), will return ``'0'``; if true then - it returns empty string. - - :returns: Display string for the quantity. - """ - if value is None: - return '' - if int(value) == value: - value = int(value) - if empty_zero and value == 0: - return '' - return str(value) - return str(value).rstrip('0') - def render_time_ago(self, value): """ Return a human-friendly string, indicating how long ago diff --git a/src/wuttjamaican/batch.py b/src/wuttjamaican/batch.py index 98e8aa9..98325ff 100644 --- a/src/wuttjamaican/batch.py +++ b/src/wuttjamaican/batch.py @@ -296,54 +296,6 @@ class BatchHandler(GenericHandler): that depending on the workflow. """ - def do_remove_row(self, row): - """ - Remove a row from its batch. This will: - - * call :meth:`remove_row()` - * decrement the batch - :attr:`~wuttjamaican.db.model.batch.BatchMixin.row_count` - * call :meth:`refresh_batch_status()` - - So, callers should use ``do_remove_row()``, but subclass - should (usually) override :meth:`remove_row()` etc. - """ - batch = row.batch - - self.remove_row(row) - - if batch.row_count is not None: - batch.row_count -= 1 - - self.refresh_batch_status(batch) - - def remove_row(self, row): - """ - Remove a row from its batch. - - Callers should use :meth:`do_remove_row()` instead, which - calls this method automatically. - - Subclass can override this method; the default logic just - deletes the row. - """ - session = self.app.get_session(row) - session.delete(row) - - def refresh_batch_status(self, batch): - """ - Update the batch status as needed. - - This method is called when some row data has changed for the - batch, e.g. from :meth:`do_remove_row()`. - - It does nothing by default; subclass may override to set these - attributes on the batch: - - * :attr:`~wuttjamaican.db.model.batch.BatchMixin.status_code` - * :attr:`~wuttjamaican.db.model.batch.BatchMixin.status_text` - """ - def why_not_execute(self, batch, user=None, **kwargs): """ Returns text indicating the reason (if any) that a given batch @@ -416,19 +368,6 @@ class BatchHandler(GenericHandler): :returns: Markdown text describing batch execution. """ - def get_effective_rows(self, batch): - """ - This should return a list of "effective" rows for the batch. - - In other words, which rows should be "acted upon" when the - batch is executed. - - The default logic returns the full list of batch - :attr:`~wuttjamaican.db.model.batch.BatchMixin.rows`, but - subclass may need to filter by status code etc. - """ - return batch.rows - def do_execute(self, batch, user, progress=None, **kwargs): """ Perform the execution steps for a batch. @@ -456,9 +395,6 @@ class BatchHandler(GenericHandler): :param \**kwargs: Additional kwargs as needed. These are passed as-is to :meth:`why_not_execute()` and :meth:`execute()`. - - :returns: Whatever was returned from :meth:`execute()` - often - ``None``. """ if batch.executed: raise ValueError(f"batch has already been executed: {batch}") @@ -467,10 +403,9 @@ class BatchHandler(GenericHandler): if reason: raise RuntimeError(f"batch execution not allowed: {reason}") - result = self.execute(batch, user=user, progress=progress, **kwargs) + self.execute(batch, user=user, progress=progress, **kwargs) batch.executed = datetime.datetime.now() batch.executed_by = user - return result def execute(self, batch, user=None, progress=None, **kwargs): """ @@ -493,10 +428,6 @@ class BatchHandler(GenericHandler): :param \**kwargs: Additional kwargs which may affect the batch execution behavior. There are none by default, but some handlers may declare/use them. - - :returns: ``None`` by default, but subclass can return - whatever it likes, in which case that will be also returned - to the caller from :meth:`do_execute()`. """ def do_delete(self, batch, user, dry_run=False, progress=None, **kwargs): diff --git a/src/wuttjamaican/db/model/batch.py b/src/wuttjamaican/db/model/batch.py index 9b414b5..f6ba154 100644 --- a/src/wuttjamaican/db/model/batch.py +++ b/src/wuttjamaican/db/model/batch.py @@ -182,6 +182,7 @@ class BatchMixin: Reference to the :class:`~wuttjamaican.db.model.auth.User` who executed the batch. + """ @declared_attr @@ -230,8 +231,7 @@ class BatchMixin: return orm.relationship( User, primaryjoin=lambda: User.uuid == cls.created_by_uuid, - foreign_keys=lambda: [cls.created_by_uuid], - cascade_backrefs=False) + foreign_keys=lambda: [cls.created_by_uuid]) executed = sa.Column(sa.DateTime(timezone=True), nullable=True) @@ -242,8 +242,7 @@ class BatchMixin: return orm.relationship( User, primaryjoin=lambda: User.uuid == cls.executed_by_uuid, - foreign_keys=lambda: [cls.executed_by_uuid], - cascade_backrefs=False) + foreign_keys=lambda: [cls.executed_by_uuid]) def __repr__(self): cls = self.__class__.__name__ @@ -405,14 +404,12 @@ class BatchRowMixin: order_by=lambda: row_class.sequence, collection_class=ordering_list('sequence', count_from=1), cascade='all, delete-orphan', - cascade_backrefs=False, back_populates='batch') # now, here's the `BatchRow.batch` return orm.relationship( batch_class, - back_populates='rows', - cascade_backrefs=False) + back_populates='rows') sequence = sa.Column(sa.Integer(), nullable=False) diff --git a/src/wuttjamaican/db/model/upgrades.py b/src/wuttjamaican/db/model/upgrades.py index 750a9a6..6893ba6 100644 --- a/src/wuttjamaican/db/model/upgrades.py +++ b/src/wuttjamaican/db/model/upgrades.py @@ -52,7 +52,6 @@ class Upgrade(Base): created_by = orm.relationship( 'User', foreign_keys=[created_by_uuid], - cascade_backrefs=False, doc=""" :class:`~wuttjamaican.db.model.auth.User` who created the upgrade record. @@ -83,7 +82,6 @@ class Upgrade(Base): executed_by = orm.relationship( 'User', foreign_keys=[executed_by_uuid], - cascade_backrefs=False, doc=""" :class:`~wuttjamaican.db.model.auth.User` who executed the upgrade. diff --git a/src/wuttjamaican/install.py b/src/wuttjamaican/install.py index 28dfb60..cf9e3ac 100644 --- a/src/wuttjamaican/install.py +++ b/src/wuttjamaican/install.py @@ -461,7 +461,7 @@ class InstallHandler(GenericHandler): if self.schema_installed: self.rprint("\n\tyou can run the web app with:") self.rprint(f"\n\t[blue]cd {sys.prefix}[/blue]") - self.rprint("\t[blue]bin/wutta -c app/web.conf webapp -r[/blue]") + self.rprint("\t[blue]bin/pserve file+ini:app/web.conf[/blue]") self.rprint() diff --git a/src/wuttjamaican/util.py b/src/wuttjamaican/util.py index 161dbce..dfb9b13 100644 --- a/src/wuttjamaican/util.py +++ b/src/wuttjamaican/util.py @@ -171,26 +171,6 @@ def make_title(text): return ' '.join([x.capitalize() for x in words]) -def make_full_name(*parts): - """ - Make a "full name" from the given parts. - - :param \*parts: Distinct name values which should be joined - together to make the full name. - - :returns: The full name. - - For instance:: - - make_full_name('First', '', 'Last', 'Suffix') - # => "First Last Suffix" - """ - parts = [(part or '').strip() - for part in parts] - parts = [part for part in parts if part] - return ' '.join(parts) - - def make_true_uuid(): """ Generate a new v7 UUID value. diff --git a/tests/test_app.py b/tests/test_app.py index 7c0f963..7168164 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -384,10 +384,6 @@ app_title = WuttaTest text = self.app.make_title('foo_bar') self.assertEqual(text, "Foo Bar") - def test_make_full_name(self): - name = self.app.make_full_name('Fred', '', 'Flintstone', '') - self.assertEqual(name, "Fred Flintstone") - def test_make_uuid(self): uuid = self.app.make_uuid() self.assertEqual(len(uuid), 32) @@ -427,17 +423,6 @@ app_title = WuttaTest session = self.app.get_session(user) self.assertIs(session, mysession) - def test_render_boolean(self): - - # null - self.assertEqual(self.app.render_boolean(None), "") - - # true - self.assertEqual(self.app.render_boolean(True), "Yes") - - # false - self.assertEqual(self.app.render_boolean(False), "No") - def test_render_currency(self): # null @@ -475,7 +460,7 @@ app_title = WuttaTest dt = datetime.datetime(2024, 12, 11, 8, 30, tzinfo=datetime.timezone.utc) self.assertEqual(self.app.render_datetime(dt), '2024-12-11 08:30+0000') - def test_render_error(self): + def test_simple_error(self): # with description try: @@ -491,23 +476,6 @@ app_title = WuttaTest result = self.app.render_error(error) self.assertEqual(result, "RuntimeError") - def test_render_quantity(self): - - # null - self.assertEqual(self.app.render_quantity(None), "") - - # integer decimals become integers - value = decimal.Decimal('1.000') - self.assertEqual(self.app.render_quantity(value), "1") - - # but decimal places are preserved - value = decimal.Decimal('1.234') - self.assertEqual(self.app.render_quantity(value), "1.234") - - # zero can be empty string - self.assertEqual(self.app.render_quantity(0), "0") - self.assertEqual(self.app.render_quantity(0, empty_zero=True), "") - def test_render_time_ago(self): with patch.object(mod, 'humanize') as humanize: humanize.naturaltime.return_value = 'now' diff --git a/tests/test_batch.py b/tests/test_batch.py index 7a2e643..6075fd0 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -115,41 +115,6 @@ else: handler.add_row(batch, row) self.assertEqual(batch.row_count, 1) - def test_remove_row(self): - model = self.app.model - handler = self.make_handler() - user = model.User(username='barney') - self.session.add(user) - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - row = handler.make_row() - handler.add_row(batch, row) - self.session.flush() - self.assertEqual(batch.row_count, 1) - handler.do_remove_row(row) - self.session.flush() - self.assertEqual(batch.row_count, 0) - - def test_get_effective_rows(self): - model = self.app.model - handler = self.make_handler() - - user = model.User(username='barney') - self.session.add(user) - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - self.session.flush() - - self.assertEqual(handler.get_effective_rows(batch), []) - - row = handler.make_row() - handler.add_row(batch, row) - self.session.flush() - - rows = handler.get_effective_rows(batch) - self.assertEqual(len(rows), 1) - self.assertIs(rows[0], row) - def test_do_execute(self): model = self.app.model user = model.User(username='barney') diff --git a/tests/test_install.py b/tests/test_install.py index ba410b1..78ae370 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -214,7 +214,7 @@ default.url = {db_url} handler.schema_installed = True handler.show_goodbye() rprint.assert_any_call("\n\t[bold green]initial setup is complete![/bold green]") - rprint.assert_any_call("\t[blue]bin/wutta -c app/web.conf webapp -r[/blue]") + rprint.assert_any_call("\t[blue]bin/pserve file+ini:app/web.conf[/blue]") def test_require_prompt_toolkit_installed(self): # nb. this assumes we *do* have prompt_toolkit installed diff --git a/tests/test_util.py b/tests/test_util.py index 0f11918..8015ff0 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -263,13 +263,6 @@ class TestMakeTitle(TestCase): self.assertEqual(text, "Foo Bar") -class TestMakeFullName(TestCase): - - def test_basic(self): - name = mod.make_full_name('Fred', '', 'Flintstone', '') - self.assertEqual(name, 'Fred Flintstone') - - class TestProgressLoop(TestCase): def test_basic(self):