Compare commits
No commits in common. "d31c5c79217568df9761dce1c46506892a2d7406" and "ba6c955463370951cef914b76ac4a6b5369c2abc" have entirely different histories.
d31c5c7921
...
ba6c955463
|
@ -25,7 +25,6 @@ class Device:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# the id is the chid of the device
|
# the id is the chid of the device
|
||||||
self.id = kwargs["id"]
|
self.id = kwargs["id"]
|
||||||
self.uuid = kwargs.get("uuid")
|
|
||||||
self.pk = self.id
|
self.pk = self.id
|
||||||
self.shortid = self.pk[:6].upper()
|
self.shortid = self.pk[:6].upper()
|
||||||
self.algorithm = None
|
self.algorithm = None
|
||||||
|
@ -104,14 +103,6 @@ class Device:
|
||||||
self.evidences = [Evidence(u) for u in self.uuids]
|
self.evidences = [Evidence(u) for u in self.uuids]
|
||||||
|
|
||||||
def get_last_evidence(self):
|
def get_last_evidence(self):
|
||||||
if self.last_evidence:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.uuid:
|
|
||||||
import pdb; pdb.set_trace()
|
|
||||||
self.last_evidence = Evidence(self.uuid)
|
|
||||||
return
|
|
||||||
|
|
||||||
annotations = self.get_annotations()
|
annotations = self.get_annotations()
|
||||||
if not annotations.count():
|
if not annotations.count():
|
||||||
return
|
return
|
||||||
|
@ -135,8 +126,6 @@ class Device:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def last_uuid(self):
|
def last_uuid(self):
|
||||||
if self.uuid:
|
|
||||||
return self.uuid
|
|
||||||
return self.uuids[0]
|
return self.uuids[0]
|
||||||
|
|
||||||
def get_lots(self):
|
def get_lots(self):
|
||||||
|
@ -274,38 +263,45 @@ class Device:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_websnapshot(self):
|
def is_websnapshot(self):
|
||||||
self.get_last_evidence()
|
if not self.last_evidence:
|
||||||
|
self.get_last_evidence()
|
||||||
return self.last_evidence.doc['type'] == "WebSnapshot"
|
return self.last_evidence.doc['type'] == "WebSnapshot"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_user_evidence(self):
|
def last_user_evidence(self):
|
||||||
self.get_last_evidence()
|
if not self.last_evidence:
|
||||||
|
self.get_last_evidence()
|
||||||
return self.last_evidence.doc['kv'].items()
|
return self.last_evidence.doc['kv'].items()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def manufacturer(self):
|
def manufacturer(self):
|
||||||
self.get_last_evidence()
|
if not self.last_evidence:
|
||||||
|
self.get_last_evidence()
|
||||||
return self.last_evidence.get_manufacturer()
|
return self.last_evidence.get_manufacturer()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serial_number(self):
|
def serial_number(self):
|
||||||
self.get_last_evidence()
|
if not self.last_evidence:
|
||||||
|
self.get_last_evidence()
|
||||||
return self.last_evidence.get_serial_number()
|
return self.last_evidence.get_serial_number()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
self.get_last_evidence()
|
|
||||||
if self.last_evidence.doc['type'] == "WebSnapshot":
|
if self.last_evidence.doc['type'] == "WebSnapshot":
|
||||||
return self.last_evidence.doc.get("device", {}).get("type", "")
|
return self.last_evidence.doc.get("device", {}).get("type", "")
|
||||||
|
|
||||||
|
if not self.last_evidence:
|
||||||
|
self.get_last_evidence()
|
||||||
return self.last_evidence.get_chassis()
|
return self.last_evidence.get_chassis()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self):
|
||||||
self.get_last_evidence()
|
if not self.last_evidence:
|
||||||
|
self.get_last_evidence()
|
||||||
return self.last_evidence.get_model()
|
return self.last_evidence.get_model()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def components(self):
|
def components(self):
|
||||||
self.get_last_evidence()
|
if not self.last_evidence:
|
||||||
|
self.get_last_evidence()
|
||||||
return self.last_evidence.get_components()
|
return self.last_evidence.get_components()
|
||||||
|
|
|
@ -235,15 +235,14 @@
|
||||||
|
|
||||||
<div class="tab-pane fade" id="dpps">
|
<div class="tab-pane fade" id="dpps">
|
||||||
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
|
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
|
||||||
<div class="list-group col">
|
<div class="list-group col-6">
|
||||||
{% for d in dpps %}
|
{% for d in dpps %}
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<small class="text-muted">{{ d.timestamp }}</small>
|
<small class="text-muted">{{ d.timestamp }}</small>
|
||||||
<span>{{ d.type }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
<a href="{% url 'did:device_web' d.signature %}">{{ d.signature }}</a>
|
<a href="{% url 'did:device_web' d.uuid %}">{{ d.signature }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
@ -14,7 +15,6 @@ from dashboard.mixins import DashboardView, Http403
|
||||||
from evidence.models import Annotation
|
from evidence.models import Annotation
|
||||||
from lot.models import LotTag
|
from lot.models import LotTag
|
||||||
from dpp.models import Proof
|
from dpp.models import Proof
|
||||||
from dpp.api_dlt import PROOF_TYPE
|
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from device.forms import DeviceFormSet
|
from device.forms import DeviceFormSet
|
||||||
|
|
||||||
|
@ -104,10 +104,7 @@ class DetailsView(DashboardView, TemplateView):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
self.object.initial()
|
self.object.initial()
|
||||||
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
|
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
|
||||||
dpps = Proof.objects.filter(
|
dpps = Proof.objects.filter(uuid__in=self.object.uuids)
|
||||||
uuid__in=self.object.uuids,
|
|
||||||
type=PROOF_TYPE["IssueDPP"]
|
|
||||||
)
|
|
||||||
context.update({
|
context.update({
|
||||||
'object': self.object,
|
'object': self.object,
|
||||||
'snapshot': self.object.get_last_evidence(),
|
'snapshot': self.object.get_last_evidence(),
|
||||||
|
|
|
@ -1,497 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>{{ object.type }}</title>
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.custom-container {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 30px;
|
|
||||||
margin-top: 30px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
.section-title {
|
|
||||||
color: #7a9f4f;
|
|
||||||
border-bottom: 2px solid #9cc666;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
.info-row {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.info-label {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #545f71;
|
|
||||||
}
|
|
||||||
.info-value {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.component-card {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-left: 4px solid #9cc666;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.component-card:hover {
|
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
.hash-value {
|
|
||||||
word-break: break-all;
|
|
||||||
background-color: #f3f3f3;
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
.card-title {
|
|
||||||
color: #9cc666;
|
|
||||||
}
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #9cc666;
|
|
||||||
border-color: #9cc666;
|
|
||||||
padding: 0.1em 2em;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: #8ab555;
|
|
||||||
border-color: #8ab555;
|
|
||||||
}
|
|
||||||
.btn-green-user {
|
|
||||||
background-color: #c7e3a3;
|
|
||||||
}
|
|
||||||
.btn-grey {
|
|
||||||
background-color: #f3f3f3;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
background-color: #545f71;
|
|
||||||
color: #ffffff;
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px 0;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container custom-container">
|
|
||||||
<nav class="header-nav ms-auto">
|
|
||||||
<div class="d-flex align-items-right">
|
|
||||||
<span class="nav-item">
|
|
||||||
{% if not roles and user.is_anonymous %}
|
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#validateModal">Validate</button>
|
|
||||||
{% else %}
|
|
||||||
<button class="btn btn-primary" id="buttonRole" data-bs-toggle="modal" data-bs-target="#rolesModal">Select your role</button>
|
|
||||||
<a class="btn btn-primary" href="{% url 'login:logout' %}?next={{ path }}">Logout</a>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% if role %}
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<span class="nav-item">
|
|
||||||
Current Role: {{ role }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<h1 class="text-center mb-4" style="color: #545f71;">{{ object.manufacturer }} {{ object.type }} {{ object.model }}</h1>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
{% if manuals.details.logo %}
|
|
||||||
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
{% if manuals.details.image %}
|
|
||||||
<img style="width: 100px;" src="{{ manuals.details.image }}" />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<h2 class="section-title">Details</h2>
|
|
||||||
<div class="info-row row">
|
|
||||||
<div class="col-md-4 info-label">Phid</div>
|
|
||||||
<div class="col-md-8 info-value">
|
|
||||||
<div class="hash-value">{{ object.id }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-row row">
|
|
||||||
<div class="col-md-4 info-label">Type</div>
|
|
||||||
<div class="col-md-8 info-value">{{ object.type }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if object.is_websnapshot %}
|
|
||||||
{% for snapshot_key, snapshot_value in object.last_user_evidence %}
|
|
||||||
<div class="info-row row">
|
|
||||||
<div class="col-md-4 info-label">{{ snapshot_key }}</div>
|
|
||||||
<div class="col-md-8 info-value">{{ snapshot_value|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<div class="info-row row">
|
|
||||||
<div class="col-md-4 info-label">Manufacturer</div>
|
|
||||||
<div class="col-md-8 info-value">{{ object.manufacturer|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-row row">
|
|
||||||
<div class="col-md-4 info-label">Model</div>
|
|
||||||
<div class="col-md-8 info-value">{{ object.model|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% if user.is_authenticated %}
|
|
||||||
<div class="info-row row">
|
|
||||||
<div class="col-md-4 info-label">Serial Number</div>
|
|
||||||
<div class="col-md-8 info-value">{{ object.serial_number|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<h2 class="section-title">Identifiers</h2>
|
|
||||||
{% for chid in object.hids %}
|
|
||||||
<div class="info-row">
|
|
||||||
<div class="hash-value">{{ chid|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 class="section-title mt-5">Components</h2>
|
|
||||||
<div class="row">
|
|
||||||
{% for component in object.components %}
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<div class="card component-card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">{{ component.type }}</h5>
|
|
||||||
<p class="card-text">
|
|
||||||
{% for component_key, component_value in component.items %}
|
|
||||||
{% if component_key not in 'actions,type' %}
|
|
||||||
{% if component_key != 'serialNumber' or user.is_authenticated %}
|
|
||||||
<strong>{{ component_key }}:</strong> {{ component_value }}<br />
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% if manuals.icecat %}
|
|
||||||
<h5 class="card-title">Icecat data sheet</h5>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 list-group-item d-flex align-items-center">
|
|
||||||
{% if manuals.details.logo %}
|
|
||||||
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
|
|
||||||
{% endif %}
|
|
||||||
{% if manuals.details.image %}
|
|
||||||
<img style="max-width: 100px; margin-right: 15px;" src="{{ manuals.details.image }}" />
|
|
||||||
{% endif %}
|
|
||||||
{% if manuals.details.pdf %}
|
|
||||||
<a href="{{ manuals.details.pdf }}" target="_blank">{{ manuals.details.title }}</a><br />
|
|
||||||
{% else %}
|
|
||||||
{{ manuals.details.title }}<br />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-12 accordion-item">
|
|
||||||
<h5 class="card-title accordion-header">
|
|
||||||
<button class="accordion-button collapsed" data-bs-target="#manuals-icecat" type="button"
|
|
||||||
data-bs-toggle="collapse" aria-expanded="false">
|
|
||||||
More examples
|
|
||||||
</button>
|
|
||||||
</h5>
|
|
||||||
<div id="manuals-icecat" class="row accordion-collapse collapse">
|
|
||||||
<div class="accordion-body">
|
|
||||||
{% for m in manuals.icecat %}
|
|
||||||
<div class="list-group-item d-flex align-items-center">
|
|
||||||
{% if m.logo %}
|
|
||||||
<img style="max-width: 50px; margin-right: 15px;" src="{{ m.logo }}" />
|
|
||||||
{% endif %}
|
|
||||||
{% if m.pdf %}
|
|
||||||
<a href="{{ m.pdf }}" target="_blank">{{ m.title }}</a><br />
|
|
||||||
{% else %}
|
|
||||||
{{ m.title }}<br />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if manuals.laer %}
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="card-title">Recycled Content</h5>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-2">
|
|
||||||
Metal
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="progress">
|
|
||||||
|
|
||||||
<div class="progress-bar"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: {{ manuals.laer.0.metal }}%"
|
|
||||||
aria-valuenow="{{ manuals.laer.0.metal }}"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">{{ manuals.laer.0.metal }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-2">
|
|
||||||
Plastic post Consumer
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="progress">
|
|
||||||
<div class="progress-bar"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: {{ manuals.laer.0.plastic_post_consumer }}%"
|
|
||||||
aria-valuenow="{{ manuals.laer.0.plastic_post_consumer }}"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">{{ manuals.laer.0.plastic_post_consumer }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-2">
|
|
||||||
Plastic post Industry
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<div class="progress">
|
|
||||||
<div class="progress-bar"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: {{ manuals.laer.0.plastic_post_industry }}%"
|
|
||||||
aria-valuenow="{{ manuals.laer.0.plastic_post_industry }}"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">{{ manuals.laer.0.plastic_post_industry }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar %}
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="card-title">Energy spent</h5>
|
|
||||||
|
|
||||||
{% if manuals.energystar.long_idle_watts %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
Consumption when inactivity power function is activated (watts)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.long_idle_watts }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar.short_idle_watts %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
Consumption when inactivity power function is not activated (watts)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.short_idle_watts }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar.sleep_mode_watts %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
sleep_mode_watts
|
|
||||||
Consumption when computer goes into sleep mode (watts)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.sleep_mode_watts }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar.off_mode_watts %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
Consumption when the computer is off (watts)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.off_mode_watts }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar.tec_allowance_kwh %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
Power allocation for normal operation (kwh)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.tec_allowance_kwh }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar.tec_of_model_kwh %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
Consumption of the model configuration (kwh)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.tec_of_model_kwh }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar.tec_requirement_kwh %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
Energy allowance provided (kwh)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.tec_requirement_kwh }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar.work_off_mode_watts %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
The lowest power mode which cannot be switched off (watts)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.work_off_mode_watts }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if manuals.energystar.work_weighted_power_of_model_watts %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
Weighted energy consumption from all its states (watts)
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
{{ manuals.energystar.work_weighted_power_of_model_watts }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% if manuals.ifixit %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 accordion-item">
|
|
||||||
<h5 class="card-title accordion-header">
|
|
||||||
<button class="accordion-button collapsed" data-bs-target="#manuals-repair" type="button"
|
|
||||||
data-bs-toggle="collapse" aria-expanded="false">
|
|
||||||
Repair manuals
|
|
||||||
</button>
|
|
||||||
</h5>
|
|
||||||
<div id="manuals-repair" class="row accordion-collapse collapse">
|
|
||||||
<div class="list-group col">
|
|
||||||
{% for m in manuals.ifixit %}
|
|
||||||
<div class="list-group-item d-flex align-items-center">
|
|
||||||
{% if m.image %}
|
|
||||||
<img style="max-width: 100px; margin-right: 15px;" src="{{ m.image }}" />
|
|
||||||
{% endif %}
|
|
||||||
{% if m.url %}
|
|
||||||
<a href="{{ m.url }}" target="_blank">{{ m.title }}</a><br />
|
|
||||||
{% else %}
|
|
||||||
{{ m.title }}<br />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<footer>
|
|
||||||
<p>
|
|
||||||
©{% now 'Y' %}eReuse. All rights reserved.
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
{% if user.is_anonymous and not roles %}
|
|
||||||
<div class="modal fade" id="validateModal" tabindex="-1" style="display: none;" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Validate as <span id="title-action"></span></h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<a class="btn btn-primary" type="button"
|
|
||||||
href="{% url 'login:login' %}?next={{ path }}">
|
|
||||||
User of system
|
|
||||||
</a>
|
|
||||||
{% if oidc %}
|
|
||||||
<br />
|
|
||||||
<a class="btn btn-primary mt-3" type="button" href="{# url 'oidc:login_other_inventory' #}?next={{ path }}">
|
|
||||||
User of other inventory
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer"></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="modal fade" id="rolesModal" tabindex="-1" style="display: none;" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
|
|
||||||
<form action="{{ path }}" method="get">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Select your Role <span id="title-action"></span></h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<select name="role">
|
|
||||||
{% for k, v in roles %}
|
|
||||||
<option value="{{ k }}" {% if v == role %}selected=selected{% endif %}>{{ v }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
||||||
<input type="submit" class="btn btn-primary" value="Send" />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
171
did/templates/device_web.html
Normal file
171
did/templates/device_web.html
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{ object.type }}</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.custom-container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 30px;
|
||||||
|
margin-top: 30px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
color: #7a9f4f;
|
||||||
|
border-bottom: 2px solid #9cc666;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #545f71;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.component-card {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 4px solid #9cc666;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.component-card:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.hash-value {
|
||||||
|
word-break: break-all;
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
color: #9cc666;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #9cc666;
|
||||||
|
border-color: #9cc666;
|
||||||
|
padding: 0.1em 2em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #8ab555;
|
||||||
|
border-color: #8ab555;
|
||||||
|
}
|
||||||
|
.btn-green-user {
|
||||||
|
background-color: #c7e3a3;
|
||||||
|
}
|
||||||
|
.btn-grey {
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
background-color: #545f71;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container custom-container">
|
||||||
|
<h1 class="text-center mb-4" style="color: #545f71;">{{ object.manufacturer }} {{ object.type }} {{ object.model }}</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h2 class="section-title">Details</h2>
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Phid</div>
|
||||||
|
<div class="col-md-8 info-value">
|
||||||
|
<div class="hash-value">{{ object.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Type</div>
|
||||||
|
<div class="col-md-8 info-value">{{ object.type }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object.is_websnapshot %}
|
||||||
|
{% for snapshot_key, snapshot_value in object.last_user_evidence %}
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">{{ snapshot_key }}</div>
|
||||||
|
<div class="col-md-8 info-value">{{ snapshot_value|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Manufacturer</div>
|
||||||
|
<div class="col-md-8 info-value">{{ object.manufacturer|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Model</div>
|
||||||
|
<div class="col-md-8 info-value">{{ object.model|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Serial Number</div>
|
||||||
|
<div class="col-md-8 info-value">{{ object.serial_number|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h2 class="section-title">Identifiers</h2>
|
||||||
|
{% for chid in object.hids %}
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="hash-value">{{ chid|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="section-title mt-5">Components</h2>
|
||||||
|
<div class="row">
|
||||||
|
{% for component in object.components %}
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="card component-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ component.type }}</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
{% for component_key, component_value in component.items %}
|
||||||
|
{% if component_key not in 'actions,type' %}
|
||||||
|
{% if component_key != 'serialNumber' or user.is_authenticated %}
|
||||||
|
<strong>{{ component_key }}:</strong> {{ component_value }}<br />
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
©{% now 'Y' %}eReuse. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
183
did/views.py
183
did/views.py
|
@ -1,27 +1,14 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.http import JsonResponse, Http404
|
from django.http import JsonResponse, Http404
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from dpp.api_dlt import ALGORITHM
|
|
||||||
from dpp.models import Proof
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('django')
|
|
||||||
|
|
||||||
|
|
||||||
class PublicDeviceWebView(TemplateView):
|
class PublicDeviceWebView(TemplateView):
|
||||||
template_name = "device_did.html"
|
template_name = "device_web.html"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.pk = kwargs['pk']
|
self.pk = kwargs['pk']
|
||||||
chid = self.pk.split(":")[0]
|
self.object = Device(id=self.pk)
|
||||||
proof = Proof.objects.filter(signature=self.pk).first()
|
|
||||||
if proof:
|
|
||||||
self.object = Device(id=chid, uuid=proof.uuid)
|
|
||||||
else:
|
|
||||||
self.object = Device(id=chid)
|
|
||||||
|
|
||||||
if not self.object.last_evidence:
|
if not self.object.last_evidence:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
@ -31,24 +18,12 @@ class PublicDeviceWebView(TemplateView):
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
self.context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
self.object.initial()
|
self.object.initial()
|
||||||
roles = [("Operator", "Operator")]
|
context.update({
|
||||||
role = "Operator"
|
'object': self.object
|
||||||
if self.request.user.is_anonymous:
|
|
||||||
roles = []
|
|
||||||
role = None
|
|
||||||
self.context.update({
|
|
||||||
'object': self.object,
|
|
||||||
'role': role,
|
|
||||||
'roles': roles,
|
|
||||||
'path': self.request.path,
|
|
||||||
'last_dpp': "",
|
|
||||||
'before_dpp': "",
|
|
||||||
})
|
})
|
||||||
if not self.request.user.is_anonymous:
|
return context
|
||||||
self.get_manuals()
|
|
||||||
return self.context
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_fields(self):
|
def public_fields(self):
|
||||||
|
@ -83,149 +58,3 @@ class PublicDeviceWebView(TemplateView):
|
||||||
device_data = self.get_device_data()
|
device_data = self.get_device_data()
|
||||||
return JsonResponse(device_data)
|
return JsonResponse(device_data)
|
||||||
|
|
||||||
def get_result(self):
|
|
||||||
components = []
|
|
||||||
data = {
|
|
||||||
'document': {},
|
|
||||||
'dpp': self.pk,
|
|
||||||
'algorithm': ALGORITHM,
|
|
||||||
'components': components,
|
|
||||||
'manufacturer DPP': '',
|
|
||||||
}
|
|
||||||
result = {
|
|
||||||
'@context': ['https://ereuse.org/dpp0.json'],
|
|
||||||
'data': data,
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.dpp:
|
|
||||||
data['document'] = self.dpp.snapshot.json_hw
|
|
||||||
last_dpp = self.get_last_dpp()
|
|
||||||
url_last = ''
|
|
||||||
if last_dpp:
|
|
||||||
url_last = 'https://{host}/{did}'.format(
|
|
||||||
did=last_dpp.key, host=app.config.get('HOST')
|
|
||||||
)
|
|
||||||
data['url_last'] = url_last
|
|
||||||
|
|
||||||
for c in self.dpp.snapshot.components:
|
|
||||||
components.append({c.type: c.chid})
|
|
||||||
return result
|
|
||||||
|
|
||||||
dpps = []
|
|
||||||
for d in self.device.dpps:
|
|
||||||
rr = {
|
|
||||||
'dpp': d.key,
|
|
||||||
'document': d.snapshot.json_hw,
|
|
||||||
'algorithm': ALGORITHM,
|
|
||||||
'manufacturer DPP': '',
|
|
||||||
}
|
|
||||||
dpps.append(rr)
|
|
||||||
return {
|
|
||||||
'@context': ['https://ereuse.org/dpp0.json'],
|
|
||||||
'data': dpps,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_manuals(self):
|
|
||||||
manuals = {
|
|
||||||
'ifixit': [],
|
|
||||||
'icecat': [],
|
|
||||||
'details': {},
|
|
||||||
'laer': [],
|
|
||||||
'energystar': {},
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
params = {
|
|
||||||
"manufacturer": self.object.manufacturer,
|
|
||||||
"model": self.object.model,
|
|
||||||
}
|
|
||||||
self.params = json.dumps(params)
|
|
||||||
manuals['ifixit'] = self.request_manuals('ifixit')
|
|
||||||
manuals['icecat'] = self.request_manuals('icecat')
|
|
||||||
manuals['laer'] = self.request_manuals('laer')
|
|
||||||
manuals['energystar'] = self.request_manuals('energystar') or {}
|
|
||||||
if manuals['icecat']:
|
|
||||||
manuals['details'] = manuals['icecat'][0]
|
|
||||||
except Exception as err:
|
|
||||||
logger.error("Error: {}".format(err))
|
|
||||||
|
|
||||||
self.context['manuals'] = manuals
|
|
||||||
self.parse_energystar()
|
|
||||||
|
|
||||||
def parse_energystar(self):
|
|
||||||
if not self.context.get('manuals', {}).get('energystar'):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Defined in:
|
|
||||||
# https://dev.socrata.com/foundry/data.energystar.gov/j7nq-iepp
|
|
||||||
|
|
||||||
energy_types = [
|
|
||||||
'functional_adder_allowances_kwh',
|
|
||||||
'tec_allowance_kwh',
|
|
||||||
'long_idle_watts',
|
|
||||||
'short_idle_watts',
|
|
||||||
'off_mode_watts',
|
|
||||||
'sleep_mode_watts',
|
|
||||||
'tec_of_model_kwh',
|
|
||||||
'tec_requirement_kwh',
|
|
||||||
'work_off_mode_watts',
|
|
||||||
'work_weighted_power_of_model_watts',
|
|
||||||
]
|
|
||||||
energy = {}
|
|
||||||
for field in energy_types:
|
|
||||||
energy[field] = []
|
|
||||||
|
|
||||||
for e in self.context['manuals']['energystar']:
|
|
||||||
for field in energy_types:
|
|
||||||
for k, v in e.items():
|
|
||||||
if not v:
|
|
||||||
continue
|
|
||||||
if field in k:
|
|
||||||
energy[field].append(v)
|
|
||||||
|
|
||||||
for k, v in energy.items():
|
|
||||||
if not v:
|
|
||||||
energy[k] = 0
|
|
||||||
continue
|
|
||||||
tt = sum([float(i) for i in v])
|
|
||||||
energy[k] = round(tt / len(v), 2)
|
|
||||||
|
|
||||||
self.context['manuals']['energystar'] = energy
|
|
||||||
|
|
||||||
def request_manuals(self, prefix):
|
|
||||||
#TODO reimplement manuals service
|
|
||||||
response = {
|
|
||||||
"laer": [{"metal": 40, "plastic_post_consumer": 27, "plastic_post_industry": 34}],
|
|
||||||
"energystar": [{
|
|
||||||
'functional_adder_allowances_kwh': 180,
|
|
||||||
"long_idle_watts": 240,
|
|
||||||
"short_idle_watts": 120,
|
|
||||||
"sleep_mode_watts": 30,
|
|
||||||
"off_mode_watts": 3,
|
|
||||||
"tec_allowance_kwh": 180,
|
|
||||||
"tec_of_model_kwh": 150,
|
|
||||||
"tec_requirement_kwh": 220,
|
|
||||||
"work_off_mode_watts": 70,
|
|
||||||
"work_weighted_power_of_model_watts": 240
|
|
||||||
}],
|
|
||||||
"ifixit": [
|
|
||||||
{
|
|
||||||
"image": "https://guide-images.cdn.ifixit.com/igi/156EpI4YdQeVfVPa.medium",
|
|
||||||
"url": "https://es.ifixit.com/Gu%C3%ADa/HP+ProBook+450+G4+Back+Panel+Replacement/171196?lang=en",
|
|
||||||
"title": "HP ProBook 450 G4 Back Panel Replacement"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"image": "https://guide-images.cdn.ifixit.com/igi/usTIqCKpuxVWC3Ix.140x105",
|
|
||||||
"url": "https://es.ifixit.com/Gu%C3%ADa/HP+ProBook+450+G4+Display+Assembly+Replacement/171101?lang=en",
|
|
||||||
"title": "Display Assembly Replacement"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"icecat": [
|
|
||||||
{
|
|
||||||
"logo": "https://images.icecat.biz/img/brand/thumb/1_cf8603f6de7b4c4d8ac4f5f0ef439a05.jpg",
|
|
||||||
"image": "https://guide-images.cdn.ifixit.com/igi/Q2nYjTIQfG6GaI5B.standard",
|
|
||||||
"pdf": "https://icecat.biz/rest/product-pdf?productId=32951710&lang=en",
|
|
||||||
"title": "HP ProBook 450 G3"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return response.get(prefix, {})
|
|
||||||
|
|
Loading…
Reference in a new issue