feat: add basic user feedback email mechanism
this definitely needs some more work. using pyramid_mailer for testing although not ready to declare that dependency. for now this is "broken" without it being installed.
This commit is contained in:
		
							parent
							
								
									8669ca2283
								
							
						
					
					
						commit
						4934ed1d93
					
				
					 6 changed files with 461 additions and 2 deletions
				
			
		| 
						 | 
					@ -420,7 +420,153 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="render_theme_picker()"></%def>
 | 
					<%def name="render_theme_picker()"></%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="render_feedback_button()"></%def>
 | 
					<%def name="render_feedback_button()">
 | 
				
			||||||
 | 
					  % if request.has_perm('common.feedback'):
 | 
				
			||||||
 | 
					      <wutta-feedback-form action="${url('feedback')}" />
 | 
				
			||||||
 | 
					  % endif
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="render_vue_template_feedback()">
 | 
				
			||||||
 | 
					  <script type="text/x-template" id="wutta-feedback-template">
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <b-button type="is-primary"
 | 
				
			||||||
 | 
					                @click="showFeedback()"
 | 
				
			||||||
 | 
					                icon-pack="fas"
 | 
				
			||||||
 | 
					                icon-left="comment">
 | 
				
			||||||
 | 
					        Feedback
 | 
				
			||||||
 | 
					      </b-button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <b-modal has-modal-card
 | 
				
			||||||
 | 
					               :active.sync="showDialog">
 | 
				
			||||||
 | 
					        <div class="modal-card">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <header class="modal-card-head">
 | 
				
			||||||
 | 
					            <p class="modal-card-title">User Feedback</p>
 | 
				
			||||||
 | 
					          </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <section class="modal-card-body">
 | 
				
			||||||
 | 
					            <p class="block">
 | 
				
			||||||
 | 
					              Feedback regarding this website may be submitted below.
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <b-field label="User Name"
 | 
				
			||||||
 | 
					                     :type="userName && userName.trim() ? null : 'is-danger'">
 | 
				
			||||||
 | 
					              <b-input v-model.trim="userName"
 | 
				
			||||||
 | 
					                       % if request.user:
 | 
				
			||||||
 | 
					                           disabled
 | 
				
			||||||
 | 
					                       % endif
 | 
				
			||||||
 | 
					                       />
 | 
				
			||||||
 | 
					            </b-field>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <b-field label="Referring URL">
 | 
				
			||||||
 | 
					              <b-input v-model="referrer"
 | 
				
			||||||
 | 
					                       disabled="true" />
 | 
				
			||||||
 | 
					            </b-field>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <b-field label="Message"
 | 
				
			||||||
 | 
					                     :type="message && message.trim() ? null : 'is-danger'">
 | 
				
			||||||
 | 
					              <b-input type="textarea"
 | 
				
			||||||
 | 
					                       v-model.trim="message"
 | 
				
			||||||
 | 
					                       ref="textarea" />
 | 
				
			||||||
 | 
					            </b-field>
 | 
				
			||||||
 | 
					          </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <footer class="modal-card-foot">
 | 
				
			||||||
 | 
					            <b-button @click="showDialog = false">
 | 
				
			||||||
 | 
					              Cancel
 | 
				
			||||||
 | 
					            </b-button>
 | 
				
			||||||
 | 
					            <b-button type="is-primary"
 | 
				
			||||||
 | 
					                      icon-pack="fas"
 | 
				
			||||||
 | 
					                      icon-left="paper-plane"
 | 
				
			||||||
 | 
					                      @click="sendFeedback()"
 | 
				
			||||||
 | 
					                      :disabled="submitDisabled">
 | 
				
			||||||
 | 
					              {{ sendingFeedback ? "Working, please wait..." : "Send Message" }}
 | 
				
			||||||
 | 
					            </b-button>
 | 
				
			||||||
 | 
					          </footer>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </b-modal>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="render_vue_script_feedback()">
 | 
				
			||||||
 | 
					  <script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const WuttaFeedbackForm = {
 | 
				
			||||||
 | 
					        template: '#wutta-feedback-template',
 | 
				
			||||||
 | 
					        mixins: [WuttaRequestMixin],
 | 
				
			||||||
 | 
					        props: {
 | 
				
			||||||
 | 
					            action: String,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        computed: {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            submitDisabled() {
 | 
				
			||||||
 | 
					                if (this.sendingFeedback) {
 | 
				
			||||||
 | 
					                    return true
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (!this.userName || !this.userName.trim()) {
 | 
				
			||||||
 | 
					                    return true
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (!this.message || !this.message.trim()) {
 | 
				
			||||||
 | 
					                    return true
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return false
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        methods: {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            showFeedback() {
 | 
				
			||||||
 | 
					                // nb. update referrer to include anchor hash if any
 | 
				
			||||||
 | 
					                this.referrer = location.href
 | 
				
			||||||
 | 
					                this.showDialog = true
 | 
				
			||||||
 | 
					                this.$nextTick(function() {
 | 
				
			||||||
 | 
					                    this.$refs.textarea.focus()
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sendFeedback() {
 | 
				
			||||||
 | 
					                this.sendingFeedback = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const params = {
 | 
				
			||||||
 | 
					                    referrer: this.referrer,
 | 
				
			||||||
 | 
					                    user_uuid: this.userUUID,
 | 
				
			||||||
 | 
					                    user_name: this.userName,
 | 
				
			||||||
 | 
					                    message: this.message.trim(),
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.wuttaPOST(this.action, params, response => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    this.$buefy.toast.open({
 | 
				
			||||||
 | 
					                        message: "Message sent!  Thank you for your feedback.",
 | 
				
			||||||
 | 
					                        type: 'is-info',
 | 
				
			||||||
 | 
					                        duration: 4000, // 4 seconds
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    this.showDialog = false
 | 
				
			||||||
 | 
					                    // clear out message, in case they need to send another
 | 
				
			||||||
 | 
					                    this.message = ""
 | 
				
			||||||
 | 
					                    this.sendingFeedback = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                }, response => { // failure
 | 
				
			||||||
 | 
					                    this.sendingFeedback = false
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const WuttaFeedbackFormData = {
 | 
				
			||||||
 | 
					        referrer: null,
 | 
				
			||||||
 | 
					        userUUID: ${json.dumps(request.user.uuid if request.user else None)|n},
 | 
				
			||||||
 | 
					        userName: ${json.dumps(str(request.user) if request.user else None)|n},
 | 
				
			||||||
 | 
					        showDialog: false,
 | 
				
			||||||
 | 
					        sendingFeedback: false,
 | 
				
			||||||
 | 
					        message: '',
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="render_vue_script_whole_page()">
 | 
					<%def name="render_vue_script_whole_page()">
 | 
				
			||||||
  <script>
 | 
					  <script>
 | 
				
			||||||
| 
						 | 
					@ -578,18 +724,33 @@
 | 
				
			||||||
##############################
 | 
					##############################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="render_vue_templates()">
 | 
					<%def name="render_vue_templates()">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## nb. must make wutta components first; they are stable so
 | 
				
			||||||
 | 
					  ## intermediate pages do not need to modify them.  and some pages
 | 
				
			||||||
 | 
					  ## may need the request mixin to be defined.
 | 
				
			||||||
 | 
					  ${make_wutta_components()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ${self.render_vue_template_whole_page()}
 | 
					  ${self.render_vue_template_whole_page()}
 | 
				
			||||||
  ${self.render_vue_script_whole_page()}
 | 
					  ${self.render_vue_script_whole_page()}
 | 
				
			||||||
 | 
					  % if request.has_perm('common.feedback'):
 | 
				
			||||||
 | 
					      ${self.render_vue_template_feedback()}
 | 
				
			||||||
 | 
					      ${self.render_vue_script_feedback()}
 | 
				
			||||||
 | 
					  % endif
 | 
				
			||||||
</%def>
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="modify_vue_vars()"></%def>
 | 
					<%def name="modify_vue_vars()"></%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="make_vue_components()">
 | 
					<%def name="make_vue_components()">
 | 
				
			||||||
  ${make_wutta_components()}
 | 
					 | 
				
			||||||
  <script>
 | 
					  <script>
 | 
				
			||||||
    WholePage.data = function() { return WholePageData }
 | 
					    WholePage.data = function() { return WholePageData }
 | 
				
			||||||
    Vue.component('whole-page', WholePage)
 | 
					    Vue.component('whole-page', WholePage)
 | 
				
			||||||
  </script>
 | 
					  </script>
 | 
				
			||||||
 | 
					  % if request.has_perm('common.feedback'):
 | 
				
			||||||
 | 
					      <script>
 | 
				
			||||||
 | 
					        WuttaFeedbackForm.data = function() { return WuttaFeedbackFormData }
 | 
				
			||||||
 | 
					        Vue.component('wutta-feedback-form', WuttaFeedbackForm)
 | 
				
			||||||
 | 
					      </script>
 | 
				
			||||||
 | 
					  % endif
 | 
				
			||||||
</%def>
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="make_vue_app()">
 | 
					<%def name="make_vue_app()">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										40
									
								
								src/wuttaweb/templates/temporary/feedback.html.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/wuttaweb/templates/temporary/feedback.html.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <style type="text/css">
 | 
				
			||||||
 | 
					      label {
 | 
				
			||||||
 | 
					          display: block;
 | 
				
			||||||
 | 
					          font-weight: bold;
 | 
				
			||||||
 | 
					          margin-top: 1em;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      p {
 | 
				
			||||||
 | 
					          margin: 1em 0 1em 1.5em;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      p.msg {
 | 
				
			||||||
 | 
					          white-space: pre-wrap;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <h1>User feedback from website</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <label>User Name</label>
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      % if user:
 | 
				
			||||||
 | 
					          <a href="${user_url}">${user}</a>
 | 
				
			||||||
 | 
					      % else:
 | 
				
			||||||
 | 
					          ${user_name}
 | 
				
			||||||
 | 
					      % endif
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <label>Referring URL</label>
 | 
				
			||||||
 | 
					    <p><a href="${referrer}">${referrer}</a></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <label>Client IP</label>
 | 
				
			||||||
 | 
					    <p>${client_ip}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <label>Message</label>
 | 
				
			||||||
 | 
					    <p class="msg">${message}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/wuttaweb/templates/temporary/feedback.txt.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/wuttaweb/templates/temporary/feedback.txt.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					## -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# User feedback from website
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**User Name**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					% if user:
 | 
				
			||||||
 | 
					    ${user}
 | 
				
			||||||
 | 
					% else:
 | 
				
			||||||
 | 
					    ${user_name}
 | 
				
			||||||
 | 
					% endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Referring URL**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${referrer}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Client IP**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${client_ip}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Message**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${message}
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,87 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="make_wutta_components()">
 | 
					<%def name="make_wutta_components()">
 | 
				
			||||||
 | 
					  ${self.make_wutta_request_mixin()}
 | 
				
			||||||
  ${self.make_wutta_button_component()}
 | 
					  ${self.make_wutta_button_component()}
 | 
				
			||||||
  ${self.make_wutta_filter_component()}
 | 
					  ${self.make_wutta_filter_component()}
 | 
				
			||||||
  ${self.make_wutta_filter_value_component()}
 | 
					  ${self.make_wutta_filter_value_component()}
 | 
				
			||||||
</%def>
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="make_wutta_request_mixin()">
 | 
				
			||||||
 | 
					  <script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const WuttaRequestMixin = {
 | 
				
			||||||
 | 
					        methods: {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            wuttaGET(url, params, success, failure) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.$http.get(url, {params: params}).then(response => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (response.data.error) {
 | 
				
			||||||
 | 
					                        this.$buefy.toast.open({
 | 
				
			||||||
 | 
					                            message: `Request failed:  ${'$'}{response.data.error}`,
 | 
				
			||||||
 | 
					                            type: 'is-danger',
 | 
				
			||||||
 | 
					                            duration: 4000, // 4 seconds
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        if (failure) {
 | 
				
			||||||
 | 
					                            failure(response)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        success(response)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                }, response => {
 | 
				
			||||||
 | 
					                    this.$buefy.toast.open({
 | 
				
			||||||
 | 
					                        message: "Request failed:  (unknown server error)",
 | 
				
			||||||
 | 
					                        type: 'is-danger',
 | 
				
			||||||
 | 
					                        duration: 4000, // 4 seconds
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    if (failure) {
 | 
				
			||||||
 | 
					                        failure(response)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            wuttaPOST(action, params, success, failure) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const csrftoken = ${json.dumps(h.get_csrf_token(request))|n}
 | 
				
			||||||
 | 
					                const headers = {'X-CSRF-TOKEN': csrftoken}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.$http.post(action, params, {headers: headers}).then(response => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (response.data.error) {
 | 
				
			||||||
 | 
					                        this.$buefy.toast.open({
 | 
				
			||||||
 | 
					                            message: "Submit failed:  " + (response.data.error ||
 | 
				
			||||||
 | 
					                                                           "(unknown error)"),
 | 
				
			||||||
 | 
					                            type: 'is-danger',
 | 
				
			||||||
 | 
					                            duration: 4000, // 4 seconds
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        if (failure) {
 | 
				
			||||||
 | 
					                            failure(response)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        success(response)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                }, response => {
 | 
				
			||||||
 | 
					                    this.$buefy.toast.open({
 | 
				
			||||||
 | 
					                        message: "Submit failed!  (unknown server error)",
 | 
				
			||||||
 | 
					                        type: 'is-danger',
 | 
				
			||||||
 | 
					                        duration: 4000, // 4 seconds
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    if (failure) {
 | 
				
			||||||
 | 
					                        failure(response)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="make_wutta_button_component()">
 | 
					<%def name="make_wutta_button_component()">
 | 
				
			||||||
  <script type="text/x-template" id="wutta-button-template">
 | 
					  <script type="text/x-template" id="wutta-button-template">
 | 
				
			||||||
    <b-button :type="type"
 | 
					    <b-button :type="type"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,13 +24,19 @@
 | 
				
			||||||
Common Views
 | 
					Common Views
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import colander
 | 
					import colander
 | 
				
			||||||
 | 
					from pyramid.renderers import render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb.views import View
 | 
					from wuttaweb.views import View
 | 
				
			||||||
from wuttaweb.forms import widgets
 | 
					from wuttaweb.forms import widgets
 | 
				
			||||||
from wuttaweb.db import Session
 | 
					from wuttaweb.db import Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CommonView(View):
 | 
					class CommonView(View):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Common views shared by all apps.
 | 
					    Common views shared by all apps.
 | 
				
			||||||
| 
						 | 
					@ -78,6 +84,80 @@ class CommonView(View):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return {'index_title': self.app.get_title()}
 | 
					        return {'index_title': self.app.get_title()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def feedback(self):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					        session = Session()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # validate form
 | 
				
			||||||
 | 
					        schema = self.feedback_make_schema()
 | 
				
			||||||
 | 
					        form = self.make_form(schema=schema)
 | 
				
			||||||
 | 
					        if not form.validate():
 | 
				
			||||||
 | 
					            # TODO: native Form class should better expose error(s)
 | 
				
			||||||
 | 
					            dform = form.get_deform()
 | 
				
			||||||
 | 
					            return {'error': str(dform.error)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # build email template context
 | 
				
			||||||
 | 
					        context = dict(form.validated)
 | 
				
			||||||
 | 
					        if context['user_uuid']:
 | 
				
			||||||
 | 
					            context['user'] = session.get(model.User, context['user_uuid'])
 | 
				
			||||||
 | 
					            context['user_url'] = self.request.route_url('users.view', uuid=context['user_uuid'])
 | 
				
			||||||
 | 
					        context['client_ip'] = self.request.client_addr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # send email
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.feedback_send(context)
 | 
				
			||||||
 | 
					        except Exception as error:
 | 
				
			||||||
 | 
					            log.warning("failed to send feedback email", exc_info=True)
 | 
				
			||||||
 | 
					            return {'error': str(error) or error.__class__.__name__}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {'ok': True}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def feedback_make_schema(self):
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					        schema = colander.Schema()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        schema.add(colander.SchemaNode(colander.String(),
 | 
				
			||||||
 | 
					                                       name='referrer'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        schema.add(colander.SchemaNode(colander.String(),
 | 
				
			||||||
 | 
					                                       name='user_uuid',
 | 
				
			||||||
 | 
					                                       missing=None))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        schema.add(colander.SchemaNode(colander.String(),
 | 
				
			||||||
 | 
					                                       name='user_name'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        schema.add(colander.SchemaNode(colander.String(),
 | 
				
			||||||
 | 
					                                       name='message'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return schema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def feedback_send(self, context): # pragma: no cover
 | 
				
			||||||
 | 
					        """ """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: this is definitely a stopgap bit of logic, until we
 | 
				
			||||||
 | 
					        # have a more robust way to handle email via wuttjamaican etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        from pyramid_mailer.mailer import Mailer
 | 
				
			||||||
 | 
					        from pyramid_mailer.message import Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        From = self.config.require(f'{self.config.appname}.email.default.sender')
 | 
				
			||||||
 | 
					        To = self.config.require(f'{self.config.appname}.email.feedback.to')
 | 
				
			||||||
 | 
					        Subject = self.config.get(f'{self.config.appname}.email.feedback.subject',
 | 
				
			||||||
 | 
					                                  default="User Feedback")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        text_body = render('/temporary/feedback.txt.mako', context, request=self.request)
 | 
				
			||||||
 | 
					        html_body = render('/temporary/feedback.html.mako', context, request=self.request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        msg = Message(subject=Subject,
 | 
				
			||||||
 | 
					                      sender=From,
 | 
				
			||||||
 | 
					                      recipients=[To],
 | 
				
			||||||
 | 
					                      body=text_body,
 | 
				
			||||||
 | 
					                      html=html_body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        mailer = Mailer()
 | 
				
			||||||
 | 
					        mailer.send_immediately(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setup(self, session=None):
 | 
					    def setup(self, session=None):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        View for first-time app setup, to create admin user.
 | 
					        View for first-time app setup, to create admin user.
 | 
				
			||||||
| 
						 | 
					@ -203,6 +283,8 @@ class CommonView(View):
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def _defaults(cls, config):
 | 
					    def _defaults(cls, config):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        config.add_wutta_permission_group('common', "(common)", overwrite=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # home page
 | 
					        # home page
 | 
				
			||||||
        config.add_route('home', '/')
 | 
					        config.add_route('home', '/')
 | 
				
			||||||
        config.add_view(cls, attr='home',
 | 
					        config.add_view(cls, attr='home',
 | 
				
			||||||
| 
						 | 
					@ -219,6 +301,16 @@ class CommonView(View):
 | 
				
			||||||
                                 append_slash=True,
 | 
					                                 append_slash=True,
 | 
				
			||||||
                                 renderer='/notfound.mako')
 | 
					                                 renderer='/notfound.mako')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # feedback
 | 
				
			||||||
 | 
					        config.add_route('feedback', '/feedback',
 | 
				
			||||||
 | 
					                         request_method='POST')
 | 
				
			||||||
 | 
					        config.add_view(cls, attr='feedback',
 | 
				
			||||||
 | 
					                        route_name='feedback',
 | 
				
			||||||
 | 
					                        permission='common.feedback',
 | 
				
			||||||
 | 
					                        renderer='json')
 | 
				
			||||||
 | 
					        config.add_wutta_permission('common', 'common.feedback',
 | 
				
			||||||
 | 
					                                    "Send user feedback about the app")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # setup
 | 
					        # setup
 | 
				
			||||||
        config.add_route('setup', '/setup')
 | 
					        config.add_route('setup', '/setup')
 | 
				
			||||||
        config.add_view(cls, attr='setup',
 | 
					        config.add_view(cls, attr='setup',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,9 @@
 | 
				
			||||||
# -*- coding: utf-8; -*-
 | 
					# -*- coding: utf-8; -*-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from unittest.mock import patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import colander
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttaweb.views import common as mod
 | 
					from wuttaweb.views import common as mod
 | 
				
			||||||
from tests.util import WebTestCase
 | 
					from tests.util import WebTestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,6 +55,68 @@ class TestCommonView(WebTestCase):
 | 
				
			||||||
        context = view.home(session=self.session)
 | 
					        context = view.home(session=self.session)
 | 
				
			||||||
        self.assertEqual(context['index_title'], self.app.get_title())
 | 
					        self.assertEqual(context['index_title'], self.app.get_title())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_feedback_make_schema(self):
 | 
				
			||||||
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					        schema = view.feedback_make_schema()
 | 
				
			||||||
 | 
					        self.assertIsInstance(schema, colander.Schema)
 | 
				
			||||||
 | 
					        self.assertIn('message', schema)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_feedback(self):
 | 
				
			||||||
 | 
					        self.pyramid_config.add_route('users.view', '/users/{uuid}')
 | 
				
			||||||
 | 
					        model = self.app.model
 | 
				
			||||||
 | 
					        user = model.User(username='barney')
 | 
				
			||||||
 | 
					        self.session.add(user)
 | 
				
			||||||
 | 
					        self.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        view = self.make_view()
 | 
				
			||||||
 | 
					        with patch.object(view, 'feedback_send') as feedback_send:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # basic send, no user
 | 
				
			||||||
 | 
					            self.request.client_addr = '127.0.0.1'
 | 
				
			||||||
 | 
					            self.request.method = 'POST'
 | 
				
			||||||
 | 
					            self.request.POST = {
 | 
				
			||||||
 | 
					                'referrer': '/foo',
 | 
				
			||||||
 | 
					                'user_name': "Barney Rubble",
 | 
				
			||||||
 | 
					                'message': "hello world",
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            context = view.feedback()
 | 
				
			||||||
 | 
					            self.assertEqual(context, {'ok': True})
 | 
				
			||||||
 | 
					            feedback_send.assert_called_once()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # reset
 | 
				
			||||||
 | 
					            feedback_send.reset_mock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # basic send, with user
 | 
				
			||||||
 | 
					            self.request.user = user
 | 
				
			||||||
 | 
					            self.request.POST['user_uuid'] = user.uuid
 | 
				
			||||||
 | 
					            with patch.object(mod, 'Session', return_value=self.session):
 | 
				
			||||||
 | 
					                context = view.feedback()
 | 
				
			||||||
 | 
					            self.assertEqual(context, {'ok': True})
 | 
				
			||||||
 | 
					            feedback_send.assert_called_once()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # reset
 | 
				
			||||||
 | 
					            self.request.user = None
 | 
				
			||||||
 | 
					            feedback_send.reset_mock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # invalid form data
 | 
				
			||||||
 | 
					            self.request.POST = {'message': 'hello world'}
 | 
				
			||||||
 | 
					            context = view.feedback()
 | 
				
			||||||
 | 
					            self.assertEqual(list(context), ['error'])
 | 
				
			||||||
 | 
					            self.assertIn('Required', context['error'])
 | 
				
			||||||
 | 
					            feedback_send.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # error on send
 | 
				
			||||||
 | 
					            self.request.POST = {
 | 
				
			||||||
 | 
					                'referrer': '/foo',
 | 
				
			||||||
 | 
					                'user_name': "Barney Rubble",
 | 
				
			||||||
 | 
					                'message': "hello world",
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            feedback_send.side_effect = RuntimeError
 | 
				
			||||||
 | 
					            context = view.feedback()
 | 
				
			||||||
 | 
					            feedback_send.assert_called_once()
 | 
				
			||||||
 | 
					            self.assertEqual(list(context), ['error'])
 | 
				
			||||||
 | 
					            self.assertIn('RuntimeError', context['error'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_setup(self):
 | 
					    def test_setup(self):
 | 
				
			||||||
        self.pyramid_config.add_route('home', '/')
 | 
					        self.pyramid_config.add_route('home', '/')
 | 
				
			||||||
        self.pyramid_config.add_route('login', '/login')
 | 
					        self.pyramid_config.add_route('login', '/login')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue