WuttaWeb 0.3.0__tar.gz → 0.4.0__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 (91) hide show
  1. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/CHANGELOG.md +12 -0
  2. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/PKG-INFO +2 -2
  3. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/index.rst +2 -0
  4. wuttaweb-0.4.0/docs/api/wuttaweb/views.master.rst +6 -0
  5. wuttaweb-0.4.0/docs/api/wuttaweb/views.settings.rst +6 -0
  6. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/pyproject.toml +2 -2
  7. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/menus.py +3 -2
  8. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/subscribers.py +1 -0
  9. wuttaweb-0.4.0/src/wuttaweb/templates/appinfo/index.mako +56 -0
  10. wuttaweb-0.4.0/src/wuttaweb/templates/master/index.mako +13 -0
  11. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/views/__init__.py +2 -0
  12. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/views/base.py +8 -0
  13. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/views/common.py +4 -1
  14. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/views/essential.py +1 -0
  15. wuttaweb-0.4.0/src/wuttaweb/views/master.py +443 -0
  16. wuttaweb-0.4.0/src/wuttaweb/views/settings.py +47 -0
  17. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/forms/test_base.py +4 -2
  18. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/test_subscribers.py +4 -2
  19. wuttaweb-0.4.0/tests/utils.py +11 -0
  20. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/views/test_base.py +5 -1
  21. wuttaweb-0.4.0/tests/views/test_master.py +282 -0
  22. wuttaweb-0.4.0/tests/views/test_settings.py +13 -0
  23. wuttaweb-0.4.0/tests/views/utils.py +57 -0
  24. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/.gitignore +0 -0
  25. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/COPYING.txt +0 -0
  26. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/README.md +0 -0
  27. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/Makefile +0 -0
  28. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/_static/.keepme +0 -0
  29. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/index.rst +0 -0
  30. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/app.rst +0 -0
  31. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/auth.rst +0 -0
  32. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/db.rst +0 -0
  33. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/forms.base.rst +0 -0
  34. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/forms.rst +0 -0
  35. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/handler.rst +0 -0
  36. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/helpers.rst +0 -0
  37. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/menus.rst +0 -0
  38. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/static.rst +0 -0
  39. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/subscribers.rst +0 -0
  40. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/util.rst +0 -0
  41. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/views.auth.rst +0 -0
  42. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/views.base.rst +0 -0
  43. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/views.common.rst +0 -0
  44. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/views.essential.rst +0 -0
  45. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/api/wuttaweb/views.rst +0 -0
  46. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/conf.py +0 -0
  47. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/glossary.rst +0 -0
  48. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/index.rst +0 -0
  49. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/make.bat +0 -0
  50. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/docs/narr/index.rst +0 -0
  51. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/__init__.py +0 -0
  52. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/_version.py +0 -0
  53. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/app.py +0 -0
  54. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/auth.py +0 -0
  55. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/db.py +0 -0
  56. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/forms/__init__.py +0 -0
  57. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/forms/base.py +0 -0
  58. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/handler.py +0 -0
  59. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/helpers.py +0 -0
  60. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/static/__init__.py +0 -0
  61. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  62. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/static/img/logo.png +0 -0
  63. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/static/img/testing.png +0 -0
  64. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  65. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  66. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/base.mako +0 -0
  67. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/base_meta.mako +0 -0
  68. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  69. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  70. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  71. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/form.mako +0 -0
  72. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  73. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/home.mako +0 -0
  74. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/templates/page.mako +0 -0
  75. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/util.py +0 -0
  76. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/src/wuttaweb/views/auth.py +0 -0
  77. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tasks.py +0 -0
  78. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/__init__.py +0 -0
  79. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/forms/__init__.py +0 -0
  80. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/test_app.py +0 -0
  81. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/test_auth.py +0 -0
  82. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/test_handler.py +0 -0
  83. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/test_helpers.py +0 -0
  84. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/test_menus.py +0 -0
  85. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/test_static.py +0 -0
  86. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/test_util.py +0 -0
  87. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/views/__init__.py +0 -0
  88. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/views/test___init__.py +0 -0
  89. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/views/test_auth.py +0 -0
  90. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tests/views/test_common.py +0 -0
  91. {wuttaweb-0.3.0 → wuttaweb-0.4.0}/tox.ini +0 -0
@@ -5,6 +5,18 @@ All notable changes to wuttaweb will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v0.4.0 (2024-08-05)
9
+
10
+ ### Feat
11
+
12
+ - add basic App Info view (index only)
13
+ - add initial `MasterView` support
14
+
15
+ ### Fix
16
+
17
+ - add `notfound()` View method; auto-append trailing slash
18
+ - bump min version for wuttjamaican
19
+
8
20
  ## v0.3.0 (2024-08-05)
9
21
 
10
22
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: WuttaWeb
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Web App for Wutta Framework
5
5
  Project-URL: Homepage, https://wuttaproject.org/
6
6
  Project-URL: Repository, https://forgejo.wuttaproject.org/wutta/wuttaweb
@@ -31,7 +31,7 @@ Requires-Dist: pyramid-tm
31
31
  Requires-Dist: pyramid>=2
32
32
  Requires-Dist: waitress
33
33
  Requires-Dist: webhelpers2
34
- Requires-Dist: wuttjamaican[db]>=0.7.0
34
+ Requires-Dist: wuttjamaican[db]>=0.8.3
35
35
  Requires-Dist: zope-sqlalchemy>=1.5
36
36
  Provides-Extra: docs
37
37
  Requires-Dist: furo; extra == 'docs'
@@ -23,3 +23,5 @@
23
23
  views.base
24
24
  views.common
25
25
  views.essential
26
+ views.master
27
+ views.settings
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.views.master``
3
+ =========================
4
+
5
+ .. automodule:: wuttaweb.views.master
6
+ :members:
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.views.settings``
3
+ ===========================
4
+
5
+ .. automodule:: wuttaweb.views.settings
6
+ :members:
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.3.0"
9
+ version = "0.4.0"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
@@ -37,7 +37,7 @@ dependencies = [
37
37
  "pyramid_tm",
38
38
  "waitress",
39
39
  "WebHelpers2",
40
- "WuttJamaican[db]>=0.7.0",
40
+ "WuttJamaican[db]>=0.8.3",
41
41
  "zope.sqlalchemy>=1.5",
42
42
  ]
43
43
 
@@ -118,8 +118,9 @@ class MenuHandler(GenericHandler):
118
118
  'type': 'menu',
119
119
  'items': [
120
120
  {
121
- 'title': "TODO!",
122
- 'url': '#',
121
+ 'title': "App Info",
122
+ 'route': 'appinfo',
123
+ 'perm': 'appinfo.list',
123
124
  },
124
125
  ],
125
126
  }
@@ -249,6 +249,7 @@ def before_render(event):
249
249
  context['h'] = helpers
250
250
  context['url'] = request.route_url
251
251
  context['json'] = json
252
+ context['b'] = 'o' if request.use_oruga else 'b' # for buefy
252
253
 
253
254
  # TODO: this should be avoided somehow, for non-traditional web
254
255
  # apps, esp. "API" web apps. (in the meantime can configure the
@@ -0,0 +1,56 @@
1
+ ## -*- coding: utf-8; -*-
2
+ <%inherit file="/master/index.mako" />
3
+
4
+ <%def name="page_content()">
5
+
6
+ <nav class="panel item-panel">
7
+ <p class="panel-heading">Application</p>
8
+ <div class="panel-block">
9
+ <div style="width: 100%;">
10
+ <b-field horizontal label="Distribution">
11
+ <span>${app.get_distribution(obj=app.get_web_handler()) or f'?? - set config for `{app.appname}.app_dist`'}</span>
12
+ </b-field>
13
+ <b-field horizontal label="Version">
14
+ <span>${app.get_version(obj=app.get_web_handler()) or f'?? - set config for `{app.appname}.app_dist`'}</span>
15
+ </b-field>
16
+ <b-field horizontal label="App Title">
17
+ <span>${app.get_title()}</span>
18
+ </b-field>
19
+ </div>
20
+ </div>
21
+ </nav>
22
+
23
+ <nav class="panel item-panel">
24
+ <p class="panel-heading">Configuration Files</p>
25
+ <div class="panel-block">
26
+ <div style="width: 100%;">
27
+ <${b}-table :data="configFiles">
28
+
29
+ <${b}-table-column field="priority"
30
+ label="Priority"
31
+ v-slot="props">
32
+ {{ props.row.priority }}
33
+ </${b}-table-column>
34
+
35
+ <${b}-table-column field="path"
36
+ label="File Path"
37
+ v-slot="props">
38
+ {{ props.row.path }}
39
+ </${b}-table-column>
40
+
41
+ </${b}-table>
42
+ </div>
43
+ </div>
44
+ </nav>
45
+
46
+ </%def>
47
+
48
+ <%def name="modify_this_page_vars()">
49
+ ${parent.modify_this_page_vars()}
50
+ <script>
51
+ ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(config.get_prioritized_files(), 1)])|n}
52
+ </script>
53
+ </%def>
54
+
55
+
56
+ ${parent.body()}
@@ -0,0 +1,13 @@
1
+ ## -*- coding: utf-8; -*-
2
+ <%inherit file="/page.mako" />
3
+
4
+ <%def name="title()">${index_title}</%def>
5
+
6
+ <%def name="content_title()"></%def>
7
+
8
+ <%def name="page_content()">
9
+ <p>TODO: index page content</p>
10
+ </%def>
11
+
12
+
13
+ ${parent.body()}
@@ -27,9 +27,11 @@ For convenience, from this ``wuttaweb.views`` namespace you can access
27
27
  the following:
28
28
 
29
29
  * :class:`~wuttaweb.views.base.View`
30
+ * :class:`~wuttaweb.views.master.MasterView`
30
31
  """
31
32
 
32
33
  from .base import View
34
+ from .master import MasterView
33
35
 
34
36
 
35
37
  def includeme(config):
@@ -73,6 +73,14 @@ class View:
73
73
  """
74
74
  return forms.Form(self.request, **kwargs)
75
75
 
76
+ def notfound(self):
77
+ """
78
+ Convenience method, to raise a HTTP 404 Not Found exception::
79
+
80
+ raise self.notfound()
81
+ """
82
+ return httpexceptions.HTTPNotFound()
83
+
76
84
  def redirect(self, url, **kwargs):
77
85
  """
78
86
  Convenience method to return a HTTP 302 response.
@@ -53,7 +53,10 @@ class CommonView(View):
53
53
  @classmethod
54
54
  def _defaults(cls, config):
55
55
 
56
- # home
56
+ # auto-correct URLs which require trailing slash
57
+ config.add_notfound_view(cls, attr='notfound', append_slash=True)
58
+
59
+ # home page
57
60
  config.add_route('home', '/')
58
61
  config.add_view(cls, attr='home',
59
62
  route_name='home',
@@ -39,6 +39,7 @@ def defaults(config, **kwargs):
39
39
 
40
40
  config.include(mod('wuttaweb.views.auth'))
41
41
  config.include(mod('wuttaweb.views.common'))
42
+ config.include(mod('wuttaweb.views.settings'))
42
43
 
43
44
 
44
45
  def includeme(config):
@@ -0,0 +1,443 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # wuttaweb -- Web App for Wutta Framework
5
+ # Copyright © 2024 Lance Edgar
6
+ #
7
+ # This file is part of Wutta Framework.
8
+ #
9
+ # Wutta Framework is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by the Free
11
+ # Software Foundation, either version 3 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Wutta Framework is distributed in the hope that it will be useful, but
15
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17
+ # more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with
20
+ # Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ ################################################################################
23
+ """
24
+ Base Logic for Master Views
25
+ """
26
+
27
+ from pyramid.renderers import render_to_response
28
+
29
+ from wuttaweb.views import View
30
+
31
+
32
+ class MasterView(View):
33
+ """
34
+ Base class for "master" views.
35
+
36
+ Master views typically map to a table in a DB, though not always.
37
+ They essentially are a set of CRUD views for a certain type of
38
+ data record.
39
+
40
+ Many attributes may be overridden in subclass. For instance to
41
+ define :attr:`model_class`::
42
+
43
+ from wuttaweb.views import MasterView
44
+ from wuttjamaican.db.model import Person
45
+
46
+ class MyPersonView(MasterView):
47
+ model_class = Person
48
+
49
+ def includeme(config):
50
+ MyPersonView.defaults(config)
51
+
52
+ .. note::
53
+
54
+ Many of these attributes will only exist if they have been
55
+ explicitly defined in a subclass. There are corresponding
56
+ ``get_xxx()`` methods which should be used instead of accessing
57
+ these attributes directly.
58
+
59
+ .. attribute:: model_class
60
+
61
+ Optional reference to a data model class. While not strictly
62
+ required, most views will set this to a SQLAlchemy mapped
63
+ class,
64
+ e.g. :class:`wuttjamaican:wuttjamaican.db.model.auth.User`.
65
+
66
+ Code should not access this directly but instead call
67
+ :meth:`get_model_class()`.
68
+
69
+ .. attribute:: model_name
70
+
71
+ Optional override for the view's data model name,
72
+ e.g. ``'WuttaWidget'``.
73
+
74
+ Code should not access this directly but instead call
75
+ :meth:`get_model_name()`.
76
+
77
+ .. attribute:: model_name_normalized
78
+
79
+ Optional override for the view's "normalized" data model name,
80
+ e.g. ``'wutta_widget'``.
81
+
82
+ Code should not access this directly but instead call
83
+ :meth:`get_model_name_normalized()`.
84
+
85
+ .. attribute:: model_title
86
+
87
+ Optional override for the view's "humanized" (singular) model
88
+ title, e.g. ``"Wutta Widget"``.
89
+
90
+ Code should not access this directly but instead call
91
+ :meth:`get_model_title()`.
92
+
93
+ .. attribute:: model_title_plural
94
+
95
+ Optional override for the view's "humanized" (plural) model
96
+ title, e.g. ``"Wutta Widgets"``.
97
+
98
+ Code should not access this directly but instead call
99
+ :meth:`get_model_title_plural()`.
100
+
101
+ .. attribute:: route_prefix
102
+
103
+ Optional override for the view's route prefix,
104
+ e.g. ``'wutta_widgets'``.
105
+
106
+ Code should not access this directly but instead call
107
+ :meth:`get_route_prefix()`.
108
+
109
+ .. attribute:: url_prefix
110
+
111
+ Optional override for the view's URL prefix,
112
+ e.g. ``'/widgets'``.
113
+
114
+ Code should not access this directly but instead call
115
+ :meth:`get_url_prefix()`.
116
+
117
+ .. attribute:: template_prefix
118
+
119
+ Optional override for the view's template prefix,
120
+ e.g. ``'/widgets'``.
121
+
122
+ Code should not access this directly but instead call
123
+ :meth:`get_template_prefix()`.
124
+
125
+ .. attribute:: listable
126
+
127
+ Boolean indicating whether the view model supports "listing" -
128
+ i.e. it should have an :meth:`index()` view.
129
+ """
130
+
131
+ ##############################
132
+ # attributes
133
+ ##############################
134
+
135
+ listable = True
136
+
137
+ ##############################
138
+ # view methods
139
+ ##############################
140
+
141
+ def index(self):
142
+ """
143
+ View to "list" (filter/browse) the model data.
144
+
145
+ This is the "default" view for the model and is what user sees
146
+ when visiting the "root" path under the :attr:`url_prefix`,
147
+ e.g. ``/widgets/``.
148
+ """
149
+ return self.render_to_response('index', {})
150
+
151
+ ##############################
152
+ # support methods
153
+ ##############################
154
+
155
+ def get_index_title(self):
156
+ """
157
+ Returns the main index title for the master view.
158
+
159
+ By default this returns the value from
160
+ :meth:`get_model_title_plural()`. Subclass may override as
161
+ needed.
162
+ """
163
+ return self.get_model_title_plural()
164
+
165
+ def render_to_response(self, template, context):
166
+ """
167
+ Locate and render an appropriate template, with the given
168
+ context, and return a :term:`response`.
169
+
170
+ The specified ``template`` should be only the "base name" for
171
+ the template - e.g. ``'index'`` or ``'edit'``. This method
172
+ will then try to locate a suitable template file, based on
173
+ values from :meth:`get_template_prefix()` and
174
+ :meth:`get_fallback_templates()`.
175
+
176
+ In practice this *usually* means two different template paths
177
+ will be attempted, e.g. if ``template`` is ``'edit'`` and
178
+ :attr:`template_prefix` is ``'/widgets'``:
179
+
180
+ * ``/widgets/edit.mako``
181
+ * ``/master/edit.mako``
182
+
183
+ The first template found to exist will be used for rendering.
184
+ It then calls
185
+ :func:`pyramid:pyramid.renderers.render_to_response()` and
186
+ returns the result.
187
+
188
+ :param template: Base name for the template.
189
+
190
+ :param context: Data dict to be used as template context.
191
+
192
+ :returns: Response object containing the rendered template.
193
+ """
194
+ defaults = {
195
+ 'index_title': self.get_index_title(),
196
+ }
197
+
198
+ # merge defaults + caller-provided context
199
+ defaults.update(context)
200
+ context = defaults
201
+
202
+ # first try the template path most specific to this view
203
+ template_prefix = self.get_template_prefix()
204
+ mako_path = f'{template_prefix}/{template}.mako'
205
+ try:
206
+ return render_to_response(mako_path, context, request=self.request)
207
+ except IOError:
208
+
209
+ # failing that, try one or more fallback templates
210
+ for fallback in self.get_fallback_templates(template):
211
+ try:
212
+ return render_to_response(fallback, context, request=self.request)
213
+ except IOError:
214
+ pass
215
+
216
+ # if we made it all the way here, then we found no
217
+ # templates at all, in which case re-attempt the first and
218
+ # let that error raise on up
219
+ return render_to_response(mako_path, context, request=self.request)
220
+
221
+ def get_fallback_templates(self, template):
222
+ """
223
+ Returns a list of "fallback" template paths which may be
224
+ attempted for rendering a view. This is used within
225
+ :meth:`render_to_response()` if the "first guess" template
226
+ file was not found.
227
+
228
+ :param template: Base name for a template (without prefix), e.g.
229
+ ``'custom'``.
230
+
231
+ :returns: List of full template paths to be tried, based on
232
+ the specified template. For instance if ``template`` is
233
+ ``'custom'`` this will (by default) return::
234
+
235
+ ['/master/custom.mako']
236
+ """
237
+ return [f'/master/{template}.mako']
238
+
239
+ ##############################
240
+ # class methods
241
+ ##############################
242
+
243
+ @classmethod
244
+ def get_model_class(cls):
245
+ """
246
+ Returns the model class for the view (if defined).
247
+
248
+ A model class will *usually* be a SQLAlchemy mapped class,
249
+ e.g. :class:`wuttjamaican:wuttjamaican.db.model.base.Person`.
250
+
251
+ There is no default value here, but a subclass may override by
252
+ assigning :attr:`model_class`.
253
+
254
+ Note that the model class is not *required* - however if you
255
+ do not set the :attr:`model_class`, then you *must* set the
256
+ :attr:`model_name`.
257
+ """
258
+ if hasattr(cls, 'model_class'):
259
+ return cls.model_class
260
+
261
+ @classmethod
262
+ def get_model_name(cls):
263
+ """
264
+ Returns the model name for the view.
265
+
266
+ A model name should generally be in the format of a Python
267
+ class name, e.g. ``'WuttaWidget'``. (Note this is
268
+ *singular*, not plural.)
269
+
270
+ The default logic will call :meth:`get_model_class()` and
271
+ return that class name as-is. A subclass may override by
272
+ assigning :attr:`model_name`.
273
+ """
274
+ if hasattr(cls, 'model_name'):
275
+ return cls.model_name
276
+
277
+ return cls.get_model_class().__name__
278
+
279
+ @classmethod
280
+ def get_model_name_normalized(cls):
281
+ """
282
+ Returns the "normalized" model name for the view.
283
+
284
+ A normalized model name should generally be in the format of a
285
+ Python variable name, e.g. ``'wutta_widget'``. (Note this is
286
+ *singular*, not plural.)
287
+
288
+ The default logic will call :meth:`get_model_name()` and
289
+ simply lower-case the result. A subclass may override by
290
+ assigning :attr:`model_name_normalized`.
291
+ """
292
+ if hasattr(cls, 'model_name_normalized'):
293
+ return cls.model_name_normalized
294
+
295
+ return cls.get_model_name().lower()
296
+
297
+ @classmethod
298
+ def get_model_title(cls):
299
+ """
300
+ Returns the "humanized" (singular) model title for the view.
301
+
302
+ The model title will be displayed to the user, so should have
303
+ proper grammar and capitalization, e.g. ``"Wutta Widget"``.
304
+ (Note this is *singular*, not plural.)
305
+
306
+ The default logic will call :meth:`get_model_name()` and use
307
+ the result as-is. A subclass may override by assigning
308
+ :attr:`model_title`.
309
+ """
310
+ if hasattr(cls, 'model_title'):
311
+ return cls.model_title
312
+
313
+ return cls.get_model_name()
314
+
315
+ @classmethod
316
+ def get_model_title_plural(cls):
317
+ """
318
+ Returns the "humanized" (plural) model title for the view.
319
+
320
+ The model title will be displayed to the user, so should have
321
+ proper grammar and capitalization, e.g. ``"Wutta Widgets"``.
322
+ (Note this is *plural*, not singular.)
323
+
324
+ The default logic will call :meth:`get_model_title()` and
325
+ simply add a ``'s'`` to the end. A subclass may override by
326
+ assigning :attr:`model_title_plural`.
327
+ """
328
+ if hasattr(cls, 'model_title_plural'):
329
+ return cls.model_title_plural
330
+
331
+ model_title = cls.get_model_title()
332
+ return f"{model_title}s"
333
+
334
+ @classmethod
335
+ def get_route_prefix(cls):
336
+ """
337
+ Returns the "route prefix" for the master view. This prefix
338
+ is used for all named routes defined by the view class.
339
+
340
+ For instance if route prefix is ``'widgets'`` then a view
341
+ might have these routes:
342
+
343
+ * ``'widgets'``
344
+ * ``'widgets.create'``
345
+ * ``'widgets.edit'``
346
+ * ``'widgets.delete'``
347
+
348
+ The default logic will call
349
+ :meth:`get_model_name_normalized()` and simply add an ``'s'``
350
+ to the end, making it plural. A subclass may override by
351
+ assigning :attr:`route_prefix`.
352
+ """
353
+ if hasattr(cls, 'route_prefix'):
354
+ return cls.route_prefix
355
+
356
+ model_name = cls.get_model_name_normalized()
357
+ return f'{model_name}s'
358
+
359
+ @classmethod
360
+ def get_url_prefix(cls):
361
+ """
362
+ Returns the "URL prefix" for the master view. This prefix is
363
+ used for all URLs defined by the view class.
364
+
365
+ Using the same example as in :meth:`get_route_prefix()`, the
366
+ URL prefix would be ``'/widgets'`` and the view would have
367
+ defined routes for these URLs:
368
+
369
+ * ``/widgets/``
370
+ * ``/widgets/new``
371
+ * ``/widgets/XXX/edit``
372
+ * ``/widgets/XXX/delete``
373
+
374
+ The default logic will call :meth:`get_route_prefix()` and
375
+ simply add a ``'/'`` to the beginning. A subclass may
376
+ override by assigning :attr:`url_prefix`.
377
+ """
378
+ if hasattr(cls, 'url_prefix'):
379
+ return cls.url_prefix
380
+
381
+ route_prefix = cls.get_route_prefix()
382
+ return f'/{route_prefix}'
383
+
384
+ @classmethod
385
+ def get_template_prefix(cls):
386
+ """
387
+ Returns the "template prefix" for the master view. This
388
+ prefix is used to guess which template path to render for a
389
+ given view.
390
+
391
+ Using the same example as in :meth:`get_url_prefix()`, the
392
+ template prefix would also be ``'/widgets'`` and the templates
393
+ assumed for those routes would be:
394
+
395
+ * ``/widgets/index.mako``
396
+ * ``/widgets/create.mako``
397
+ * ``/widgets/edit.mako``
398
+ * ``/widgets/delete.mako``
399
+
400
+ The default logic will call :meth:`get_url_prefix()` and
401
+ return that value as-is. A subclass may override by assigning
402
+ :attr:`template_prefix`.
403
+ """
404
+ if hasattr(cls, 'template_prefix'):
405
+ return cls.template_prefix
406
+
407
+ return cls.get_url_prefix()
408
+
409
+ ##############################
410
+ # configuration
411
+ ##############################
412
+
413
+ @classmethod
414
+ def defaults(cls, config):
415
+ """
416
+ Provide default Pyramid configuration for a master view.
417
+
418
+ This is generally called from within the module's
419
+ ``includeme()`` function, e.g.::
420
+
421
+ from wuttaweb.views import MasterView
422
+
423
+ class WidgetView(MasterView):
424
+ model_name = 'Widget'
425
+
426
+ def includeme(config):
427
+ WidgetView.defaults(config)
428
+
429
+ :param config: Reference to the app's
430
+ :class:`pyramid:pyramid.config.Configurator` instance.
431
+ """
432
+ cls._defaults(config)
433
+
434
+ @classmethod
435
+ def _defaults(cls, config):
436
+ route_prefix = cls.get_route_prefix()
437
+ url_prefix = cls.get_url_prefix()
438
+
439
+ # index view
440
+ if cls.listable:
441
+ config.add_route(route_prefix, f'{url_prefix}/')
442
+ config.add_view(cls, attr='index',
443
+ route_name=route_prefix)