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
 | 
			
		||||
    all apps.
 | 
			
		||||
 | 
			
		||||
    :returns: Instance of
 | 
			
		||||
       :class:`pyramid:pyramid.config.Configurator`.
 | 
			
		||||
    """
 | 
			
		||||
    settings.setdefault('mako.directories', ['wuttaweb:templates'])
 | 
			
		||||
    settings.setdefault('pyramid_deform.template_search_path',
 | 
			
		||||
                        'wuttaweb:templates/deform')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +123,11 @@ def make_pyramid_config(settings):
 | 
			
		|||
    # configure user authorization / authentication
 | 
			
		||||
    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_deform')
 | 
			
		||||
    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
 | 
			
		||||
    instead.
 | 
			
		||||
    """
 | 
			
		||||
    settings.setdefault('mako.directories', ['wuttaweb:templates'])
 | 
			
		||||
 | 
			
		||||
    wutta_config = make_wutta_config(settings)
 | 
			
		||||
    pyramid_config = make_pyramid_config(settings)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -323,6 +323,7 @@ class Form:
 | 
			
		|||
        """
 | 
			
		||||
        context['form'] = self
 | 
			
		||||
        context.setdefault('form_attrs', {})
 | 
			
		||||
        context.setdefault('request', self.request)
 | 
			
		||||
 | 
			
		||||
        # auto disable button on submit
 | 
			
		||||
        if self.auto_disable_submit:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,12 +38,20 @@ instance:
 | 
			
		|||
 | 
			
		||||
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.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.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>
 | 
			
		||||
          </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">
 | 
			
		||||
 | 
			
		||||
              ## Current Context
 | 
			
		||||
              <div id="current-context" class="level-item">
 | 
			
		||||
                % if index_title:
 | 
			
		||||
                    % if index_url:
 | 
			
		||||
                        <span class="header-text">
 | 
			
		||||
                          ${h.link_to(index_title, index_url)}
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <h1 class="title">${h.link_to(index_title, index_url)}</h1>
 | 
			
		||||
                    % else:
 | 
			
		||||
                        <h1 class="title">${index_title}</h1>
 | 
			
		||||
                    % endif
 | 
			
		||||
| 
						 | 
				
			
			@ -226,6 +224,23 @@
 | 
			
		|||
              </div>
 | 
			
		||||
 | 
			
		||||
            </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 -->
 | 
			
		||||
        </header>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -318,8 +333,7 @@
 | 
			
		|||
        <div class="navbar-dropdown">
 | 
			
		||||
          % if request.is_root:
 | 
			
		||||
              ${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()}" />
 | 
			
		||||
              <a @click="stopBeingRoot()"
 | 
			
		||||
                 class="navbar-item has-background-danger has-text-white">
 | 
			
		||||
| 
						 | 
				
			
			@ -328,8 +342,7 @@
 | 
			
		|||
              ${h.end_form()}
 | 
			
		||||
          % elif request.is_admin:
 | 
			
		||||
              ${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()}" />
 | 
			
		||||
              <a @click="startBeingRoot()"
 | 
			
		||||
                 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">
 | 
			
		||||
  ${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
 | 
			
		||||
    ${h.csrf_token(request)}
 | 
			
		||||
 | 
			
		||||
    <section>
 | 
			
		||||
      % 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
 | 
			
		||||
 | 
			
		||||
from webhelpers2.html import HTML, tags
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_form_data(request):
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			@ -257,3 +259,44 @@ def get_liburl(
 | 
			
		|||
 | 
			
		||||
    elif key == 'bb_vue_fontawesome':
 | 
			
		||||
        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 wuttaweb.views import View
 | 
			
		||||
from wuttaweb.util import get_form_data
 | 
			
		||||
from wuttaweb.db import Session
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MasterView(View):
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +100,14 @@ class MasterView(View):
 | 
			
		|||
       Code should not access this directly but instead call
 | 
			
		||||
       :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
 | 
			
		||||
 | 
			
		||||
       Optional override for the view's route prefix,
 | 
			
		||||
| 
						 | 
				
			
			@ -125,17 +135,29 @@ class MasterView(View):
 | 
			
		|||
    .. attribute:: listable
 | 
			
		||||
 | 
			
		||||
       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
 | 
			
		||||
    ##############################
 | 
			
		||||
 | 
			
		||||
    # features
 | 
			
		||||
    listable = True
 | 
			
		||||
    configurable = False
 | 
			
		||||
 | 
			
		||||
    # current action
 | 
			
		||||
    configuring = False
 | 
			
		||||
 | 
			
		||||
    ##############################
 | 
			
		||||
    # view methods
 | 
			
		||||
    # index methods
 | 
			
		||||
    ##############################
 | 
			
		||||
 | 
			
		||||
    def index(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -145,8 +167,304 @@ class MasterView(View):
 | 
			
		|||
        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/``.
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
| 
						 | 
				
			
			@ -162,6 +480,16 @@ class MasterView(View):
 | 
			
		|||
        """
 | 
			
		||||
        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):
 | 
			
		||||
        """
 | 
			
		||||
        Locate and render an appropriate template, with the given
 | 
			
		||||
| 
						 | 
				
			
			@ -192,7 +520,11 @@ class MasterView(View):
 | 
			
		|||
        :returns: Response object containing the rendered template.
 | 
			
		||||
        """
 | 
			
		||||
        defaults = {
 | 
			
		||||
            'master': self,
 | 
			
		||||
            'route_prefix': self.get_route_prefix(),
 | 
			
		||||
            'index_title': self.get_index_title(),
 | 
			
		||||
            'index_url': self.get_index_url(),
 | 
			
		||||
            'config_title': self.get_config_title(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # merge defaults + caller-provided context
 | 
			
		||||
| 
						 | 
				
			
			@ -406,6 +738,26 @@ class MasterView(View):
 | 
			
		|||
 | 
			
		||||
        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
 | 
			
		||||
    ##############################
 | 
			
		||||
| 
						 | 
				
			
			@ -436,8 +788,15 @@ class MasterView(View):
 | 
			
		|||
        route_prefix = cls.get_route_prefix()
 | 
			
		||||
        url_prefix = cls.get_url_prefix()
 | 
			
		||||
 | 
			
		||||
        # index view
 | 
			
		||||
        # index
 | 
			
		||||
        if cls.listable:
 | 
			
		||||
            config.add_route(route_prefix, f'{url_prefix}/')
 | 
			
		||||
            config.add_view(cls, attr='index',
 | 
			
		||||
                            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):
 | 
			
		||||
    """
 | 
			
		||||
    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_title_plural = "App Info"
 | 
			
		||||
    route_prefix = 'appinfo'
 | 
			
		||||
    configurable = True
 | 
			
		||||
 | 
			
		||||
    def configure_get_simple_settings(self):
 | 
			
		||||
        """ """
 | 
			
		||||
        return [
 | 
			
		||||
 | 
			
		||||
            # basics
 | 
			
		||||
            {'name': f'{self.app.appname}.app_title'},
 | 
			
		||||
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def defaults(config, **kwargs):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,17 @@ from pyramid.config import Configurator
 | 
			
		|||
from pyramid.router import Router
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
 | 
			
		||||
from unittest import TestCase
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from pyramid import testing
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -290,3 +291,45 @@ class TestGetFormData(TestCase):
 | 
			
		|||
        request = self.make_request(POST=None, content_type='application/json')
 | 
			
		||||
        data = util.get_form_data(request)
 | 
			
		||||
        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; -*-
 | 
			
		||||
 | 
			
		||||
import functools
 | 
			
		||||
from unittest import TestCase
 | 
			
		||||
from unittest.mock import MagicMock
 | 
			
		||||
from unittest.mock import MagicMock, patch
 | 
			
		||||
 | 
			
		||||
from pyramid import testing
 | 
			
		||||
from pyramid.response import Response
 | 
			
		||||
from pyramid.httpexceptions import HTTPFound
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.conf import WuttaConfig
 | 
			
		||||
from wuttaweb.views import master
 | 
			
		||||
from wuttaweb.subscribers import new_request_set_user
 | 
			
		||||
 | 
			
		||||
from tests.views.utils import WebTestCase
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
class TestMasterView(WebTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_defaults(self):
 | 
			
		||||
        master.MasterView.model_name = 'Widget'
 | 
			
		||||
| 
						 | 
				
			
			@ -233,6 +215,37 @@ class TestMasterView(TestCase):
 | 
			
		|||
        self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
 | 
			
		||||
        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
 | 
			
		||||
    ##############################
 | 
			
		||||
| 
						 | 
				
			
			@ -245,6 +258,10 @@ class TestMasterView(TestCase):
 | 
			
		|||
 | 
			
		||||
    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
 | 
			
		||||
        # (nb. it skips /widgets/index.mako since that doesn't exist)
 | 
			
		||||
        master.MasterView.model_name = 'Widget'
 | 
			
		||||
| 
						 | 
				
			
			@ -255,12 +272,14 @@ class TestMasterView(TestCase):
 | 
			
		|||
 | 
			
		||||
        # basic sanity check using /appinfo/index.mako
 | 
			
		||||
        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)
 | 
			
		||||
        response = view.render_to_response('index', {})
 | 
			
		||||
        self.assertIsInstance(response, Response)
 | 
			
		||||
        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
 | 
			
		||||
        master.MasterView.model_name = 'Widget'
 | 
			
		||||
| 
						 | 
				
			
			@ -275,8 +294,77 @@ class TestMasterView(TestCase):
 | 
			
		|||
        
 | 
			
		||||
        # basic sanity check using /appinfo
 | 
			
		||||
        master.MasterView.model_name = 'AppInfo'
 | 
			
		||||
        master.MasterView.route_prefix = 'appinfo'
 | 
			
		||||
        master.MasterView.template_prefix = '/appinfo'
 | 
			
		||||
        view = master.MasterView(self.request)
 | 
			
		||||
        response = view.index()
 | 
			
		||||
        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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,3 +11,8 @@ class TestAppInfoView(WebTestCase):
 | 
			
		|||
        # just a sanity check
 | 
			
		||||
        view = settings.AppInfoView(self.request)
 | 
			
		||||
        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