Compare commits
No commits in common. "72d044b6e8cda3d1c39a1572b2a3b2ceae0c068e" and "5d35eec1d828d7224c27436a90bc59aa2aaa0c4a" have entirely different histories.
72d044b6e8
...
5d35eec1d8
13
README.md
13
README.md
@ -1,7 +1,6 @@
|
|||||||
# 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
|
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.
|
||||||
application is built using Flask and serves an HTML page that interacts with the Flask backend to manage YAML data.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -11,8 +10,7 @@ application is built using Flask and serves an HTML page that interacts with the
|
|||||||
|
|
||||||
## 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
|
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
|
||||||
purposes.
|
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
@ -62,14 +60,11 @@ 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
|
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.
|
||||||
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
|
For questions regarding the use, modification, or redistribution of this software, please contact Isaak Buslovich at isaak.buslovich@potsdam.de.
|
||||||
isaak.buslovich@potsdam.de.
|
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
|
|||||||
263
index.html
263
index.html
@ -2,270 +2,117 @@
|
|||||||
<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>Invoice Email Sender</title>
|
<title>YAML Editor</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Lora:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Roboto: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: var(--font-family);
|
font-family: 'Roboto', sans-serif;
|
||||||
background-color: var(--light-color);
|
background-color: #f8f9fa;
|
||||||
color: var(--secondary-color);
|
color: #212529;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 850px;
|
max-width: 800px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: var(--border-radius);
|
border-radius: 10px;
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
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: var(--primary-color);
|
background-color: #007bff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
margin: 10px 5px;
|
margin: 10px 5px;
|
||||||
border-radius: var(--border-radius);
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color var(--transition-speed);
|
transition: background-color 0.3s;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
background-color: var(--primary-dark);
|
background-color: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"], input[type="password"], textarea {
|
textarea {
|
||||||
width: calc(100% - 20px);
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid #ccc;
|
||||||
border-radius: var(--border-radius);
|
border-radius: 5px;
|
||||||
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">
|
||||||
<div>
|
<label for="yamlContent"></label><textarea id="yamlContent" rows="10"></textarea><br>
|
||||||
<div class="nav-tab active" onclick="changeTab(event, 'config')">Configuration</div>
|
<button class="button" onclick="loadData()">Reload Data</button>
|
||||||
<div class="nav-tab" onclick="changeTab(event, 'csv')">CSV Management</div>
|
<button class="button" onclick="saveData()">Save Data</button>
|
||||||
<div class="nav-tab" onclick="changeTab(event, 'email')">Email Operations</div>
|
<div id="statusMessage"></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>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
function displayStatus(message) {
|
||||||
// Event listeners for buttons
|
document.getElementById('statusMessage').textContent = message;
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 'active' class from all tabs
|
|
||||||
for (let i = 0; i < tabs.length; i++) {
|
|
||||||
tabs[i].classList.remove("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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() {
|
async function loadData() {
|
||||||
fetch('/data')
|
try {
|
||||||
.then(response => {
|
displayStatus('Loading data...');
|
||||||
if (!response.ok) {
|
const response = await fetch('/data');
|
||||||
throw new Error('Network response was not ok: ' + response.statusText);
|
if (!response.ok) {
|
||||||
}
|
displayStatus(`HTTP error! Status: ${response.status}`);
|
||||||
return response.json();
|
return;
|
||||||
})
|
}
|
||||||
.then(data => {
|
const data = await response.json();
|
||||||
if (!data.email || typeof data.email_template !== 'string') {
|
if (typeof data !== 'object' || data === null || !('text' in data)) {
|
||||||
throw new Error('Incorrect data structure from server');
|
displayStatus('Invalid format in response data');
|
||||||
}
|
return;
|
||||||
document.getElementById('user').value = data.email.user || '';
|
}
|
||||||
document.getElementById('password').value = data.email.password || '';
|
document.getElementById('yamlContent').value = data.text || '';
|
||||||
document.getElementById('ccEmail').value = data.email.cc_email || '';
|
} catch (error) {
|
||||||
document.getElementById('bccEmail').value = data.email.bcc_email || '';
|
console.error('Error loading data:', error);
|
||||||
document.getElementById('emailTemplate').textContent = data.email_template || '';
|
displayStatus(`Failed to load data: ${error.message}`);
|
||||||
updateStatusBar("Configuration Loaded Successfully");
|
} finally {
|
||||||
})
|
displayStatus('');
|
||||||
.catch(error => {
|
}
|
||||||
console.error('Error loading configuration:', error);
|
|
||||||
updateStatusBar(`Error loading configuration: ${error}`, true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveConfig() {
|
function saveData() {
|
||||||
const emailData = {
|
const content = document.getElementById('yamlContent').value;
|
||||||
user: document.getElementById('user').value,
|
displayStatus('Saving data...');
|
||||||
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({
|
body: JSON.stringify({text: content}),
|
||||||
email: emailData,
|
|
||||||
email_template: emailTemplateData
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Network response was not ok: ' + response.statusText);
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.text();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
alert(data);
|
||||||
alert("Configuration Saved Successfully");
|
displayStatus('');
|
||||||
updateStatusBar("Configuration Saved Successfully");
|
|
||||||
} else {
|
|
||||||
throw new Error("Failed to save configuration: " + data.message);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error saving configuration:', error);
|
console.error('Error:', error);
|
||||||
updateStatusBar(`Error saving configuration: ${error}`, true);
|
displayStatus('Failed to save data.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatusBar(message, isError = false) {
|
loadData();
|
||||||
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 = 'config.yaml' # Ensure this is the correct path to your YAML file
|
DATA_FILE = 'data.yaml'
|
||||||
|
|
||||||
|
|
||||||
def read_yaml():
|
def read_yaml():
|
||||||
@ -23,20 +23,17 @@ 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 'email' and 'email_template' containing their respective contents.
|
dict: A dictionary with the key 'text' containing file content.
|
||||||
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 {'email': {}, 'email_template': ''}, None
|
return {'text': ''}, 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 {
|
return {'text': data.get('text', '')}, None
|
||||||
'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}"
|
||||||
@ -48,13 +45,12 @@ def read_yaml():
|
|||||||
return None, f"Unexpected error: {e}"
|
return None, f"Unexpected error: {e}"
|
||||||
|
|
||||||
|
|
||||||
def write_yaml(email_data, email_template_data):
|
def write_yaml(content):
|
||||||
"""
|
"""
|
||||||
Writes content to the YAML file.
|
Writes content to the YAML file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
email_data (dict): The data for the 'email' section.
|
content (str): The text content to be saved in the file.
|
||||||
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.
|
||||||
@ -63,10 +59,7 @@ def write_yaml(email_data, email_template_data):
|
|||||||
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({
|
yaml.dump({'text': content}, file)
|
||||||
'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)
|
||||||
@ -82,20 +75,19 @@ def handle_data():
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON response for GET requests.
|
JSON response for GET requests.
|
||||||
JSON response for POST requests indicating success or failure.
|
Text message for POST requests.
|
||||||
"""
|
"""
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
data, error = read_yaml()
|
data, error = read_yaml()
|
||||||
if error:
|
if error:
|
||||||
return jsonify({'success': False, 'message': error}), 500
|
return error, 500
|
||||||
return jsonify(data), 200
|
return jsonify(data), 200
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
email_data = request.json.get('email', {})
|
content = request.json.get('text', '')
|
||||||
email_template_data = request.json.get('email_template', '')
|
success, error = write_yaml(content)
|
||||||
success, error = write_yaml(email_data, email_template_data)
|
|
||||||
if error:
|
if error:
|
||||||
return jsonify({'success': False, 'message': error}), 500
|
return error, 500
|
||||||
return jsonify({'success': True, 'message': "File saved successfully"}), 200
|
return "File saved successfully", 200
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user