Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# MetaGreenData
# GreenMetaData

## Usage

Expand Down
2 changes: 1 addition & 1 deletion interface/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MetaGreenData</title>
<title>GreenMetaData</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
Expand Down
126 changes: 124 additions & 2 deletions interface/templates/computing.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
<i class="fas fa-arrow-left"></i> Back
</a>
<h2 class="mb-0">Computing Impact Form</h2>
<div class="ms-auto">
<input type="file" id="yaml-upload" accept=".yml,.yaml" style="display: none;">
<button class="btn btn-outline-primary" onclick="document.getElementById('yaml-upload').click()">
<i class="fas fa-file-upload"></i> Import YAML
</button>
<button class="btn btn-outline-info" id="resume-btn" style="display: none;"
onclick="restoreSession()">
<i class="fas fa-history"></i> Resume Session
</button>
</div>
</div>

<!-- Basic Information Section -->
Expand Down Expand Up @@ -373,8 +383,8 @@ <h3 class="mb-3">Preview & Download</h3>
</div>

<!-- Live Preview Sidebar -->
<div class="col-md-4 position-fixed end-0"
style="top: 0; height: 100vh; background: #1e2124; padding: 2rem; border-radius: 12px 0 0 12px; box-shadow: -4px 0 15px rgba(0,0,0,0.1);">
<div class="col-md-4 position-sticky"
style="top: 2rem; height: calc(100vh - 2rem); align-self: flex-start; background: #1e2124; padding: 2rem; border-radius: 12px; box-shadow: -4px 0 15px rgba(0,0,0,0.1);">
<h4 class="text-white mb-3">Live Preview</h4>
<pre id="yml-preview" class="text-light p-3 bg-dark"
style="font-family: 'Monaco', 'Consolas', monospace; font-size: 0.9rem; white-space: pre-wrap; border-radius: 8px; max-height: calc(100vh - 6rem); overflow-y: auto;">Loading preview...</pre>
Expand Down Expand Up @@ -586,6 +596,118 @@ <h4 class="text-white mb-3">Live Preview</h4>

// Initial preview update
updatePreview();

// --- File Upload & Resume Session Logic ---

// 1. File Upload Handler
document.getElementById('yaml-upload').addEventListener('change', function (e) {
if (!e.target.files.length) return;

const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);

// Add CSRF token
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;

fetch('{% url "upload_yaml" %}', {
method: 'POST',
body: formData,
headers: { 'X-CSRFToken': csrftoken }
})
.then(response => response.json())
.then(result => {
if (result.success) {
populateForm(result.data);
alert('Import successful!');
} else {
alert('Import failed: ' + result.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred during import.');
})
.finally(() => {
e.target.value = ''; // Reset input
});
});

// 2. Populate Form Helper
function populateForm(data) {
// Iterate over flat data keys
Object.entries(data).forEach(([key, value]) => {
// Handle array fields (Checkboxes)
if (key.endsWith('[]')) {
const cleanKey = key.replace('[]', '');
// Checkboxes might be prefixed in HTML (e.g. embodied-...)
// We try matching by name attribute ending with the key
const checkboxes = document.querySelectorAll(`input[type="checkbox"][name$="${cleanKey}"]`);
checkboxes.forEach(cb => {
cb.checked = Array.isArray(value) && value.includes(cb.value);
});
} else {
// Handle standard inputs
// Try to find input by name (handling potential prefixes)
let input = document.querySelector(`[name="${key}"]`) ||
document.querySelector(`[name="embodied-${key}"]`) ||
document.querySelector(`[name="operational-${key}"]`);

if (input) {
input.value = value;
// Trigger input event to update validation/state
input.dispatchEvent(new Event('input'));
}
}
});

// Refresh preview and data collection
['basic-form', 'embodied-form', 'operational-form'].forEach(formId => {
collectFormData(document.getElementById(formId));
});
}

// 3. Auto-Save to LocalStorage
function saveToLocalStorage() {
localStorage.setItem('greenMetaData_computing', JSON.stringify(formData));
document.getElementById('resume-btn').style.display = 'inline-block';
}

// Debounced save
let saveTimer;
function triggerAutoSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(saveToLocalStorage, 1000);
}

// Hook into existing event listeners (add to the end of input listeners)
['basic-form', 'embodied-form', 'operational-form'].forEach(formId => {
const form = document.getElementById(formId);
if (form) {
form.addEventListener('input', triggerAutoSave);
form.addEventListener('change', triggerAutoSave);
}
});

// 4. Check & Restore Session
window.restoreSession = function () {
const saved = localStorage.getItem('greenMetaData_computing');
if (saved) {
try {
const data = JSON.parse(saved);
populateForm(data); // Re-use the populate logic
// Provide visual feedback
alert('Session restored from local storage.');
} catch (e) {
console.error('Error restoring session:', e);
}
}
};

// On load, check if session exists
if (localStorage.getItem('greenMetaData_computing')) {
document.getElementById('resume-btn').style.display = 'inline-block';
}
</script>
{% endblock %}
{% endblock %}
2 changes: 1 addition & 1 deletion interface/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% block content %}
<div class="container mt-5">
<div class="text-center mb-5">
<h1 class="display-4">MetaGreenData</h1>
<h1 class="display-4">GreenMetaData</h1>
<p class="lead">Generate standardized environmental impact metadata for your research</p>
</div>

Expand Down
36 changes: 34 additions & 2 deletions interface/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
from django.test import TestCase
from django.test import TestCase, Client
from django.urls import reverse
from django.core.files.uploadedfile import SimpleUploadedFile
import json

# Create your tests here.
class UploadYamlTests(TestCase):
def test_upload_yaml_success(self):
url = reverse('upload_yaml')
yaml_content = b"""
title: Test Project
description: A test description
keywords:
- test
- green
Computing:
Embodied:
Carbon footprint:
Value (in gCO2e): 100
Source and method: Test Method
"""
uploaded_file = SimpleUploadedFile("test.yaml", yaml_content, content_type="text/yaml")

response = self.client.post(url, {'file': uploaded_file})
self.assertEqual(response.status_code, 200)

data = response.json()
self.assertTrue(data['success'])
self.assertEqual(data['data']['title'], 'Test Project')
self.assertEqual(data['data']['carbon_footprint'], 100)

def test_upload_invalid_file(self):
url = reverse('upload_yaml')
file = SimpleUploadedFile("test.txt", b"content", content_type="text/plain")
response = self.client.post(url, {'file': file})
self.assertEqual(response.status_code, 400)
1 change: 1 addition & 0 deletions interface/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
path('', views.computing_form_view, name='computing'),
path('yml-preview/', views.get_yml_preview, name='yml_preview'),
path('download/', views.download_yml, name='download_yml'),
path('upload/', views.upload_yaml, name='upload_yaml'),
]
92 changes: 92 additions & 0 deletions interface/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import yaml

def parse_yaml_to_form_data(yaml_content):
"""
Parses the GreenMetaData YAML content and returns a flat dictionary
matching the form field names.
"""
try:
data = yaml.safe_load(yaml_content)
form_data = {}

if not data:
return form_data

# Basic Info
form_data['title'] = data.get('title', '')
form_data['description'] = data.get('description', '')
if 'keywords' in data:
form_data['keywords'] = ', '.join(data['keywords'])
form_data['repository_url'] = data.get('repository', '')

# Computing
computing = data.get('Computing', {})
if not computing:
return form_data

# Embodied
embodied = computing.get('Embodied', {})

# Mapping for Embodied fields (YAML key -> Form field prefix)
embodied_map = {
'Carbon footprint': 'carbon_footprint',
'Depletion of Abiotic Resources (Minerals, Metals)': 'depletion_abiotic',
'Particule Matter Emissions': 'particulate_matter',
'Acidification potential': 'acidification_potential',
'Ionising Radiation Related to Human Health': 'ionising_radiation',
'Photochemical Ozone Formation': 'photochemical_ozone',
'Abiotic Depletion Potential (Fossil Fuels)': 'abiotic_depletion_fossil',
'Freshwater Eco-Toxicity Potential': 'freshwater_ecotoxicity'
}

for yaml_key, form_prefix in embodied_map.items():
section = embodied.get(yaml_key, {})
# Handle different value keys based on the section
value_key = next((k for k in section.keys() if k.startswith('Value')), None)

if value_key:
form_data[form_prefix] = section.get(value_key, '')
form_data[f'{form_prefix}_source'] = section.get('Source and method', '')

# Operational
operational = computing.get('Operational', {})

# Impact Values
impact_values = operational.get('Impact values', {})
form_data['energy_consumption'] = impact_values.get('Energy consumption (in kWh)', '')
form_data['operational-carbon_footprint'] = impact_values.get('Carbon footprint (in gCO2e)', '')
form_data['water_consumption'] = impact_values.get('Water consumption (in liters)', '')

# Methods and Scope
methods = operational.get('Methods and scope', {})

# Boundaries
form_data['software_boundaries[]'] = methods.get('Software boundaries', {}).get('Stages included', [])
form_data['tool_stages[]'] = methods.get('Tool stages', {}).get('Stages included', [])

hardware = methods.get('Hardware boundaries', {})
form_data['hardware_boundaries[]'] = hardware.get('Components included', [])
form_data['details_of_hardware'] = hardware.get('Details on hardware used', '')

# Infrastructure
infra = methods.get('Infrastructure', {})
form_data['infrastructure_elements[]'] = infra.get('Elements included', [])

pue = infra.get('Power Usage Effectiveness (PUE)', {})
form_data['pue_value'] = pue.get('Value', '')
form_data['pue_method'] = pue.get('Estimation method used', '')

wue = infra.get('Water usage effectiveness', {})
form_data['wue_value'] = wue.get('Value (in L/kWh)', '')
form_data['wue_method'] = wue.get('Estimation method used', '')

# Electricity Carbon Intensity
eci = methods.get('Electricity carbon intensity', {})
form_data['electrical_carbon_intensity'] = eci.get('Value (in gCO2e/kWh)', '')
form_data['electrical_carbon_intensity_source'] = eci.get('Source', '')

return form_data

except Exception as e:
print(f"Error parsing YAML: {e}")
return {}
18 changes: 18 additions & 0 deletions interface/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
OperationalImpactForm
)
from .models import BasicInformation
from .utils import parse_yaml_to_form_data

def index(request):
return render(request, "home.html")
Expand Down Expand Up @@ -155,3 +156,20 @@ def computing_form_view(request):
'operational_form': OperationalImpactForm(prefix='operational'),
}
return render(request, 'computing.html', context)

@require_http_methods(["POST"])
def upload_yaml(request):
try:
if 'file' not in request.FILES:
return JsonResponse({'error': 'No file uploaded'}, status=400)

file = request.FILES['file']
if not file.name.endswith(('.yaml', '.yml')):
return JsonResponse({'error': 'Invalid file format. Please upload a YAML file.'}, status=400)

content = file.read().decode('utf-8')
form_data = parse_yaml_to_form_data(content)

return JsonResponse({'success': True, 'data': form_data})
except Exception as e:
return JsonResponse({'error': str(e)}, status=400)