feat: add basic configure view for appinfo
This commit is contained in:
		
							parent
							
								
									dd207a4a05
								
							
						
					
					
						commit
						ed67cdb2d8
					
				
					 15 changed files with 847 additions and 42 deletions
				
			
		| 
						 | 
					@ -110,7 +110,11 @@ def make_pyramid_config(settings):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    The config is initialized with certain features deemed useful for
 | 
					    The config is initialized with certain features deemed useful for
 | 
				
			||||||
    all apps.
 | 
					    all apps.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :returns: Instance of
 | 
				
			||||||
 | 
					       :class:`pyramid:pyramid.config.Configurator`.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    settings.setdefault('mako.directories', ['wuttaweb:templates'])
 | 
				
			||||||
    settings.setdefault('pyramid_deform.template_search_path',
 | 
					    settings.setdefault('pyramid_deform.template_search_path',
 | 
				
			||||||
                        'wuttaweb:templates/deform')
 | 
					                        'wuttaweb:templates/deform')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -119,6 +123,11 @@ def make_pyramid_config(settings):
 | 
				
			||||||
    # configure user authorization / authentication
 | 
					    # configure user authorization / authentication
 | 
				
			||||||
    pyramid_config.set_security_policy(WuttaSecurityPolicy())
 | 
					    pyramid_config.set_security_policy(WuttaSecurityPolicy())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # require CSRF token for POST
 | 
				
			||||||
 | 
					    pyramid_config.set_default_csrf_options(require_csrf=True,
 | 
				
			||||||
 | 
					                                            token='_csrf',
 | 
				
			||||||
 | 
					                                            header='X-CSRF-TOKEN')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pyramid_config.include('pyramid_beaker')
 | 
					    pyramid_config.include('pyramid_beaker')
 | 
				
			||||||
    pyramid_config.include('pyramid_deform')
 | 
					    pyramid_config.include('pyramid_deform')
 | 
				
			||||||
    pyramid_config.include('pyramid_mako')
 | 
					    pyramid_config.include('pyramid_mako')
 | 
				
			||||||
| 
						 | 
					@ -143,8 +152,6 @@ def main(global_config, **settings):
 | 
				
			||||||
    will need to define their own ``main()`` function, and use that
 | 
					    will need to define their own ``main()`` function, and use that
 | 
				
			||||||
    instead.
 | 
					    instead.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    settings.setdefault('mako.directories', ['wuttaweb:templates'])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    wutta_config = make_wutta_config(settings)
 | 
					    wutta_config = make_wutta_config(settings)
 | 
				
			||||||
    pyramid_config = make_pyramid_config(settings)
 | 
					    pyramid_config = make_pyramid_config(settings)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -323,6 +323,7 @@ class Form:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        context['form'] = self
 | 
					        context['form'] = self
 | 
				
			||||||
        context.setdefault('form_attrs', {})
 | 
					        context.setdefault('form_attrs', {})
 | 
				
			||||||
 | 
					        context.setdefault('request', self.request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # auto disable button on submit
 | 
					        # auto disable button on submit
 | 
				
			||||||
        if self.auto_disable_submit:
 | 
					        if self.auto_disable_submit:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,12 +38,20 @@ instance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This module contains the following references:
 | 
					This module contains the following references:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* :func:`~wuttaweb.util.get_liburl()`
 | 
					 | 
				
			||||||
* all names from :mod:`webhelpers2:webhelpers2.html`
 | 
					* all names from :mod:`webhelpers2:webhelpers2.html`
 | 
				
			||||||
* all names from :mod:`webhelpers2:webhelpers2.html.tags`
 | 
					* all names from :mod:`webhelpers2:webhelpers2.html.tags`
 | 
				
			||||||
 | 
					* :func:`~wuttaweb.util.get_liburl()`
 | 
				
			||||||
 | 
					* :func:`~wuttaweb.util.get_csrf_token()`
 | 
				
			||||||
 | 
					* :func:`~wuttaweb.util.render_csrf_token()` (as :func:`csrf_token()`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. function:: csrf_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   This is a shorthand reference to
 | 
				
			||||||
 | 
					   :func:`wuttaweb.util.render_csrf_token()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from webhelpers2.html import *
 | 
					from webhelpers2.html import *
 | 
				
			||||||
from webhelpers2.html.tags import *
 | 
					from webhelpers2.html.tags import *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb.util import get_liburl
 | 
					from wuttaweb.util import get_liburl, get_csrf_token, render_csrf_token as csrf_token
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/wuttaweb/templates/appinfo/configure.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/wuttaweb/templates/appinfo/configure.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					<%inherit file="/configure.mako" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="form_content()">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <h3 class="block is-size-3">Basics</h3>
 | 
				
			||||||
 | 
					  <div class="block" style="padding-left: 2rem; width: 50%;">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <b-field label="App Title">
 | 
				
			||||||
 | 
					      <b-input name="${app.appname}.app_title"
 | 
				
			||||||
 | 
					               v-model="simpleSettings['${app.appname}.app_title']"
 | 
				
			||||||
 | 
					               @input="settingsNeedSaved = true">
 | 
				
			||||||
 | 
					      </b-input>
 | 
				
			||||||
 | 
					    </b-field>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${parent.body()}
 | 
				
			||||||
| 
						 | 
					@ -209,16 +209,14 @@
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </nav>
 | 
					          </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <nav class="level" style="margin: 0.5rem auto;">
 | 
					          <nav class="level" style="margin: 0.5rem 0.5rem 0.5rem auto;">
 | 
				
			||||||
            <div class="level-left">
 | 
					            <div class="level-left">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              ## Current Context
 | 
					              ## Current Context
 | 
				
			||||||
              <div id="current-context" class="level-item">
 | 
					              <div id="current-context" class="level-item">
 | 
				
			||||||
                % if index_title:
 | 
					                % if index_title:
 | 
				
			||||||
                    % if index_url:
 | 
					                    % if index_url:
 | 
				
			||||||
                        <span class="header-text">
 | 
					                        <h1 class="title">${h.link_to(index_title, index_url)}</h1>
 | 
				
			||||||
                          ${h.link_to(index_title, index_url)}
 | 
					 | 
				
			||||||
                        </span>
 | 
					 | 
				
			||||||
                    % else:
 | 
					                    % else:
 | 
				
			||||||
                        <h1 class="title">${index_title}</h1>
 | 
					                        <h1 class="title">${index_title}</h1>
 | 
				
			||||||
                    % endif
 | 
					                    % endif
 | 
				
			||||||
| 
						 | 
					@ -226,6 +224,23 @@
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            </div><!-- level-left -->
 | 
					            </div><!-- level-left -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="level-right">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              ## TODO
 | 
				
			||||||
 | 
					              % if master and master.configurable and not master.configuring:
 | 
				
			||||||
 | 
					                  <div class="level-item">
 | 
				
			||||||
 | 
					                    <b-button type="is-primary"
 | 
				
			||||||
 | 
					                              tag="a"
 | 
				
			||||||
 | 
					                              href="${url(f'{route_prefix}.configure')}"
 | 
				
			||||||
 | 
					                              icon-pack="fas"
 | 
				
			||||||
 | 
					                              icon-left="cog">
 | 
				
			||||||
 | 
					                      Configure
 | 
				
			||||||
 | 
					                    </b-button>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					              % endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            </div> <!-- level-right -->
 | 
				
			||||||
          </nav><!-- level -->
 | 
					          </nav><!-- level -->
 | 
				
			||||||
        </header>
 | 
					        </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -318,8 +333,7 @@
 | 
				
			||||||
        <div class="navbar-dropdown">
 | 
					        <div class="navbar-dropdown">
 | 
				
			||||||
          % if request.is_root:
 | 
					          % if request.is_root:
 | 
				
			||||||
              ${h.form(url('stop_root'), ref='stopBeingRootForm')}
 | 
					              ${h.form(url('stop_root'), ref='stopBeingRootForm')}
 | 
				
			||||||
              ## TODO
 | 
					              ${h.csrf_token(request)}
 | 
				
			||||||
              ## ${h.csrf_token(request)}
 | 
					 | 
				
			||||||
              <input type="hidden" name="referrer" value="${request.current_route_url()}" />
 | 
					              <input type="hidden" name="referrer" value="${request.current_route_url()}" />
 | 
				
			||||||
              <a @click="stopBeingRoot()"
 | 
					              <a @click="stopBeingRoot()"
 | 
				
			||||||
                 class="navbar-item has-background-danger has-text-white">
 | 
					                 class="navbar-item has-background-danger has-text-white">
 | 
				
			||||||
| 
						 | 
					@ -328,8 +342,7 @@
 | 
				
			||||||
              ${h.end_form()}
 | 
					              ${h.end_form()}
 | 
				
			||||||
          % elif request.is_admin:
 | 
					          % elif request.is_admin:
 | 
				
			||||||
              ${h.form(url('become_root'), ref='startBeingRootForm')}
 | 
					              ${h.form(url('become_root'), ref='startBeingRootForm')}
 | 
				
			||||||
              ## TODO
 | 
					              ${h.csrf_token(request)}
 | 
				
			||||||
              ## ${h.csrf_token(request)}
 | 
					 | 
				
			||||||
              <input type="hidden" name="referrer" value="${request.current_route_url()}" />
 | 
					              <input type="hidden" name="referrer" value="${request.current_route_url()}" />
 | 
				
			||||||
              <a @click="startBeingRoot()"
 | 
					              <a @click="startBeingRoot()"
 | 
				
			||||||
                 class="navbar-item has-background-danger has-text-white">
 | 
					                 class="navbar-item has-background-danger has-text-white">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										181
									
								
								src/wuttaweb/templates/configure.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								src/wuttaweb/templates/configure.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,181 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					<%inherit file="/page.mako" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="title()">Configure ${config_title}</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="page_content()">
 | 
				
			||||||
 | 
					  <br />
 | 
				
			||||||
 | 
					  ${self.buttons_content()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ${h.form(request.current_route_url(), enctype='multipart/form-data', ref='saveSettingsForm', **{'@submit': 'saveSettingsFormSubmit'})}
 | 
				
			||||||
 | 
					  ${h.csrf_token(request)}
 | 
				
			||||||
 | 
					  ${self.form_content()}
 | 
				
			||||||
 | 
					  ${h.end_form()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <b-modal has-modal-card
 | 
				
			||||||
 | 
					           :active.sync="purgeSettingsShowDialog">
 | 
				
			||||||
 | 
					    <div class="modal-card">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <header class="modal-card-head">
 | 
				
			||||||
 | 
					        <p class="modal-card-title">Remove All Settings</p>
 | 
				
			||||||
 | 
					      </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <section class="modal-card-body">
 | 
				
			||||||
 | 
					        <p class="block">
 | 
				
			||||||
 | 
					          Really remove all settings for ${config_title} from the DB?
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					        <p class="block">
 | 
				
			||||||
 | 
					          Note that when you <span class="is-italic">save</span>
 | 
				
			||||||
 | 
					          settings, any existing settings are first removed and then
 | 
				
			||||||
 | 
					          new ones are saved.
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					        <p class="block">
 | 
				
			||||||
 | 
					          But here you can remove existing without saving new
 | 
				
			||||||
 | 
					          ones.  It is basically "factory reset" for
 | 
				
			||||||
 | 
					          ${config_title}.
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					      </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <footer class="modal-card-foot">
 | 
				
			||||||
 | 
					        <b-button @click="purgeSettingsShowDialog = false">
 | 
				
			||||||
 | 
					          Cancel
 | 
				
			||||||
 | 
					        </b-button>
 | 
				
			||||||
 | 
					        ${h.form(request.current_route_url())}
 | 
				
			||||||
 | 
					        ${h.csrf_token(request)}
 | 
				
			||||||
 | 
					        ${h.hidden('remove_settings', 'true')}
 | 
				
			||||||
 | 
					        <b-button type="is-danger"
 | 
				
			||||||
 | 
					                  native-type="submit"
 | 
				
			||||||
 | 
					                  :disabled="purgingSettings"
 | 
				
			||||||
 | 
					                  icon-pack="fas"
 | 
				
			||||||
 | 
					                  icon-left="trash"
 | 
				
			||||||
 | 
					                  @click="purgingSettings = true">
 | 
				
			||||||
 | 
					          {{ purgingSettings ? "Working, please wait..." : "Remove All Settings for ${config_title}" }}
 | 
				
			||||||
 | 
					        </b-button>
 | 
				
			||||||
 | 
					        ${h.end_form()}
 | 
				
			||||||
 | 
					      </footer>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </b-modal>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="buttons_content()">
 | 
				
			||||||
 | 
					  <div class="level">
 | 
				
			||||||
 | 
					    <div class="level-left">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="level-item">
 | 
				
			||||||
 | 
					        ${self.intro_message()}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="level-item">
 | 
				
			||||||
 | 
					        ${self.save_undo_buttons()}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="level-right">
 | 
				
			||||||
 | 
					      <div class="level-item">
 | 
				
			||||||
 | 
					        ${self.purge_button()}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="intro_message()">
 | 
				
			||||||
 | 
					  <p class="block">
 | 
				
			||||||
 | 
					    This page lets you modify the settings for ${config_title}.
 | 
				
			||||||
 | 
					  </p>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="save_undo_buttons()">
 | 
				
			||||||
 | 
					  <div class="buttons"
 | 
				
			||||||
 | 
					       v-if="settingsNeedSaved">
 | 
				
			||||||
 | 
					    <b-button type="is-primary"
 | 
				
			||||||
 | 
					              @click="saveSettings"
 | 
				
			||||||
 | 
					              :disabled="savingSettings"
 | 
				
			||||||
 | 
					              icon-pack="fas"
 | 
				
			||||||
 | 
					              icon-left="save">
 | 
				
			||||||
 | 
					      {{ savingSettings ? "Working, please wait..." : "Save All Settings" }}
 | 
				
			||||||
 | 
					    </b-button>
 | 
				
			||||||
 | 
					    <b-button tag="a" href="${request.current_route_url()}"
 | 
				
			||||||
 | 
					              icon-pack="fas"
 | 
				
			||||||
 | 
					              icon-left="undo"
 | 
				
			||||||
 | 
					              @click="undoChanges = true"
 | 
				
			||||||
 | 
					              :disabled="undoChanges">
 | 
				
			||||||
 | 
					      {{ undoChanges ? "Working, please wait..." : "Undo All Changes" }}
 | 
				
			||||||
 | 
					    </b-button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="purge_button()">
 | 
				
			||||||
 | 
					  <b-button type="is-danger"
 | 
				
			||||||
 | 
					            @click="purgeSettingsShowDialog = true"
 | 
				
			||||||
 | 
					            icon-pack="fas"
 | 
				
			||||||
 | 
					            icon-left="trash">
 | 
				
			||||||
 | 
					    Remove All Settings
 | 
				
			||||||
 | 
					  </b-button>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="form_content()">
 | 
				
			||||||
 | 
					  <b-notification type="is-warning"
 | 
				
			||||||
 | 
					                  :closable="false">
 | 
				
			||||||
 | 
					    <h4 class="block is-size-4">
 | 
				
			||||||
 | 
					      TODO: you must define the
 | 
				
			||||||
 | 
					      <span class="is-family-monospace"><%def name="form_content()"></span>
 | 
				
			||||||
 | 
					      template block
 | 
				
			||||||
 | 
					    </h4>
 | 
				
			||||||
 | 
					    <p class="block">
 | 
				
			||||||
 | 
					      or if you need more control, define the
 | 
				
			||||||
 | 
					      <span class="is-family-monospace"><%def name="page_content()"></span>
 | 
				
			||||||
 | 
					      template block
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					    <p class="block">
 | 
				
			||||||
 | 
					      for a real-world example see template at
 | 
				
			||||||
 | 
					      <span class="is-family-monospace">wuttaweb:templates/appinfo/configure.mako</span>
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </b-notification>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="modify_this_page_vars()">
 | 
				
			||||||
 | 
					  ${parent.modify_this_page_vars()}
 | 
				
			||||||
 | 
					  <script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    % if simple_settings is not Undefined:
 | 
				
			||||||
 | 
					        ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n}
 | 
				
			||||||
 | 
					    % endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ThisPageData.purgeSettingsShowDialog = false
 | 
				
			||||||
 | 
					    ThisPageData.purgingSettings = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ThisPageData.settingsNeedSaved = false
 | 
				
			||||||
 | 
					    ThisPageData.undoChanges = false
 | 
				
			||||||
 | 
					    ThisPageData.savingSettings = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ThisPage.methods.saveSettings = function() {
 | 
				
			||||||
 | 
					        this.savingSettings = true
 | 
				
			||||||
 | 
					        this.$refs.saveSettingsForm.submit()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // nb. this is here to avoid auto-submitting form when user
 | 
				
			||||||
 | 
					    // presses ENTER while some random input field has focus
 | 
				
			||||||
 | 
					    ThisPage.methods.saveSettingsFormSubmit = function(event) {
 | 
				
			||||||
 | 
					        if (!this.savingSettings) {
 | 
				
			||||||
 | 
					            event.preventDefault()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // cf. https://stackoverflow.com/a/56551646
 | 
				
			||||||
 | 
					    ThisPage.methods.beforeWindowUnload = function(e) {
 | 
				
			||||||
 | 
					        if (this.settingsNeedSaved && !this.savingSettings && !this.undoChanges) {
 | 
				
			||||||
 | 
					            e.preventDefault()
 | 
				
			||||||
 | 
					            e.returnValue = ''
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ThisPage.created = function() {
 | 
				
			||||||
 | 
					        window.addEventListener('beforeunload', this.beforeWindowUnload)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${parent.body()}
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script type="text/x-template" id="${form.vue_tagname}-template">
 | 
					<script type="text/x-template" id="${form.vue_tagname}-template">
 | 
				
			||||||
  ${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
 | 
					  ${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
 | 
				
			||||||
 | 
					    ${h.csrf_token(request)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <section>
 | 
					    <section>
 | 
				
			||||||
      % for fieldname in form:
 | 
					      % for fieldname in form:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/wuttaweb/templates/master/configure.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/wuttaweb/templates/master/configure.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					<%inherit file="/configure.mako" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## NB. /master/configure.mako is only a placeholder.
 | 
				
			||||||
 | 
					## there is no reason to *inherit* from this template;
 | 
				
			||||||
 | 
					## you can always just inherit from /configure.mako
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${parent.body()}
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,8 @@ Web Utilities
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import importlib
 | 
					import importlib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from webhelpers2.html import HTML, tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_form_data(request):
 | 
					def get_form_data(request):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
| 
						 | 
					@ -257,3 +259,44 @@ def get_liburl(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    elif key == 'bb_vue_fontawesome':
 | 
					    elif key == 'bb_vue_fontawesome':
 | 
				
			||||||
        return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm'
 | 
					        return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_csrf_token(request):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function, returns the effective CSRF token (raw
 | 
				
			||||||
 | 
					    string) for the given request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    See also :func:`render_csrf_token()`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    token = request.session.get_csrf_token()
 | 
				
			||||||
 | 
					    if token is None:
 | 
				
			||||||
 | 
					        token = request.session.new_csrf_token()
 | 
				
			||||||
 | 
					    return token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def render_csrf_token(request, name='_csrf'):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function, returns CSRF hidden input inside hidden div,
 | 
				
			||||||
 | 
					    e.g.:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. code-block:: html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       <div style="display: none;">
 | 
				
			||||||
 | 
					          <input type="hidden" name="_csrf" value="TOKEN" />
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This function is part of :mod:`wuttaweb.helpers` (as
 | 
				
			||||||
 | 
					    :func:`~wuttaweb.helpers.csrf_token()`) which means you can do
 | 
				
			||||||
 | 
					    this in page templates:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. code-block:: mako
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       ${h.form(request.current_route_url())}
 | 
				
			||||||
 | 
					       ${h.csrf_token(request)}
 | 
				
			||||||
 | 
					       <!-- other fields etc. -->
 | 
				
			||||||
 | 
					       ${h.end_form()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    See also :func:`get_csrf_token()`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    token = get_csrf_token(request)
 | 
				
			||||||
 | 
					    return HTML.tag('div', tags.hidden(name, value=token), style='display:none;')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,8 @@ Base Logic for Master Views
 | 
				
			||||||
from pyramid.renderers import render_to_response
 | 
					from pyramid.renderers import render_to_response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb.views import View
 | 
					from wuttaweb.views import View
 | 
				
			||||||
 | 
					from wuttaweb.util import get_form_data
 | 
				
			||||||
 | 
					from wuttaweb.db import Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MasterView(View):
 | 
					class MasterView(View):
 | 
				
			||||||
| 
						 | 
					@ -98,6 +100,14 @@ class MasterView(View):
 | 
				
			||||||
       Code should not access this directly but instead call
 | 
					       Code should not access this directly but instead call
 | 
				
			||||||
       :meth:`get_model_title_plural()`.
 | 
					       :meth:`get_model_title_plural()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: config_title
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Optional override for the view's "config" title, e.g. ``"Wutta
 | 
				
			||||||
 | 
					       Widgets"`` (to be displayed as **Configure Wutta Widgets**).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Code should not access this directly but instead call
 | 
				
			||||||
 | 
					       :meth:`get_config_title()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. attribute:: route_prefix
 | 
					    .. attribute:: route_prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       Optional override for the view's route prefix,
 | 
					       Optional override for the view's route prefix,
 | 
				
			||||||
| 
						 | 
					@ -125,17 +135,29 @@ class MasterView(View):
 | 
				
			||||||
    .. attribute:: listable
 | 
					    .. attribute:: listable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
       Boolean indicating whether the view model supports "listing" -
 | 
					       Boolean indicating whether the view model supports "listing" -
 | 
				
			||||||
       i.e. it should have an :meth:`index()` view.
 | 
					       i.e. it should have an :meth:`index()` view.  Default value is
 | 
				
			||||||
 | 
					       ``True``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: configurable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Boolean indicating whether the master view supports
 | 
				
			||||||
 | 
					       "configuring" - i.e. it should have a :meth:`configure()` view.
 | 
				
			||||||
 | 
					       Default value is ``False``.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # attributes
 | 
					    # attributes
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # features
 | 
				
			||||||
    listable = True
 | 
					    listable = True
 | 
				
			||||||
 | 
					    configurable = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # current action
 | 
				
			||||||
 | 
					    configuring = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # view methods
 | 
					    # index methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def index(self):
 | 
					    def index(self):
 | 
				
			||||||
| 
						 | 
					@ -145,8 +167,304 @@ class MasterView(View):
 | 
				
			||||||
        This is the "default" view for the model and is what user sees
 | 
					        This is the "default" view for the model and is what user sees
 | 
				
			||||||
        when visiting the "root" path under the :attr:`url_prefix`,
 | 
					        when visiting the "root" path under the :attr:`url_prefix`,
 | 
				
			||||||
        e.g. ``/widgets/``.
 | 
					        e.g. ``/widgets/``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        By default, this view is included only if :attr:`listable` is
 | 
				
			||||||
 | 
					        true.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.render_to_response('index', {})
 | 
					        context = {
 | 
				
			||||||
 | 
					            'index_url': None,  # avoid title link since this *is* the index
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return self.render_to_response('index', context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ##############################
 | 
				
			||||||
 | 
					    # configure methods
 | 
				
			||||||
 | 
					    ##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        View for configuring aspects of the app which are pertinent to
 | 
				
			||||||
 | 
					        this master view and/or model.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        By default, this view is included only if :attr:`configurable`
 | 
				
			||||||
 | 
					        is true.  It usually maps to a URL like ``/widgets/configure``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The expected workflow is as follows:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * user navigates to Configure page
 | 
				
			||||||
 | 
					        * user modifies settings and clicks Save
 | 
				
			||||||
 | 
					        * this view then *deletes* all "known" settings
 | 
				
			||||||
 | 
					        * then it saves user-submitted settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        That is unless ``remove_settings`` is requested, in which case
 | 
				
			||||||
 | 
					        settings are deleted but then none are saved.  The "known"
 | 
				
			||||||
 | 
					        settings by default include only the "simple" settings.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        As a general rule, a particular setting should be configurable
 | 
				
			||||||
 | 
					        by (at most) one master view.  Some settings may never be
 | 
				
			||||||
 | 
					        exposed at all.  But when exposing a setting, careful thought
 | 
				
			||||||
 | 
					        should be given to where it logically/best belongs.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Some settings are "simple" and a master view subclass need
 | 
				
			||||||
 | 
					        only provide their basic definitions via
 | 
				
			||||||
 | 
					        :meth:`configure_get_simple_settings()`.  If complex settings
 | 
				
			||||||
 | 
					        are needed, subclass must override one or more other methods
 | 
				
			||||||
 | 
					        to achieve the aim(s).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See also related methods, used by this one:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * :meth:`configure_get_simple_settings()`
 | 
				
			||||||
 | 
					        * :meth:`configure_get_context()`
 | 
				
			||||||
 | 
					        * :meth:`configure_gather_settings()`
 | 
				
			||||||
 | 
					        * :meth:`configure_remove_settings()`
 | 
				
			||||||
 | 
					        * :meth:`configure_save_settings()`
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.configuring = True
 | 
				
			||||||
 | 
					        config_title = self.get_config_title()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # was form submitted?
 | 
				
			||||||
 | 
					        if self.request.method == 'POST':
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # maybe just remove settings
 | 
				
			||||||
 | 
					            if self.request.POST.get('remove_settings'):
 | 
				
			||||||
 | 
					                self.configure_remove_settings()
 | 
				
			||||||
 | 
					                self.request.session.flash(f"All settings for {config_title} have been removed.",
 | 
				
			||||||
 | 
					                                           'warning')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # reload configure page
 | 
				
			||||||
 | 
					                return self.redirect(self.request.current_route_url())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # gather/save settings
 | 
				
			||||||
 | 
					            data = get_form_data(self.request)
 | 
				
			||||||
 | 
					            settings = self.configure_gather_settings(data)
 | 
				
			||||||
 | 
					            self.configure_remove_settings()
 | 
				
			||||||
 | 
					            self.configure_save_settings(settings)
 | 
				
			||||||
 | 
					            self.request.session.flash("Settings have been saved.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # reload configure page
 | 
				
			||||||
 | 
					            return self.redirect(self.request.current_route_url())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # render configure page
 | 
				
			||||||
 | 
					        context = self.configure_get_context()
 | 
				
			||||||
 | 
					        return self.render_to_response('configure', context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure_get_context(
 | 
				
			||||||
 | 
					            self,
 | 
				
			||||||
 | 
					            simple_settings=None,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the full context dict, for rendering the
 | 
				
			||||||
 | 
					        :meth:`configure()` page template.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Default context will include ``simple_settings`` (normalized
 | 
				
			||||||
 | 
					        to just name/value).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        You may need to override this method, to add additional
 | 
				
			||||||
 | 
					        "complex" settings etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param simple_settings: Optional list of simple settings, if
 | 
				
			||||||
 | 
					           already initialized.  Otherwise it is retrieved via
 | 
				
			||||||
 | 
					           :meth:`configure_get_simple_settings()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :returns: Context dict for the page template.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        context = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # simple settings
 | 
				
			||||||
 | 
					        if simple_settings is None:
 | 
				
			||||||
 | 
					            simple_settings = self.configure_get_simple_settings()
 | 
				
			||||||
 | 
					        if simple_settings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # we got some, so "normalize" each definition to name/value
 | 
				
			||||||
 | 
					            normalized = {}
 | 
				
			||||||
 | 
					            for simple in simple_settings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # name
 | 
				
			||||||
 | 
					                name = simple['name']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # value
 | 
				
			||||||
 | 
					                if 'value' in simple:
 | 
				
			||||||
 | 
					                    value = simple['value']
 | 
				
			||||||
 | 
					                elif simple.get('type') is bool:
 | 
				
			||||||
 | 
					                    value = self.config.get_bool(name, default=simple.get('default', False))
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    value = self.config.get(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                normalized[name] = value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # add to template context
 | 
				
			||||||
 | 
					            context['simple_settings'] = normalized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure_get_simple_settings(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        This should return a list of "simple" setting definitions for
 | 
				
			||||||
 | 
					        the :meth:`configure()` view, which can be handled in a more
 | 
				
			||||||
 | 
					        automatic way.  (This is as opposed to some settings which are
 | 
				
			||||||
 | 
					        more complex and must be handled manually; those should not be
 | 
				
			||||||
 | 
					        part of this method's return value.)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Basically a "simple" setting is one which can be represented
 | 
				
			||||||
 | 
					        by a single field/widget on the Configure page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The setting definitions returned must each be a dict of
 | 
				
			||||||
 | 
					        "attributes" for the setting.  For instance a *very* simple
 | 
				
			||||||
 | 
					        setting might be::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           {'name': 'wutta.app_title'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The ``name`` is required, everything else is optional.  Here
 | 
				
			||||||
 | 
					        is a more complete example::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           {
 | 
				
			||||||
 | 
					               'name': 'wutta.production',
 | 
				
			||||||
 | 
					               'type': bool,
 | 
				
			||||||
 | 
					               'default': False,
 | 
				
			||||||
 | 
					               'save_if_empty': False,
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Note that if specified, the ``default`` should be of the same
 | 
				
			||||||
 | 
					        data type as defined for the setting (``bool`` in the above
 | 
				
			||||||
 | 
					        example).  The default ``type`` is ``str``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Normally if a setting's value is effectively null, the setting
 | 
				
			||||||
 | 
					        is removed instead of keeping it in the DB.  This behavior can
 | 
				
			||||||
 | 
					        be changed per-setting via the ``save_if_empty`` flag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :returns: List of setting definition dicts as described above.
 | 
				
			||||||
 | 
					           Note that their order does not matter since the template
 | 
				
			||||||
 | 
					           must explicitly define field layout etc.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure_gather_settings(
 | 
				
			||||||
 | 
					            self,
 | 
				
			||||||
 | 
					            data,
 | 
				
			||||||
 | 
					            simple_settings=None,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Collect the full set of "normalized" settings from user
 | 
				
			||||||
 | 
					        request, so that :meth:`configure()` can save them.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Settings are gathered from the given request (e.g. POST)
 | 
				
			||||||
 | 
					        ``data``, but also taking into account what we know based on
 | 
				
			||||||
 | 
					        the simple setting definitions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Subclass may need to override this method if complex settings
 | 
				
			||||||
 | 
					        are required.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param data: Form data submitted via POST request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param simple_settings: Optional list of simple settings, if
 | 
				
			||||||
 | 
					           already initialized.  Otherwise it is retrieved via
 | 
				
			||||||
 | 
					           :meth:`configure_get_simple_settings()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This method must return a list of normalized settings, similar
 | 
				
			||||||
 | 
					        in spirit to the definition syntax used in
 | 
				
			||||||
 | 
					        :meth:`configure_get_simple_settings()`.  However the format
 | 
				
			||||||
 | 
					        returned here is minimal and contains just name/value::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					           {
 | 
				
			||||||
 | 
					               'name': 'wutta.app_title',
 | 
				
			||||||
 | 
					               'value': 'Wutta Wutta',
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Note that the ``value`` will always be a string.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Also note, whereas it's possible ``data`` will not contain all
 | 
				
			||||||
 | 
					        known settings, the return value *should* (potentially)
 | 
				
			||||||
 | 
					        contain all of them.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The one exception is when a simple setting has null value, by
 | 
				
			||||||
 | 
					        default it will not be included in the result (hence, not
 | 
				
			||||||
 | 
					        saved to DB) unless the setting definition has the
 | 
				
			||||||
 | 
					        ``save_if_empty`` flag set.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        settings = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # simple settings
 | 
				
			||||||
 | 
					        if simple_settings is None:
 | 
				
			||||||
 | 
					            simple_settings = self.configure_get_simple_settings()
 | 
				
			||||||
 | 
					        if simple_settings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # we got some, so "normalize" each definition to name/value
 | 
				
			||||||
 | 
					            for simple in simple_settings:
 | 
				
			||||||
 | 
					                name = simple['name']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if name in data:
 | 
				
			||||||
 | 
					                    value = data[name]
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    value = simple.get('default')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if simple.get('type') is bool:
 | 
				
			||||||
 | 
					                    value = str(bool(value)).lower()
 | 
				
			||||||
 | 
					                elif simple.get('type') is int:
 | 
				
			||||||
 | 
					                    value = str(int(value or '0'))
 | 
				
			||||||
 | 
					                elif value is None:
 | 
				
			||||||
 | 
					                    value = ''
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    value = str(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # only want to save this setting if we received a
 | 
				
			||||||
 | 
					                # value, or if empty values are okay to save
 | 
				
			||||||
 | 
					                if value or simple.get('save_if_empty'):
 | 
				
			||||||
 | 
					                    settings.append({'name': name,
 | 
				
			||||||
 | 
					                                     'value': value})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure_remove_settings(
 | 
				
			||||||
 | 
					            self,
 | 
				
			||||||
 | 
					            simple_settings=None,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Remove all "known" settings from the DB; this is called by
 | 
				
			||||||
 | 
					        :meth:`configure()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The point of this method is to ensure *all* "known" settings
 | 
				
			||||||
 | 
					        which are managed by this master view, are purged from the DB.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The default logic can handle this automatically for simple
 | 
				
			||||||
 | 
					        settings; subclass must override for any complex settings.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param simple_settings: Optional list of simple settings, if
 | 
				
			||||||
 | 
					           already initialized.  Otherwise it is retrieved via
 | 
				
			||||||
 | 
					           :meth:`configure_get_simple_settings()`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        names = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # simple settings
 | 
				
			||||||
 | 
					        if simple_settings is None:
 | 
				
			||||||
 | 
					            simple_settings = self.configure_get_simple_settings()
 | 
				
			||||||
 | 
					        if simple_settings:
 | 
				
			||||||
 | 
					            names.extend([simple['name']
 | 
				
			||||||
 | 
					                          for simple in simple_settings])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if names:
 | 
				
			||||||
 | 
					            # nb. must avoid self.Session here in case that does not
 | 
				
			||||||
 | 
					            # point to our primary app DB
 | 
				
			||||||
 | 
					            session = Session()
 | 
				
			||||||
 | 
					            for name in names:
 | 
				
			||||||
 | 
					                self.app.delete_setting(session, name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure_save_settings(self, settings):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Save the given settings to the DB; this is called by
 | 
				
			||||||
 | 
					        :meth:`configure()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This method expected a list of name/value dicts and will
 | 
				
			||||||
 | 
					        simply save each to the DB, with no "conversion" logic.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param settings: List of normalized setting definitions, as
 | 
				
			||||||
 | 
					           returned by :meth:`configure_gather_settings()`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # app = self.get_rattail_app()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # nb. must avoid self.Session here in case that does not point
 | 
				
			||||||
 | 
					        # to our primary app DB
 | 
				
			||||||
 | 
					        session = Session()
 | 
				
			||||||
 | 
					        for setting in settings:
 | 
				
			||||||
 | 
					            self.app.save_setting(session, setting['name'], setting['value'],
 | 
				
			||||||
 | 
					                                  force_create=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # support methods
 | 
					    # support methods
 | 
				
			||||||
| 
						 | 
					@ -162,6 +480,16 @@ class MasterView(View):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.get_model_title_plural()
 | 
					        return self.get_model_title_plural()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_index_url(self, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the URL for master's :meth:`index()` view.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        NB. this returns ``None`` if :attr:`listable` is false.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.listable:
 | 
				
			||||||
 | 
					            route_prefix = self.get_route_prefix()
 | 
				
			||||||
 | 
					            return self.request.route_url(route_prefix, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_to_response(self, template, context):
 | 
					    def render_to_response(self, template, context):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Locate and render an appropriate template, with the given
 | 
					        Locate and render an appropriate template, with the given
 | 
				
			||||||
| 
						 | 
					@ -192,7 +520,11 @@ class MasterView(View):
 | 
				
			||||||
        :returns: Response object containing the rendered template.
 | 
					        :returns: Response object containing the rendered template.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        defaults = {
 | 
					        defaults = {
 | 
				
			||||||
 | 
					            'master': self,
 | 
				
			||||||
 | 
					            'route_prefix': self.get_route_prefix(),
 | 
				
			||||||
            'index_title': self.get_index_title(),
 | 
					            'index_title': self.get_index_title(),
 | 
				
			||||||
 | 
					            'index_url': self.get_index_url(),
 | 
				
			||||||
 | 
					            'config_title': self.get_config_title(),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # merge defaults + caller-provided context
 | 
					        # merge defaults + caller-provided context
 | 
				
			||||||
| 
						 | 
					@ -406,6 +738,26 @@ class MasterView(View):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return cls.get_url_prefix()
 | 
					        return cls.get_url_prefix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_config_title(cls):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the "config title" for the view/model.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The config title is used for page title in the
 | 
				
			||||||
 | 
					        :meth:`configure()` view, as well as links to it.  It is
 | 
				
			||||||
 | 
					        usually plural, e.g. ``"Wutta Widgets"`` in which case that
 | 
				
			||||||
 | 
					        winds up being displayed in the web app as: **Configure Wutta
 | 
				
			||||||
 | 
					        Widgets**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The default logic will call :meth:`get_model_title_plural()`
 | 
				
			||||||
 | 
					        and return that as-is.  A subclass may override by assigning
 | 
				
			||||||
 | 
					        :attr:`config_title`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if hasattr(cls, 'config_title'):
 | 
				
			||||||
 | 
					            return cls.config_title
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return cls.get_model_title_plural()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # configuration
 | 
					    # configuration
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
| 
						 | 
					@ -436,8 +788,15 @@ class MasterView(View):
 | 
				
			||||||
        route_prefix = cls.get_route_prefix()
 | 
					        route_prefix = cls.get_route_prefix()
 | 
				
			||||||
        url_prefix = cls.get_url_prefix()
 | 
					        url_prefix = cls.get_url_prefix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # index view
 | 
					        # index
 | 
				
			||||||
        if cls.listable:
 | 
					        if cls.listable:
 | 
				
			||||||
            config.add_route(route_prefix, f'{url_prefix}/')
 | 
					            config.add_route(route_prefix, f'{url_prefix}/')
 | 
				
			||||||
            config.add_view(cls, attr='index',
 | 
					            config.add_view(cls, attr='index',
 | 
				
			||||||
                            route_name=route_prefix)
 | 
					                            route_name=route_prefix)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # configure
 | 
				
			||||||
 | 
					        if cls.configurable:
 | 
				
			||||||
 | 
					            config.add_route(f'{route_prefix}.configure',
 | 
				
			||||||
 | 
					                             f'{url_prefix}/configure')
 | 
				
			||||||
 | 
					            config.add_view(cls, attr='configure',
 | 
				
			||||||
 | 
					                            route_name=f'{route_prefix}.configure')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,11 +29,26 @@ from wuttaweb.views import MasterView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppInfoView(MasterView):
 | 
					class AppInfoView(MasterView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Master view for the overall app, to show/edit config etc.
 | 
					    Master view for the core app info, to show/edit config etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Notable URLs provided by this class:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * ``/appinfo/``
 | 
				
			||||||
 | 
					    * ``/appinfo/configure``
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    model_name = 'AppInfo'
 | 
					    model_name = 'AppInfo'
 | 
				
			||||||
    model_title_plural = "App Info"
 | 
					    model_title_plural = "App Info"
 | 
				
			||||||
    route_prefix = 'appinfo'
 | 
					    route_prefix = 'appinfo'
 | 
				
			||||||
 | 
					    configurable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def configure_get_simple_settings(self):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # basics
 | 
				
			||||||
 | 
					            {'name': f'{self.app.appname}.app_title'},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def defaults(config, **kwargs):
 | 
					def defaults(config, **kwargs):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,17 @@ from pyramid.config import Configurator
 | 
				
			||||||
from pyramid.router import Router
 | 
					from pyramid.router import Router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb import app as mod
 | 
					from wuttaweb import app as mod
 | 
				
			||||||
 | 
					from wuttjamaican.conf import WuttaConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestWebAppProvider(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_basic(self):
 | 
				
			||||||
 | 
					        # nb. just normal usage here, confirm it does the one thing we
 | 
				
			||||||
 | 
					        # need it to..
 | 
				
			||||||
 | 
					        config = WuttaConfig()
 | 
				
			||||||
 | 
					        app = config.get_app()
 | 
				
			||||||
 | 
					        handler = app.get_web_handler()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestMakeWuttaConfig(FileConfigTestCase):
 | 
					class TestMakeWuttaConfig(FileConfigTestCase):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
# -*- coding: utf-8; -*-
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from unittest import TestCase
 | 
					from unittest import TestCase
 | 
				
			||||||
 | 
					from unittest.mock import patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from pyramid import testing
 | 
					from pyramid import testing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -290,3 +291,45 @@ class TestGetFormData(TestCase):
 | 
				
			||||||
        request = self.make_request(POST=None, content_type='application/json')
 | 
					        request = self.make_request(POST=None, content_type='application/json')
 | 
				
			||||||
        data = util.get_form_data(request)
 | 
					        data = util.get_form_data(request)
 | 
				
			||||||
        self.assertEqual(data, {'foo2': 'baz'})
 | 
					        self.assertEqual(data, {'foo2': 'baz'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestGetCsrfToken(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        self.config = WuttaConfig()
 | 
				
			||||||
 | 
					        self.request = testing.DummyRequest(wutta_config=self.config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_same_token(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # same token returned for same request
 | 
				
			||||||
 | 
					        # TODO: dummy request is always returning same token!
 | 
				
			||||||
 | 
					        # so this isn't really testing anything.. :(
 | 
				
			||||||
 | 
					        first = util.get_csrf_token(self.request)
 | 
				
			||||||
 | 
					        self.assertIsNotNone(first)
 | 
				
			||||||
 | 
					        second = util.get_csrf_token(self.request)
 | 
				
			||||||
 | 
					        self.assertEqual(first, second)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: ideally would make a new request here and confirm it
 | 
				
			||||||
 | 
					        # gets a different token, but see note above..
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_new_token(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # nb. dummy request always returns same token, so must
 | 
				
			||||||
 | 
					        # trick it into thinking it doesn't have one yet
 | 
				
			||||||
 | 
					        with patch.object(self.request.session, 'get_csrf_token', return_value=None):
 | 
				
			||||||
 | 
					            token = util.get_csrf_token(self.request)
 | 
				
			||||||
 | 
					            self.assertIsNotNone(token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestRenderCsrfToken(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        self.config = WuttaConfig()
 | 
				
			||||||
 | 
					        self.request = testing.DummyRequest(wutta_config=self.config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_basics(self):
 | 
				
			||||||
 | 
					        html = util.render_csrf_token(self.request)
 | 
				
			||||||
 | 
					        self.assertIn('type="hidden"', html)
 | 
				
			||||||
 | 
					        self.assertIn('name="_csrf"', html)
 | 
				
			||||||
 | 
					        token = util.get_csrf_token(self.request)
 | 
				
			||||||
 | 
					        self.assertIn(f'value="{token}"', html)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,39 +1,21 @@
 | 
				
			||||||
# -*- coding: utf-8; -*-
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import functools
 | 
				
			||||||
from unittest import TestCase
 | 
					from unittest import TestCase
 | 
				
			||||||
from unittest.mock import MagicMock
 | 
					from unittest.mock import MagicMock, patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from pyramid import testing
 | 
					from pyramid import testing
 | 
				
			||||||
from pyramid.response import Response
 | 
					from pyramid.response import Response
 | 
				
			||||||
 | 
					from pyramid.httpexceptions import HTTPFound
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttjamaican.conf import WuttaConfig
 | 
					from wuttjamaican.conf import WuttaConfig
 | 
				
			||||||
from wuttaweb.views import master
 | 
					from wuttaweb.views import master
 | 
				
			||||||
from wuttaweb.subscribers import new_request_set_user
 | 
					from wuttaweb.subscribers import new_request_set_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from tests.views.utils import WebTestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestMasterView(TestCase):
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self):
 | 
					class TestMasterView(WebTestCase):
 | 
				
			||||||
        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):
 | 
					    def test_defaults(self):
 | 
				
			||||||
        master.MasterView.model_name = 'Widget'
 | 
					        master.MasterView.model_name = 'Widget'
 | 
				
			||||||
| 
						 | 
					@ -233,6 +215,37 @@ class TestMasterView(TestCase):
 | 
				
			||||||
        self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
 | 
					        self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
 | 
				
			||||||
        del master.MasterView.model_class
 | 
					        del master.MasterView.model_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_config_title(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # error by default (since no model class)
 | 
				
			||||||
 | 
					        self.assertRaises(AttributeError, master.MasterView.get_config_title)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # subclass may specify config title
 | 
				
			||||||
 | 
					        master.MasterView.config_title = 'Widgets'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_config_title(), "Widgets")
 | 
				
			||||||
 | 
					        del master.MasterView.config_title
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # subclass may specify *plural* model title
 | 
				
			||||||
 | 
					        master.MasterView.model_title_plural = 'People'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_config_title(), "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_config_title(), "Wutta Widgets")
 | 
				
			||||||
 | 
					        del master.MasterView.model_title
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # or it may specify model name
 | 
				
			||||||
 | 
					        master.MasterView.model_name = 'Blaster'
 | 
				
			||||||
 | 
					        self.assertEqual(master.MasterView.get_config_title(), "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_config_title(), "Dinosaurs")
 | 
				
			||||||
 | 
					        del master.MasterView.model_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
    # support methods
 | 
					    # support methods
 | 
				
			||||||
    ##############################
 | 
					    ##############################
 | 
				
			||||||
| 
						 | 
					@ -245,6 +258,10 @@ class TestMasterView(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_render_to_response(self):
 | 
					    def test_render_to_response(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def widgets(request): return {}
 | 
				
			||||||
 | 
					        self.pyramid_config.add_route('widgets', '/widgets/')
 | 
				
			||||||
 | 
					        self.pyramid_config.add_view(widgets, route_name='widgets')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # basic sanity check using /master/index.mako
 | 
					        # basic sanity check using /master/index.mako
 | 
				
			||||||
        # (nb. it skips /widgets/index.mako since that doesn't exist)
 | 
					        # (nb. it skips /widgets/index.mako since that doesn't exist)
 | 
				
			||||||
        master.MasterView.model_name = 'Widget'
 | 
					        master.MasterView.model_name = 'Widget'
 | 
				
			||||||
| 
						 | 
					@ -255,12 +272,14 @@ class TestMasterView(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # basic sanity check using /appinfo/index.mako
 | 
					        # basic sanity check using /appinfo/index.mako
 | 
				
			||||||
        master.MasterView.model_name = 'AppInfo'
 | 
					        master.MasterView.model_name = 'AppInfo'
 | 
				
			||||||
        master.MasterView.template_prefix = '/appinfo'
 | 
					        master.MasterView.route_prefix = 'appinfo'
 | 
				
			||||||
 | 
					        master.MasterView.url_prefix = '/appinfo'
 | 
				
			||||||
        view = master.MasterView(self.request)
 | 
					        view = master.MasterView(self.request)
 | 
				
			||||||
        response = view.render_to_response('index', {})
 | 
					        response = view.render_to_response('index', {})
 | 
				
			||||||
        self.assertIsInstance(response, Response)
 | 
					        self.assertIsInstance(response, Response)
 | 
				
			||||||
        del master.MasterView.model_name
 | 
					        del master.MasterView.model_name
 | 
				
			||||||
        del master.MasterView.template_prefix
 | 
					        del master.MasterView.route_prefix
 | 
				
			||||||
 | 
					        del master.MasterView.url_prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # bad template name causes error
 | 
					        # bad template name causes error
 | 
				
			||||||
        master.MasterView.model_name = 'Widget'
 | 
					        master.MasterView.model_name = 'Widget'
 | 
				
			||||||
| 
						 | 
					@ -275,8 +294,77 @@ class TestMasterView(TestCase):
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # basic sanity check using /appinfo
 | 
					        # basic sanity check using /appinfo
 | 
				
			||||||
        master.MasterView.model_name = 'AppInfo'
 | 
					        master.MasterView.model_name = 'AppInfo'
 | 
				
			||||||
 | 
					        master.MasterView.route_prefix = 'appinfo'
 | 
				
			||||||
        master.MasterView.template_prefix = '/appinfo'
 | 
					        master.MasterView.template_prefix = '/appinfo'
 | 
				
			||||||
        view = master.MasterView(self.request)
 | 
					        view = master.MasterView(self.request)
 | 
				
			||||||
        response = view.index()
 | 
					        response = view.index()
 | 
				
			||||||
        del master.MasterView.model_name
 | 
					        del master.MasterView.model_name
 | 
				
			||||||
 | 
					        del master.MasterView.route_prefix
 | 
				
			||||||
 | 
					        del master.MasterView.template_prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_configure(self):
 | 
				
			||||||
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # setup
 | 
				
			||||||
 | 
					        master.MasterView.model_name = 'AppInfo'
 | 
				
			||||||
 | 
					        master.MasterView.route_prefix = 'appinfo'
 | 
				
			||||||
 | 
					        master.MasterView.template_prefix = '/appinfo'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # mock settings
 | 
				
			||||||
 | 
					        settings = [
 | 
				
			||||||
 | 
					            {'name': 'wutta.app_title'},
 | 
				
			||||||
 | 
					            {'name': 'wutta.foo', 'value': 'bar'},
 | 
				
			||||||
 | 
					            {'name': 'wutta.flag', 'type': bool},
 | 
				
			||||||
 | 
					            {'name': 'wutta.number', 'type': int, 'default': 42},
 | 
				
			||||||
 | 
					            {'name': 'wutta.value1', 'save_if_empty': True},
 | 
				
			||||||
 | 
					            {'name': 'wutta.value2', 'save_if_empty': False},
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        view = master.MasterView(self.request)
 | 
				
			||||||
 | 
					        with patch.object(self.request, 'current_route_url',
 | 
				
			||||||
 | 
					                          return_value='/appinfo/configure'):
 | 
				
			||||||
 | 
					            with patch.object(master.MasterView, 'configure_get_simple_settings',
 | 
				
			||||||
 | 
					                              return_value=settings):
 | 
				
			||||||
 | 
					                with patch.object(master, 'Session', return_value=self.session):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # get the form page
 | 
				
			||||||
 | 
					                    response = view.configure()
 | 
				
			||||||
 | 
					                    self.assertIsInstance(response, Response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # post request to save settings
 | 
				
			||||||
 | 
					                    self.request.method = 'POST'
 | 
				
			||||||
 | 
					                    self.request.POST = {
 | 
				
			||||||
 | 
					                        'wutta.app_title': 'Wutta',
 | 
				
			||||||
 | 
					                        'wutta.foo': 'bar',
 | 
				
			||||||
 | 
					                        'wutta.flag': 'true',
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    response = view.configure()
 | 
				
			||||||
 | 
					                    # nb. should get redirect back to configure page
 | 
				
			||||||
 | 
					                    self.assertIsInstance(response, HTTPFound)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # should now have 5 settings
 | 
				
			||||||
 | 
					                    count = self.session.query(model.Setting).count()
 | 
				
			||||||
 | 
					                    self.assertEqual(count, 5)
 | 
				
			||||||
 | 
					                    get_setting = functools.partial(self.app.get_setting, self.session)
 | 
				
			||||||
 | 
					                    self.assertEqual(get_setting('wutta.app_title'), 'Wutta')
 | 
				
			||||||
 | 
					                    self.assertEqual(get_setting('wutta.foo'), 'bar')
 | 
				
			||||||
 | 
					                    self.assertEqual(get_setting('wutta.flag'), 'true')
 | 
				
			||||||
 | 
					                    self.assertEqual(get_setting('wutta.number'), '42')
 | 
				
			||||||
 | 
					                    self.assertEqual(get_setting('wutta.value1'), '')
 | 
				
			||||||
 | 
					                    self.assertEqual(get_setting('wutta.value2'), None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # post request to remove settings
 | 
				
			||||||
 | 
					                    self.request.method = 'POST'
 | 
				
			||||||
 | 
					                    self.request.POST = {'remove_settings': '1'}
 | 
				
			||||||
 | 
					                    response = view.configure()
 | 
				
			||||||
 | 
					                    # nb. should get redirect back to configure page
 | 
				
			||||||
 | 
					                    self.assertIsInstance(response, HTTPFound)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # should now have 0 settings
 | 
				
			||||||
 | 
					                    count = self.session.query(model.Setting).count()
 | 
				
			||||||
 | 
					                    self.assertEqual(count, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # teardown
 | 
				
			||||||
 | 
					        del master.MasterView.model_name
 | 
				
			||||||
 | 
					        del master.MasterView.route_prefix
 | 
				
			||||||
        del master.MasterView.template_prefix
 | 
					        del master.MasterView.template_prefix
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,3 +11,8 @@ class TestAppInfoView(WebTestCase):
 | 
				
			||||||
        # just a sanity check
 | 
					        # just a sanity check
 | 
				
			||||||
        view = settings.AppInfoView(self.request)
 | 
					        view = settings.AppInfoView(self.request)
 | 
				
			||||||
        response = view.index()
 | 
					        response = view.index()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_configure_get_simple_settings(self):
 | 
				
			||||||
 | 
					        # just a sanity check
 | 
				
			||||||
 | 
					        view = settings.AppInfoView(self.request)
 | 
				
			||||||
 | 
					        simple = view.configure_get_simple_settings()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue