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