loutilities 3.11.1.dev1__tar.gz → 3.12.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. {loutilities-3.11.1.dev1/loutilities.egg-info → loutilities-3.12.0.dev1}/PKG-INFO +1 -1
  2. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables.py +7 -1
  3. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/version.py +1 -1
  4. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1/loutilities.egg-info}/PKG-INFO +1 -1
  5. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities.egg-info/SOURCES.txt +2 -1
  6. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/tests/test_sqlalchemy_helpers.py +2 -2
  7. loutilities-3.12.0.dev1/tests/test_tables.py +447 -0
  8. loutilities-3.12.0.dev1/tests/test_user_tables.py +283 -0
  9. loutilities-3.11.1.dev1/tests/test_tables.py +0 -224
  10. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/.gitattributes +0 -0
  11. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/.gitignore +0 -0
  12. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/README.md +0 -0
  13. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/__init__.py +0 -0
  14. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/agegrade.py +0 -0
  15. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/apikey.py +0 -0
  16. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/applytemplate.py +0 -0
  17. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/bfile.py +0 -0
  18. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/boolexpr.py +0 -0
  19. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/config.py +0 -0
  20. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/configparser.py +0 -0
  21. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/csvu.py +0 -0
  22. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/csvwt.py +0 -0
  23. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/extconfigparser.py +0 -0
  24. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/filetrigger.py +0 -0
  25. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/filtercsv.py +0 -0
  26. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/filters.py +0 -0
  27. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/flask/__init__.py +0 -0
  28. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/flask/user/__init__.py +0 -0
  29. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/flask/user/views.py +0 -0
  30. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/flask_helpers/__init__.py +0 -0
  31. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/flask_helpers/as_blueprint.py +0 -0
  32. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/flask_helpers/blueprints.py +0 -0
  33. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/flask_helpers/decorators.py +0 -0
  34. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/flask_helpers/mailer.py +0 -0
  35. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/geo.py +0 -0
  36. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/googleauth.py +0 -0
  37. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/kmlutils.py +0 -0
  38. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/makerst.py +0 -0
  39. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/namesplitter.py +0 -0
  40. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/nesteddict.py +0 -0
  41. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/nicknames.csv +0 -0
  42. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/nicknames.py +0 -0
  43. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/renderrun.py +0 -0
  44. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/sqlalchemy_helpers.py +0 -0
  45. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/readme.md +0 -0
  46. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/background-post-data-manager.js +0 -0
  47. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/branding.css +0 -0
  48. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/buttons.colvis.js +0 -0
  49. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/charts.css +0 -0
  50. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/charts.js +0 -0
  51. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/datatables-childrow.js +0 -0
  52. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/datatables.css +0 -0
  53. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/datatables.dataRender.datetime.js +0 -0
  54. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/datatables.dataRender.ellipsis.js +0 -0
  55. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/datatables.dataRender.googledoc.js +0 -0
  56. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/datatables.js +0 -0
  57. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor-saeditor.js +0 -0
  58. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.buttons.editchildrowrefresh.js +0 -0
  59. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.buttons.editrefresh.js +0 -0
  60. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.buttons.separator.js +0 -0
  61. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.ckeditor5.js +0 -0
  62. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.css +0 -0
  63. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.displayController.onPage.js +0 -0
  64. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.fieldType.display.js +0 -0
  65. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.googledoc.js +0 -0
  66. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/editor.select2.mymethods.js +0 -0
  67. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/eventscalendar.css +0 -0
  68. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/filters.css +0 -0
  69. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/filters.js +0 -0
  70. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/jquery.ui.dialog-clickoutside.js +0 -0
  71. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/jqueryui.theme.adjust.css +0 -0
  72. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/legend.js +0 -0
  73. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/mutex-promise.js +0 -0
  74. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/user/admin/beforedatatables.js +0 -0
  75. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/user/admin/groups.js +0 -0
  76. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/static/utils.js +0 -0
  77. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/bare-layout.jinja2 +0 -0
  78. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/datatables.html +0 -0
  79. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/datatables.jinja2 +0 -0
  80. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/layout-base.jinja2 +0 -0
  81. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/layout.html +0 -0
  82. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/layout.jinja2 +0 -0
  83. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/security/change_password.jinja2 +0 -0
  84. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/security/email/reset_instructions.html +0 -0
  85. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/security/email/reset_instructions.txt +0 -0
  86. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/security/forgot_password.jinja2 +0 -0
  87. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/security/login_user.jinja2 +0 -0
  88. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/security/reset_password.jinja2 +0 -0
  89. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/tables-assets/templates/select-view.jinja2 +0 -0
  90. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/textreader.py +0 -0
  91. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/timeu.py +0 -0
  92. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/transform.py +0 -0
  93. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/__init__.py +0 -0
  94. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/applogging.py +0 -0
  95. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/audit_mixin.py +0 -0
  96. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/model.py +0 -0
  97. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/roles.py +0 -0
  98. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/scripts/__init__.py +0 -0
  99. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/scripts/users_init.py +0 -0
  100. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/settings.py +0 -0
  101. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/tablefiles.py +0 -0
  102. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/tables.py +0 -0
  103. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/views/__init__.py +0 -0
  104. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/user/views/userrole.py +0 -0
  105. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/wxextensions.py +0 -0
  106. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities/xmldict.py +0 -0
  107. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities.egg-info/dependency_links.txt +0 -0
  108. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities.egg-info/entry_points.txt +0 -0
  109. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities.egg-info/not-zip-safe +0 -0
  110. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities.egg-info/requires.txt +0 -0
  111. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/loutilities.egg-info/top_level.txt +0 -0
  112. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/setup.cfg +0 -0
  113. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/setup.py +0 -0
  114. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/tests/__init__.py +0 -0
  115. {loutilities-3.11.1.dev1 → loutilities-3.12.0.dev1}/tests/models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loutilities
3
- Version: 3.11.1.dev1
3
+ Version: 3.12.0.dev1
4
4
  Summary: some hopefully useful utilities
5
5
  Home-page: http://github.com/louking/loutilities
6
6
  Author: Lou King
@@ -1095,7 +1095,13 @@ class CrudApi(MethodView):
1095
1095
  # DataTables options string, data: and buttons: are passed separately
1096
1096
  # self.dtoptions can update what we come up with
1097
1097
  dt_options = {
1098
- 'dom': '<"H"lBpfr>t<"F"i>',
1098
+ # 'dom': '<"H"lBpfr>t<"F"i>',
1099
+ 'layout':{
1100
+ 'topStart': ['pageLength', 'buttons'],
1101
+ 'topEnd': ['search', 'paging'],
1102
+ 'bottomStart': ['info'],
1103
+ 'bottomEnd': None,
1104
+ },
1099
1105
  'columns': [
1100
1106
  ],
1101
1107
  'rowId': self.idSrc,
@@ -1,2 +1,2 @@
1
1
  # this string is used for the version string in the documentation, as well as the egg
2
- __version__ = '3.11.1.dev1'
2
+ __version__ = '3.12.0.dev1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loutilities
3
- Version: 3.11.1.dev1
3
+ Version: 3.12.0.dev1
4
4
  Summary: some hopefully useful utilities
5
5
  Home-page: http://github.com/louking/loutilities
6
6
  Author: Lou King
@@ -108,4 +108,5 @@ loutilities/user/views/userrole.py
108
108
  tests/__init__.py
109
109
  tests/models.py
110
110
  tests/test_sqlalchemy_helpers.py
111
- tests/test_tables.py
111
+ tests/test_tables.py
112
+ tests/test_user_tables.py
@@ -164,7 +164,7 @@ class SqlalchemyHelpersTest(unittest.TestCase):
164
164
  del currinst.id
165
165
  del currinst._sa_instance_state
166
166
  del newinst._sa_instance_state
167
- self.assertEquals(newinst.__dict__, currinst.__dict__)
167
+ self.assertEqual(newinst.__dict__, currinst.__dict__)
168
168
 
169
169
  def test_insert_or_update_insert(self):
170
170
  oldrecords = self.populate_severalattrs(5)
@@ -197,4 +197,4 @@ class SqlalchemyHelpersTest(unittest.TestCase):
197
197
  del currinst.id
198
198
  del currinst._sa_instance_state
199
199
  del newinst._sa_instance_state
200
- self.assertEquals(newinst.__dict__, currinst.__dict__)
200
+ self.assertEqual(newinst.__dict__, currinst.__dict__)
@@ -0,0 +1,447 @@
1
+ '''
2
+ test_tables - tests for loutilities.tables
3
+ '''
4
+ # standard
5
+ import unittest
6
+
7
+ # pypi
8
+ from flask import Flask, json
9
+ from sqlalchemy import create_engine
10
+ from sqlalchemy.orm import sessionmaker, scoped_session
11
+ from sqlalchemy.pool import StaticPool
12
+
13
+ # homegrown
14
+ from loutilities import tables
15
+ from .models import User, Address, Base
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # DbWrapper: presents the db.session interface expected by DbCrudApi
20
+ # (Flask-SQLAlchemy uses db.session; here we wrap a scoped_session)
21
+ # ---------------------------------------------------------------------------
22
+ class DbWrapper:
23
+ def __init__(self, session):
24
+ self.session = session
25
+
26
+
27
+ def _make_engine():
28
+ """In-memory SQLite engine with StaticPool so all connections share one DB."""
29
+ return create_engine(
30
+ 'sqlite://',
31
+ connect_args={'check_same_thread': False},
32
+ poolclass=StaticPool,
33
+ )
34
+
35
+
36
+ def _make_scoped_session(engine):
37
+ """Create a scoped session and bind query_property to model classes."""
38
+ Session = scoped_session(sessionmaker(bind=engine))
39
+ # query_property lets Model.query work like Flask-SQLAlchemy
40
+ User.query = Session.query_property()
41
+ Address.query = Session.query_property()
42
+ return Session
43
+
44
+
45
+ # ===========================================================================
46
+ # Standalone utility function tests
47
+ # ===========================================================================
48
+
49
+ class TestIsJsonable(unittest.TestCase):
50
+ def test_serializable_types(self):
51
+ self.assertTrue(tables.is_jsonable({'a': 1}))
52
+ self.assertTrue(tables.is_jsonable([1, 2, 3]))
53
+ self.assertTrue(tables.is_jsonable('string'))
54
+ self.assertTrue(tables.is_jsonable(42))
55
+ self.assertTrue(tables.is_jsonable(None))
56
+
57
+ def test_non_serializable(self):
58
+ self.assertFalse(tables.is_jsonable({1, 2, 3})) # set is not JSON-serializable
59
+
60
+
61
+ class TestCopyopts(unittest.TestCase):
62
+ def test_dict(self):
63
+ d = {'a': 1, 'b': [2, 3], 'c': {'d': 4}}
64
+ self.assertEqual(tables.copyopts(d), d)
65
+
66
+ def test_list(self):
67
+ lst = [1, 'x', {'y': 2}]
68
+ self.assertEqual(tables.copyopts(lst), lst)
69
+
70
+ def test_non_serializable_becomes_str(self):
71
+ result = tables.copyopts({'a': {1, 2}})
72
+ self.assertIsInstance(result['a'], str)
73
+
74
+ def test_scalar(self):
75
+ self.assertEqual(tables.copyopts(42), 42)
76
+ self.assertEqual(tables.copyopts('hello'), 'hello')
77
+
78
+
79
+ class TestGetDbattr(unittest.TestCase):
80
+ """get_dbattr traverses using type() at each intermediate level."""
81
+
82
+ def test_simple(self):
83
+ class Obj:
84
+ value = 7
85
+ self.assertEqual(tables.get_dbattr(Obj(), 'value'), 7)
86
+
87
+ def test_dotted(self):
88
+ # Intermediate traversal uses type(getattr(...)), so inner.value must be a class attribute
89
+ class Inner:
90
+ value = 99
91
+ class Outer:
92
+ inner = Inner()
93
+ self.assertEqual(tables.get_dbattr(Outer(), 'inner.value'), 99)
94
+
95
+
96
+ class TestGetattrdeep(unittest.TestCase):
97
+ def test_simple(self):
98
+ class Obj:
99
+ pass
100
+ obj = Obj()
101
+ obj.name = 'hello'
102
+ self.assertEqual(tables.getattrdeep(obj, 'name'), 'hello')
103
+
104
+ def test_dotted(self):
105
+ class Inner:
106
+ pass
107
+ class Outer:
108
+ pass
109
+ inner = Inner()
110
+ inner.val = 42
111
+ outer = Outer()
112
+ outer.inner = inner
113
+ self.assertEqual(tables.getattrdeep(outer, 'inner.val'), 42)
114
+
115
+
116
+ class TestSetattrdeep(unittest.TestCase):
117
+ def test_simple(self):
118
+ class Obj:
119
+ pass
120
+ obj = Obj()
121
+ tables.setattrdeep(obj, 'x', 100)
122
+ self.assertEqual(obj.x, 100)
123
+
124
+ def test_dotted(self):
125
+ class Inner:
126
+ pass
127
+ class Outer:
128
+ pass
129
+ inner = Inner()
130
+ outer = Outer()
131
+ outer.inner = inner
132
+ tables.setattrdeep(outer, 'inner.val', 55)
133
+ self.assertEqual(inner.val, 55)
134
+
135
+
136
+ class TestGetRequestAction(unittest.TestCase):
137
+ def test_present(self):
138
+ self.assertEqual(tables.get_request_action({'action': 'create'}), 'create')
139
+ self.assertEqual(tables.get_request_action({'action': 'remove'}), 'remove')
140
+
141
+ def test_absent(self):
142
+ self.assertIsNone(tables.get_request_action({}))
143
+ self.assertIsNone(tables.get_request_action({'other': 'x'}))
144
+
145
+
146
+ class TestGetRequestData(unittest.TestCase):
147
+ def test_basic_parse(self):
148
+ form = {
149
+ 'data[1][name]': 'Alice',
150
+ 'data[1][age]': '30',
151
+ 'data[2][name]': 'Bob',
152
+ }
153
+ data = tables.get_request_data(form)
154
+ self.assertEqual(data[1]['name'], 'Alice')
155
+ self.assertEqual(data[1]['age'], '30')
156
+ self.assertEqual(data[2]['name'], 'Bob')
157
+
158
+ def test_action_key_ignored(self):
159
+ form = {'action': 'create', 'data[1][name]': 'Test'}
160
+ data = tables.get_request_data(form)
161
+ self.assertNotIn('action', data)
162
+
163
+ def test_string_id(self):
164
+ form = {'data[abc][x]': '1'}
165
+ data = tables.get_request_data(form)
166
+ self.assertEqual(data['abc']['x'], '1')
167
+
168
+
169
+ # ===========================================================================
170
+ # DataTablesEditor tests
171
+ # ===========================================================================
172
+
173
+ class TestDataTablesEditor(unittest.TestCase):
174
+ def setUp(self):
175
+ class Row:
176
+ pass
177
+ self.Row = Row
178
+ self.dte = tables.DataTablesEditor(
179
+ dbmapping={'name': 'name', 'age': 'age'},
180
+ formmapping={'name': 'name', 'age': 'age'},
181
+ )
182
+
183
+ def test_get_response_data_simple(self):
184
+ row = self.Row()
185
+ row.name = 'Alice'
186
+ row.age = 30
187
+ data = self.dte.get_response_data(row)
188
+ self.assertEqual(data['name'], 'Alice')
189
+ self.assertEqual(data['age'], 30)
190
+
191
+ def test_set_dbrow_simple(self):
192
+ row = self.Row()
193
+ self.dte.set_dbrow({'name': 'Bob', 'age': '25'}, row)
194
+ self.assertEqual(row.name, 'Bob')
195
+ self.assertEqual(row.age, '25')
196
+
197
+ def test_readonly_field_not_written(self):
198
+ dte = tables.DataTablesEditor(
199
+ dbmapping={'name': '__readonly__', 'age': 'age'},
200
+ formmapping={'name': 'name', 'age': 'age'},
201
+ )
202
+ row = self.Row()
203
+ row.name = 'original'
204
+ dte.set_dbrow({'name': 'changed', 'age': '25'}, row)
205
+ self.assertEqual(row.name, 'original') # skipped because __readonly__
206
+
207
+ def test_null2emptystring_get(self):
208
+ dte = tables.DataTablesEditor({'name': 'name'}, {'name': 'name'}, null2emptystring=True)
209
+ row = self.Row()
210
+ row.name = None
211
+ data = dte.get_response_data(row)
212
+ self.assertEqual(data['name'], '')
213
+
214
+ def test_skip_formmapping_field(self):
215
+ dte = tables.DataTablesEditor({}, {'name': 'name', 'computed': '__skip__'})
216
+ row = self.Row()
217
+ row.name = 'x'
218
+ data = dte.get_response_data(row)
219
+ self.assertNotIn('computed', data)
220
+
221
+ def test_callable_formmapping(self):
222
+ dte = tables.DataTablesEditor({}, {'upper': lambda r: r.name.upper()})
223
+ row = self.Row()
224
+ row.name = 'alice'
225
+ self.assertEqual(dte.get_response_data(row)['upper'], 'ALICE')
226
+
227
+ def test_callable_dbmapping(self):
228
+ dte = tables.DataTablesEditor({'name': lambda form: form.get('name', '').strip()}, {})
229
+ row = self.Row()
230
+ dte.set_dbrow({'name': ' Bob '}, row)
231
+ self.assertEqual(row.name, 'Bob')
232
+
233
+ def test_response_hook_called(self):
234
+ calls = []
235
+ def hook(data):
236
+ calls.append(True)
237
+ data['_extra'] = 'added'
238
+ self.dte.set_response_hook(hook)
239
+ row = self.Row()
240
+ row.name = 'Test'
241
+ row.age = 1
242
+ data = self.dte.get_response_data(row)
243
+ self.assertTrue(calls)
244
+ self.assertEqual(data['_extra'], 'added')
245
+
246
+
247
+ # ===========================================================================
248
+ # CrudApi abstract method tests
249
+ # The base class raises tables.NotImplementedError for open/nexttablerow/close/permission.
250
+ # ===========================================================================
251
+
252
+ class TestCrudApiAbstractMethods(unittest.TestCase):
253
+ def setUp(self):
254
+ # Minimum kwargs to avoid TypeError in __init__ (rule defaults to /endpoint)
255
+ self.api = tables.CrudApi(app=Flask(__name__), endpoint='testapi')
256
+
257
+ def test_open_raises(self):
258
+ with self.assertRaises(tables.NotImplementedError):
259
+ self.api.open()
260
+
261
+ def test_nexttablerow_raises(self):
262
+ with self.assertRaises(tables.NotImplementedError):
263
+ self.api.nexttablerow()
264
+
265
+ def test_close_raises(self):
266
+ with self.assertRaises(tables.NotImplementedError):
267
+ self.api.close()
268
+
269
+ def test_permission_raises(self):
270
+ with self.assertRaises(tables.NotImplementedError):
271
+ self.api.permission()
272
+
273
+
274
+ # ===========================================================================
275
+ # DbCrudApi integration tests using in-memory SQLite
276
+ # ===========================================================================
277
+
278
+ class TestDbCrudApiIntegration(unittest.TestCase):
279
+ def setUp(self):
280
+ self.app = Flask(__name__)
281
+ self.app.config['TESTING'] = True
282
+
283
+ self.engine = _make_engine()
284
+ Base.metadata.create_all(self.engine)
285
+ self.Session = _make_scoped_session(self.engine)
286
+ self.db = DbWrapper(self.Session)
287
+
288
+ class UserApi(tables.DbCrudApi):
289
+ def permission(self):
290
+ return True
291
+
292
+ self.api = UserApi(
293
+ app=self.app,
294
+ endpoint='userapi',
295
+ rule='/users',
296
+ db=self.db,
297
+ model=User,
298
+ dbmapping={'name': 'name'},
299
+ formmapping={'name': 'name'},
300
+ clientcolumns=[
301
+ {'data': 'name', 'name': 'name', 'label': 'Name', '_unique': True}
302
+ ],
303
+ )
304
+ self.api.register()
305
+ self.client = self.app.test_client()
306
+
307
+ def tearDown(self):
308
+ Base.metadata.drop_all(self.engine)
309
+ self.Session.remove()
310
+
311
+ def test_create_returns_row_data(self):
312
+ resp = self.client.post('/users/rest', data={
313
+ 'action': 'create',
314
+ 'data[0][name]': 'Alice',
315
+ })
316
+ self.assertEqual(resp.status_code, 200)
317
+ data = json.loads(resp.data)
318
+ self.assertIn('data', data)
319
+ self.assertEqual(data['data'][0]['name'], 'Alice')
320
+
321
+ def test_get_returns_list_of_rows(self):
322
+ self.client.post('/users/rest', data={'action': 'create', 'data[0][name]': 'Bob'})
323
+ resp = self.client.get('/users/rest')
324
+ self.assertEqual(resp.status_code, 200)
325
+ data = json.loads(resp.data)
326
+ self.assertIsInstance(data, list)
327
+ self.assertTrue(any(row.get('name') == 'Bob' for row in data))
328
+
329
+ def test_update_row(self):
330
+ # SQLite auto-increment gives the first inserted row id=1
331
+ self.client.post('/users/rest', data={'action': 'create', 'data[0][name]': 'Carol'})
332
+ resp = self.client.put('/users/rest/1', data={
333
+ 'action': 'edit',
334
+ 'data[1][name]': 'Caroline',
335
+ })
336
+ self.assertEqual(resp.status_code, 200)
337
+ data = json.loads(resp.data)
338
+ self.assertIn('data', data)
339
+ self.assertEqual(data['data'][0]['name'], 'Caroline')
340
+
341
+ def test_delete_row(self):
342
+ self.client.post('/users/rest', data={'action': 'create', 'data[0][name]': 'Dave'})
343
+ # delete action comes from request.args (formrequest=False)
344
+ resp = self.client.delete('/users/rest/1', query_string={'action': 'remove'})
345
+ self.assertEqual(resp.status_code, 200)
346
+ resp = self.client.get('/users/rest')
347
+ data = json.loads(resp.data)
348
+ self.assertFalse(any(row.get('name') == 'Dave' for row in data))
349
+
350
+ def test_duplicate_unique_returns_error_json(self):
351
+ """Creating a duplicate unique value returns HTTP 200 with error/fieldErrors."""
352
+ self.client.post('/users/rest', data={'action': 'create', 'data[0][name]': 'Eve'})
353
+ resp = self.client.post('/users/rest', data={'action': 'create', 'data[0][name]': 'Eve'})
354
+ self.assertEqual(resp.status_code, 200)
355
+ data = json.loads(resp.data)
356
+ self.assertTrue('error' in data or 'fieldErrors' in data)
357
+
358
+
359
+ # ===========================================================================
360
+ # DbCrudApiRolePermissions tests
361
+ # ===========================================================================
362
+
363
+ def _mock_user(roles):
364
+ class MockUser:
365
+ def __init__(self, r):
366
+ self._roles = r
367
+ def has_role(self, role):
368
+ return role in self._roles
369
+ return MockUser(roles)
370
+
371
+
372
+ class TestDbCrudApiRolePermissions(unittest.TestCase):
373
+ _ep_counter = 0
374
+
375
+ def _make_api(self, **extra):
376
+ TestDbCrudApiRolePermissions._ep_counter += 1
377
+ ep = 'roleapi_{}'.format(TestDbCrudApiRolePermissions._ep_counter)
378
+
379
+ # Inline subclass: disable auth decorator and expose current_user as property
380
+ class RoleApi(tables.DbCrudApiRolePermissions):
381
+ decorators = [] # bypass auth_required() for unit testing
382
+
383
+ @property
384
+ def current_user(self):
385
+ return self._mock_user
386
+
387
+ api = RoleApi(
388
+ app=self.app,
389
+ endpoint=ep,
390
+ rule='/' + ep,
391
+ db=self.db,
392
+ model=User,
393
+ dbmapping={'name': 'name'},
394
+ formmapping={'name': 'name'},
395
+ clientcolumns=[{'data': 'name', 'name': 'name', 'label': 'Name'}],
396
+ **extra,
397
+ )
398
+ return api
399
+
400
+ def setUp(self):
401
+ self.app = Flask(__name__)
402
+ self.engine = _make_engine()
403
+ Base.metadata.create_all(self.engine)
404
+ self.Session = _make_scoped_session(self.engine)
405
+ self.db = DbWrapper(self.Session)
406
+
407
+ def tearDown(self):
408
+ Base.metadata.drop_all(self.engine)
409
+ self.Session.remove()
410
+
411
+ def test_roles_accepted_grants_permission(self):
412
+ api = self._make_api(roles_accepted=['admin'])
413
+ api._mock_user = _mock_user(['admin'])
414
+ self.assertTrue(api.permission())
415
+
416
+ def test_roles_accepted_denies_permission(self):
417
+ api = self._make_api(roles_accepted=['admin'])
418
+ api._mock_user = _mock_user(['user'])
419
+ self.assertFalse(api.permission())
420
+
421
+ def test_no_roles_configured_grants_permission(self):
422
+ api = self._make_api()
423
+ api._mock_user = _mock_user([])
424
+ self.assertTrue(api.permission())
425
+
426
+ def test_roles_required_all_must_be_present(self):
427
+ api = self._make_api(roles_required=['admin', 'superuser'])
428
+ api._mock_user = _mock_user(['admin']) # only one role → denied
429
+ self.assertFalse(api.permission())
430
+ api._mock_user = _mock_user(['admin', 'superuser']) # both → granted
431
+ self.assertTrue(api.permission())
432
+
433
+ def test_roles_accepted_and_required_raises(self):
434
+ with self.assertRaises(tables.ParameterError):
435
+ self._make_api(roles_accepted=['admin'], roles_required=['superuser'])
436
+
437
+ def test_roles_accepted_string_normalised_to_list(self):
438
+ api = self._make_api(roles_accepted='admin')
439
+ self.assertEqual(api.roles_accepted, ['admin'])
440
+
441
+ def test_roles_required_string_normalised_to_list(self):
442
+ api = self._make_api(roles_required='admin')
443
+ self.assertEqual(api.roles_required, ['admin'])
444
+
445
+
446
+ if __name__ == '__main__':
447
+ unittest.main()