Compare commits

..

No commits in common. "feature/f31-device-enviromental-impact" and "main" have entirely different histories.

17 changed files with 20 additions and 266 deletions

View file

@ -29,17 +29,14 @@
<li class="nav-item">
<a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a>
</li>
{% if dpps %}
<li class="nav-item">
<a href="#dpps" class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">{% trans 'Dpps' %}</a>
</li>
{% endif %}
{% if dpps %}
<li class="nav-item">
<a href="#dpps" class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">{% trans 'Dpps' %}</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
</li>
<li class="nav-item">
<a href="#environmental_impact" class="nav-link" data-bs-toggle="tab" data-bs-target="#environmental_impact">{% trans 'Environmental impact' %}</a>
</li>
</ul>
</div>
</div>
@ -245,104 +242,23 @@
</div>
</div>
<div class="tab-pane fade" id="environmental_impact">
<div class="container-fluid py-3">
<div class="d-flex justify-content-end mb-3">
<a class="btn btn-success">
<i class="bi bi-file-earmark-pdf"></i>
{% trans 'Export to PDF' %}
</a>
</div>
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card h-100 border-success">
<div class="card-body text-center">
<div class="mb-3">
<i class="bi bi-arrow-down-circle text-success" style="font-size: 2rem;"></i>
</div>
<h5 class="card-title text-success">Carbon Reduction</h5>
<h2 class="mb-2">{{ impact.carbon_saved }}</h2>
<p class="card-text text-muted">kg CO₂e saved</p>
</div>
{% if dpps %}
<div class="tab-pane fade" id="dpps">
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
<div class="list-group col">
{% for d in dpps %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<small class="text-muted">{{ d.timestamp }}</small>
<span>{{ d.type }}</span>
</div>
<p class="mb-1">
<a href="{% url 'did:device_web' d.signature %}">{{ d.signature }}</a>
</p>
</div>
<div class="col-md-4">
<div class="card h-100 border-danger">
<div class="card-body text-center">
<div class="mb-3">
<i class="bi bi-cloud-fill text-danger" style="font-size: 2rem;"></i>
</div>
<h5 class="card-title text-danger">Carbon Consumed</h5>
<h2 class="mb-2">{{ impact.co2_emissions }}</h2>
<p class="card-text text-muted">kg CO₂e consumed</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-success">
<div class="card-body text-center">
<div class="mb-3">
<i class="bi bi-recycle text-success" style="font-size: 2rem;"></i>
</div>
<h5 class="card-title text-success">Additional Impact Metric</h5>
<h2 class="mb-2">85%</h2>
<p class="card-text text-muted">whatever</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Impact Details</h5>
<div class="table-responsive">
<table class="table table-bordered">
<tbody>
<tr>
<th scope="row" class="bg-light" style="width: 30%;">Manufacturing Impact Avoided</th>
<td>
<span class="text-success">{{ impact.carbon_saved }}</span> kg CO₂e
<br />
<small class="text-muted">Based on average laptop manufacturing emissions</small>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-3">
<h6>Calculation Method</h6>
<small class="text-muted">Based on industry standards X Y and Z</small>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% if dpps %}
<div class="tab-pane fade" id="dpps">
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
<div class="list-group col">
{% for d in dpps %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<small class="text-muted">{{ d.timestamp }}</small>
<span>{{ d.type }}</span>
</div>
<p class="mb-1">
<a href="{% url 'did:device_web' d.signature %}">{{ d.signature }}</a>
</p>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -9,5 +9,6 @@ urlpatterns = [
path("<str:pk>/", views.DetailsView.as_view(), name="details"),
path("<str:pk>/annotation/add", views.AddAnnotationView.as_view(), name="add_annotation"),
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"),
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web")
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
]

View file

@ -14,7 +14,6 @@ from evidence.models import Annotation
from lot.models import LotTag
from device.models import Device
from device.forms import DeviceFormSet
from environmental_impact.algorithms.algorithm_factory import FactoryEnvironmentImpactAlgorithm
if settings.DPP:
from dpp.models import Proof
from dpp.api_dlt import PROOF_TYPE
@ -111,16 +110,10 @@ class DetailsView(DashboardView, TemplateView):
uuid__in=self.object.uuids,
type=PROOF_TYPE["IssueDPP"]
)
enviromental_impact_algorithm = FactoryEnvironmentImpactAlgorithm.run_environmental_impact_calculation(
"dummy_calc"
)
enviromental_impact = enviromental_impact_algorithm.get_device_environmental_impact(
self.object)
context.update({
'object': self.object,
'snapshot': self.object.get_last_evidence(),
'lot_tags': lot_tags,
'impact': enviromental_impact,
'dpps': dpps,
})
return context

View file

@ -89,7 +89,6 @@ INSTALLED_APPS = [
"dashboard",
"admin",
"api",
"environmental_impact"
]
DPP = config("DPP", default=False, cast=bool)

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,30 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .dummy_calculator import DummyEnvironmentalImpactAlgorithm
if TYPE_CHECKING:
from .algorithm_interface import EnvironmentImpactAlgorithm
class AlgorithmNames():
"""
Enum class for the different types of algorithms.
"""
DUMMY_CALC = "dummy_calc"
algorithm_names = {
DUMMY_CALC: DummyEnvironmentalImpactAlgorithm()
}
class FactoryEnvironmentImpactAlgorithm():
@staticmethod
def run_environmental_impact_calculation(algorithm_name: str) -> EnvironmentImpactAlgorithm:
try:
return AlgorithmNames.algorithm_names[algorithm_name]
except KeyError:
raise ValueError("Invalid algorithm name. Valid options are: " +
", ".join(AlgorithmNames.algorithm_names.keys()))

View file

@ -1,11 +0,0 @@
from abc import ABC, abstractmethod
from functools import lru_cache
from device.models import Device
from environmental_impact.models import EnvironmentalImpact
class EnvironmentImpactAlgorithm(ABC):
@abstractmethod
def get_device_environmental_impact(self, device: Device) -> EnvironmentalImpact:
pass

View file

@ -1,33 +0,0 @@
from device.models import Device
from .algorithm_interface import EnvironmentImpactAlgorithm
from environmental_impact.models import EnvironmentalImpact
class DummyEnvironmentalImpactAlgorithm(EnvironmentImpactAlgorithm):
def get_device_environmental_impact(self, device: Device) -> EnvironmentalImpact:
# TODO Make a constants file / class
avg_watts = 40 # Arbitrary laptop average consumption
co2_per_kwh = 0.475
power_on_hours = self.get_power_on_hours_from(device)
energy_kwh = (power_on_hours * avg_watts) / 1000
co2_emissions = energy_kwh * co2_per_kwh
return EnvironmentalImpact(co2_emissions=co2_emissions)
def get_power_on_hours_from(self, device: Device) -> int:
# TODO how do I check if the device is a legacy workbench? Is there a better way?
is_legacy_workbench = False if device.last_evidence.inxi else True
if not is_legacy_workbench:
storage_components = device.components[9]
str_time = storage_components.get('time of used', -1)
else:
str_time = ""
uptime_in_hours = self.convert_str_time_to_hours(str_time, is_legacy_workbench)
return uptime_in_hours
def convert_str_time_to_hours(self, time_str: str, is_legacy_workbench: bool) -> int:
if is_legacy_workbench:
return -1 # TODO Power on hours not available in legacy workbench
else:
multipliers = {'y': 365 * 24, 'd': 24, 'h': 1}
return sum(int(part[:-1]) * multipliers[part[-1]] for part in time_str.split())

View file

@ -1,6 +0,0 @@
from django.apps import AppConfig
class EnvironmentalImpactConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "environmental_impact"

View file

@ -1,8 +0,0 @@
from dataclasses import dataclass
from django.db import models
@dataclass
class EnvironmentalImpact:
carbon_saved: float = 0.0
co2_emissions: float = 0.0

View file

@ -1,44 +0,0 @@
from unittest.mock import patch
import uuid
from django.test import TestCase
from device.models import Device
from environmental_impact.models import EnvironmentalImpact
from environmental_impact.algorithms.dummy_calculator import DummyEnvironmentalImpactAlgorithm
from evidence.models import Evidence
class DummyEnvironmentalImpactAlgorithmTests(TestCase):
@patch('evidence.models.Evidence.get_doc', return_value={'credentialSubject': {}})
@patch('evidence.models.Evidence.get_time', return_value=None)
def setUp(self, mock_get_time, mock_get_doc):
self.device = Device(id='1')
evidence = self.device.last_evidence = Evidence(uuid=uuid.uuid4())
evidence.inxi = True
evidence.doc = {'credentialSubject': {}}
self.algorithm = DummyEnvironmentalImpactAlgorithm()
def test_get_power_on_hours_from_legacy_device(self):
# TODO is there a way to check that?
pass
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
def test_get_power_on_hours_from_inxi_device(self, mock_get_components):
hours = self.algorithm.get_power_on_hours_from(self.device)
self.assertEqual(
hours, 8811, "Inxi-parsed devices should correctly compute power-on hours")
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
def test_convert_str_time_to_hours(self, mock_get_components):
result = self.algorithm.convert_str_time_to_hours('1y 2d 3h', False)
self.assertEqual(
result, 8811, "String to hours conversion should match expected output")
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
def test_environmental_impact_calculation(self, mock_get_components):
impact = self.algorithm.get_device_environmental_impact(self.device)
self.assertIsInstance(impact, EnvironmentalImpact,
"Output should be an EnvironmentalImpact instance")
expected_co2 = 8811 * 40 * 0.475 / 1000
self.assertAlmostEqual(impact.co2_emissions, expected_co2,
2, "CO2 emissions calculation should be accurate")

View file

@ -1,17 +0,0 @@
from environmental_impact.algorithms.algorithm_factory import FactoryEnvironmentImpactAlgorithm
from django.test import TestCase
from environmental_impact.algorithms.dummy_calculator import DummyEnvironmentalImpactAlgorithm
class FactoryEnvironmentImpactAlgorithmTests(TestCase):
def test_valid_algorithm_name(self):
algorithm = FactoryEnvironmentImpactAlgorithm.run_environmental_impact_calculation(
'dummy_calc')
self.assertIsInstance(algorithm, DummyEnvironmentalImpactAlgorithm,
"Factory should return a DummyEnvironmentalImpactAlgorithm instance")
def test_invalid_algorithm_name(self):
with self.assertRaises(ValueError):
FactoryEnvironmentImpactAlgorithm.run_environmental_impact_calculation(
'invalid_calc')

View file

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.