Compare commits
2 Commits
5d35eec1d8
...
72d044b6e8
| Author | SHA1 | Date | |
|---|---|---|---|
| 72d044b6e8 | |||
| dc1b2ecb08 |
13
README.md
13
README.md
@ -1,6 +1,7 @@
|
|||||||
# Flask YAML Editor
|
# Flask YAML Editor
|
||||||
|
|
||||||
Flask YAML Editor is a web application that provides an interface for reading and updating YAML file content. This application is built using Flask and serves an HTML page that interacts with the Flask backend to manage YAML data.
|
Flask YAML Editor is a web application that provides an interface for reading and updating YAML file content. This
|
||||||
|
application is built using Flask and serves an HTML page that interacts with the Flask backend to manage YAML data.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -10,7 +11,8 @@ Flask YAML Editor is a web application that provides an interface for reading an
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
|
These instructions will get you a copy of the project up and running on your local machine for development and testing
|
||||||
|
purposes.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
@ -60,11 +62,14 @@ pip install Flask PyYAML
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under a custom licence. This licence permits private and non-commercial use, testing, and use by the author's employer under certain conditions. It prohibits modification, distribution, and commercial use by others without a separate licence.
|
This project is licensed under a custom licence. This licence permits private and non-commercial use, testing, and use
|
||||||
|
by the author's employer under certain conditions. It prohibits modification, distribution, and commercial use by others
|
||||||
|
without a separate licence.
|
||||||
|
|
||||||
See the [LICENSE.md](LICENSE.md) file for more details.
|
See the [LICENSE.md](LICENSE.md) file for more details.
|
||||||
|
|
||||||
For questions regarding the use, modification, or redistribution of this software, please contact Isaak Buslovich at isaak.buslovich@potsdam.de.
|
For questions regarding the use, modification, or redistribution of this software, please contact Isaak Buslovich at
|
||||||
|
isaak.buslovich@potsdam.de.
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
|
|||||||
257
index.html
257
index.html
@ -2,117 +2,270 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>YAML Editor</title>
|
<title>Invoice Email Sender</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Lora:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
/* color scheme */
|
||||||
|
--primary-color: #007bff;
|
||||||
|
--primary-dark: #0056b3;
|
||||||
|
--secondary-color: #333;
|
||||||
|
--light-color: #f4f7fa;
|
||||||
|
--lighter-color: #f8f9fa;
|
||||||
|
--error-color: #ee5555;
|
||||||
|
|
||||||
|
/* Font and border styling */
|
||||||
|
--font-family: 'Lora', serif;
|
||||||
|
--border-radius: 10px;
|
||||||
|
--border-color: #ccc;
|
||||||
|
--border-focus-color: #007bff;
|
||||||
|
|
||||||
|
/* Shadow and transitions */
|
||||||
|
--box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||||
|
--transition-speed: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: var(--font-family);
|
||||||
background-color: #f8f9fa;
|
background-color: var(--light-color);
|
||||||
color: #212529;
|
color: var(--secondary-color);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 800px;
|
max-width: 850px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 10px;
|
border-radius: var(--border-radius);
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--box-shadow);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-tab {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--primary-color);
|
||||||
|
transition: color var(--transition-speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab:hover, .nav-tab.active {
|
||||||
|
color: var(--primary-dark);
|
||||||
|
border-bottom: 3px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none; /* Hide all tab content by default */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: left;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
background-color: #007bff;
|
background: var(--primary-color);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
margin: 10px 5px;
|
margin: 10px 5px;
|
||||||
border-radius: 5px;
|
border-radius: var(--border-radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.3s;
|
transition: background-color var(--transition-speed);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
background-color: #0056b3;
|
background-color: var(--primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
input[type="text"], input[type="password"], textarea {
|
||||||
width: 100%;
|
width: calc(100% - 20px);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 5px;
|
border-radius: var(--border-radius);
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: var(--lighter-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus, input[type="password"]:focus, textarea:focus {
|
||||||
|
border-color: var(--border-focus-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: .9rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="text/javascript"
|
|
||||||
src="https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.2.7.nomodule.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container" id="appContainer">
|
<div class="container" id="appContainer">
|
||||||
<label for="yamlContent"></label><textarea id="yamlContent" rows="10"></textarea><br>
|
<div>
|
||||||
<button class="button" onclick="loadData()">Reload Data</button>
|
<div class="nav-tab active" onclick="changeTab(event, 'config')">Configuration</div>
|
||||||
<button class="button" onclick="saveData()">Save Data</button>
|
<div class="nav-tab" onclick="changeTab(event, 'csv')">CSV Management</div>
|
||||||
<div id="statusMessage"></div>
|
<div class="nav-tab" onclick="changeTab(event, 'email')">Email Operations</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="config" class="tab-content active">
|
||||||
|
<label for="user">User:</label>
|
||||||
|
<input type="text" id="user" name="user">
|
||||||
|
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
|
||||||
|
<label for="ccEmail">CC Email:</label>
|
||||||
|
<input type="text" id="ccEmail" name="ccEmail">
|
||||||
|
|
||||||
|
<label for="bccEmail">BCC Email:</label>
|
||||||
|
<input type="text" id="bccEmail" name="bccEmail">
|
||||||
|
|
||||||
|
<label for="emailTemplate">Email Template:</label>
|
||||||
|
<textarea id="emailTemplate" rows="6"></textarea>
|
||||||
|
|
||||||
|
<button class="button" id="loadConfigBtn">Load Config</button>
|
||||||
|
<button class="button" id="saveConfigBtn">Save Config</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="csv" class="tab-content">
|
||||||
|
<!-- CSV Management Content -->
|
||||||
|
<label for="csvPath">CSV Path:</label>
|
||||||
|
<input type="text" id="csvPath" name="csvPath">
|
||||||
|
<button class="button">Load CSV</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="email" class="tab-content">
|
||||||
|
<!-- Email Operations Content -->
|
||||||
|
<button class="button">Send Emails</button>
|
||||||
|
<textarea rows="10">Log output here...</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-bar" id="statusMessage">Status: Ready</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
function displayStatus(message) {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
document.getElementById('statusMessage').textContent = message;
|
// Event listeners for buttons
|
||||||
|
document.getElementById('loadConfigBtn').addEventListener('click', loadConfig);
|
||||||
|
document.getElementById('saveConfigBtn').addEventListener('click', saveConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
function changeTab(event, tabId) {
|
||||||
|
const tabContents = document.getElementsByClassName("tab-content");
|
||||||
|
const tabs = document.getElementsByClassName("nav-tab");
|
||||||
|
|
||||||
|
// Hide all tab contents
|
||||||
|
for (let i = 0; i < tabContents.length; i++) {
|
||||||
|
tabContents[i].style.display = "none"; // Hide
|
||||||
|
tabContents[i].classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
// Remove 'active' class from all tabs
|
||||||
try {
|
for (let i = 0; i < tabs.length; i++) {
|
||||||
displayStatus('Loading data...');
|
tabs[i].classList.remove("active");
|
||||||
const response = await fetch('/data');
|
}
|
||||||
|
|
||||||
|
// Show the clicked tab content and add 'active' class
|
||||||
|
document.getElementById(tabId).style.display = "block";
|
||||||
|
document.getElementById(tabId).classList.add("active");
|
||||||
|
event.currentTarget.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfig() {
|
||||||
|
fetch('/data')
|
||||||
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
displayStatus(`HTTP error! Status: ${response.status}`);
|
throw new Error('Network response was not ok: ' + response.statusText);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
return response.json();
|
||||||
if (typeof data !== 'object' || data === null || !('text' in data)) {
|
})
|
||||||
displayStatus('Invalid format in response data');
|
.then(data => {
|
||||||
return;
|
if (!data.email || typeof data.email_template !== 'string') {
|
||||||
}
|
throw new Error('Incorrect data structure from server');
|
||||||
document.getElementById('yamlContent').value = data.text || '';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading data:', error);
|
|
||||||
displayStatus(`Failed to load data: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
displayStatus('');
|
|
||||||
}
|
}
|
||||||
|
document.getElementById('user').value = data.email.user || '';
|
||||||
|
document.getElementById('password').value = data.email.password || '';
|
||||||
|
document.getElementById('ccEmail').value = data.email.cc_email || '';
|
||||||
|
document.getElementById('bccEmail').value = data.email.bcc_email || '';
|
||||||
|
document.getElementById('emailTemplate').textContent = data.email_template || '';
|
||||||
|
updateStatusBar("Configuration Loaded Successfully");
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading configuration:', error);
|
||||||
|
updateStatusBar(`Error loading configuration: ${error}`, true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveData() {
|
function saveConfig() {
|
||||||
const content = document.getElementById('yamlContent').value;
|
const emailData = {
|
||||||
displayStatus('Saving data...');
|
user: document.getElementById('user').value,
|
||||||
|
password: document.getElementById('password').value,
|
||||||
|
cc_email: document.getElementById('ccEmail').value,
|
||||||
|
bcc_email: document.getElementById('bccEmail').value
|
||||||
|
};
|
||||||
|
|
||||||
|
const emailTemplateData = document.getElementById('emailTemplate').value;
|
||||||
|
|
||||||
fetch('/data', {
|
fetch('/data', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({text: content}),
|
body: JSON.stringify({
|
||||||
|
email: emailData,
|
||||||
|
email_template: emailTemplateData
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
throw new Error('Network response was not ok: ' + response.statusText);
|
||||||
}
|
}
|
||||||
return response.text();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
alert(data);
|
if (data.success) {
|
||||||
displayStatus('');
|
alert("Configuration Saved Successfully");
|
||||||
|
updateStatusBar("Configuration Saved Successfully");
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to save configuration: " + data.message);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error saving configuration:', error);
|
||||||
displayStatus('Failed to save data.');
|
updateStatusBar(`Error saving configuration: ${error}`, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData();
|
function updateStatusBar(message, isError = false) {
|
||||||
|
const statusBar = document.getElementById('statusMessage');
|
||||||
|
statusBar.textContent = message;
|
||||||
|
if (isError) {
|
||||||
|
statusBar.style.color = 'var(--error-color)';
|
||||||
|
} else {
|
||||||
|
statusBar.style.color = 'initial'; // or any other color for normal messages
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
34
main.py
34
main.py
@ -15,7 +15,7 @@ import logging
|
|||||||
app = Flask(__name__, static_folder='.', static_url_path='')
|
app = Flask(__name__, static_folder='.', static_url_path='')
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
DATA_FILE = 'data.yaml'
|
DATA_FILE = 'config.yaml' # Ensure this is the correct path to your YAML file
|
||||||
|
|
||||||
|
|
||||||
def read_yaml():
|
def read_yaml():
|
||||||
@ -23,17 +23,20 @@ def read_yaml():
|
|||||||
Reads and returns content from the YAML file.
|
Reads and returns content from the YAML file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary with the key 'text' containing file content.
|
dict: A dictionary with the key 'email' and 'email_template' containing their respective contents.
|
||||||
None: If an error occurs, along with an error message.
|
None: If an error occurs, along with an error message.
|
||||||
"""
|
"""
|
||||||
file_path = Path(DATA_FILE)
|
file_path = Path(DATA_FILE)
|
||||||
try:
|
try:
|
||||||
if not file_path.is_file():
|
if not file_path.is_file():
|
||||||
logging.info("YAML file not found. Returning empty data.")
|
logging.info("YAML file not found. Returning empty data.")
|
||||||
return {'text': ''}, None
|
return {'email': {}, 'email_template': ''}, None
|
||||||
with file_path.open() as file:
|
with file_path.open() as file:
|
||||||
data = yaml.safe_load(file) or {}
|
data = yaml.safe_load(file) or {}
|
||||||
return {'text': data.get('text', '')}, None
|
return {
|
||||||
|
'email': data.get('email', {}),
|
||||||
|
'email_template': data.get('email_template', '')
|
||||||
|
}, None
|
||||||
except yaml.YAMLError as e:
|
except yaml.YAMLError as e:
|
||||||
logging.error(f"YAML Error: {e}", exc_info=True)
|
logging.error(f"YAML Error: {e}", exc_info=True)
|
||||||
return None, f"YAML parsing error: {e}"
|
return None, f"YAML parsing error: {e}"
|
||||||
@ -45,12 +48,13 @@ def read_yaml():
|
|||||||
return None, f"Unexpected error: {e}"
|
return None, f"Unexpected error: {e}"
|
||||||
|
|
||||||
|
|
||||||
def write_yaml(content):
|
def write_yaml(email_data, email_template_data):
|
||||||
"""
|
"""
|
||||||
Writes content to the YAML file.
|
Writes content to the YAML file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content (str): The text content to be saved in the file.
|
email_data (dict): The data for the 'email' section.
|
||||||
|
email_template_data (str): The data for the 'email_template' section.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the operation was successful, False otherwise.
|
bool: True if the operation was successful, False otherwise.
|
||||||
@ -59,7 +63,10 @@ def write_yaml(content):
|
|||||||
file_path = Path(DATA_FILE)
|
file_path = Path(DATA_FILE)
|
||||||
try:
|
try:
|
||||||
with file_path.open('w') as file:
|
with file_path.open('w') as file:
|
||||||
yaml.dump({'text': content}, file)
|
yaml.dump({
|
||||||
|
'email': email_data,
|
||||||
|
'email_template': email_template_data
|
||||||
|
}, file)
|
||||||
return True, None
|
return True, None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error writing YAML file: {e}", exc_info=True)
|
logging.error(f"Error writing YAML file: {e}", exc_info=True)
|
||||||
@ -75,19 +82,20 @@ def handle_data():
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON response for GET requests.
|
JSON response for GET requests.
|
||||||
Text message for POST requests.
|
JSON response for POST requests indicating success or failure.
|
||||||
"""
|
"""
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
data, error = read_yaml()
|
data, error = read_yaml()
|
||||||
if error:
|
if error:
|
||||||
return error, 500
|
return jsonify({'success': False, 'message': error}), 500
|
||||||
return jsonify(data), 200
|
return jsonify(data), 200
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
content = request.json.get('text', '')
|
email_data = request.json.get('email', {})
|
||||||
success, error = write_yaml(content)
|
email_template_data = request.json.get('email_template', '')
|
||||||
|
success, error = write_yaml(email_data, email_template_data)
|
||||||
if error:
|
if error:
|
||||||
return error, 500
|
return jsonify({'success': False, 'message': error}), 500
|
||||||
return "File saved successfully", 200
|
return jsonify({'success': True, 'message': "File saved successfully"}), 200
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user