OMG a ridiculous commit to overhaul import handler config etc.
- add `MasterView.configurable` concept, `/configure.mako` template - add new master view for DataSync Threads (needs content) - tweak view config for DataSync Changes accordingly - update the Configure DataSync page per `configurable` concept - add new Configure Import/Export page, per `configurable` - add basic views for Raw Permissions
This commit is contained in:
		
							parent
							
								
									282185c5af
								
							
						
					
					
						commit
						cc4b2278e7
					
				
					 10 changed files with 735 additions and 238 deletions
				
			
		
							
								
								
									
										175
									
								
								tailbone/templates/configure.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								tailbone/templates/configure.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,175 @@ | |||
| ## -*- coding: utf-8; -*- | ||||
| <%inherit file="/page.mako" /> | ||||
| 
 | ||||
| <%def name="title()">Configure ${config_title}</%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> | ||||
|     <once-button tag="a" href="${request.current_route_url()}" | ||||
|                  @click="undoChanges = true" | ||||
|                  icon-left="undo" | ||||
|                  text="Undo All Changes"> | ||||
|     </once-button> | ||||
|   </div> | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="purge_button()"> | ||||
|   <b-button type="is-danger" | ||||
|             @click="purgeSettingsInit()" | ||||
|             icon-pack="fas" | ||||
|             icon-left="trash"> | ||||
|     Remove All Settings | ||||
|   </b-button> | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="buttons_row()"> | ||||
|   <div class="level"> | ||||
|     <div class="level-left"> | ||||
| 
 | ||||
|       <div class="level-item"> | ||||
|         <p class="block"> | ||||
|           This tool lets you modify the ${config_title} configuration. | ||||
|         </p> | ||||
|       </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="page_content()"> | ||||
|   ${parent.page_content()} | ||||
| 
 | ||||
|   <br /> | ||||
| 
 | ||||
|   ${self.buttons_row()} | ||||
| 
 | ||||
|   <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"> | ||||
|           If you like we can remove all ${config_title} | ||||
|           settings from the DB. | ||||
|         </p> | ||||
|         <p class="block"> | ||||
|           Note that the tool normally removes all settings first, | ||||
|           every time you click "Save Settings" - here though you can | ||||
|           "just remove and not save" the settings. | ||||
|         </p> | ||||
|         <p class="block"> | ||||
|           Note also that this will of course  | ||||
|           <span class="is-italic">not</span> remove any settings from | ||||
|           your config files, so after removing from DB, | ||||
|           <span class="is-italic">only</span> your config file | ||||
|           settings should be in effect. | ||||
|         </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" }} | ||||
|         </b-button> | ||||
|         ${h.end_form()} | ||||
|       </footer> | ||||
|     </div> | ||||
|   </b-modal> | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="modify_this_page_vars()"> | ||||
|   ${parent.modify_this_page_vars()} | ||||
|   <script type="text/javascript"> | ||||
| 
 | ||||
|     ThisPageData.purgeSettingsShowDialog = false | ||||
|     ThisPageData.purgingSettings = false | ||||
| 
 | ||||
|     ThisPageData.settingsNeedSaved = false | ||||
|     ThisPageData.undoChanges = false | ||||
|     ThisPageData.savingSettings = false | ||||
| 
 | ||||
|     ThisPage.methods.purgeSettingsInit = function() { | ||||
|         this.purgeSettingsShowDialog = true | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.methods.settingsCollectParams = function() { | ||||
|         return {} | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.methods.saveSettings = function() { | ||||
|         this.savingSettings = true | ||||
| 
 | ||||
|         let url = ${json.dumps(request.current_route_url())|n} | ||||
|         let params = this.settingsCollectParams() | ||||
|         let headers = { | ||||
|             'X-CSRF-TOKEN': this.csrftoken, | ||||
|         } | ||||
| 
 | ||||
|         this.$http.post(url, params, {headers: headers}).then((response) => { | ||||
|             if (response.data.success) { | ||||
|                 this.settingsNeedSaved = false | ||||
|                 location.href = url // reload page | ||||
|             } else { | ||||
|                 this.$buefy.toast.open({ | ||||
|                     message: "Save failed:  " + (response.data.error || "(unknown error)"), | ||||
|                     type: 'is-danger', | ||||
|                     duration: 4000, // 4 seconds | ||||
|                 }) | ||||
|             } | ||||
|         }).catch((error) => { | ||||
|             this.$buefy.toast.open({ | ||||
|                 message: "Save failed:  (unknown error)", | ||||
|                 type: 'is-danger', | ||||
|                 duration: 4000, // 4 seconds | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     // cf. https://stackoverflow.com/a/56551646 | ||||
|     ThisPage.methods.beforeWindowUnload = function(e) { | ||||
|         if (this.settingsNeedSaved && !this.undoChanges) { | ||||
|             e.preventDefault() | ||||
|             e.returnValue = '' | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.created = function() { | ||||
|         window.addEventListener('beforeunload', this.beforeWindowUnload) | ||||
|     } | ||||
| 
 | ||||
|   </script> | ||||
| </%def> | ||||
| 
 | ||||
| 
 | ||||
| ${parent.body()} | ||||
|  | @ -3,8 +3,8 @@ | |||
| 
 | ||||
| <%def name="context_menu_items()"> | ||||
|   ${parent.context_menu_items()} | ||||
|   % if master.has_perm('configure'): | ||||
|       ${h.link_to("Configure DataSync", url('datasync.configure'))} | ||||
|   % if request.has_perm('datasync.list'): | ||||
|       <li>${h.link_to("View DataSync Threads", url('datasync'))}</li> | ||||
|   % endif | ||||
| </%def> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,13 +1,10 @@ | |||
| ## -*- coding: utf-8; -*- | ||||
| <%inherit file="/page.mako" /> | ||||
| 
 | ||||
| <%def name="title()">Configure DataSync</%def> | ||||
| 
 | ||||
| <%def name="page_content()"> | ||||
|   <br /> | ||||
| <%inherit file="/configure.mako" /> | ||||
| 
 | ||||
| <%def name="buttons_row()"> | ||||
|   <div class="level"> | ||||
|     <div class="level-left"> | ||||
| 
 | ||||
|       <div class="level-item"> | ||||
|         <p class="block"> | ||||
|           This tool lets you modify the DataSync configuration.  | ||||
|  | @ -19,24 +16,13 @@ | |||
|         </p> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="level-item buttons" | ||||
|            v-if="settingsNeedSaved"> | ||||
|         <b-button type="is-primary" | ||||
|                   @click="saveSettings" | ||||
|                   :disabled="savingSettings" | ||||
|                   icon-pack="fas" | ||||
|                   icon-left="save"> | ||||
|           {{ saveSettingsButtonText }} | ||||
|         </b-button> | ||||
|         <once-button tag="a" href="${request.current_route_url()}" | ||||
|                      @click="undoChanges = true" | ||||
|                      icon-left="undo" | ||||
|                      text="Undo All Changes"> | ||||
|         </once-button> | ||||
|       <div class="level-item"> | ||||
|         ${self.save_undo_buttons()} | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="level-right"> | ||||
| 
 | ||||
|       <div class="level-item"> | ||||
|         ${h.form(url('datasync.restart'), **{'@submit': 'submitRestartDatasyncForm'})} | ||||
|         ${h.csrf_token(request)} | ||||
|  | @ -50,56 +36,16 @@ | |||
|         </b-button> | ||||
|         ${h.end_form()} | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="level-item"> | ||||
|         <b-button type="is-danger" | ||||
|                   @click="purgeSettingsInit()" | ||||
|                   icon-pack="fas" | ||||
|                   icon-left="trash"> | ||||
|           Remove All Settings | ||||
|         </b-button> | ||||
|         ${self.purge_button()} | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </%def> | ||||
| 
 | ||||
|   <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"> | ||||
|           If you like we can remove all DataSync settings from the DB. | ||||
|         </p> | ||||
|         <p class="block"> | ||||
|           Note that this tool normally removes all settings first, | ||||
|           every time you click "Save Settings".  Here though you | ||||
|           can "just remove" and <span class="is-italic">not</span> | ||||
|           save the current settings. | ||||
|         </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('purge_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" }} | ||||
|         </b-button> | ||||
|         ${h.end_form()} | ||||
|       </footer> | ||||
|     </div> | ||||
|   </b-modal> | ||||
| <%def name="page_content()"> | ||||
|   ${parent.page_content()} | ||||
| 
 | ||||
|   <b-notification type="is-warning" | ||||
|                   :active.sync="showConfigFilesNote"> | ||||
|  | @ -496,13 +442,6 @@ | |||
| 
 | ||||
|     ThisPageData.restartCommand = ${json.dumps(restart_command)|n} | ||||
| 
 | ||||
|     ThisPageData.purgeSettingsShowDialog = false | ||||
|     ThisPageData.purgingSettings = false | ||||
| 
 | ||||
|     ThisPageData.settingsNeedSaved = false | ||||
|     ThisPageData.undoChanges = false | ||||
|     ThisPageData.savingSettings = false | ||||
| 
 | ||||
|     ThisPage.computed.filteredProfilesData = function() { | ||||
|         if (this.showDisabledProfiles) { | ||||
|             return this.profilesData | ||||
|  | @ -539,13 +478,6 @@ | |||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.computed.saveSettingsButtonText = function() { | ||||
|         if (this.savingSettings) { | ||||
|             return "Working, please wait..." | ||||
|         } | ||||
|         return "Save All Settings" | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.methods.toggleDisabledProfiles = function() { | ||||
|         this.showDisabledProfiles = !this.showDisabledProfiles | ||||
|     } | ||||
|  | @ -743,53 +675,11 @@ | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.methods.purgeSettingsInit = function() { | ||||
|         this.purgeSettingsShowDialog = true | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.methods.saveSettings = function() { | ||||
|         this.savingSettings = true | ||||
|         let url = ${json.dumps(request.current_route_url())|n} | ||||
| 
 | ||||
|         let params = { | ||||
|     ThisPage.methods.settingsCollectParams = function() { | ||||
|         return { | ||||
|             profiles: this.profilesData, | ||||
|             restart_command: this.restartCommand, | ||||
|         } | ||||
| 
 | ||||
|         let headers = { | ||||
|             'X-CSRF-TOKEN': this.csrftoken, | ||||
|         } | ||||
| 
 | ||||
|         this.$http.post(url, params, {headers: headers}).then((response) => { | ||||
|             if (response.data.success) { | ||||
|                 this.settingsNeedSaved = false | ||||
|                 location.href = url // reload page | ||||
|             } else { | ||||
|                 this.$buefy.toast.open({ | ||||
|                     message: "Save failed:  " + (response.data.error || "(unknown error)"), | ||||
|                     type: 'is-danger', | ||||
|                     duration: 4000, // 4 seconds | ||||
|                 }) | ||||
|             } | ||||
|         }).catch((error) => { | ||||
|             this.$buefy.toast.open({ | ||||
|                 message: "Save failed:  (unknown error)", | ||||
|                 type: 'is-danger', | ||||
|                 duration: 4000, // 4 seconds | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     // cf. https://stackoverflow.com/a/56551646 | ||||
|     ThisPage.methods.beforeWindowUnload = function(e) { | ||||
|         if (this.settingsNeedSaved && !this.undoChanges) { | ||||
|             e.preventDefault() | ||||
|             e.returnValue = '' | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.created = function() { | ||||
|         window.addEventListener('beforeunload', this.beforeWindowUnload) | ||||
|     } | ||||
| 
 | ||||
|     % if request.has_perm('datasync.restart'): | ||||
|  |  | |||
							
								
								
									
										19
									
								
								tailbone/templates/datasync/index.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tailbone/templates/datasync/index.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| ## -*- coding: utf-8; -*- | ||||
| <%inherit file="/master/index.mako" /> | ||||
| 
 | ||||
| <%def name="context_menu_items()"> | ||||
|   ${parent.context_menu_items()} | ||||
|   % if request.has_perm('datasync_changes.list'): | ||||
|       <li>${h.link_to("View DataSync Changes", url('datasyncchanges'))}</li> | ||||
|   % endif | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="render_grid_component()"> | ||||
|   <b-notification :closable="false"> | ||||
|     TODO: this page coming soon... | ||||
|   </b-notification> | ||||
|   ${parent.render_grid_component()} | ||||
| </%def> | ||||
| 
 | ||||
| 
 | ||||
| ${parent.body()} | ||||
							
								
								
									
										197
									
								
								tailbone/templates/importing/configure.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								tailbone/templates/importing/configure.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,197 @@ | |||
| ## -*- coding: utf-8; -*- | ||||
| <%inherit file="/configure.mako" /> | ||||
| 
 | ||||
| <%def name="page_content()"> | ||||
|   ${parent.page_content()} | ||||
| 
 | ||||
|   <h3 class="is-size-3">Designated Handlers</h3> | ||||
| 
 | ||||
|   <b-table :data="handlersData" | ||||
|            narrowed | ||||
|            icon-pack="fas" | ||||
|            :default-sort="['host_title', 'asc']"> | ||||
|       <template slot-scope="props"> | ||||
|         <b-table-column field="host_title" label="Data Source" sortable> | ||||
|           {{ props.row.host_title }} | ||||
|         </b-table-column> | ||||
|         <b-table-column field="local_title" label="Data Target" sortable> | ||||
|           {{ props.row.local_title }} | ||||
|         </b-table-column> | ||||
|         <b-table-column field="direction" label="Direction" sortable> | ||||
|           {{ props.row.direction_display }} | ||||
|         </b-table-column> | ||||
|         <b-table-column field="handler_spec" label="Handler Spec" sortable> | ||||
|           {{ props.row.handler_spec }} | ||||
|         </b-table-column> | ||||
|         <b-table-column field="cmd" label="Command" sortable> | ||||
|           {{ props.row.command }} {{ props.row.subcommand }} | ||||
|         </b-table-column> | ||||
|         <b-table-column field="runas" label="Default Runas" sortable> | ||||
|           {{ props.row.default_runas }} | ||||
|         </b-table-column> | ||||
|         <b-table-column label="Actions"> | ||||
|           <a href="#" class="grid-action" | ||||
|              @click.prevent="editHandler(props.row)"> | ||||
|             <i class="fas fa-edit"></i> | ||||
|             Edit | ||||
|           </a>           | ||||
|         </b-table-column> | ||||
|       </template> | ||||
|       <template slot="empty"> | ||||
|         <section class="section"> | ||||
|           <div class="content has-text-grey has-text-centered"> | ||||
|             <p> | ||||
|               <b-icon | ||||
|                  pack="fas" | ||||
|                  icon="fas fa-sad-tear" | ||||
|                  size="is-large"> | ||||
|               </b-icon> | ||||
|             </p> | ||||
|             <p>Nothing here.</p> | ||||
|           </div> | ||||
|         </section> | ||||
|       </template> | ||||
|   </b-table> | ||||
|    | ||||
|   <b-modal :active.sync="editHandlerShowDialog"> | ||||
|     <div class="card"> | ||||
|       <div class="card-content"> | ||||
| 
 | ||||
|         <b-field :label="editingHandlerDirection" horizontal expanded> | ||||
|           {{ editingHandlerHostTitle }} -> {{ editingHandlerLocalTitle }} | ||||
|         </b-field> | ||||
| 
 | ||||
|         <b-field label="Handler Spec" | ||||
|                  :type="editingHandlerSpec ? null : 'is-danger'"> | ||||
|           <b-select v-model="editingHandlerSpec"> | ||||
|             <option v-for="option in editingHandlerSpecOptions" | ||||
|                     :key="option" | ||||
|                     :value="option"> | ||||
|               {{ option }} | ||||
|             </option> | ||||
|           </b-select> | ||||
|         </b-field> | ||||
| 
 | ||||
|         <b-field grouped> | ||||
|            | ||||
|           <b-field label="Command" | ||||
|                    :type="editingHandlerCommand ? null : 'is-danger'"> | ||||
|             <div class="level"> | ||||
|               <div class="level-left"> | ||||
|                 <div class="level-item" style="margin-right: 0;"> | ||||
|                   bin/ | ||||
|                 </div> | ||||
|                 <div class="level-item" style="margin-left: 0;"> | ||||
|                   <b-input v-model="editingHandlerCommand"> | ||||
|                   </b-input> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </b-field> | ||||
| 
 | ||||
|           <b-field label="Subcommand" | ||||
|                    :type="editingHandlerSubcommand ? null : 'is-danger'"> | ||||
|             <b-input v-model="editingHandlerSubcommand"> | ||||
|             </b-input> | ||||
|           </b-field> | ||||
| 
 | ||||
|           <b-field label="Default Runas"> | ||||
|             <b-input v-model="editingHandlerRunas"> | ||||
|             </b-input> | ||||
|           </b-field> | ||||
| 
 | ||||
|         </b-field> | ||||
| 
 | ||||
|         <b-field grouped> | ||||
| 
 | ||||
|           <b-button @click="editHandlerShowDialog = false" | ||||
|                     class="control"> | ||||
|             Cancel | ||||
|           </b-button> | ||||
| 
 | ||||
|           <b-button type="is-primary" | ||||
|                     class="control" | ||||
|                     @click="updateHandler()" | ||||
|                     :disabled="updateHandlerDisabled"> | ||||
|             Update Handler | ||||
|           </b-button> | ||||
| 
 | ||||
|         </b-field> | ||||
| 
 | ||||
|       </div> | ||||
|     </div> | ||||
|   </b-modal> | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="modify_this_page_vars()"> | ||||
|   ${parent.modify_this_page_vars()} | ||||
|   <script type="text/javascript"> | ||||
| 
 | ||||
|     ThisPageData.handlersData = ${json.dumps(handlers_data)|n} | ||||
| 
 | ||||
|     ThisPageData.editHandlerShowDialog = false | ||||
|     ThisPageData.editingHandler = null | ||||
|     ThisPageData.editingHandlerHostTitle = null | ||||
|     ThisPageData.editingHandlerLocalTitle = null | ||||
|     ThisPageData.editingHandlerDirection = 'import' | ||||
|     ThisPageData.editingHandlerSpec = null | ||||
|     ThisPageData.editingHandlerSpecOptions = [] | ||||
|     ThisPageData.editingHandlerCommand = null | ||||
|     ThisPageData.editingHandlerSubcommand = null | ||||
|     ThisPageData.editingHandlerRunas = null | ||||
| 
 | ||||
|     ThisPageData.settingsNeedSaved = false | ||||
|     ThisPageData.undoChanges = false | ||||
|     ThisPageData.savingSettings = false | ||||
| 
 | ||||
|     ThisPage.computed.updateHandlerDisabled = function() { | ||||
|         if (!this.editingHandlerSpec) { | ||||
|             return true | ||||
|         } | ||||
|         if (!this.editingHandlerCommand) { | ||||
|             return true | ||||
|         } | ||||
|         if (!this.editingHandlerSubcommand) { | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.methods.editHandler = function(row) { | ||||
|         this.editingHandler = row | ||||
| 
 | ||||
|         this.editingHandlerHostTitle = row.host_title | ||||
|         this.editingHandlerLocalTitle = row.local_title | ||||
|         this.editingHandlerDirection = row.direction_display | ||||
|         this.editingHandlerSpec = row.handler_spec | ||||
|         this.editingHandlerSpecOptions = row.spec_options | ||||
|         this.editingHandlerCommand = row.command | ||||
|         this.editingHandlerSubcommand = row.subcommand | ||||
|         this.editingHandlerRunas = row.default_runas | ||||
| 
 | ||||
|         this.editHandlerShowDialog = true | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.methods.updateHandler = function() { | ||||
|         let row = this.editingHandler | ||||
| 
 | ||||
|         row.handler_spec = this.editingHandlerSpec | ||||
|         row.command = this.editingHandlerCommand | ||||
|         row.subcommand = this.editingHandlerSubcommand | ||||
|         row.default_runas = this.editingHandlerRunas | ||||
| 
 | ||||
|         this.settingsNeedSaved = true | ||||
|         this.editHandlerShowDialog = false | ||||
|     } | ||||
| 
 | ||||
|     ThisPage.methods.settingsCollectParams = function() { | ||||
|         return { | ||||
|             handlers: this.handlersData, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|   </script> | ||||
| </%def> | ||||
| 
 | ||||
| 
 | ||||
| ${parent.body()} | ||||
|  | @ -162,6 +162,9 @@ | |||
|           <li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li> | ||||
|       % endif | ||||
|   % endif | ||||
|   % if master.configurable and master.has_perm('configure'): | ||||
|       <li>${h.link_to("Configure {}".format(config_title), url('{}.configure'.format(route_prefix)))}</li> | ||||
|   % endif | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="grid_tools()"> | ||||
|  |  | |||
|  | @ -32,54 +32,44 @@ import logging | |||
| 
 | ||||
| from rattail.db import model | ||||
| from rattail.datasync.config import load_profiles | ||||
| from rattail.datasync.util import get_lastrun, purge_datasync_settings | ||||
| from rattail.datasync.util import purge_datasync_settings | ||||
| 
 | ||||
| from tailbone.views import MasterView | ||||
| from tailbone.util import csrf_token | ||||
| 
 | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class DataSyncChangeView(MasterView): | ||||
| class DataSyncThreadView(MasterView): | ||||
|     """ | ||||
|     Master view for the DataSyncChange model. | ||||
|     Master view for DataSync itself. | ||||
| 
 | ||||
|     This should (eventually) show all running threads in the main | ||||
|     index view, with status for each, sort of akin to "dashboard". | ||||
|     For now it only serves the config view. | ||||
|     """ | ||||
|     model_class = model.DataSyncChange | ||||
|     url_prefix = '/datasync/changes' | ||||
|     permission_prefix = 'datasync' | ||||
|     normalized_model_name = 'datasyncthread' | ||||
|     model_title = "DataSync Thread" | ||||
|     model_key = 'key' | ||||
|     route_prefix = 'datasync' | ||||
|     url_prefix = '/datasync' | ||||
|     viewable = False | ||||
|     creatable = False | ||||
|     editable = False | ||||
|     bulk_deletable = True | ||||
|     deletable = False | ||||
|     filterable = False | ||||
|     pageable = False | ||||
| 
 | ||||
|     labels = { | ||||
|         'batch_id': "Batch ID", | ||||
|     } | ||||
|     configurable = True | ||||
|     config_title = "DataSync" | ||||
| 
 | ||||
|     grid_columns = [ | ||||
|         'source', | ||||
|         'batch_id', | ||||
|         'batch_sequence', | ||||
|         'payload_type', | ||||
|         'payload_key', | ||||
|         'deletion', | ||||
|         'obtained', | ||||
|         'consumer', | ||||
|         'key', | ||||
|     ] | ||||
| 
 | ||||
|     def configure_grid(self, g): | ||||
|         super(DataSyncChangeView, self).configure_grid(g) | ||||
| 
 | ||||
|         # batch_sequence | ||||
|         g.set_label('batch_sequence', "Batch Seq.") | ||||
|         g.filters['batch_sequence'].label = "Batch Sequence" | ||||
| 
 | ||||
|         g.set_sort_defaults('obtained') | ||||
|         g.set_type('obtained', 'datetime') | ||||
| 
 | ||||
|     def template_kwargs_index(self, **kwargs): | ||||
|         kwargs['allow_filemon_restart'] = bool(self.rattail_config.get('tailbone', 'filemon.restart')) | ||||
|         return kwargs | ||||
|     def get_data(self, session=None): | ||||
|         data = [] | ||||
|         return data | ||||
| 
 | ||||
|     def restart(self): | ||||
|         cmd = self.rattail_config.getlist('tailbone', 'datasync.restart', | ||||
|  | @ -93,23 +83,7 @@ class DataSyncChangeView(MasterView): | |||
|             self.request.session.flash("DataSync daemon could not be restarted; result was: {}".format(result), 'error') | ||||
|         return self.redirect(self.request.get_referrer(default=self.request.route_url('datasyncchanges'))) | ||||
| 
 | ||||
|     def configure(self): | ||||
|         """ | ||||
|         View for configuring the DataSync daemon. | ||||
|         """ | ||||
|         if self.request.method == 'POST': | ||||
|             # if self.request.is_xhr and not self.request.POST: | ||||
|             if self.request.POST.get('purge_settings'): | ||||
|                 self.delete_settings() | ||||
|                 self.request.session.flash("Settings have been removed.") | ||||
|                 return self.redirect(self.request.current_route_url()) | ||||
|             else: | ||||
|                 data = self.request.json_body | ||||
|                 self.save_settings(data) | ||||
|                 self.request.session.flash("Settings have been saved.  " | ||||
|                                            "You should probably restart DataSync now.") | ||||
|                 return self.json_response({'success': True}) | ||||
| 
 | ||||
|     def configure_get_context(self): | ||||
|         profiles = load_profiles(self.rattail_config, | ||||
|                                  include_disabled=True, | ||||
|                                  ignore_problems=True) | ||||
|  | @ -148,27 +122,21 @@ class DataSyncChangeView(MasterView): | |||
|             profiles_data.append(data) | ||||
| 
 | ||||
|         return { | ||||
|             'master': self, | ||||
|             # TODO: really only buefy themes are supported here | ||||
|             'use_buefy': self.get_use_buefy(), | ||||
|             'index_title': "DataSync Changes", | ||||
|             'index_url': self.get_index_url(), | ||||
|             'profiles': profiles, | ||||
|             'profiles_data': profiles_data, | ||||
|             'restart_command': self.rattail_config.get('tailbone', 'datasync.restart'), | ||||
|             'system_user': getpass.getuser(), | ||||
|         } | ||||
| 
 | ||||
|     def save_settings(self, data): | ||||
|         model = self.model | ||||
| 
 | ||||
|         # collect new settings | ||||
|     def configure_gather_settings(self, data): | ||||
|         settings = [] | ||||
|         watch = [] | ||||
| 
 | ||||
|         for profile in data['profiles']: | ||||
|             pkey = profile['key'] | ||||
|             if profile['enabled']: | ||||
|                 watch.append(pkey) | ||||
| 
 | ||||
|             settings.extend([ | ||||
|                 {'name': 'rattail.datasync.{}.watcher'.format(pkey), | ||||
|                  'value': profile['watcher_spec']}, | ||||
|  | @ -183,10 +151,12 @@ class DataSyncChangeView(MasterView): | |||
|                 {'name': 'rattail.datasync.{}.consumers.runas'.format(pkey), | ||||
|                  'value': profile['watcher_default_runas']}, | ||||
|             ]) | ||||
| 
 | ||||
|             consumers = [] | ||||
|             if profile['watcher_consumes_self']: | ||||
|                 consumers = ['self'] | ||||
|             else: | ||||
| 
 | ||||
|                 for consumer in profile['consumers_data']: | ||||
|                     ckey = consumer['key'] | ||||
|                     if consumer['enabled']: | ||||
|  | @ -205,10 +175,12 @@ class DataSyncChangeView(MasterView): | |||
|                         {'name': 'rattail.datasync.{}.consumer.{}.runas'.format(pkey, ckey), | ||||
|                          'value': consumer['consumer_runas']}, | ||||
|                     ]) | ||||
| 
 | ||||
|             settings.extend([ | ||||
|                 {'name': 'rattail.datasync.{}.consumers'.format(pkey), | ||||
|                  'value': ', '.join(consumers)}, | ||||
|             ]) | ||||
| 
 | ||||
|         settings.extend([ | ||||
|             {'name': 'rattail.datasync.watch', | ||||
|              'value': ', '.join(watch)}, | ||||
|  | @ -216,15 +188,9 @@ class DataSyncChangeView(MasterView): | |||
|              'value': data['restart_command']}, | ||||
|         ]) | ||||
| 
 | ||||
|         # delete all current settings | ||||
|         self.delete_settings() | ||||
|         return settings | ||||
| 
 | ||||
|         # create all new settings | ||||
|         for setting in settings: | ||||
|             self.Session.add(model.Setting(name=setting['name'], | ||||
|                                            value=setting['value'])) | ||||
| 
 | ||||
|     def delete_settings(self): | ||||
|     def configure_remove_settings(self): | ||||
|         purge_datasync_settings(self.rattail_config, self.Session()) | ||||
| 
 | ||||
|     @classmethod | ||||
|  | @ -235,33 +201,65 @@ class DataSyncChangeView(MasterView): | |||
|     @classmethod | ||||
|     def _datasync_defaults(cls, config): | ||||
|         permission_prefix = cls.get_permission_prefix() | ||||
|         route_prefix = cls.get_route_prefix() | ||||
|         url_prefix = cls.get_url_prefix() | ||||
| 
 | ||||
|         # fix permission group title | ||||
|         config.add_tailbone_permission_group(permission_prefix, label="DataSync") | ||||
| 
 | ||||
|         # restart datasync | ||||
|         # restart | ||||
|         config.add_tailbone_permission(permission_prefix, | ||||
|                                        '{}.restart'.format(permission_prefix), | ||||
|                                        label="Restart the DataSync daemon") | ||||
|         config.add_route('datasync.restart', '/datasync/restart', | ||||
|         config.add_route('{}.restart'.format(route_prefix), | ||||
|                          '{}/restart'.format(url_prefix), | ||||
|                          request_method='POST') | ||||
|         config.add_view(cls, attr='restart', | ||||
|                         route_name='datasync.restart', | ||||
|                         route_name='{}.restart'.format(route_prefix), | ||||
|                         permission='{}.restart'.format(permission_prefix)) | ||||
| 
 | ||||
|         # configure datasync | ||||
|         config.add_tailbone_permission(permission_prefix, | ||||
|                                        '{}.configure'.format(permission_prefix), | ||||
|                                        label="Configure the DataSync daemon") | ||||
|         config.add_route('datasync.configure', '/datasync/configure') | ||||
|         config.add_view(cls, attr='configure', | ||||
|                         route_name='datasync.configure', | ||||
|                         permission='{}.configure'.format(permission_prefix), | ||||
|                         renderer='/datasync/configure.mako') | ||||
| 
 | ||||
| class DataSyncChangeView(MasterView): | ||||
|     """ | ||||
|     Master view for the DataSyncChange model. | ||||
|     """ | ||||
|     model_class = model.DataSyncChange | ||||
|     url_prefix = '/datasync/changes' | ||||
|     permission_prefix = 'datasync_changes' | ||||
|     creatable = False | ||||
|     editable = False | ||||
|     bulk_deletable = True | ||||
| 
 | ||||
|     labels = { | ||||
|         'batch_id': "Batch ID", | ||||
|     } | ||||
| 
 | ||||
|     grid_columns = [ | ||||
|         'source', | ||||
|         'batch_id', | ||||
|         'batch_sequence', | ||||
|         'payload_type', | ||||
|         'payload_key', | ||||
|         'deletion', | ||||
|         'obtained', | ||||
|         'consumer', | ||||
|     ] | ||||
| 
 | ||||
|     def configure_grid(self, g): | ||||
|         super(DataSyncChangeView, self).configure_grid(g) | ||||
| 
 | ||||
|         # batch_sequence | ||||
|         g.set_label('batch_sequence', "Batch Seq.") | ||||
|         g.filters['batch_sequence'].label = "Batch Sequence" | ||||
| 
 | ||||
|         g.set_sort_defaults('obtained') | ||||
|         g.set_type('obtained', 'datetime') | ||||
| 
 | ||||
|     def template_kwargs_index(self, **kwargs): | ||||
|         kwargs['allow_filemon_restart'] = bool(self.rattail_config.get('tailbone', 'filemon.restart')) | ||||
|         return kwargs | ||||
| 
 | ||||
| # TODO: deprecate / remove this | ||||
| DataSyncChangesView = DataSyncChangeView | ||||
| 
 | ||||
| 
 | ||||
| def includeme(config): | ||||
|     DataSyncThreadView.defaults(config) | ||||
|     DataSyncChangeView.defaults(config) | ||||
|  |  | |||
|  | @ -35,6 +35,7 @@ import time | |||
| 
 | ||||
| import json | ||||
| import six | ||||
| import sqlalchemy as sa | ||||
| 
 | ||||
| from rattail.exceptions import ConfigurationError | ||||
| from rattail.threads import Thread | ||||
|  | @ -66,14 +67,19 @@ class ImportingView(MasterView): | |||
|     filterable = False | ||||
|     pageable = False | ||||
| 
 | ||||
|     configurable = True | ||||
|     config_title = "Import / Export" | ||||
| 
 | ||||
|     labels = { | ||||
|         'host_title': "Data Source", | ||||
|         'local_title': "Data Target", | ||||
|         'direction_display': "Direction", | ||||
|     } | ||||
| 
 | ||||
|     grid_columns = [ | ||||
|         'host_title', | ||||
|         'local_title', | ||||
|         'direction_display', | ||||
|         'handler_spec', | ||||
|     ] | ||||
| 
 | ||||
|  | @ -84,6 +90,7 @@ class ImportingView(MasterView): | |||
|         'handler_spec', | ||||
|         'host_title', | ||||
|         'local_title', | ||||
|         'direction_display', | ||||
|         'models', | ||||
|     ] | ||||
| 
 | ||||
|  | @ -105,18 +112,14 @@ class ImportingView(MasterView): | |||
|         app = self.get_rattail_app() | ||||
|         data = [] | ||||
| 
 | ||||
|         for Handler in app.all_import_handlers(): | ||||
|             handler = Handler(self.rattail_config) | ||||
|         for handler in app.get_designated_import_handlers( | ||||
|                 ignore_errors=True, sort=True): | ||||
|             data.append(self.normalize(handler)) | ||||
| 
 | ||||
|         data.sort(key=lambda handler: (handler['host_title'],  | ||||
|                                        handler['local_title'])) | ||||
|         return data | ||||
| 
 | ||||
|     def normalize(self, handler): | ||||
|         Handler = handler.__class__ | ||||
|         return { | ||||
|             '_handler': handler, | ||||
|     def normalize(self, handler, keep_handler=True): | ||||
|         data = { | ||||
|             'key': handler.get_key(), | ||||
|             'generic_title': handler.get_generic_title(), | ||||
|             'host_key': handler.host_key, | ||||
|  | @ -124,7 +127,31 @@ class ImportingView(MasterView): | |||
|             'local_key': handler.local_key, | ||||
|             'local_title': handler.get_generic_local_title(), | ||||
|             'handler_spec': handler.get_spec(), | ||||
|         }         | ||||
|             'direction': handler.direction, | ||||
|             'direction_display': handler.direction.capitalize(), | ||||
|         } | ||||
| 
 | ||||
|         if keep_handler: | ||||
|             data['_handler'] = handler | ||||
| 
 | ||||
|         alternates = getattr(handler, 'alternate_handlers', None) | ||||
|         if alternates: | ||||
|             data['alternates'] = [] | ||||
|             for alternate in alternates: | ||||
|                 data['alternates'].append(self.normalize( | ||||
|                     alternate, keep_handler=keep_handler)) | ||||
| 
 | ||||
|         cmd = self.get_cmd_for_handler(handler, ignore_errors=True) | ||||
|         if cmd: | ||||
|             data['cmd'] = ' '.join(cmd) | ||||
|             data['command'] = cmd[0] | ||||
|             data['subcommand'] = cmd[1] | ||||
| 
 | ||||
|         runas = self.get_runas_for_handler(handler) | ||||
|         if runas: | ||||
|             data['default_runas'] = runas | ||||
| 
 | ||||
|         return data | ||||
| 
 | ||||
|     def configure_grid(self, g): | ||||
|         super(ImportingView, self).configure_grid(g) | ||||
|  | @ -139,9 +166,9 @@ class ImportingView(MasterView): | |||
|         """ | ||||
|         key = self.request.matchdict['key'] | ||||
|         app = self.get_rattail_app() | ||||
|         for Handler in app.all_import_handlers(): | ||||
|             if Handler.get_key() == key: | ||||
|                 return self.normalize(Handler(self.rattail_config)) | ||||
|         handler = app.get_designated_import_handler(key, ignore_errors=True) | ||||
|         if handler: | ||||
|             return self.normalize(handler) | ||||
|         raise self.notfound() | ||||
| 
 | ||||
|     def get_instance_title(self, handler_info): | ||||
|  | @ -206,8 +233,8 @@ class ImportingView(MasterView): | |||
|     def cache_runjob_form_values(self, handler, form): | ||||
|         handler_key = handler.get_key() | ||||
| 
 | ||||
|         def make_key(key): | ||||
|             return 'rattail.importing.{}.{}'.format(handler_key, key) | ||||
|         def make_key(field): | ||||
|             return 'rattail.importing.{}.{}'.format(handler_key, field) | ||||
| 
 | ||||
|         for field in form.fields: | ||||
|             key = make_key(field) | ||||
|  | @ -216,8 +243,8 @@ class ImportingView(MasterView): | |||
|     def read_cached_runjob_values(self, handler, form): | ||||
|         handler_key = handler.get_key() | ||||
| 
 | ||||
|         def make_key(key): | ||||
|             return 'rattail.importing.{}.{}'.format(handler_key, key) | ||||
|         def make_key(field): | ||||
|             return 'rattail.importing.{}.{}'.format(handler_key, field) | ||||
| 
 | ||||
|         for field in form.fields: | ||||
|             key = make_key(field) | ||||
|  | @ -331,8 +358,10 @@ class ImportingView(MasterView): | |||
| 
 | ||||
|         # invoke handler command via subprocess | ||||
|         try: | ||||
|             result = subprocess.run(cmd, check=True, capture_output=True) | ||||
|             output = result.stderr.decode('utf_8').strip() | ||||
|             result = subprocess.run(cmd, check=True, | ||||
|                                     stdout=subprocess.PIPE, | ||||
|                                     stderr=subprocess.STDOUT) | ||||
|             output = result.stdout.decode('utf_8').strip() | ||||
| 
 | ||||
|         except Exception as error: | ||||
|             log.warning("failed to invoke handler cmd: %s", cmd, exc_info=True) | ||||
|  | @ -346,14 +375,14 @@ class ImportingView(MasterView): | |||
| {} | ||||
| ``` | ||||
| 
 | ||||
| And here is the STDERR output: | ||||
| And here is the output: | ||||
| 
 | ||||
| ``` | ||||
| {} | ||||
| ``` | ||||
| """.format(handler.direction.capitalize(), | ||||
|            ' '.join(cmd), | ||||
|            error.stderr.decode('utf_8').strip()) | ||||
|            error.stdout.decode('utf_8').strip()) | ||||
|                 msg = markdown.markdown(msg, extensions=['fenced_code']) | ||||
|                 msg = HTML.literal(msg) | ||||
|                 msg = HTML.tag('div', class_='tailbone-markdown', c=[msg]) | ||||
|  | @ -394,21 +423,35 @@ And here is the STDERR output: | |||
|         notes = HTML.literal(notes) | ||||
|         return HTML.tag('div', class_='tailbone-markdown', c=[notes]) | ||||
| 
 | ||||
|     def make_runjob_cmd(self, handler, form, typ, port=None): | ||||
|     def get_cmd_for_handler(self, handler, ignore_errors=False): | ||||
|         handler_key = handler.get_key() | ||||
| 
 | ||||
|         option = '{}.cmd'.format(handler_key) | ||||
|         cmd = self.rattail_config.getlist('rattail.importing', option) | ||||
|         cmd = self.rattail_config.getlist('rattail.importing', | ||||
|                                           '{}.cmd'.format(handler_key)) | ||||
|         if not cmd or len(cmd) != 2: | ||||
|             msg = ("Missing or invalid config; please set '{}' in the " | ||||
|                    "[rattail.importing] section of your config file".format(option)) | ||||
|             raise ConfigurationError(msg) | ||||
|             cmd = self.rattail_config.getlist('rattail.importing', | ||||
|                                               '{}.default_cmd'.format(handler_key)) | ||||
| 
 | ||||
|         command, subcommand = cmd | ||||
|             if not cmd or len(cmd) != 2: | ||||
|                 msg = ("Missing or invalid config; please set '{}.default_cmd' in the " | ||||
|                        "[rattail.importing] section of your config file".format(handler_key)) | ||||
|                 if ignore_errors: | ||||
|                     return | ||||
|                 raise ConfigurationError(msg) | ||||
| 
 | ||||
|         option = '{}.runas'.format(handler_key) | ||||
|         runas = self.rattail_config.require('rattail.importing', option) | ||||
|         return cmd | ||||
| 
 | ||||
|     def get_runas_for_handler(self, handler): | ||||
|         handler_key = handler.get_key() | ||||
|         runas = self.rattail_config.get('rattail.importing', | ||||
|                                         '{}.runas'.format(handler_key)) | ||||
|         if runas: | ||||
|             return runas | ||||
|         return self.rattail_config.get('rattail', 'runas.default') | ||||
| 
 | ||||
|     def make_runjob_cmd(self, handler, form, typ, port=None): | ||||
|         command, subcommand = self.get_cmd_for_handler(handler) | ||||
|         runas = self.get_runas_for_handler(handler) | ||||
|         data = form.validated | ||||
| 
 | ||||
|         if typ == 'true': | ||||
|  | @ -460,7 +503,10 @@ And here is the STDERR output: | |||
|             cmd.append('--dry-run') | ||||
| 
 | ||||
|         if data['warnings']: | ||||
|             cmd.append('--warnings') | ||||
|             if typ == 'true': | ||||
|                 cmd.append('--warnings') | ||||
|             else: | ||||
|                 cmd.append('-W') | ||||
| 
 | ||||
|         return cmd | ||||
| 
 | ||||
|  | @ -479,6 +525,54 @@ cd {prefix} | |||
|         self.request.session['rattail.importing.runjob.notes'] = markdown.markdown( | ||||
|             notes, extensions=['fenced_code', 'codehilite']) | ||||
| 
 | ||||
|     def configure_get_context(self): | ||||
|         app = self.get_rattail_app() | ||||
|         handlers_data = [] | ||||
| 
 | ||||
|         for handler in app.get_designated_import_handlers( | ||||
|                 with_alternates=True, | ||||
|                 ignore_errors=True, sort=True): | ||||
| 
 | ||||
|             data = self.normalize(handler, keep_handler=False) | ||||
| 
 | ||||
|             data['spec_options'] = [handler.get_spec()] | ||||
|             for alternate in handler.alternate_handlers: | ||||
|                 data['spec_options'].append(alternate.get_spec()) | ||||
|             data['spec_options'].sort() | ||||
| 
 | ||||
|             handlers_data.append(data) | ||||
| 
 | ||||
|         return { | ||||
|             'handlers_data': handlers_data, | ||||
|         } | ||||
| 
 | ||||
|     def configure_gather_settings(self, data): | ||||
|         settings = [] | ||||
| 
 | ||||
|         for handler in data['handlers']: | ||||
|             key = handler['key'] | ||||
| 
 | ||||
|             settings.extend([ | ||||
|                 {'name': 'rattail.importing.{}.handler'.format(key), | ||||
|                  'value': handler['handler_spec']}, | ||||
|                 {'name': 'rattail.importing.{}.cmd'.format(key), | ||||
|                  'value': '{} {}'.format(handler['command'], | ||||
|                                          handler['subcommand'])}, | ||||
|                 {'name': 'rattail.importing.{}.runas'.format(key), | ||||
|                  'value': handler['default_runas']}, | ||||
|             ]) | ||||
| 
 | ||||
|         return settings | ||||
| 
 | ||||
|     def configure_remove_settings(self): | ||||
|         model = self.model | ||||
|         self.Session.query(model.Setting)\ | ||||
|                     .filter(sa.or_( | ||||
|                         model.Setting.name.like('rattail.importing.%.handler'), | ||||
|                         model.Setting.name.like('rattail.importing.%.cmd'), | ||||
|                         model.Setting.name.like('rattail.importing.%.runas')))\ | ||||
|                     .delete(synchronize_session=False) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def defaults(cls, config): | ||||
|         cls._defaults(config) | ||||
|  | @ -493,7 +587,7 @@ cd {prefix} | |||
|         # run job | ||||
|         config.add_tailbone_permission(permission_prefix, | ||||
|                                        '{}.runjob'.format(permission_prefix), | ||||
|                                        "Run an arbitrary import / export job") | ||||
|                                        "Run an arbitrary Import / Export Job") | ||||
|         config.add_route('{}.runjob'.format(route_prefix), | ||||
|                          '{}/runjob'.format(instance_url_prefix)) | ||||
|         config.add_view(cls, attr='runjob',  | ||||
|  |  | |||
|  | @ -114,6 +114,7 @@ class MasterView(View): | |||
|     execute_progress_initial_msg = None | ||||
|     supports_prev_next = False | ||||
|     supports_import_batch_from_file = False | ||||
|     configurable = False | ||||
| 
 | ||||
|     # set to True to add "View *global* Objects" permission, and | ||||
|     # expose / leverage the ``local_only`` object flag | ||||
|  | @ -2032,6 +2033,16 @@ class MasterView(View): | |||
|         """ | ||||
|         return getattr(cls, 'index_title', cls.get_model_title_plural()) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def get_config_title(cls): | ||||
|         """ | ||||
|         Returns the view's "config title". | ||||
|         """ | ||||
|         if hasattr(cls, 'config_title'): | ||||
|             return cls.config_title | ||||
| 
 | ||||
|         return cls.get_model_title_plural() | ||||
| 
 | ||||
|     def get_action_url(self, action, instance, **kwargs): | ||||
|         """ | ||||
|         Generate a URL for the given action on the given instance | ||||
|  | @ -2075,6 +2086,7 @@ class MasterView(View): | |||
|             'permission_prefix': self.get_permission_prefix(), | ||||
|             'index_title': self.get_index_title(), | ||||
|             'index_url': self.get_index_url(), | ||||
|             'config_title': self.get_config_title(), | ||||
|             'action_url': self.get_action_url, | ||||
|             'grid_index': self.grid_index, | ||||
|             'help_url': self.get_help_url(), | ||||
|  | @ -3982,7 +3994,46 @@ class MasterView(View): | |||
|         return diffs.Diff(old_data, new_data, **kwargs) | ||||
| 
 | ||||
|     ############################## | ||||
|     # Config Stuff | ||||
|     # Configuration Views | ||||
|     ############################## | ||||
| 
 | ||||
|     def configure(self): | ||||
|         """ | ||||
|         Generic view for configuring some aspect of the software. | ||||
|         """ | ||||
|         if self.request.method == 'POST': | ||||
|             if self.request.POST.get('remove_settings'): | ||||
|                 self.configure_remove_settings() | ||||
|                 self.request.session.flash("Settings have been removed.") | ||||
|                 return self.redirect(self.request.current_route_url()) | ||||
|             else: | ||||
|                 data = self.request.json_body | ||||
|                 settings = self.configure_gather_settings(data) | ||||
|                 self.configure_remove_settings() | ||||
|                 self.configure_save_settings(settings) | ||||
|                 self.request.session.flash("Settings have been saved.") | ||||
|                 return self.json_response({'success': True}) | ||||
| 
 | ||||
|         context = self.configure_get_context() | ||||
|         return self.render_to_response('configure', context) | ||||
| 
 | ||||
|     def configure_get_context(self): | ||||
|         return {} | ||||
| 
 | ||||
|     def configure_gather_settings(self, data): | ||||
|         return [] | ||||
| 
 | ||||
|     def configure_remove_settings(self): | ||||
|         pass | ||||
| 
 | ||||
|     def configure_save_settings(self, settings): | ||||
|         model = self.model | ||||
|         for setting in settings: | ||||
|             self.Session.add(model.Setting(name=setting['name'], | ||||
|                                            value=setting['value'])) | ||||
| 
 | ||||
|     ############################## | ||||
|     # Pyramid View Config | ||||
|     ############################## | ||||
| 
 | ||||
|     @classmethod | ||||
|  | @ -4025,6 +4076,7 @@ class MasterView(View): | |||
|         model_key = cls.get_model_key() | ||||
|         model_title = cls.get_model_title() | ||||
|         model_title_plural = cls.get_model_title_plural() | ||||
|         config_title = cls.get_config_title() | ||||
|         if cls.has_rows: | ||||
|             row_model_title = cls.get_row_model_title() | ||||
| 
 | ||||
|  | @ -4087,6 +4139,17 @@ class MasterView(View): | |||
|                 config.add_view(cls, attr='download_results_rows', route_name='{}.download_results_rows'.format(route_prefix), | ||||
|                                 permission='{}.download_results_rows'.format(permission_prefix)) | ||||
| 
 | ||||
|         # configure | ||||
|         if cls.configurable: | ||||
|             config.add_tailbone_permission(permission_prefix, | ||||
|                                            '{}.configure'.format(permission_prefix), | ||||
|                                            label="Configure {}".format(config_title)) | ||||
|             config.add_route('{}.configure'.format(route_prefix), | ||||
|                              '{}/configure'.format(url_prefix)) | ||||
|             config.add_view(cls, attr='configure', | ||||
|                             route_name='{}.configure'.format(route_prefix), | ||||
|                             permission='{}.configure'.format(permission_prefix)) | ||||
| 
 | ||||
|         # quickie (search) | ||||
|         if cls.supports_quickie_search: | ||||
|             config.add_tailbone_permission(permission_prefix, '{}.quickie'.format(permission_prefix), | ||||
|  |  | |||
							
								
								
									
										58
									
								
								tailbone/views/permissions.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								tailbone/views/permissions.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  Rattail -- Retail Software Framework | ||||
| #  Copyright © 2010-2021 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/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| Raw Permission Views | ||||
| """ | ||||
| 
 | ||||
| from __future__ import unicode_literals, absolute_import | ||||
| 
 | ||||
| from sqlalchemy import orm | ||||
| 
 | ||||
| from rattail.db import model | ||||
| 
 | ||||
| from tailbone.views import MasterView | ||||
| 
 | ||||
| 
 | ||||
| class PermissionView(MasterView): | ||||
|     """ | ||||
|     Master view for the permissions model. | ||||
|     """ | ||||
|     model_class = model.Permission | ||||
|     model_title = "Raw Permission" | ||||
|     editable = False | ||||
|     bulk_deletable = True | ||||
| 
 | ||||
|     grid_columns = [ | ||||
|         'role', | ||||
|         'permission', | ||||
|     ] | ||||
| 
 | ||||
|     def query(self, session): | ||||
|         model = self.model | ||||
|         query = super(PermissionView, self).query(session) | ||||
|         query = query.options(orm.joinedload(model.Permission.role)) | ||||
|         return query | ||||
| 
 | ||||
| 
 | ||||
| def includeme(config): | ||||
|     PermissionView.defaults(config) | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar