From bae8071a39ee66fdafebd0d4a3483f9af7626c39 Mon Sep 17 00:00:00 2001 From: akhil06232 Date: Fri, 13 Feb 2026 12:26:22 +0530 Subject: [PATCH 1/2] Fix: App name inconsistency (#29) and compute form UI overlap (#28) --- README.md | 2 +- interface/templates/base.html | 2 +- interface/templates/computing.html | 4 ++-- interface/templates/home.html | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 69ef168..3f3a81f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MetaGreenData +# GreenMetaData ## Usage diff --git a/interface/templates/base.html b/interface/templates/base.html index d0fe7e2..a1f6bc0 100644 --- a/interface/templates/base.html +++ b/interface/templates/base.html @@ -4,7 +4,7 @@ - MetaGreenData + GreenMetaData diff --git a/interface/templates/computing.html b/interface/templates/computing.html index 36a91f1..789e280 100644 --- a/interface/templates/computing.html +++ b/interface/templates/computing.html @@ -373,8 +373,8 @@

Preview & Download

-
+

Live Preview

Loading preview...
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

From 2dadc92f94a7cc2a8cf117547b0b8258775c60d0 Mon Sep 17 00:00:00 2001 From: akhil06232 Date: Fri, 13 Feb 2026 12:31:54 +0530 Subject: [PATCH 2/2] Feat: Implement Upload YAML (#31) and Resume WIP (#30) --- interface/templates/computing.html | 122 +++++++++++++++++++++++++++++ interface/tests.py | 36 ++++++++- interface/urls.py | 1 + interface/utils.py | 92 ++++++++++++++++++++++ interface/views.py | 18 +++++ 5 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 interface/utils.py diff --git a/interface/templates/computing.html b/interface/templates/computing.html index 789e280..f180781 100644 --- a/interface/templates/computing.html +++ b/interface/templates/computing.html @@ -42,6 +42,16 @@ Back

Computing Impact Form

+
+ + + +
@@ -586,6 +596,118 @@

Live Preview

// 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/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)