// 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';
+ }
{% endblock %}
{% endblock %}
\ No newline at end of file
diff --git a/interface/templates/home.html b/interface/templates/home.html
index 4af27eb..b8038ea 100644
--- a/interface/templates/home.html
+++ b/interface/templates/home.html
@@ -2,7 +2,7 @@
{% block content %}
-
MetaGreenData
+
GreenMetaData
Generate standardized environmental impact metadata for your research
diff --git a/interface/tests.py b/interface/tests.py
index 7ce503c..df12e03 100644
--- a/interface/tests.py
+++ b/interface/tests.py
@@ -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)
diff --git a/interface/urls.py b/interface/urls.py
index a5dfdbc..474a904 100644
--- a/interface/urls.py
+++ b/interface/urls.py
@@ -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'),
]
\ No newline at end of file
diff --git a/interface/utils.py b/interface/utils.py
new file mode 100644
index 0000000..633ac78
--- /dev/null
+++ b/interface/utils.py
@@ -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 {}
diff --git a/interface/views.py b/interface/views.py
index ed39efb..2d62e56 100644
--- a/interface/views.py
+++ b/interface/views.py
@@ -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")
@@ -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)