Compare commits
	
		
			4 commits
		
	
	
		
			f5891d36fa
			...
			01aa08b33d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 01aa08b33d | ||
|   | 7766ca6b12 | ||
|   | 9a739381ae | ||
|   | 9ac4f7525e | 
					 22 changed files with 981 additions and 9 deletions
				
			
		
							
								
								
									
										12
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -5,6 +5,18 @@ All notable changes to wuttaweb will be documented in this file. | ||||||
| The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) | 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). | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). | ||||||
| 
 | 
 | ||||||
|  | ## v0.4.0 (2024-08-05) | ||||||
|  | 
 | ||||||
|  | ### Feat | ||||||
|  | 
 | ||||||
|  | - add basic App Info view (index only) | ||||||
|  | - add initial `MasterView` support | ||||||
|  | 
 | ||||||
|  | ### Fix | ||||||
|  | 
 | ||||||
|  | - add `notfound()` View method; auto-append trailing slash | ||||||
|  | - bump min version for wuttjamaican | ||||||
|  | 
 | ||||||
| ## v0.3.0 (2024-08-05) | ## v0.3.0 (2024-08-05) | ||||||
| 
 | 
 | ||||||
| ### Feat | ### Feat | ||||||
|  |  | ||||||
|  | @ -23,3 +23,5 @@ | ||||||
|    views.base |    views.base | ||||||
|    views.common |    views.common | ||||||
|    views.essential |    views.essential | ||||||
|  |    views.master | ||||||
|  |    views.settings | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								docs/api/wuttaweb/views.master.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wuttaweb/views.master.rst
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | 
 | ||||||
|  | ``wuttaweb.views.master`` | ||||||
|  | ========================= | ||||||
|  | 
 | ||||||
|  | .. automodule:: wuttaweb.views.master | ||||||
|  |    :members: | ||||||
							
								
								
									
										6
									
								
								docs/api/wuttaweb/views.settings.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wuttaweb/views.settings.rst
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | 
 | ||||||
|  | ``wuttaweb.views.settings`` | ||||||
|  | =========================== | ||||||
|  | 
 | ||||||
|  | .. automodule:: wuttaweb.views.settings | ||||||
|  |    :members: | ||||||
|  | @ -6,7 +6,7 @@ build-backend = "hatchling.build" | ||||||
| 
 | 
 | ||||||
| [project] | [project] | ||||||
| name = "WuttaWeb" | name = "WuttaWeb" | ||||||
| version = "0.3.0" | version = "0.4.0" | ||||||
| description = "Web App for Wutta Framework" | description = "Web App for Wutta Framework" | ||||||
| readme = "README.md" | readme = "README.md" | ||||||
| authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] | authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] | ||||||
|  |  | ||||||
|  | @ -118,8 +118,9 @@ class MenuHandler(GenericHandler): | ||||||
|             'type': 'menu', |             'type': 'menu', | ||||||
|             'items': [ |             'items': [ | ||||||
|                 { |                 { | ||||||
|                     'title': "TODO!", |                     'title': "App Info", | ||||||
|                     'url': '#', |                     'route': 'appinfo', | ||||||
|  |                     'perm': 'appinfo.list', | ||||||
|                 }, |                 }, | ||||||
|             ], |             ], | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -249,6 +249,7 @@ def before_render(event): | ||||||
|     context['h'] = helpers |     context['h'] = helpers | ||||||
|     context['url'] = request.route_url |     context['url'] = request.route_url | ||||||
|     context['json'] = json |     context['json'] = json | ||||||
|  |     context['b'] = 'o' if request.use_oruga else 'b' # for buefy | ||||||
| 
 | 
 | ||||||
|     # TODO: this should be avoided somehow, for non-traditional web |     # TODO: this should be avoided somehow, for non-traditional web | ||||||
|     # apps, esp. "API" web apps.  (in the meantime can configure the |     # apps, esp. "API" web apps.  (in the meantime can configure the | ||||||
|  |  | ||||||
							
								
								
									
										56
									
								
								src/wuttaweb/templates/appinfo/index.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/wuttaweb/templates/appinfo/index.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | ||||||
|  | ## -*- coding: utf-8; -*- | ||||||
|  | <%inherit file="/master/index.mako" /> | ||||||
|  | 
 | ||||||
|  | <%def name="page_content()"> | ||||||
|  | 
 | ||||||
|  |   <nav class="panel item-panel"> | ||||||
|  |     <p class="panel-heading">Application</p> | ||||||
|  |     <div class="panel-block"> | ||||||
|  |       <div style="width: 100%;"> | ||||||
|  |         <b-field horizontal label="Distribution"> | ||||||
|  |           <span>${app.get_distribution(obj=app.get_web_handler()) or f'?? - set config for `{app.appname}.app_dist`'}</span> | ||||||
|  |         </b-field> | ||||||
|  |         <b-field horizontal label="Version"> | ||||||
|  |           <span>${app.get_version(obj=app.get_web_handler()) or f'?? - set config for `{app.appname}.app_dist`'}</span> | ||||||
|  |         </b-field> | ||||||
|  |         <b-field horizontal label="App Title"> | ||||||
|  |           <span>${app.get_title()}</span> | ||||||
|  |         </b-field> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </nav> | ||||||
|  | 
 | ||||||
|  |   <nav class="panel item-panel"> | ||||||
|  |     <p class="panel-heading">Configuration Files</p> | ||||||
|  |     <div class="panel-block"> | ||||||
|  |       <div style="width: 100%;"> | ||||||
|  |         <${b}-table :data="configFiles"> | ||||||
|  | 
 | ||||||
|  |           <${b}-table-column field="priority" | ||||||
|  |                           label="Priority" | ||||||
|  |                           v-slot="props"> | ||||||
|  |             {{ props.row.priority }} | ||||||
|  |           </${b}-table-column> | ||||||
|  | 
 | ||||||
|  |           <${b}-table-column field="path" | ||||||
|  |                           label="File Path" | ||||||
|  |                           v-slot="props"> | ||||||
|  |             {{ props.row.path }} | ||||||
|  |           </${b}-table-column> | ||||||
|  | 
 | ||||||
|  |         </${b}-table> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </nav> | ||||||
|  | 
 | ||||||
|  | </%def> | ||||||
|  | 
 | ||||||
|  | <%def name="modify_this_page_vars()"> | ||||||
|  |   ${parent.modify_this_page_vars()} | ||||||
|  |   <script> | ||||||
|  |     ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(config.get_prioritized_files(), 1)])|n} | ||||||
|  |   </script> | ||||||
|  | </%def> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ${parent.body()} | ||||||
							
								
								
									
										13
									
								
								src/wuttaweb/templates/master/index.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/wuttaweb/templates/master/index.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | ## -*- coding: utf-8; -*- | ||||||
|  | <%inherit file="/page.mako" /> | ||||||
|  | 
 | ||||||
|  | <%def name="title()">${index_title}</%def> | ||||||
|  | 
 | ||||||
|  | <%def name="content_title()"></%def> | ||||||
|  | 
 | ||||||
|  | <%def name="page_content()"> | ||||||
|  |   <p>TODO: index page content</p> | ||||||
|  | </%def> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ${parent.body()} | ||||||
|  | @ -27,9 +27,11 @@ For convenience, from this ``wuttaweb.views`` namespace you can access | ||||||
| the following: | the following: | ||||||
| 
 | 
 | ||||||
| * :class:`~wuttaweb.views.base.View` | * :class:`~wuttaweb.views.base.View` | ||||||
|  | * :class:`~wuttaweb.views.master.MasterView` | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from .base import View | from .base import View | ||||||
|  | from .master import MasterView | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def includeme(config): | def includeme(config): | ||||||
|  |  | ||||||
|  | @ -73,6 +73,14 @@ class View: | ||||||
|         """ |         """ | ||||||
|         return forms.Form(self.request, **kwargs) |         return forms.Form(self.request, **kwargs) | ||||||
| 
 | 
 | ||||||
|  |     def notfound(self): | ||||||
|  |         """ | ||||||
|  |         Convenience method, to raise a HTTP 404 Not Found exception:: | ||||||
|  | 
 | ||||||
|  |            raise self.notfound() | ||||||
|  |         """ | ||||||
|  |         return httpexceptions.HTTPNotFound() | ||||||
|  | 
 | ||||||
|     def redirect(self, url, **kwargs): |     def redirect(self, url, **kwargs): | ||||||
|         """ |         """ | ||||||
|         Convenience method to return a HTTP 302 response. |         Convenience method to return a HTTP 302 response. | ||||||
|  |  | ||||||
|  | @ -53,7 +53,10 @@ class CommonView(View): | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _defaults(cls, config): |     def _defaults(cls, config): | ||||||
| 
 | 
 | ||||||
|         # home |         # auto-correct URLs which require trailing slash | ||||||
|  |         config.add_notfound_view(cls, attr='notfound', append_slash=True) | ||||||
|  | 
 | ||||||
|  |         # home page | ||||||
|         config.add_route('home', '/') |         config.add_route('home', '/') | ||||||
|         config.add_view(cls, attr='home', |         config.add_view(cls, attr='home', | ||||||
|                         route_name='home', |                         route_name='home', | ||||||
|  |  | ||||||
|  | @ -39,6 +39,7 @@ def defaults(config, **kwargs): | ||||||
| 
 | 
 | ||||||
|     config.include(mod('wuttaweb.views.auth')) |     config.include(mod('wuttaweb.views.auth')) | ||||||
|     config.include(mod('wuttaweb.views.common')) |     config.include(mod('wuttaweb.views.common')) | ||||||
|  |     config.include(mod('wuttaweb.views.settings')) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def includeme(config): | def includeme(config): | ||||||
|  |  | ||||||
							
								
								
									
										443
									
								
								src/wuttaweb/views/master.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										443
									
								
								src/wuttaweb/views/master.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,443 @@ | ||||||
|  | # -*- coding: utf-8; -*- | ||||||
|  | ################################################################################ | ||||||
|  | # | ||||||
|  | #  wuttaweb -- Web App for Wutta Framework | ||||||
|  | #  Copyright © 2024 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 <http://www.gnu.org/licenses/>. | ||||||
|  | # | ||||||
|  | ################################################################################ | ||||||
|  | """ | ||||||
|  | Base Logic for Master Views | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from pyramid.renderers import render_to_response | ||||||
|  | 
 | ||||||
|  | from wuttaweb.views import View | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MasterView(View): | ||||||
|  |     """ | ||||||
|  |     Base class for "master" views. | ||||||
|  | 
 | ||||||
|  |     Master views typically map to a table in a DB, though not always. | ||||||
|  |     They essentially are a set of CRUD views for a certain type of | ||||||
|  |     data record. | ||||||
|  | 
 | ||||||
|  |     Many attributes may be overridden in subclass.  For instance to | ||||||
|  |     define :attr:`model_class`:: | ||||||
|  | 
 | ||||||
|  |        from wuttaweb.views import MasterView | ||||||
|  |        from wuttjamaican.db.model import Person | ||||||
|  | 
 | ||||||
|  |        class MyPersonView(MasterView): | ||||||
|  |            model_class = Person | ||||||
|  | 
 | ||||||
|  |        def includeme(config): | ||||||
|  |            MyPersonView.defaults(config) | ||||||
|  | 
 | ||||||
|  |     .. note:: | ||||||
|  | 
 | ||||||
|  |        Many of these attributes will only exist if they have been | ||||||
|  |        explicitly defined in a subclass.  There are corresponding | ||||||
|  |        ``get_xxx()`` methods which should be used instead of accessing | ||||||
|  |        these attributes directly. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: model_class | ||||||
|  | 
 | ||||||
|  |        Optional reference to a data model class.  While not strictly | ||||||
|  |        required, most views will set this to a SQLAlchemy mapped | ||||||
|  |        class, | ||||||
|  |        e.g. :class:`wuttjamaican:wuttjamaican.db.model.auth.User`. | ||||||
|  | 
 | ||||||
|  |        Code should not access this directly but instead call | ||||||
|  |        :meth:`get_model_class()`. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: model_name | ||||||
|  | 
 | ||||||
|  |        Optional override for the view's data model name, | ||||||
|  |        e.g. ``'WuttaWidget'``. | ||||||
|  | 
 | ||||||
|  |        Code should not access this directly but instead call | ||||||
|  |        :meth:`get_model_name()`. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: model_name_normalized | ||||||
|  | 
 | ||||||
|  |        Optional override for the view's "normalized" data model name, | ||||||
|  |        e.g. ``'wutta_widget'``. | ||||||
|  | 
 | ||||||
|  |        Code should not access this directly but instead call | ||||||
|  |        :meth:`get_model_name_normalized()`. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: model_title | ||||||
|  | 
 | ||||||
|  |        Optional override for the view's "humanized" (singular) model | ||||||
|  |        title, e.g. ``"Wutta Widget"``. | ||||||
|  | 
 | ||||||
|  |        Code should not access this directly but instead call | ||||||
|  |        :meth:`get_model_title()`. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: model_title_plural | ||||||
|  | 
 | ||||||
|  |        Optional override for the view's "humanized" (plural) model | ||||||
|  |        title, e.g. ``"Wutta Widgets"``. | ||||||
|  | 
 | ||||||
|  |        Code should not access this directly but instead call | ||||||
|  |        :meth:`get_model_title_plural()`. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: route_prefix | ||||||
|  | 
 | ||||||
|  |        Optional override for the view's route prefix, | ||||||
|  |        e.g. ``'wutta_widgets'``. | ||||||
|  | 
 | ||||||
|  |        Code should not access this directly but instead call | ||||||
|  |        :meth:`get_route_prefix()`. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: url_prefix | ||||||
|  | 
 | ||||||
|  |        Optional override for the view's URL prefix, | ||||||
|  |        e.g. ``'/widgets'``. | ||||||
|  | 
 | ||||||
|  |        Code should not access this directly but instead call | ||||||
|  |        :meth:`get_url_prefix()`. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: template_prefix | ||||||
|  | 
 | ||||||
|  |        Optional override for the view's template prefix, | ||||||
|  |        e.g. ``'/widgets'``. | ||||||
|  | 
 | ||||||
|  |        Code should not access this directly but instead call | ||||||
|  |        :meth:`get_template_prefix()`. | ||||||
|  | 
 | ||||||
|  |     .. attribute:: listable | ||||||
|  | 
 | ||||||
|  |        Boolean indicating whether the view model supports "listing" - | ||||||
|  |        i.e. it should have an :meth:`index()` view. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     ############################## | ||||||
|  |     # attributes | ||||||
|  |     ############################## | ||||||
|  | 
 | ||||||
|  |     listable = True | ||||||
|  | 
 | ||||||
|  |     ############################## | ||||||
|  |     # view methods | ||||||
|  |     ############################## | ||||||
|  | 
 | ||||||
|  |     def index(self): | ||||||
|  |         """ | ||||||
|  |         View to "list" (filter/browse) the model data. | ||||||
|  | 
 | ||||||
|  |         This is the "default" view for the model and is what user sees | ||||||
|  |         when visiting the "root" path under the :attr:`url_prefix`, | ||||||
|  |         e.g. ``/widgets/``. | ||||||
|  |         """ | ||||||
|  |         return self.render_to_response('index', {}) | ||||||
|  | 
 | ||||||
|  |     ############################## | ||||||
|  |     # support methods | ||||||
|  |     ############################## | ||||||
|  | 
 | ||||||
|  |     def get_index_title(self): | ||||||
|  |         """ | ||||||
|  |         Returns the main index title for the master view. | ||||||
|  | 
 | ||||||
|  |         By default this returns the value from | ||||||
|  |         :meth:`get_model_title_plural()`.  Subclass may override as | ||||||
|  |         needed. | ||||||
|  |         """ | ||||||
|  |         return self.get_model_title_plural() | ||||||
|  | 
 | ||||||
|  |     def render_to_response(self, template, context): | ||||||
|  |         """ | ||||||
|  |         Locate and render an appropriate template, with the given | ||||||
|  |         context, and return a :term:`response`. | ||||||
|  | 
 | ||||||
|  |         The specified ``template`` should be only the "base name" for | ||||||
|  |         the template - e.g.  ``'index'`` or ``'edit'``.  This method | ||||||
|  |         will then try to locate a suitable template file, based on | ||||||
|  |         values from :meth:`get_template_prefix()` and | ||||||
|  |         :meth:`get_fallback_templates()`. | ||||||
|  | 
 | ||||||
|  |         In practice this *usually* means two different template paths | ||||||
|  |         will be attempted, e.g. if ``template`` is ``'edit'`` and | ||||||
|  |         :attr:`template_prefix` is ``'/widgets'``: | ||||||
|  | 
 | ||||||
|  |         * ``/widgets/edit.mako`` | ||||||
|  |         * ``/master/edit.mako`` | ||||||
|  | 
 | ||||||
|  |         The first template found to exist will be used for rendering. | ||||||
|  |         It then calls | ||||||
|  |         :func:`pyramid:pyramid.renderers.render_to_response()` and | ||||||
|  |         returns the result. | ||||||
|  | 
 | ||||||
|  |         :param template: Base name for the template. | ||||||
|  | 
 | ||||||
|  |         :param context: Data dict to be used as template context. | ||||||
|  | 
 | ||||||
|  |         :returns: Response object containing the rendered template. | ||||||
|  |         """ | ||||||
|  |         defaults = { | ||||||
|  |             'index_title': self.get_index_title(), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         # merge defaults + caller-provided context | ||||||
|  |         defaults.update(context) | ||||||
|  |         context = defaults | ||||||
|  | 
 | ||||||
|  |         # first try the template path most specific to this view | ||||||
|  |         template_prefix = self.get_template_prefix() | ||||||
|  |         mako_path = f'{template_prefix}/{template}.mako' | ||||||
|  |         try: | ||||||
|  |             return render_to_response(mako_path, context, request=self.request) | ||||||
|  |         except IOError: | ||||||
|  | 
 | ||||||
|  |             # failing that, try one or more fallback templates | ||||||
|  |             for fallback in self.get_fallback_templates(template): | ||||||
|  |                 try: | ||||||
|  |                     return render_to_response(fallback, context, request=self.request) | ||||||
|  |                 except IOError: | ||||||
|  |                     pass | ||||||
|  | 
 | ||||||
|  |             # if we made it all the way here, then we found no | ||||||
|  |             # templates at all, in which case re-attempt the first and | ||||||
|  |             # let that error raise on up | ||||||
|  |             return render_to_response(mako_path, context, request=self.request) | ||||||
|  | 
 | ||||||
|  |     def get_fallback_templates(self, template): | ||||||
|  |         """ | ||||||
|  |         Returns a list of "fallback" template paths which may be | ||||||
|  |         attempted for rendering a view.  This is used within | ||||||
|  |         :meth:`render_to_response()` if the "first guess" template | ||||||
|  |         file was not found. | ||||||
|  | 
 | ||||||
|  |         :param template: Base name for a template (without prefix), e.g. | ||||||
|  |            ``'custom'``. | ||||||
|  | 
 | ||||||
|  |         :returns: List of full template paths to be tried, based on | ||||||
|  |            the specified template.  For instance if ``template`` is | ||||||
|  |            ``'custom'`` this will (by default) return:: | ||||||
|  | 
 | ||||||
|  |               ['/master/custom.mako'] | ||||||
|  |         """ | ||||||
|  |         return [f'/master/{template}.mako'] | ||||||
|  | 
 | ||||||
|  |     ############################## | ||||||
|  |     # class methods | ||||||
|  |     ############################## | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def get_model_class(cls): | ||||||
|  |         """ | ||||||
|  |         Returns the model class for the view (if defined). | ||||||
|  | 
 | ||||||
|  |         A model class will *usually* be a SQLAlchemy mapped class, | ||||||
|  |         e.g. :class:`wuttjamaican:wuttjamaican.db.model.base.Person`. | ||||||
|  | 
 | ||||||
|  |         There is no default value here, but a subclass may override by | ||||||
|  |         assigning :attr:`model_class`. | ||||||
|  | 
 | ||||||
|  |         Note that the model class is not *required* - however if you | ||||||
|  |         do not set the :attr:`model_class`, then you *must* set the | ||||||
|  |         :attr:`model_name`. | ||||||
|  |         """ | ||||||
|  |         if hasattr(cls, 'model_class'): | ||||||
|  |             return cls.model_class | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def get_model_name(cls): | ||||||
|  |         """ | ||||||
|  |         Returns the model name for the view. | ||||||
|  | 
 | ||||||
|  |         A model name should generally be in the format of a Python | ||||||
|  |         class name, e.g. ``'WuttaWidget'``.  (Note this is | ||||||
|  |         *singular*, not plural.) | ||||||
|  | 
 | ||||||
|  |         The default logic will call :meth:`get_model_class()` and | ||||||
|  |         return that class name as-is.  A subclass may override by | ||||||
|  |         assigning :attr:`model_name`. | ||||||
|  |         """ | ||||||
|  |         if hasattr(cls, 'model_name'): | ||||||
|  |             return cls.model_name | ||||||
|  | 
 | ||||||
|  |         return cls.get_model_class().__name__ | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def get_model_name_normalized(cls): | ||||||
|  |         """ | ||||||
|  |         Returns the "normalized" model name for the view. | ||||||
|  | 
 | ||||||
|  |         A normalized model name should generally be in the format of a | ||||||
|  |         Python variable name, e.g. ``'wutta_widget'``.  (Note this is | ||||||
|  |         *singular*, not plural.) | ||||||
|  | 
 | ||||||
|  |         The default logic will call :meth:`get_model_name()` and | ||||||
|  |         simply lower-case the result.  A subclass may override by | ||||||
|  |         assigning :attr:`model_name_normalized`. | ||||||
|  |         """ | ||||||
|  |         if hasattr(cls, 'model_name_normalized'): | ||||||
|  |             return cls.model_name_normalized | ||||||
|  | 
 | ||||||
|  |         return cls.get_model_name().lower() | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def get_model_title(cls): | ||||||
|  |         """ | ||||||
|  |         Returns the "humanized" (singular) model title for the view. | ||||||
|  | 
 | ||||||
|  |         The model title will be displayed to the user, so should have | ||||||
|  |         proper grammar and capitalization, e.g. ``"Wutta Widget"``. | ||||||
|  |         (Note this is *singular*, not plural.) | ||||||
|  | 
 | ||||||
|  |         The default logic will call :meth:`get_model_name()` and use | ||||||
|  |         the result as-is.  A subclass may override by assigning | ||||||
|  |         :attr:`model_title`. | ||||||
|  |         """ | ||||||
|  |         if hasattr(cls, 'model_title'): | ||||||
|  |             return cls.model_title | ||||||
|  | 
 | ||||||
|  |         return cls.get_model_name() | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def get_model_title_plural(cls): | ||||||
|  |         """ | ||||||
|  |         Returns the "humanized" (plural) model title for the view. | ||||||
|  | 
 | ||||||
|  |         The model title will be displayed to the user, so should have | ||||||
|  |         proper grammar and capitalization, e.g. ``"Wutta Widgets"``. | ||||||
|  |         (Note this is *plural*, not singular.) | ||||||
|  | 
 | ||||||
|  |         The default logic will call :meth:`get_model_title()` and | ||||||
|  |         simply add a ``'s'`` to the end.  A subclass may override by | ||||||
|  |         assigning :attr:`model_title_plural`. | ||||||
|  |         """ | ||||||
|  |         if hasattr(cls, 'model_title_plural'): | ||||||
|  |             return cls.model_title_plural | ||||||
|  | 
 | ||||||
|  |         model_title = cls.get_model_title() | ||||||
|  |         return f"{model_title}s" | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def get_route_prefix(cls): | ||||||
|  |         """ | ||||||
|  |         Returns the "route prefix" for the master view.  This prefix | ||||||
|  |         is used for all named routes defined by the view class. | ||||||
|  | 
 | ||||||
|  |         For instance if route prefix is ``'widgets'`` then a view | ||||||
|  |         might have these routes: | ||||||
|  | 
 | ||||||
|  |         * ``'widgets'`` | ||||||
|  |         * ``'widgets.create'`` | ||||||
|  |         * ``'widgets.edit'`` | ||||||
|  |         * ``'widgets.delete'`` | ||||||
|  | 
 | ||||||
|  |         The default logic will call | ||||||
|  |         :meth:`get_model_name_normalized()` and simply add an ``'s'`` | ||||||
|  |         to the end, making it plural.  A subclass may override by | ||||||
|  |         assigning :attr:`route_prefix`. | ||||||
|  |         """ | ||||||
|  |         if hasattr(cls, 'route_prefix'): | ||||||
|  |             return cls.route_prefix | ||||||
|  | 
 | ||||||
|  |         model_name = cls.get_model_name_normalized() | ||||||
|  |         return f'{model_name}s' | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def get_url_prefix(cls): | ||||||
|  |         """ | ||||||
|  |         Returns the "URL prefix" for the master view.  This prefix is | ||||||
|  |         used for all URLs defined by the view class. | ||||||
|  | 
 | ||||||
|  |         Using the same example as in :meth:`get_route_prefix()`, the | ||||||
|  |         URL prefix would be ``'/widgets'`` and the view would have | ||||||
|  |         defined routes for these URLs: | ||||||
|  | 
 | ||||||
|  |         * ``/widgets/`` | ||||||
|  |         * ``/widgets/new`` | ||||||
|  |         * ``/widgets/XXX/edit`` | ||||||
|  |         * ``/widgets/XXX/delete`` | ||||||
|  | 
 | ||||||
|  |         The default logic will call :meth:`get_route_prefix()` and | ||||||
|  |         simply add a ``'/'`` to the beginning.  A subclass may | ||||||
|  |         override by assigning :attr:`url_prefix`. | ||||||
|  |         """ | ||||||
|  |         if hasattr(cls, 'url_prefix'): | ||||||
|  |             return cls.url_prefix | ||||||
|  | 
 | ||||||
|  |         route_prefix = cls.get_route_prefix() | ||||||
|  |         return f'/{route_prefix}' | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def get_template_prefix(cls): | ||||||
|  |         """ | ||||||
|  |         Returns the "template prefix" for the master view.  This | ||||||
|  |         prefix is used to guess which template path to render for a | ||||||
|  |         given view. | ||||||
|  | 
 | ||||||
|  |         Using the same example as in :meth:`get_url_prefix()`, the | ||||||
|  |         template prefix would also be ``'/widgets'`` and the templates | ||||||
|  |         assumed for those routes would be: | ||||||
|  | 
 | ||||||
|  |         * ``/widgets/index.mako`` | ||||||
|  |         * ``/widgets/create.mako`` | ||||||
|  |         * ``/widgets/edit.mako`` | ||||||
|  |         * ``/widgets/delete.mako`` | ||||||
|  | 
 | ||||||
|  |         The default logic will call :meth:`get_url_prefix()` and | ||||||
|  |         return that value as-is.  A subclass may override by assigning | ||||||
|  |         :attr:`template_prefix`. | ||||||
|  |         """ | ||||||
|  |         if hasattr(cls, 'template_prefix'): | ||||||
|  |             return cls.template_prefix | ||||||
|  | 
 | ||||||
|  |         return cls.get_url_prefix() | ||||||
|  | 
 | ||||||
|  |     ############################## | ||||||
|  |     # configuration | ||||||
|  |     ############################## | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def defaults(cls, config): | ||||||
|  |         """ | ||||||
|  |         Provide default Pyramid configuration for a master view. | ||||||
|  | 
 | ||||||
|  |         This is generally called from within the module's | ||||||
|  |         ``includeme()`` function, e.g.:: | ||||||
|  | 
 | ||||||
|  |            from wuttaweb.views import MasterView | ||||||
|  | 
 | ||||||
|  |            class WidgetView(MasterView): | ||||||
|  |                model_name = 'Widget' | ||||||
|  | 
 | ||||||
|  |            def includeme(config): | ||||||
|  |                WidgetView.defaults(config) | ||||||
|  | 
 | ||||||
|  |         :param config: Reference to the app's | ||||||
|  |            :class:`pyramid:pyramid.config.Configurator` instance. | ||||||
|  |         """ | ||||||
|  |         cls._defaults(config) | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def _defaults(cls, config): | ||||||
|  |         route_prefix = cls.get_route_prefix() | ||||||
|  |         url_prefix = cls.get_url_prefix() | ||||||
|  | 
 | ||||||
|  |         # index view | ||||||
|  |         if cls.listable: | ||||||
|  |             config.add_route(route_prefix, f'{url_prefix}/') | ||||||
|  |             config.add_view(cls, attr='index', | ||||||
|  |                             route_name=route_prefix) | ||||||
							
								
								
									
										47
									
								
								src/wuttaweb/views/settings.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/wuttaweb/views/settings.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | # -*- coding: utf-8; -*- | ||||||
|  | ################################################################################ | ||||||
|  | # | ||||||
|  | #  wuttaweb -- Web App for Wutta Framework | ||||||
|  | #  Copyright © 2024 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 <http://www.gnu.org/licenses/>. | ||||||
|  | # | ||||||
|  | ################################################################################ | ||||||
|  | """ | ||||||
|  | Views for app settings | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from wuttaweb.views import MasterView | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AppInfoView(MasterView): | ||||||
|  |     """ | ||||||
|  |     Master view for the overall app, to show/edit config etc. | ||||||
|  |     """ | ||||||
|  |     model_name = 'AppInfo' | ||||||
|  |     model_title_plural = "App Info" | ||||||
|  |     route_prefix = 'appinfo' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def defaults(config, **kwargs): | ||||||
|  |     base = globals() | ||||||
|  | 
 | ||||||
|  |     AppInfoView = kwargs.get('AppInfoView', base['AppInfoView']) | ||||||
|  |     AppInfoView.defaults(config) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def includeme(config): | ||||||
|  |     defaults(config) | ||||||
|  | @ -46,8 +46,10 @@ class TestFieldList(TestCase): | ||||||
| class TestForm(TestCase): | class TestForm(TestCase): | ||||||
| 
 | 
 | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.config = WuttaConfig() |         self.config = WuttaConfig(defaults={ | ||||||
|         self.request = testing.DummyRequest(wutta_config=self.config) |             'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler', | ||||||
|  |         }) | ||||||
|  |         self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False) | ||||||
| 
 | 
 | ||||||
|         self.pyramid_config = testing.setUp(request=self.request, settings={ |         self.pyramid_config = testing.setUp(request=self.request, settings={ | ||||||
|             'mako.directories': ['wuttaweb:templates'], |             'mako.directories': ['wuttaweb:templates'], | ||||||
|  |  | ||||||
|  | @ -214,10 +214,12 @@ class TestNewRequestSetUser(TestCase): | ||||||
| class TestBeforeRender(TestCase): | class TestBeforeRender(TestCase): | ||||||
| 
 | 
 | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.config = WuttaConfig() |         self.config = WuttaConfig(defaults={ | ||||||
|  |             'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler', | ||||||
|  |         }) | ||||||
| 
 | 
 | ||||||
|     def make_request(self): |     def make_request(self): | ||||||
|         request = testing.DummyRequest() |         request = testing.DummyRequest(use_oruga=False) | ||||||
|         request.registry.settings = {'wutta_config': self.config} |         request.registry.settings = {'wutta_config': self.config} | ||||||
|         request.wutta_config = self.config |         request.wutta_config = self.config | ||||||
|         return request |         return request | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								tests/utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tests/utils.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | # -*- coding: utf-8; -*- | ||||||
|  | 
 | ||||||
|  | from wuttaweb.menus import MenuHandler | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NullMenuHandler(MenuHandler): | ||||||
|  |     """ | ||||||
|  |     Dummy menu handler for testing. | ||||||
|  |     """ | ||||||
|  |     def make_menus(self, request, **kwargs): | ||||||
|  |         return [] | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| from unittest import TestCase | from unittest import TestCase | ||||||
| 
 | 
 | ||||||
| from pyramid import testing | from pyramid import testing | ||||||
| from pyramid.httpexceptions import HTTPFound, HTTPForbidden | from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound | ||||||
| 
 | 
 | ||||||
| from wuttjamaican.conf import WuttaConfig | from wuttjamaican.conf import WuttaConfig | ||||||
| from wuttaweb.views import base | from wuttaweb.views import base | ||||||
|  | @ -31,6 +31,10 @@ class TestView(TestCase): | ||||||
|         form = self.view.make_form() |         form = self.view.make_form() | ||||||
|         self.assertIsInstance(form, Form) |         self.assertIsInstance(form, Form) | ||||||
| 
 | 
 | ||||||
|  |     def test_notfound(self): | ||||||
|  |         error = self.view.notfound() | ||||||
|  |         self.assertIsInstance(error, HTTPNotFound) | ||||||
|  | 
 | ||||||
|     def test_redirect(self): |     def test_redirect(self): | ||||||
|         error = self.view.redirect('/') |         error = self.view.redirect('/') | ||||||
|         self.assertIsInstance(error, HTTPFound) |         self.assertIsInstance(error, HTTPFound) | ||||||
|  |  | ||||||
							
								
								
									
										282
									
								
								tests/views/test_master.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								tests/views/test_master.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,282 @@ | ||||||
|  | # -*- coding: utf-8; -*- | ||||||
|  | 
 | ||||||
|  | from unittest import TestCase | ||||||
|  | from unittest.mock import MagicMock | ||||||
|  | 
 | ||||||
|  | from pyramid import testing | ||||||
|  | from pyramid.response import Response | ||||||
|  | 
 | ||||||
|  | from wuttjamaican.conf import WuttaConfig | ||||||
|  | from wuttaweb.views import master | ||||||
|  | from wuttaweb.subscribers import new_request_set_user | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestMasterView(TestCase): | ||||||
|  | 
 | ||||||
|  |     def setUp(self): | ||||||
|  |         self.config = WuttaConfig(defaults={ | ||||||
|  |             'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler', | ||||||
|  |         }) | ||||||
|  |         self.app = self.config.get_app() | ||||||
|  |         self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False) | ||||||
|  |         self.pyramid_config = testing.setUp(request=self.request, settings={ | ||||||
|  |             'wutta_config': self.config, | ||||||
|  |             'mako.directories': ['wuttaweb:templates'], | ||||||
|  |         }) | ||||||
|  |         self.pyramid_config.include('pyramid_mako') | ||||||
|  |         self.pyramid_config.include('wuttaweb.static') | ||||||
|  |         self.pyramid_config.include('wuttaweb.views.essential') | ||||||
|  |         self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render', | ||||||
|  |                                            'pyramid.events.BeforeRender') | ||||||
|  | 
 | ||||||
|  |         event = MagicMock(request=self.request) | ||||||
|  |         new_request_set_user(event) | ||||||
|  | 
 | ||||||
|  |     def tearDown(self): | ||||||
|  |         testing.tearDown() | ||||||
|  | 
 | ||||||
|  |     def test_defaults(self): | ||||||
|  |         master.MasterView.model_name = 'Widget' | ||||||
|  |         # TODO: should inspect pyramid routes after this, to be certain | ||||||
|  |         master.MasterView.defaults(self.pyramid_config) | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |     ############################## | ||||||
|  |     # class methods | ||||||
|  |     ############################## | ||||||
|  | 
 | ||||||
|  |     def test_get_model_class(self): | ||||||
|  |          | ||||||
|  |         # no model class by default | ||||||
|  |         self.assertIsNone(master.MasterView.get_model_class()) | ||||||
|  | 
 | ||||||
|  |         # subclass may specify | ||||||
|  |         MyModel = MagicMock() | ||||||
|  |         master.MasterView.model_class = MyModel | ||||||
|  |         self.assertIs(master.MasterView.get_model_class(), MyModel) | ||||||
|  |         del master.MasterView.model_class | ||||||
|  | 
 | ||||||
|  |     def test_get_model_name(self): | ||||||
|  |          | ||||||
|  |         # error by default (since no model class) | ||||||
|  |         self.assertRaises(AttributeError, master.MasterView.get_model_name) | ||||||
|  | 
 | ||||||
|  |         # subclass may specify model name | ||||||
|  |         master.MasterView.model_name = 'Widget' | ||||||
|  |         self.assertEqual(master.MasterView.get_model_name(), 'Widget') | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |         # or it may specify model class | ||||||
|  |         MyModel = MagicMock(__name__='Blaster') | ||||||
|  |         master.MasterView.model_class = MyModel | ||||||
|  |         self.assertEqual(master.MasterView.get_model_name(), 'Blaster') | ||||||
|  |         del master.MasterView.model_class | ||||||
|  | 
 | ||||||
|  |     def test_get_model_name_normalized(self): | ||||||
|  |          | ||||||
|  |         # error by default (since no model class) | ||||||
|  |         self.assertRaises(AttributeError, master.MasterView.get_model_name_normalized) | ||||||
|  | 
 | ||||||
|  |         # subclass may specify *normalized* model name | ||||||
|  |         master.MasterView.model_name_normalized = 'widget' | ||||||
|  |         self.assertEqual(master.MasterView.get_model_name_normalized(), 'widget') | ||||||
|  |         del master.MasterView.model_name_normalized | ||||||
|  | 
 | ||||||
|  |         # or it may specify *standard* model name | ||||||
|  |         master.MasterView.model_name = 'Blaster' | ||||||
|  |         self.assertEqual(master.MasterView.get_model_name_normalized(), 'blaster') | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |         # or it may specify model class | ||||||
|  |         MyModel = MagicMock(__name__='Dinosaur') | ||||||
|  |         master.MasterView.model_class = MyModel | ||||||
|  |         self.assertEqual(master.MasterView.get_model_name_normalized(), 'dinosaur') | ||||||
|  |         del master.MasterView.model_class | ||||||
|  | 
 | ||||||
|  |     def test_get_model_title(self): | ||||||
|  |          | ||||||
|  |         # error by default (since no model class) | ||||||
|  |         self.assertRaises(AttributeError, master.MasterView.get_model_title) | ||||||
|  | 
 | ||||||
|  |         # subclass may specify  model title | ||||||
|  |         master.MasterView.model_title = 'Wutta Widget' | ||||||
|  |         self.assertEqual(master.MasterView.get_model_title(), "Wutta Widget") | ||||||
|  |         del master.MasterView.model_title | ||||||
|  | 
 | ||||||
|  |         # or it may specify model name | ||||||
|  |         master.MasterView.model_name = 'Blaster' | ||||||
|  |         self.assertEqual(master.MasterView.get_model_title(), "Blaster") | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |         # or it may specify model class | ||||||
|  |         MyModel = MagicMock(__name__='Dinosaur') | ||||||
|  |         master.MasterView.model_class = MyModel | ||||||
|  |         self.assertEqual(master.MasterView.get_model_title(), "Dinosaur") | ||||||
|  |         del master.MasterView.model_class | ||||||
|  | 
 | ||||||
|  |     def test_get_model_title_plural(self): | ||||||
|  |          | ||||||
|  |         # error by default (since no model class) | ||||||
|  |         self.assertRaises(AttributeError, master.MasterView.get_model_title_plural) | ||||||
|  | 
 | ||||||
|  |         # subclass may specify *plural* model title | ||||||
|  |         master.MasterView.model_title_plural = 'People' | ||||||
|  |         self.assertEqual(master.MasterView.get_model_title_plural(), "People") | ||||||
|  |         del master.MasterView.model_title_plural | ||||||
|  | 
 | ||||||
|  |         # or it may specify *singular* model title | ||||||
|  |         master.MasterView.model_title = 'Wutta Widget' | ||||||
|  |         self.assertEqual(master.MasterView.get_model_title_plural(), "Wutta Widgets") | ||||||
|  |         del master.MasterView.model_title | ||||||
|  | 
 | ||||||
|  |         # or it may specify model name | ||||||
|  |         master.MasterView.model_name = 'Blaster' | ||||||
|  |         self.assertEqual(master.MasterView.get_model_title_plural(), "Blasters") | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |         # or it may specify model class | ||||||
|  |         MyModel = MagicMock(__name__='Dinosaur') | ||||||
|  |         master.MasterView.model_class = MyModel | ||||||
|  |         self.assertEqual(master.MasterView.get_model_title_plural(), "Dinosaurs") | ||||||
|  |         del master.MasterView.model_class | ||||||
|  | 
 | ||||||
|  |     def test_get_route_prefix(self): | ||||||
|  |          | ||||||
|  |         # error by default (since no model class) | ||||||
|  |         self.assertRaises(AttributeError, master.MasterView.get_route_prefix) | ||||||
|  | 
 | ||||||
|  |         # subclass may specify route prefix | ||||||
|  |         master.MasterView.route_prefix = 'widgets' | ||||||
|  |         self.assertEqual(master.MasterView.get_route_prefix(), 'widgets') | ||||||
|  |         del master.MasterView.route_prefix | ||||||
|  | 
 | ||||||
|  |         # subclass may specify *normalized* model name | ||||||
|  |         master.MasterView.model_name_normalized = 'blaster' | ||||||
|  |         self.assertEqual(master.MasterView.get_route_prefix(), 'blasters') | ||||||
|  |         del master.MasterView.model_name_normalized | ||||||
|  | 
 | ||||||
|  |         # or it may specify *standard* model name | ||||||
|  |         master.MasterView.model_name = 'Dinosaur' | ||||||
|  |         self.assertEqual(master.MasterView.get_route_prefix(), 'dinosaurs') | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |         # or it may specify model class | ||||||
|  |         MyModel = MagicMock(__name__='Truck') | ||||||
|  |         master.MasterView.model_class = MyModel | ||||||
|  |         self.assertEqual(master.MasterView.get_route_prefix(), 'trucks') | ||||||
|  |         del master.MasterView.model_class | ||||||
|  | 
 | ||||||
|  |     def test_get_url_prefix(self): | ||||||
|  |          | ||||||
|  |         # error by default (since no model class) | ||||||
|  |         self.assertRaises(AttributeError, master.MasterView.get_url_prefix) | ||||||
|  | 
 | ||||||
|  |         # subclass may specify url prefix | ||||||
|  |         master.MasterView.url_prefix = '/widgets' | ||||||
|  |         self.assertEqual(master.MasterView.get_url_prefix(), '/widgets') | ||||||
|  |         del master.MasterView.url_prefix | ||||||
|  | 
 | ||||||
|  |         # or it may specify route prefix | ||||||
|  |         master.MasterView.route_prefix = 'trucks' | ||||||
|  |         self.assertEqual(master.MasterView.get_url_prefix(), '/trucks') | ||||||
|  |         del master.MasterView.route_prefix | ||||||
|  | 
 | ||||||
|  |         # or it may specify *normalized* model name | ||||||
|  |         master.MasterView.model_name_normalized = 'blaster' | ||||||
|  |         self.assertEqual(master.MasterView.get_url_prefix(), '/blasters') | ||||||
|  |         del master.MasterView.model_name_normalized | ||||||
|  | 
 | ||||||
|  |         # or it may specify *standard* model name | ||||||
|  |         master.MasterView.model_name = 'Dinosaur' | ||||||
|  |         self.assertEqual(master.MasterView.get_url_prefix(), '/dinosaurs') | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |         # or it may specify model class | ||||||
|  |         MyModel = MagicMock(__name__='Machine') | ||||||
|  |         master.MasterView.model_class = MyModel | ||||||
|  |         self.assertEqual(master.MasterView.get_url_prefix(), '/machines') | ||||||
|  |         del master.MasterView.model_class | ||||||
|  | 
 | ||||||
|  |     def test_get_template_prefix(self): | ||||||
|  |          | ||||||
|  |         # error by default (since no model class) | ||||||
|  |         self.assertRaises(AttributeError, master.MasterView.get_template_prefix) | ||||||
|  | 
 | ||||||
|  |         # subclass may specify template prefix | ||||||
|  |         master.MasterView.template_prefix = '/widgets' | ||||||
|  |         self.assertEqual(master.MasterView.get_template_prefix(), '/widgets') | ||||||
|  |         del master.MasterView.template_prefix | ||||||
|  | 
 | ||||||
|  |         # or it may specify url prefix | ||||||
|  |         master.MasterView.url_prefix = '/trees' | ||||||
|  |         self.assertEqual(master.MasterView.get_template_prefix(), '/trees') | ||||||
|  |         del master.MasterView.url_prefix | ||||||
|  | 
 | ||||||
|  |         # or it may specify route prefix | ||||||
|  |         master.MasterView.route_prefix = 'trucks' | ||||||
|  |         self.assertEqual(master.MasterView.get_template_prefix(), '/trucks') | ||||||
|  |         del master.MasterView.route_prefix | ||||||
|  | 
 | ||||||
|  |         # or it may specify *normalized* model name | ||||||
|  |         master.MasterView.model_name_normalized = 'blaster' | ||||||
|  |         self.assertEqual(master.MasterView.get_template_prefix(), '/blasters') | ||||||
|  |         del master.MasterView.model_name_normalized | ||||||
|  | 
 | ||||||
|  |         # or it may specify *standard* model name | ||||||
|  |         master.MasterView.model_name = 'Dinosaur' | ||||||
|  |         self.assertEqual(master.MasterView.get_template_prefix(), '/dinosaurs') | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |         # or it may specify model class | ||||||
|  |         MyModel = MagicMock(__name__='Machine') | ||||||
|  |         master.MasterView.model_class = MyModel | ||||||
|  |         self.assertEqual(master.MasterView.get_template_prefix(), '/machines') | ||||||
|  |         del master.MasterView.model_class | ||||||
|  | 
 | ||||||
|  |     ############################## | ||||||
|  |     # support methods | ||||||
|  |     ############################## | ||||||
|  | 
 | ||||||
|  |     def test_get_index_title(self): | ||||||
|  |         master.MasterView.model_title_plural = "Wutta Widgets" | ||||||
|  |         view = master.MasterView(self.request) | ||||||
|  |         self.assertEqual(view.get_index_title(), "Wutta Widgets") | ||||||
|  |         del master.MasterView.model_title_plural | ||||||
|  | 
 | ||||||
|  |     def test_render_to_response(self): | ||||||
|  | 
 | ||||||
|  |         # basic sanity check using /master/index.mako | ||||||
|  |         # (nb. it skips /widgets/index.mako since that doesn't exist) | ||||||
|  |         master.MasterView.model_name = 'Widget' | ||||||
|  |         view = master.MasterView(self.request) | ||||||
|  |         response = view.render_to_response('index', {}) | ||||||
|  |         self.assertIsInstance(response, Response) | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |         # basic sanity check using /appinfo/index.mako | ||||||
|  |         master.MasterView.model_name = 'AppInfo' | ||||||
|  |         master.MasterView.template_prefix = '/appinfo' | ||||||
|  |         view = master.MasterView(self.request) | ||||||
|  |         response = view.render_to_response('index', {}) | ||||||
|  |         self.assertIsInstance(response, Response) | ||||||
|  |         del master.MasterView.model_name | ||||||
|  |         del master.MasterView.template_prefix | ||||||
|  | 
 | ||||||
|  |         # bad template name causes error | ||||||
|  |         master.MasterView.model_name = 'Widget' | ||||||
|  |         self.assertRaises(IOError, view.render_to_response, 'nonexistent', {}) | ||||||
|  |         del master.MasterView.model_name | ||||||
|  | 
 | ||||||
|  |     ############################## | ||||||
|  |     # view methods | ||||||
|  |     ############################## | ||||||
|  | 
 | ||||||
|  |     def test_index(self): | ||||||
|  |          | ||||||
|  |         # basic sanity check using /appinfo | ||||||
|  |         master.MasterView.model_name = 'AppInfo' | ||||||
|  |         master.MasterView.template_prefix = '/appinfo' | ||||||
|  |         view = master.MasterView(self.request) | ||||||
|  |         response = view.index() | ||||||
|  |         del master.MasterView.model_name | ||||||
|  |         del master.MasterView.template_prefix | ||||||
							
								
								
									
										13
									
								
								tests/views/test_settings.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/views/test_settings.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | # -*- coding: utf-8; -*- | ||||||
|  | 
 | ||||||
|  | from tests.views.utils import WebTestCase | ||||||
|  | 
 | ||||||
|  | from wuttaweb.views import settings | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestAppInfoView(WebTestCase): | ||||||
|  | 
 | ||||||
|  |     def test_index(self): | ||||||
|  |         # just a sanity check | ||||||
|  |         view = settings.AppInfoView(self.request) | ||||||
|  |         response = view.index() | ||||||
							
								
								
									
										57
									
								
								tests/views/utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								tests/views/utils.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | ||||||
|  | # -*- coding: utf-8; -*- | ||||||
|  | 
 | ||||||
|  | from unittest import TestCase | ||||||
|  | from unittest.mock import MagicMock | ||||||
|  | 
 | ||||||
|  | from pyramid import testing | ||||||
|  | 
 | ||||||
|  | from wuttjamaican.conf import WuttaConfig | ||||||
|  | from wuttaweb import subscribers | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class WebTestCase(TestCase): | ||||||
|  |     """ | ||||||
|  |     Base class for test suites requiring a full (typical) web app. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def setUp(self): | ||||||
|  |         self.setup_web() | ||||||
|  | 
 | ||||||
|  |     def setup_web(self): | ||||||
|  |         self.config = WuttaConfig(defaults={ | ||||||
|  |             'wutta.db.default.url': 'sqlite://', | ||||||
|  |             'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler', | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         self.request = testing.DummyRequest() | ||||||
|  | 
 | ||||||
|  |         self.pyramid_config = testing.setUp(request=self.request, settings={ | ||||||
|  |             'wutta_config': self.config, | ||||||
|  |             'mako.directories': ['wuttaweb:templates'], | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         # init db | ||||||
|  |         self.app = self.config.get_app() | ||||||
|  |         model = self.app.model | ||||||
|  |         model.Base.metadata.create_all(bind=self.config.appdb_engine) | ||||||
|  |         self.session = self.app.make_session() | ||||||
|  | 
 | ||||||
|  |         # init web | ||||||
|  |         self.pyramid_config.include('pyramid_mako') | ||||||
|  |         self.pyramid_config.include('wuttaweb.static') | ||||||
|  |         self.pyramid_config.include('wuttaweb.views.essential') | ||||||
|  |         self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render', | ||||||
|  |                                            'pyramid.events.BeforeRender') | ||||||
|  | 
 | ||||||
|  |         # setup new request w/ anonymous user | ||||||
|  |         event = MagicMock(request=self.request) | ||||||
|  |         subscribers.new_request(event) | ||||||
|  |         def user_getter(request, **kwargs): pass | ||||||
|  |         subscribers.new_request_set_user(event, db_session=self.session, | ||||||
|  |                                          user_getter=user_getter) | ||||||
|  | 
 | ||||||
|  |     def tearDown(self): | ||||||
|  |         self.teardown_web() | ||||||
|  | 
 | ||||||
|  |     def teardown_web(self): | ||||||
|  |         testing.tearDown() | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue