Compare commits
	
		
			No commits in common. "84ab93108158c8ea4cd2b0c7269cbc229b38fd29" and "c800ebf4e4f7427cb13fc91f226b73d0eb05f72a" have entirely different histories.
		
	
	
		
			84ab931081
			...
			c800ebf4e4
		
	
		
					 11 changed files with 28 additions and 181 deletions
				
			
		|  | @ -42,10 +42,9 @@ dependencies = [ | |||
|         "pyramid_fanstatic", | ||||
|         "pyramid_mako", | ||||
|         "pyramid_tm", | ||||
|         "SQLAlchemy-Utils", | ||||
|         "waitress", | ||||
|         "WebHelpers2", | ||||
|         "WuttJamaican[db]>=0.19.1", | ||||
|         "WuttJamaican[db]>=0.19.0", | ||||
|         "zope.sqlalchemy>=1.5", | ||||
| ] | ||||
| 
 | ||||
|  |  | |||
|  | @ -155,28 +155,6 @@ class WuttaEnum(colander.Enum): | |||
|         return widgets.SelectWidget(**kwargs) | ||||
| 
 | ||||
| 
 | ||||
| class WuttaMoney(colander.Money): | ||||
|     """ | ||||
|     Custom schema type for "money" fields. | ||||
| 
 | ||||
|     This is a subclass of :class:`colander:colander.Money`, but uses | ||||
|     the custom :class:`~wuttaweb.forms.widgets.WuttaMoneyInputWidget` | ||||
|     by default. | ||||
| 
 | ||||
|     :param request: Current :term:`request` object. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, request, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.request = request | ||||
|         self.config = self.request.wutta_config | ||||
|         self.app = self.config.get_app() | ||||
| 
 | ||||
|     def widget_maker(self, **kwargs): | ||||
|         """ """ | ||||
|         return widgets.WuttaMoneyInputWidget(self.request, **kwargs) | ||||
| 
 | ||||
| 
 | ||||
| class WuttaSet(colander.Set): | ||||
|     """ | ||||
|     Custom schema type for :class:`python:set` fields. | ||||
|  |  | |||
|  | @ -41,7 +41,6 @@ in the namespace: | |||
| """ | ||||
| 
 | ||||
| import datetime | ||||
| import decimal | ||||
| import os | ||||
| 
 | ||||
| import colander | ||||
|  | @ -195,42 +194,6 @@ class WuttaDateTimeWidget(DateTimeInputWidget): | |||
|         return super().serialize(field, cstruct, **kw) | ||||
| 
 | ||||
| 
 | ||||
| class WuttaMoneyInputWidget(MoneyInputWidget): | ||||
|     """ | ||||
|     Custom widget for "money" fields.  This is used by default for | ||||
|     :class:`~wuttaweb.forms.schema.WuttaMoney` type nodes. | ||||
| 
 | ||||
|     The main purpose of this widget is to leverage | ||||
|     :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_currency()` | ||||
|     for the readonly display. | ||||
| 
 | ||||
|     This is a subclass of | ||||
|     :class:`deform:deform.widget.MoneyInputWidget` and uses these | ||||
|     Deform templates: | ||||
| 
 | ||||
|     * ``moneyinput`` | ||||
| 
 | ||||
|     :param request: Current :term:`request` object. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, request, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.request = request | ||||
|         self.config = self.request.wutta_config | ||||
|         self.app = self.config.get_app() | ||||
| 
 | ||||
|     def serialize(self, field, cstruct, **kw): | ||||
|         """ """ | ||||
|         readonly = kw.get('readonly', self.readonly) | ||||
|         if readonly: | ||||
|             if cstruct in (colander.null, None): | ||||
|                 return "" | ||||
|             cstruct = decimal.Decimal(cstruct) | ||||
|             return self.app.render_currency(cstruct) | ||||
| 
 | ||||
|         return super().serialize(field, cstruct, **kw) | ||||
| 
 | ||||
| 
 | ||||
| class FileDownloadWidget(Widget): | ||||
|     """ | ||||
|     Widget for use with :class:`~wuttaweb.forms.schema.FileDownload` | ||||
|  |  | |||
|  | @ -32,7 +32,6 @@ from collections import namedtuple, OrderedDict | |||
| 
 | ||||
| import sqlalchemy as sa | ||||
| from sqlalchemy import orm | ||||
| from sqlalchemy_utils import get_columns | ||||
| 
 | ||||
| import paginate | ||||
| from paginate_sqlalchemy import SqlalchemyOrmPage | ||||
|  | @ -1117,16 +1116,19 @@ class Grid: | |||
|         filters = filters or {} | ||||
| 
 | ||||
|         if self.model_class: | ||||
|             # nb. i first tried self.get_model_columns() but my notes | ||||
|             # say that was too aggressive in many cases.  then i tried | ||||
|             # using the *subset* of self.columns, just the ones which | ||||
|             # corresponded to a property on the model class.  and now | ||||
|             # i am using sa-utils to give the "true" column list.. | ||||
|             for col in get_columns(self.model_class): | ||||
|                 if col.key in filters: | ||||
|             # TODO: i tried using self.get_model_columns() here but in | ||||
|             # many cases that will be too aggressive.  however it is | ||||
|             # often the case that the *grid* columns are a subset of | ||||
|             # the unerlying *table* columns.  so until a better way | ||||
|             # is found, we choose "too few" instead of "too many" | ||||
|             # filters here.  surely must improve it at some point. | ||||
|             for key in self.columns: | ||||
|                 if key in filters: | ||||
|                     continue | ||||
|                 prop = getattr(self.model_class, col.key) | ||||
|                 filters[prop.key] = self.make_filter(prop) | ||||
|                 prop = getattr(self.model_class, key, None) | ||||
|                 if (prop and hasattr(prop, 'property') | ||||
|                     and isinstance(prop.property, orm.ColumnProperty)): | ||||
|                     filters[prop.key] = self.make_filter(prop) | ||||
| 
 | ||||
|         return filters | ||||
| 
 | ||||
|  |  | |||
|  | @ -92,9 +92,6 @@ class SessionProgress(ProgressBase): | |||
|     """ | ||||
| 
 | ||||
|     def __init__(self, request, key, success_msg=None, success_url=None, error_url=None): | ||||
|         self.request = request | ||||
|         self.config = self.request.wutta_config | ||||
|         self.app = self.config.get_app() | ||||
|         self.key = key | ||||
|         self.success_msg = success_msg | ||||
|         self.success_url = success_url | ||||
|  | @ -140,7 +137,7 @@ class SessionProgress(ProgressBase): | |||
|         """ | ||||
|         self.session.load() | ||||
|         self.session['error'] = True | ||||
|         self.session['error_msg'] = self.app.render_error(error) | ||||
|         self.session['error_msg'] = str(error) | ||||
|         self.session['error_url'] = error_url or self.error_url | ||||
|         self.session.save() | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,7 +32,6 @@ import uuid as _uuid | |||
| import warnings | ||||
| 
 | ||||
| import sqlalchemy as sa | ||||
| from sqlalchemy import orm | ||||
| 
 | ||||
| import colander | ||||
| from webhelpers2.html import HTML, tags | ||||
|  | @ -479,36 +478,24 @@ def render_csrf_token(request, name='_csrf'): | |||
|     return HTML.tag('div', tags.hidden(name, value=token, id=None), style='display:none;') | ||||
| 
 | ||||
| 
 | ||||
| def get_model_fields(config, model_class, include_fk=False): | ||||
| def get_model_fields(config, model_class=None): | ||||
|     """ | ||||
|     Convenience function to return a list of field names for the given | ||||
|     :term:`data model` class. | ||||
|     model class. | ||||
| 
 | ||||
|     This logic only supports SQLAlchemy mapped classes and will use | ||||
|     that to determine the field listing if applicable.  Otherwise this | ||||
|     returns ``None``. | ||||
| 
 | ||||
|     :param config: App :term:`config object`. | ||||
| 
 | ||||
|     :param model_class: Data model class. | ||||
| 
 | ||||
|     :param include_fk: Whether to include foreign key column names in | ||||
|        the result.  They are excluded by default, since the | ||||
|        relationship names are also included and generally preferred. | ||||
| 
 | ||||
|     :returns: List of field names, or ``None`` if it could not be | ||||
|        determined. | ||||
|     """ | ||||
|     if not model_class: | ||||
|         return | ||||
| 
 | ||||
|     try: | ||||
|         mapper = sa.inspect(model_class) | ||||
|     except sa.exc.NoInspectionAvailable: | ||||
|         return | ||||
| 
 | ||||
|     if include_fk: | ||||
|         fields = [prop.key for prop in mapper.iterate_properties] | ||||
|     else: | ||||
|         fields = [prop.key for prop in mapper.iterate_properties | ||||
|                   if not prop_is_fk(mapper, prop)] | ||||
|     fields = [prop.key for prop in mapper.iterate_properties] | ||||
| 
 | ||||
|     # nb. we never want the continuum 'versions' prop | ||||
|     app = config.get_app() | ||||
|  | @ -518,20 +505,6 @@ def get_model_fields(config, model_class, include_fk=False): | |||
|     return fields | ||||
| 
 | ||||
| 
 | ||||
| def prop_is_fk(mapper, prop): | ||||
|     """ """ | ||||
|     if not isinstance(prop, orm.ColumnProperty): | ||||
|         return False | ||||
| 
 | ||||
|     prop_columns = [col.name for col in prop.columns] | ||||
|     for rel in mapper.relationships: | ||||
|         rel_columns = [col.name for col in rel.local_columns] | ||||
|         if rel_columns == prop_columns: | ||||
|             return True | ||||
| 
 | ||||
|     return False | ||||
| 
 | ||||
| 
 | ||||
| def make_json_safe(value, key=None, warn=True): | ||||
|     """ | ||||
|     Convert a Python value as needed, to ensure it is compatible with | ||||
|  |  | |||
|  | @ -80,15 +80,6 @@ class TestWuttaEnum(WebTestCase): | |||
|         self.assertIsInstance(widget, widgets.SelectWidget) | ||||
| 
 | ||||
| 
 | ||||
| class TestWuttaMoney(WebTestCase): | ||||
| 
 | ||||
|     def test_widget_maker(self): | ||||
|         enum = self.app.enum | ||||
|         typ = mod.WuttaMoney(self.request) | ||||
|         widget = typ.widget_maker() | ||||
|         self.assertIsInstance(widget, widgets.WuttaMoneyInputWidget) | ||||
| 
 | ||||
| 
 | ||||
| class TestObjectRef(DataTestCase): | ||||
| 
 | ||||
|     def setUp(self): | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| 
 | ||||
| import datetime | ||||
| import decimal | ||||
| from unittest.mock import patch | ||||
| 
 | ||||
| import colander | ||||
|  | @ -108,36 +107,6 @@ class TestWuttaDateTimeWidget(WebTestCase): | |||
|         self.assertEqual(result, '2024-12-12 13:49+0000') | ||||
| 
 | ||||
| 
 | ||||
| class TestWuttaMoneyInputWidget(WebTestCase): | ||||
| 
 | ||||
|     def make_field(self, node, **kwargs): | ||||
|         # TODO: not sure why default renderer is in use even though | ||||
|         # pyramid_deform was included in setup?  but this works.. | ||||
|         kwargs.setdefault('renderer', deform.Form.default_renderer) | ||||
|         return deform.Field(node, **kwargs) | ||||
| 
 | ||||
|     def make_widget(self, **kwargs): | ||||
|         return mod.WuttaMoneyInputWidget(self.request, **kwargs) | ||||
| 
 | ||||
|     def test_serialize(self): | ||||
|         node = colander.SchemaNode(WuttaDateTime()) | ||||
|         field = self.make_field(node) | ||||
|         widget = self.make_widget() | ||||
|         amount = decimal.Decimal('12.34') | ||||
| 
 | ||||
|         # editable widget has normal text input | ||||
|         result = widget.serialize(field, str(amount)) | ||||
|         self.assertIn('<b-input', result) | ||||
| 
 | ||||
|         # readonly is rendered per app convention | ||||
|         result = widget.serialize(field, str(amount), readonly=True) | ||||
|         self.assertEqual(result, '$12.34') | ||||
| 
 | ||||
|         # readonly w/ null value | ||||
|         result = widget.serialize(field, None, readonly=True) | ||||
|         self.assertEqual(result, '') | ||||
| 
 | ||||
| 
 | ||||
| class TestFileDownloadWidget(WebTestCase): | ||||
| 
 | ||||
|     def make_field(self, node, **kwargs): | ||||
|  |  | |||
|  | @ -982,17 +982,6 @@ class TestGrid(WebTestCase): | |||
|         self.assertEqual(filters['value'], 42) | ||||
|         self.assertEqual(myfilters['value'], 42) | ||||
| 
 | ||||
|         # filters for all *true* columns by default, despite grid.columns | ||||
|         with patch.object(mod.Grid, 'make_filter'): | ||||
|             # nb. filters are MagicMock instances | ||||
|             grid = self.make_grid(model_class=model.User, | ||||
|                                   columns=['username', 'person']) | ||||
|             filters = grid.make_backend_filters() | ||||
|             self.assertIn('username', filters) | ||||
|             self.assertIn('active', filters) | ||||
|             # nb. relationship not included by default | ||||
|             self.assertNotIn('person', filters) | ||||
| 
 | ||||
|     def test_make_filter(self): | ||||
|         model = self.app.model | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,8 +5,6 @@ from unittest import TestCase | |||
| from pyramid import testing | ||||
| from beaker.session import Session as BeakerSession | ||||
| 
 | ||||
| from wuttjamaican.testing import ConfigTestCase | ||||
| 
 | ||||
| from wuttaweb import progress as mod | ||||
| 
 | ||||
| 
 | ||||
|  | @ -33,11 +31,10 @@ class TestGetProgressSession(TestCase): | |||
|         self.assertEqual(session.id, 'mockid.progress.foo') | ||||
| 
 | ||||
| 
 | ||||
| class TestSessionProgress(ConfigTestCase): | ||||
| class TestSessionProgress(TestCase): | ||||
| 
 | ||||
|     def setUp(self): | ||||
|         self.setup_config() | ||||
|         self.request = testing.DummyRequest(wutta_config=self.config) | ||||
|         self.request = testing.DummyRequest() | ||||
|         self.request.session.id = 'mockid' | ||||
| 
 | ||||
|     def test_error_url(self): | ||||
|  |  | |||
|  | @ -11,8 +11,6 @@ from fanstatic import Library, Resource | |||
| from pyramid import testing | ||||
| 
 | ||||
| from wuttjamaican.conf import WuttaConfig | ||||
| from wuttjamaican.testing import ConfigTestCase | ||||
| 
 | ||||
| from wuttaweb import util as mod | ||||
| 
 | ||||
| 
 | ||||
|  | @ -465,10 +463,14 @@ class TestGetFormData(TestCase): | |||
|         self.assertEqual(data, {'foo2': 'baz'}) | ||||
| 
 | ||||
| 
 | ||||
| class TestGetModelFields(ConfigTestCase): | ||||
| class TestGetModelFields(TestCase): | ||||
| 
 | ||||
|     def setUp(self): | ||||
|         self.config = WuttaConfig() | ||||
|         self.app = self.config.get_app() | ||||
| 
 | ||||
|     def test_empty_model_class(self): | ||||
|         fields = mod.get_model_fields(self.config, None) | ||||
|         fields = mod.get_model_fields(self.config) | ||||
|         self.assertIsNone(fields) | ||||
| 
 | ||||
|     def test_unknown_model_class(self): | ||||
|  | @ -480,19 +482,6 @@ class TestGetModelFields(ConfigTestCase): | |||
|         fields = mod.get_model_fields(self.config, model.Setting) | ||||
|         self.assertEqual(fields, ['name', 'value']) | ||||
| 
 | ||||
|     def test_include_fk(self): | ||||
|         model = self.app.model | ||||
| 
 | ||||
|         # fk excluded by default | ||||
|         fields = mod.get_model_fields(self.config, model.User) | ||||
|         self.assertNotIn('person_uuid', fields) | ||||
|         self.assertIn('person', fields) | ||||
| 
 | ||||
|         # fk can be included | ||||
|         fields = mod.get_model_fields(self.config, model.User, include_fk=True) | ||||
|         self.assertIn('person_uuid', fields) | ||||
|         self.assertIn('person', fields) | ||||
| 
 | ||||
|     def test_avoid_versions(self): | ||||
|         model = self.app.model | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue