Add basic support for Poser reports, list/create
This commit is contained in:
		
							parent
							
								
									a3195267c9
								
							
						
					
					
						commit
						72177aef0a
					
				
					 8 changed files with 512 additions and 4 deletions
				
			
		|  | @ -674,10 +674,18 @@ class Form(object): | |||
|            case the validator pertains to the form at large instead of | ||||
|            one of the fields. | ||||
| 
 | ||||
|            TODO: what should the validator look like? | ||||
| 
 | ||||
|         :param validator: Callable validator for the node. | ||||
|         """ | ||||
|         self.validators[key] = validator | ||||
| 
 | ||||
|         # we normally apply the validator when creating the schema, so | ||||
|         # if this form already has a schema, then go ahead and apply | ||||
|         # the validator to it | ||||
|         if self.schema and key in self.schema: | ||||
|             self.schema[key].validator = validator | ||||
| 
 | ||||
|     def set_required(self, key, required=True): | ||||
|         """ | ||||
|         Set whether or not value is required for a given field. | ||||
|  |  | |||
							
								
								
									
										77
									
								
								tailbone/templates/poser/reports/view.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								tailbone/templates/poser/reports/view.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,77 @@ | |||
| ## -*- coding: utf-8; -*- | ||||
| <%inherit file="/master/view.mako" /> | ||||
| 
 | ||||
| <%def name="render_form_buttons()"> | ||||
|   <div v-if="!showUploadForm" class="buttons"> | ||||
|     <b-button type="is-primary" | ||||
|               @click="heckYeah()"> | ||||
|       Upload Replacement Module | ||||
|     </b-button> | ||||
|     <once-button type="is-primary" | ||||
|                  tag="a" | ||||
|                  % if instance.get('error'): | ||||
|                  href="#" disabled | ||||
|                  % else: | ||||
|                  href="${url('generate_specific_report', type_key=instance['report'].type_key)}" | ||||
|                  % endif | ||||
|                  text="Generate this Report"> | ||||
|     </once-button> | ||||
|   </div> | ||||
|   <div v-if="showUploadForm"> | ||||
|     ${h.form(master.get_action_url('replace', instance), enctype='multipart/form-data', **{'@submit': 'uploadSubmitting = true'})} | ||||
|     ${h.csrf_token(request)} | ||||
|     <b-field label="New Module File" horizontal> | ||||
| 
 | ||||
|       <b-field class="file is-primary" | ||||
|                :class="{'has-name': !!uploadFile}" | ||||
|                > | ||||
|         <b-upload name="replacement_module" | ||||
|                   v-model="uploadFile" | ||||
|                   class="file-label"> | ||||
|           <span class="file-cta"> | ||||
|             <b-icon class="file-icon" pack="fas" icon="upload"></b-icon> | ||||
|             <span class="file-label">Click to upload</span> | ||||
|           </span> | ||||
|         </b-upload> | ||||
|         <span v-if="uploadFile" | ||||
|               class="file-name"> | ||||
|           {{ uploadFile.name }} | ||||
|         </span> | ||||
|       </b-field> | ||||
| 
 | ||||
|       <div class="buttons"> | ||||
|         <b-button @click="showUploadForm = false"> | ||||
|           Cancel | ||||
|         </b-button> | ||||
|         <b-button type="is-primary" | ||||
|                   native-type="submit" | ||||
|                   :disabled="uploadSubmitting || !uploadFile" | ||||
|                   icon-pack="fas" | ||||
|                   icon-left="save"> | ||||
|           {{ uploadSubmitting ? "Working, please wait..." : "Save" }} | ||||
|         </b-button> | ||||
|       </div> | ||||
| 
 | ||||
|     </b-field> | ||||
|     ${h.end_form()} | ||||
|   </div> | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="modify_this_page_vars()"> | ||||
|   ${parent.modify_this_page_vars()} | ||||
|   <script type="text/javascript"> | ||||
| 
 | ||||
|     ${form.component_studly}Data.showUploadForm = false | ||||
| 
 | ||||
|     ${form.component_studly}Data.uploadFile = null | ||||
| 
 | ||||
|     ${form.component_studly}Data.uploadSubmitting = false | ||||
| 
 | ||||
|     ${form.component_studly}.methods.heckYeah = function() { | ||||
|         this.showUploadForm = true | ||||
|     } | ||||
| 
 | ||||
|   </script> | ||||
| </%def> | ||||
| 
 | ||||
| ${parent.body()} | ||||
							
								
								
									
										57
									
								
								tailbone/templates/poser/setup.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								tailbone/templates/poser/setup.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | |||
| ## -*- coding: utf-8; -*- | ||||
| <%inherit file="/page.mako" /> | ||||
| 
 | ||||
| <%def name="title()">Poser Setup</%def> | ||||
| 
 | ||||
| <%def name="page_content()"> | ||||
|   <br /> | ||||
| 
 | ||||
|   <p class="block"> | ||||
|     Before you can use Poser features, ${app_title} must create the | ||||
|     file structure for it. | ||||
|   </p> | ||||
| 
 | ||||
|   <p class="block"> | ||||
|     A new folder will be created at this location:    | ||||
|     <span class="is-family-monospace has-text-weight-bold"> | ||||
|       ${poser_dir} | ||||
|     </span> | ||||
|   </p> | ||||
| 
 | ||||
|   <p class="block"> | ||||
|     Once set up, ${app_title} can generate code for certain features, | ||||
|     in the Poser folder.  You can then access these features from | ||||
|     within ${app_title}. | ||||
|   </p> | ||||
| 
 | ||||
|   <p class="block"> | ||||
|     You are free to edit most files in the Poser folder as well.  | ||||
|     When you do so ${app_title} should pick up on the changes with no | ||||
|     need for app restart. | ||||
|   </p> | ||||
| 
 | ||||
|   <p class="block"> | ||||
|     Proceed? | ||||
|   </p> | ||||
| 
 | ||||
|   ${h.form(request.current_route_url(), **{'@submit': 'setupSubmitting = true'})} | ||||
|   ${h.csrf_token(request)} | ||||
|   <b-button type="is-primary" | ||||
|             native-type="submit" | ||||
|             :disabled="setupSubmitting"> | ||||
|     {{ setupSubmitting ? "Working, please wait..." : "Go for it!" }} | ||||
|   </b-button> | ||||
|   ${h.end_form()} | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="modify_this_page_vars()"> | ||||
|   ${parent.modify_this_page_vars()} | ||||
|   <script type="text/javascript"> | ||||
| 
 | ||||
|     ThisPageData.setupSubmitting = false | ||||
| 
 | ||||
|   </script> | ||||
| </%def> | ||||
| 
 | ||||
| 
 | ||||
| ${parent.body()} | ||||
|  | @ -32,7 +32,7 @@ import rattail | |||
| from rattail.db import model | ||||
| from rattail.batch import consume_batch_id | ||||
| from rattail.mail import send_email | ||||
| from rattail.util import OrderedDict | ||||
| from rattail.util import OrderedDict, simple_error | ||||
| from rattail.files import resource_path | ||||
| 
 | ||||
| from pyramid import httpexceptions | ||||
|  | @ -188,6 +188,32 @@ class CommonView(View): | |||
|         """ | ||||
|         raise Exception("Congratulations, you have triggered a bogus error.") | ||||
| 
 | ||||
|     def poser_setup(self): | ||||
|         if not self.request.is_root: | ||||
|             raise self.forbidden() | ||||
| 
 | ||||
|         use_buefy = self.get_use_buefy() | ||||
|         app = self.get_rattail_app() | ||||
|         app_title = self.rattail_config.app_title() | ||||
|         poser_handler = app.get_poser_handler() | ||||
| 
 | ||||
|         if self.request.method == 'POST': | ||||
|             try: | ||||
|                 path = poser_handler.make_poser_dir() | ||||
|             except Exception as error: | ||||
|                 self.request.session.flash(simple_error(error), 'error') | ||||
|             else: | ||||
|                 self.request.session.flash("Poser folder created at:  {}".format(path)) | ||||
|                 self.request.session.flash("Please restart the web app!", 'warning') | ||||
|                 return self.redirect(self.request.route_url('home')) | ||||
| 
 | ||||
|         return { | ||||
|             'use_buefy': use_buefy, | ||||
|             'app_title': app_title, | ||||
|             'index_title': app_title, | ||||
|             'poser_dir': poser_handler.get_default_poser_dir(), | ||||
|         } | ||||
| 
 | ||||
|     @classmethod | ||||
|     def defaults(cls, config): | ||||
|         cls._defaults(config) | ||||
|  | @ -249,6 +275,14 @@ class CommonView(View): | |||
|         config.add_route('bogus_error', '/bogus-error') | ||||
|         config.add_view(cls, attr='bogus_error', route_name='bogus_error', permission='errors.bogus') | ||||
| 
 | ||||
|         # make poser dir | ||||
|         config.add_route('poser_setup', '/poser-setup') | ||||
|         config.add_view(cls, attr='poser_setup', | ||||
|                         route_name='poser_setup', | ||||
|                         renderer='/poser/setup.mako', | ||||
|                         # nb. root only | ||||
|                         permission='admin') | ||||
| 
 | ||||
| 
 | ||||
| def includeme(config): | ||||
|     CommonView.defaults(config) | ||||
|  |  | |||
|  | @ -638,7 +638,7 @@ class MasterView(View): | |||
|         """ | ||||
|         self.creating = True | ||||
|         if form is None: | ||||
|             form = self.make_form(self.get_model_class()) | ||||
|             form = self.make_create_form() | ||||
|         if self.request.method == 'POST': | ||||
|             if self.validate_form(form): | ||||
|                 # let save_create_form() return alternate object if necessary | ||||
|  | @ -651,6 +651,9 @@ class MasterView(View): | |||
|             context['dform'] = form.make_deform_form() | ||||
|         return self.render_to_response(template, context) | ||||
| 
 | ||||
|     def make_create_form(self): | ||||
|         return self.make_form(self.get_model_class()) | ||||
| 
 | ||||
|     def save_create_form(self, form): | ||||
|         uploads = self.normalize_uploads(form) | ||||
|         self.before_create(form) | ||||
|  | @ -3618,7 +3621,10 @@ class MasterView(View): | |||
|         raise NotImplementedError | ||||
| 
 | ||||
|     def render_downloadable_file(self, obj, field): | ||||
|         if hasattr(obj, field): | ||||
|             filename = getattr(obj, field) | ||||
|         else: | ||||
|             filename = obj[field] | ||||
|         if not filename: | ||||
|             return "" | ||||
|         path = self.download_path(obj, filename) | ||||
|  |  | |||
							
								
								
									
										31
									
								
								tailbone/views/poser/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								tailbone/views/poser/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  Rattail -- Retail Software Framework | ||||
| #  Copyright © 2010-2022 Lance Edgar | ||||
| # | ||||
| #  This file is part of Rattail. | ||||
| # | ||||
| #  Rattail is free software: you can redistribute it and/or modify it under the | ||||
| #  terms of the GNU General Public License as published by the Free Software | ||||
| #  Foundation, either version 3 of the License, or (at your option) any later | ||||
| #  version. | ||||
| # | ||||
| #  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY | ||||
| #  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | ||||
| #  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more | ||||
| #  details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Rattail.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| Poser Views | ||||
| """ | ||||
| 
 | ||||
| from __future__ import unicode_literals, absolute_import | ||||
| 
 | ||||
| 
 | ||||
| def includeme(config): | ||||
|     config.include('tailbone.views.poser.reports') | ||||
							
								
								
									
										294
									
								
								tailbone/views/poser/reports.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								tailbone/views/poser/reports.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,294 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  Rattail -- Retail Software Framework | ||||
| #  Copyright © 2010-2022 Lance Edgar | ||||
| # | ||||
| #  This file is part of Rattail. | ||||
| # | ||||
| #  Rattail is free software: you can redistribute it and/or modify it under the | ||||
| #  terms of the GNU General Public License as published by the Free Software | ||||
| #  Foundation, either version 3 of the License, or (at your option) any later | ||||
| #  version. | ||||
| # | ||||
| #  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY | ||||
| #  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | ||||
| #  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more | ||||
| #  details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Rattail.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| Poser Report Views | ||||
| """ | ||||
| 
 | ||||
| from __future__ import unicode_literals, absolute_import | ||||
| 
 | ||||
| import os | ||||
| 
 | ||||
| import six | ||||
| 
 | ||||
| from rattail.util import simple_error | ||||
| 
 | ||||
| import colander | ||||
| from deform import widget as dfwidget | ||||
| from webhelpers2.html import HTML, tags | ||||
| 
 | ||||
| from tailbone.views import MasterView | ||||
| 
 | ||||
| 
 | ||||
| class PoserReportView(MasterView): | ||||
|     """ | ||||
|     Master view for Poser reports | ||||
|     """ | ||||
|     normalized_model_name = 'poserreport' | ||||
|     model_title = "Poser Report" | ||||
|     model_key = 'report_key' | ||||
|     route_prefix = 'poser.reports' | ||||
|     url_prefix = '/poser/reports' | ||||
|     filterable = False | ||||
|     pageable = False | ||||
|     editable = False            # TODO: should allow this somehow? | ||||
|     downloadable = True | ||||
| 
 | ||||
|     labels = { | ||||
|         'report_key': "Poser Key", | ||||
|     } | ||||
| 
 | ||||
|     grid_columns = [ | ||||
|         'report_key', | ||||
|         'report_name', | ||||
|         'description', | ||||
|         'error', | ||||
|     ] | ||||
| 
 | ||||
|     form_fields = [ | ||||
|         'report_key', | ||||
|         'report_name', | ||||
|         'description', | ||||
|         'flavor', | ||||
|         'include_comments', | ||||
|         'module_file', | ||||
|         'module_file_path', | ||||
|         'error', | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self, request): | ||||
|         super(PoserReportView, self).__init__(request) | ||||
|         app = self.get_rattail_app() | ||||
|         self.poser_handler = app.get_poser_handler() | ||||
| 
 | ||||
|         # nb. pre-load all reports b/c all views potentially need | ||||
|         # access to the data set | ||||
|         self.data = self.get_data() | ||||
| 
 | ||||
|     def get_data(self, session=None): | ||||
|         if hasattr(self, 'data'): | ||||
|             return self.data | ||||
| 
 | ||||
|         try: | ||||
|             return self.poser_handler.get_all_reports() | ||||
| 
 | ||||
|         except Exception as error: | ||||
|             self.request.session.flash(simple_error(error), 'error') | ||||
| 
 | ||||
|             if not self.request.is_root: | ||||
|                 self.request.session.flash("You must become root in order " | ||||
|                                            "to do Poser Setup.", 'error') | ||||
|             else: | ||||
|                 link = tags.link_to("Poser Setup", | ||||
|                                     self.request.route_url('poser_setup')) | ||||
|                 msg = HTML.literal("Please see the {} page.".format(link)) | ||||
|                 self.request.session.flash(msg, 'error') | ||||
|             return [] | ||||
| 
 | ||||
|     def configure_grid(self, g): | ||||
|         super(PoserReportView, self).configure_grid(g) | ||||
| 
 | ||||
|         g.sorters['report_key'] = g.make_simple_sorter('report_key', foldcase=True) | ||||
|         g.sorters['report_name'] = g.make_simple_sorter('report_name', foldcase=True) | ||||
| 
 | ||||
|         g.set_renderer('error', self.render_report_error) | ||||
| 
 | ||||
|         g.set_sort_defaults('report_name') | ||||
| 
 | ||||
|         g.set_link('report_key') | ||||
|         g.set_link('report_name') | ||||
|         g.set_link('description') | ||||
|         g.set_link('error') | ||||
| 
 | ||||
|         g.set_searchable('report_key') | ||||
|         g.set_searchable('report_name') | ||||
|         g.set_searchable('description') | ||||
| 
 | ||||
|         if self.request.has_perm('report_output.create'): | ||||
|             g.more_actions.append(self.make_action( | ||||
|                 'generate', icon='arrow-circle-right', | ||||
|                 url=self.get_generate_url)) | ||||
| 
 | ||||
|     def get_generate_url(self, report, i=None): | ||||
|         if not report.get('error'): | ||||
|             return self.request.route_url('generate_specific_report', | ||||
|                                           type_key=report['report'].type_key) | ||||
| 
 | ||||
|     def render_report_error(self, report, field): | ||||
|         error = report.get('error') | ||||
|         if error: | ||||
|             return HTML.tag('span', class_='has-background-warning', c=[error]) | ||||
| 
 | ||||
|     def get_instance(self): | ||||
|         report_key = self.request.matchdict['report_key'] | ||||
|         for report in self.get_data(): | ||||
|             if report['report_key'] == report_key: | ||||
|                 return report | ||||
|         raise self.notfound() | ||||
| 
 | ||||
|     def get_instance_title(self, report): | ||||
|         return report['report_name'] | ||||
| 
 | ||||
|     def make_form_schema(self): | ||||
|         return PoserReportSchema() | ||||
| 
 | ||||
|     def make_create_form(self): | ||||
|         return self.make_form({}) | ||||
| 
 | ||||
|     def save_create_form(self, form): | ||||
|         self.before_create(form) | ||||
| 
 | ||||
|         report = self.poser_handler.make_report( | ||||
|             form.validated['report_key'], | ||||
|             form.validated['report_name'], | ||||
|             form.validated['description'], | ||||
|             flavor=form.validated['flavor'], | ||||
|             include_comments=form.validated['include_comments']) | ||||
| 
 | ||||
|         return report | ||||
| 
 | ||||
|     def configure_form(self, f): | ||||
|         super(PoserReportView, self).configure_form(f) | ||||
|         report = f.model_instance | ||||
| 
 | ||||
|         # report_key | ||||
|         f.set_default('report_key', 'cool_widgets') | ||||
|         f.set_helptext('report_key', "Unique computer-friendly key for the report type.") | ||||
|         if self.creating: | ||||
|             f.set_validator('report_key', self.unique_report_key) | ||||
| 
 | ||||
|         # report_name | ||||
|         f.set_default('report_name', "Cool Widgets Weekly") | ||||
|         f.set_helptext('report_name', "Human-friendly display name for the report.") | ||||
| 
 | ||||
|         # description | ||||
|         f.set_default('description', "How many cool widgets we come across each week") | ||||
|         f.set_helptext('description', "Brief description of the report.") | ||||
| 
 | ||||
|         # flavor | ||||
|         if self.creating: | ||||
|             f.set_helptext('flavor', "Determines the type of sample code to generate.") | ||||
|             flavors = self.poser_handler.get_supported_report_flavors() | ||||
|             values = [(key, flavor['description']) | ||||
|                       for key, flavor in six.iteritems(flavors)] | ||||
|             f.set_widget('flavor', dfwidget.SelectWidget(values=values)) | ||||
|             f.set_validator('flavor', colander.OneOf(flavors)) | ||||
|             if flavors: | ||||
|                 f.set_default('flavor', list(flavors)[0]) | ||||
|         else: | ||||
|             f.remove('flavor') | ||||
| 
 | ||||
|         # include_comments | ||||
|         if not self.creating: | ||||
|             f.remove('include_comments') | ||||
| 
 | ||||
|         # module_file | ||||
|         if self.creating: | ||||
|             f.remove('module_file') | ||||
|         else: | ||||
|             # nb. set this key as workaround for render method, which | ||||
|             # expects object to have this field | ||||
|             report['module_file'] = os.path.basename(report['module_file_path']) | ||||
|             f.set_renderer('module_file', self.render_downloadable_file) | ||||
| 
 | ||||
|         # error | ||||
|         if self.creating or not report.get('error'): | ||||
|             f.remove('error') | ||||
|         else: | ||||
|             f.set_renderer('error', self.render_report_error) | ||||
| 
 | ||||
|     def unique_report_key(self, node, value): | ||||
|         for report in self.get_data(): | ||||
|             if report['report_key'] == value: | ||||
|                 raise node.raise_invalid("Poser report key must be unique") | ||||
| 
 | ||||
|     def download_path(self, report, filename): | ||||
|         return report['module_file_path'] | ||||
| 
 | ||||
|     def delete_instance(self, report): | ||||
|         self.poser_handler.delete_report(report['report_key']) | ||||
| 
 | ||||
|     def replace(self): | ||||
|         app = self.get_rattail_app() | ||||
|         report = self.get_instance() | ||||
| 
 | ||||
|         value = self.request.POST['replacement_module'] | ||||
|         tempdir = app.make_temp_dir() | ||||
|         filepath = os.path.join(tempdir, os.path.basename(value.filename)) | ||||
|         with open(filepath, 'wb') as f: | ||||
|             f.write(value.file.read()) | ||||
| 
 | ||||
|         try: | ||||
|             newreport = self.poser_handler.replace_report(report['report_key'], | ||||
|                                                           filepath) | ||||
|         except Exception as error: | ||||
|             self.request.session.flash(simple_error(error), 'error') | ||||
|         else: | ||||
|             report = newreport | ||||
|         finally: | ||||
|             os.remove(filepath) | ||||
|             os.rmdir(tempdir) | ||||
| 
 | ||||
|         return self.redirect(self.get_action_url('view', report)) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def defaults(cls, config): | ||||
|         cls._poser_report_defaults(config) | ||||
|         cls._defaults(config) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def _poser_report_defaults(cls, config): | ||||
|         route_prefix = cls.get_route_prefix() | ||||
|         instance_url_prefix = cls.get_instance_url_prefix() | ||||
| 
 | ||||
|         # replace module | ||||
|         config.add_route('{}.replace'.format(route_prefix), | ||||
|                          '{}/replace'.format(instance_url_prefix), | ||||
|                          request_method='POST') | ||||
|         config.add_view(cls, attr='replace', | ||||
|                         route_name='{}.replace'.format(route_prefix), | ||||
|                          # TODO: requires root, should add custom permission? | ||||
|                         permission='admin') | ||||
| 
 | ||||
| 
 | ||||
| class PoserReportSchema(colander.MappingSchema): | ||||
| 
 | ||||
|     report_key = colander.SchemaNode(colander.String()) | ||||
| 
 | ||||
|     report_name = colander.SchemaNode(colander.String()) | ||||
| 
 | ||||
|     description = colander.SchemaNode(colander.String()) | ||||
| 
 | ||||
|     flavor = colander.SchemaNode(colander.String()) | ||||
| 
 | ||||
|     include_comments = colander.SchemaNode(colander.Bool()) | ||||
| 
 | ||||
| 
 | ||||
| def defaults(config, **kwargs): | ||||
|     base = globals() | ||||
| 
 | ||||
|     PoserReportView = kwargs.get('PoserReportView', base['PoserReportView']) | ||||
|     PoserReportView.defaults(config) | ||||
| 
 | ||||
| 
 | ||||
| def includeme(config): | ||||
|     defaults(config) | ||||
|  | @ -400,7 +400,8 @@ class ReportOutputView(ExportMasterView): | |||
|         form = forms.Form(schema=schema, request=self.request, | ||||
|                           use_buefy=use_buefy, helptext=helptext) | ||||
|         form.submit_label = "Generate this Report" | ||||
|         form.cancel_url = self.request.route_url('{}.create'.format(route_prefix)) | ||||
|         form.cancel_url = self.request.get_referrer( | ||||
|             default=self.request.route_url('{}.create'.format(route_prefix))) | ||||
| 
 | ||||
|         # must declare jquery support for date fields, ugh | ||||
|         # TODO: obviously would be nice for this to be automatic? | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar