From 35e17cbe6cb88a6507936b3a0763aef4f6c973a5 Mon Sep 17 00:00:00 2001 From: Jaden Diefenbaugh <jaden.diefenbaugh@idiap.ch> Date: Thu, 2 Aug 2018 11:56:55 -0700 Subject: [PATCH] added python & js code to handle templates trying to be written to an existing file, #108 --- beat/editor/resources.py | 19 +++++++- beat/editor/scripts/server.py | 9 +++- beat/editor/utils.py | 19 ++++++-- .../EntityTemplateGenerationButton.jsx | 44 ++++++++++++++++--- conda/js/src/helpers/api.js | 26 +++++++---- 5 files changed, 96 insertions(+), 21 deletions(-) diff --git a/beat/editor/resources.py b/beat/editor/resources.py index 86a9b464..31b9aa6b 100644 --- a/beat/editor/resources.py +++ b/beat/editor/resources.py @@ -48,6 +48,22 @@ from . import utils from beat.core.dock import Host from beat.core.environments import enumerate_packages +def make_error(status_code, message): + """Overrides flask-restful's response handling to return a custom error message + Adapted from https://stackoverflow.com/a/21639552 + + Parameters: + + status_code (int): The HTTP status code to return + + message (str): The error message text to return + """ + response = simplejson.dumps({ + 'status': status_code, + 'message': message + }) + response.status_code = status_code + return response class Layout(Resource): """Exposes toolchain layout functionality""" @@ -94,7 +110,8 @@ class Templates(Resource): data = request.get_json() entity = data.pop('entity') name = data.pop('name') - utils.generate_python_template(entity, name, self.config, **data) + confirm = data.pop('confirm') + utils.generate_python_template(entity, name, confirm, self.config, **data) class Settings(Resource): diff --git a/beat/editor/scripts/server.py b/beat/editor/scripts/server.py index b4e0f899..881accea 100644 --- a/beat/editor/scripts/server.py +++ b/beat/editor/scripts/server.py @@ -72,6 +72,7 @@ import docopt from beat.cmdline.config import Configuration + def main(user_input=None): if user_input is not None: @@ -111,7 +112,13 @@ def main(user_input=None): static_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../js') app = Flask(__name__, static_folder=static_folder, static_url_path='') - api = Api(app) + errors = errors = { + 'PythonFileAlreadyExistsError': { + 'message': "The python template file trying to be created already exists.", + 'status': 409, + } + } + api = Api(app, errors=errors) CORS(app) @app.route('/') diff --git a/beat/editor/utils.py b/beat/editor/utils.py index d2fc57fe..79eaef96 100644 --- a/beat/editor/utils.py +++ b/beat/editor/utils.py @@ -108,20 +108,31 @@ TEMPLATE_FUNCTION = dict( ) """Functions for template instantiation within beat.editor""" +class PythonFileAlreadyExistsError(Exception): + pass -def generate_python_template(entity, name, config, **kwargs): +def generate_python_template(entity, name, confirm, config, **kwargs): """Generates a template for a BEAT entity with the given named arguments Parameters: - entity (str): A valid BEAT entity (valid values are - """ + entity (str): A valid BEAT entity - s = TEMPLATE_FUNCTION[entity](**kwargs) + name (str): The name of the object to have a python file generated for + + confirm (:py:class:`boolean`): Whether to override the Python file if + one is found at the desired location + """ resource_path = os.path.join(config.path, entity) file_path = os.path.join(resource_path, name) + '.py' + if not confirm and os.path.isfile(file_path): + # python file already exists + raise PythonFileAlreadyExistsError + + s = TEMPLATE_FUNCTION[entity](**kwargs) + with open(file_path, 'w') as f: f.write(s) return s diff --git a/conda/js/src/components/EntityTemplateGenerationButton.jsx b/conda/js/src/components/EntityTemplateGenerationButton.jsx index 9e9564ea..954e4ad6 100644 --- a/conda/js/src/components/EntityTemplateGenerationButton.jsx +++ b/conda/js/src/components/EntityTemplateGenerationButton.jsx @@ -5,6 +5,7 @@ import { Modal, ModalBody, ModalHeader, + ModalFooter, } from 'reactstrap'; import { generateDatabaseTemplate, @@ -22,6 +23,7 @@ type Props = { type State = { infoOpen: boolean, + confirmOpen: boolean, }; export default class EntityTemplateGenerationButton extends React.Component<Props, State> { @@ -31,13 +33,18 @@ export default class EntityTemplateGenerationButton extends React.Component<Prop state = { infoOpen: false, + confirmOpen: false, } toggleInfo = (val: boolean = !this.state.infoOpen) => { this.setState({ infoOpen: val }); } - click = () => { + toggleConfirm = (val: boolean = !this.state.confirmOpen) => { + this.setState({ confirmOpen: val }); + } + + click = async (confirm: boolean) => { let uses; let res; switch(this.props.entity){ @@ -50,25 +57,32 @@ export default class EntityTemplateGenerationButton extends React.Component<Prop .map(p => p.sets.map(s => s.view)) .reduce((a, vs) => [...a, ...vs], []) )); - res = generateDatabaseTemplate(this.props.data.name, views); + res = await generateDatabaseTemplate(this.props.data.name, confirm, views); break; case('algorithm'): // find if the alg has parameters // find the used libraries if(!this.props.data.contents.parameters) throw new Error(`Bad alg object, no params field: ${ this.props.data.contents }`); - res = generateAlgorithmTemplate(this.props.data.name, this.props.data.contents); + res = await generateAlgorithmTemplate(this.props.data.name, confirm, this.props.data.contents); break; case('library'): // find the used libraries uses = copyObj(this.props.data.contents.uses); - res = generateLibraryTemplate(this.props.data.name, uses); + res = await generateLibraryTemplate(this.props.data.name, confirm, uses); break; default: throw new Error(`Cannot generate template for entity "${ this.props.entity }"`); } - this.toggleInfo(true); - return res; + + // inspect the result + if(res === false){ + // the python file already existed, ask for confirmation to overwrite + this.toggleConfirm(true); + } else { + // its fine + this.toggleInfo(true); + } } render = () => { @@ -77,7 +91,7 @@ export default class EntityTemplateGenerationButton extends React.Component<Prop <Button outline color='danger' className='mx-auto' - onClick={this.click} + onClick={e => this.click(false)} title={`Some types of objects (databases, algorithms, and libraries) need Python files to be used in experiments. You may generate a template Python file to provide a starting point for development.`} > Generate Python File @@ -97,6 +111,22 @@ export default class EntityTemplateGenerationButton extends React.Component<Prop </pre> </ModalBody> </Modal> + <Modal color='info' isOpen={this.state.confirmOpen} toggle={e => this.toggleConfirm(false)}> + <ModalHeader toggle={e => this.toggleConfirm(false)}> + Confirm Overwrite + </ModalHeader> + <ModalBody> + The generated Python file will overwrite the Python file that already exists at: + <pre className='preInline'> + /your/beat/prefix/{ pluralize(this.props.entity) }/{ this.props.data.name }.py + </pre> + Are you sure you want to overwrite this file? + </ModalBody> + <ModalFooter> + <Button color="primary" onClick={e => { this.toggleConfirm(false); this.click(true); }}>Overwrite</Button>{' '} + <Button color="secondary" onClick={e => this.toggleConfirm(false)}>Cancel</Button> + </ModalFooter> + </Modal> </React.Fragment> ); } diff --git a/conda/js/src/helpers/api.js b/conda/js/src/helpers/api.js index 86aa9ce2..4fbb33a4 100644 --- a/conda/js/src/helpers/api.js +++ b/conda/js/src/helpers/api.js @@ -73,6 +73,8 @@ export const fetchLayout = async (tcName: string) => { // generates a python file from a template given some args payload const templatesUrl = toUrl('templates'); +// given the BEAT entity and a dict of args, tries to generate a python file from a template +// returns either the response if successful or `false` if the python file already exists const generateTemplate = async (be: string, args: {[string]: any}) => { const templatesConfig = { headers: { @@ -83,18 +85,26 @@ const generateTemplate = async (be: string, args: {[string]: any}) => { body: JSON.stringify({ entity: be, ...args}), }; const res = await fetch(templatesUrl, templatesConfig); - const json = await res.json(); - return JSON.parse(json); + if(res.status === 200){ + // okay + const json = await res.json(); + return JSON.parse(json); + } else { + if(res.status === 409){ + // python file already exists, return false to let callers know + return false; + } + } }; -export const generateDatabaseTemplate = async (name: string, views: string[]) => { - generateTemplate('databases', {name, views}); +export const generateDatabaseTemplate = async (name: string, confirm: boolean, views: string[]) => { + return generateTemplate('databases', {name, views, confirm}); }; -export const generateAlgorithmTemplate = async (name: string, contents: any) => { - generateTemplate('algorithms', {name, contents}); +export const generateAlgorithmTemplate = async (name: string, confirm: boolean, contents: any) => { + return generateTemplate('algorithms', {name, contents, confirm}); }; -export const generateLibraryTemplate = async (name: string, uses: StringObject) => { - generateTemplate('libraries', {name, uses}); +export const generateLibraryTemplate = async (name: string, confirm: boolean, uses: StringObject) => { + return generateTemplate('libraries', {name, uses, confirm}); }; -- GitLab