Compare commits
101 commits
main
...
feature/st
|
@ -2,8 +2,7 @@ DOMAIN=localhost
|
|||
DEMO=true
|
||||
# note that with DEBUG=true, logs are more verbose (include tracebacks)
|
||||
DEBUG=true
|
||||
ALLOWED_HOSTS=${DOMAIN},${DOMAIN}:8000,127.0.0.1,127.0.0.1:8000
|
||||
DPP=false
|
||||
ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1,
|
||||
|
||||
STATIC_ROOT=/tmp/static/
|
||||
MEDIA_ROOT=/tmp/media/
|
||||
|
|
16
action/forms.py
Normal file
16
action/forms.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django import forms
|
||||
from .models import State
|
||||
|
||||
|
||||
class ChangeStateForm(forms.Form):
|
||||
previous_state = forms.CharField(widget=forms.HiddenInput())
|
||||
snapshot_uuid = forms.UUIDField(widget=forms.HiddenInput())
|
||||
new_state = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
|
||||
class AddNoteForm(forms.Form):
|
||||
snapshot_uuid = forms.UUIDField(widget=forms.HiddenInput())
|
||||
note = forms.CharField(
|
||||
required=True,
|
||||
widget=forms.Textarea(attrs={'rows': 4, 'maxlength': 200, 'placeholder': 'Max 200 characters'}),
|
||||
)
|
43
action/management/commands/create_default_states.py
Normal file
43
action/management/commands/create_default_states.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
import logging
|
||||
from django.core.management.base import BaseCommand
|
||||
from action.models import StateDefinition, Institution
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Create default StateDefinitions for a given institution. "'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('institution_name', type=str, help='The name of the institution')
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
default_states = [
|
||||
_("INBOX"),
|
||||
_("VISUAL INSPECTION"),
|
||||
_("REPAIR"),
|
||||
_("INSTALL"),
|
||||
_("TEST"),
|
||||
_("PACKAGING"),
|
||||
_("DONATION"),
|
||||
_("DISMANTLE")
|
||||
]
|
||||
|
||||
institution_name = kwargs['institution_name']
|
||||
institution = Institution.objects.filter(name=institution_name).first()
|
||||
|
||||
if not institution:
|
||||
txt = "No institution found for: %s. Please create an institution first"
|
||||
logger.error(txt, institution.name)
|
||||
return
|
||||
|
||||
for state in default_states:
|
||||
state_def, created = StateDefinition.objects.get_or_create(
|
||||
institution=institution,
|
||||
state=state
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f'Successfully created state: {state}'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(f'State already exists: {state}'))
|
83
action/migrations/0001_initial.py
Normal file
83
action/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
# Generated by Django 5.0.6 on 2024-12-11 18:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("user", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="State",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date", models.DateTimeField(auto_now_add=True)),
|
||||
("state", models.CharField(max_length=50)),
|
||||
("snapshot_uuid", models.UUIDField()),
|
||||
(
|
||||
"institution",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="StateDefinition",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("order", models.PositiveIntegerField(default=0)),
|
||||
("state", models.CharField(max_length=50)),
|
||||
(
|
||||
"institution",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["order"],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="statedefinition",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("institution", "state"), name="unique_institution_state"
|
||||
),
|
||||
),
|
||||
]
|
89
action/migrations/0002_devicelog_note.py
Normal file
89
action/migrations/0002_devicelog_note.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# Generated by Django 5.0.6 on 2024-12-17 19:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("action", "0001_initial"),
|
||||
("user", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DeviceLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date", models.DateTimeField(auto_now_add=True)),
|
||||
("event", models.CharField(max_length=255)),
|
||||
("snapshot_uuid", models.UUIDField()),
|
||||
(
|
||||
"institution",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-date"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Note",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date", models.DateTimeField(auto_now_add=True)),
|
||||
("description", models.TextField()),
|
||||
("snapshot_uuid", models.UUIDField()),
|
||||
(
|
||||
"institution",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-date"],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,3 +1,82 @@
|
|||
from django.db import models
|
||||
from django.db import models, connection
|
||||
from django.db.models import Max
|
||||
from user.models import User, Institution
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# Create your models here.
|
||||
class State(models.Model):
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
institution = models.ForeignKey(Institution, on_delete=models.SET_NULL, null=True)
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
|
||||
state = models.CharField(max_length=50)
|
||||
snapshot_uuid = models.UUIDField()
|
||||
|
||||
def clean(self):
|
||||
if not StateDefinition.objects.filter(institution=self.institution, state=self.state).exists():
|
||||
raise ValidationError(f"The state '{self.state}' is not valid for the institution '{self.institution.name}'.")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.institution.name} - {self.state} - {self.snapshot_uuid}"
|
||||
|
||||
class StateDefinition(models.Model):
|
||||
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
state = models.CharField(max_length=50)
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['institution', 'state'], name='unique_institution_state')
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk:
|
||||
# set the order to be last
|
||||
max_order = StateDefinition.objects.filter(institution=self.institution).aggregate(Max('order'))['order__max']
|
||||
self.order = (max_order or 0) + 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
institution = self.institution
|
||||
order = self.order
|
||||
super().delete(*args, **kwargs)
|
||||
# Adjust the order of other instances
|
||||
StateDefinition.objects.filter(institution=institution, order__gt=order).update(order=models.F('order') - 1)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.institution.name} - {self.state}"
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
description = models.TextField()
|
||||
snapshot_uuid = models.UUIDField()
|
||||
|
||||
class Meta:
|
||||
ordering = ['-date']
|
||||
|
||||
def __str__(self):
|
||||
return f" Note: {self.description}, by {self.user.username} @ {self.user.institution} - {self.date}, for {self.snapshot_uuid}"
|
||||
|
||||
|
||||
class DeviceLog(models.Model):
|
||||
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
event = models.CharField(max_length=255)
|
||||
snapshot_uuid = models.UUIDField()
|
||||
|
||||
class Meta:
|
||||
ordering = ['-date']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.event} by {self.user.username} @ {self.institution.name} - {self.date}, for {self.snapshot_uuid}"
|
||||
|
|
|
@ -1 +1,12 @@
|
|||
from django.urls import path, include
|
||||
from action import views
|
||||
|
||||
app_name = 'action'
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("new/", views.ChangeStateView.as_view(), name="change_state"),
|
||||
path('note/add/', views.AddNoteView.as_view(), name='add_note'),
|
||||
path('note/edit/<int:pk>', views.UpdateNoteView.as_view(), name='update_note'),
|
||||
path('note/delete/<int:pk>', views.DeleteNoteView.as_view(), name='delete_note'),
|
||||
]
|
||||
|
|
141
action/views.py
141
action/views.py
|
@ -1 +1,140 @@
|
|||
# from django.shortcuts import render
|
||||
from django.views import View
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from action.forms import ChangeStateForm, AddNoteForm
|
||||
from django.views.generic.edit import DeleteView, CreateView, UpdateView, FormView
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from action.models import State, StateDefinition, Note, DeviceLog
|
||||
from device.models import Device
|
||||
|
||||
|
||||
class ChangeStateView(FormView):
|
||||
form_class = ChangeStateForm
|
||||
|
||||
def form_valid(self, form):
|
||||
previous_state = form.cleaned_data['previous_state']
|
||||
new_state = form.cleaned_data['new_state']
|
||||
snapshot_uuid = form.cleaned_data['snapshot_uuid']
|
||||
|
||||
State.objects.create(
|
||||
snapshot_uuid=snapshot_uuid,
|
||||
state=new_state,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution,
|
||||
)
|
||||
|
||||
message = _("<Created> State '{}'. Previous State: '{}'").format(new_state, previous_state)
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=snapshot_uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution,
|
||||
)
|
||||
messages.success(self.request, _("State successfully changed from '{}' to '{}'").format(previous_state, new_state))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _("There was an error with your submission."))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
|
||||
|
||||
|
||||
class AddNoteView(FormView):
|
||||
form_class = AddNoteForm
|
||||
|
||||
def form_valid(self, form):
|
||||
note_text = form.cleaned_data['note']
|
||||
snapshot_uuid = form.cleaned_data['snapshot_uuid']
|
||||
Note.objects.create(
|
||||
snapshot_uuid=snapshot_uuid,
|
||||
description=note_text,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution,
|
||||
)
|
||||
|
||||
message = _("<Created> Note: '{}'").format(note_text)
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=snapshot_uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution,
|
||||
)
|
||||
messages.success(self.request, _("Note has been added"))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _("There was an error with your submission."))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
|
||||
|
||||
|
||||
class UpdateNoteView(UpdateView):
|
||||
model = Note
|
||||
fields = ['description']
|
||||
pk_url_kwarg = 'pk'
|
||||
|
||||
def form_valid(self, form):
|
||||
old_description = self.get_object().description
|
||||
new_description = self.object.description
|
||||
snapshot_uuid = self.object.snapshot_uuid
|
||||
|
||||
if old_description != new_description:
|
||||
message = _("<Updated> Note. Old Description: '{}'. New Description: '{}'").format(old_description, new_description)
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=snapshot_uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution,
|
||||
)
|
||||
messages.success(self.request, "Note has been updated.")
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
new_description = form.cleaned_data.get('description', '').strip()
|
||||
if not new_description:
|
||||
messages.error(self.request, _("Note cannot be empty."))
|
||||
super().form_invalid(form)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details'))
|
||||
|
||||
|
||||
class DeleteNoteView(View):
|
||||
model = Note
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.pk = kwargs['pk']
|
||||
referer = request.META.get('HTTP_REFERER')
|
||||
if not referer:
|
||||
raise Http404("No referer header found")
|
||||
|
||||
self.object = get_object_or_404(
|
||||
self.model,
|
||||
pk=self.pk,
|
||||
institution=self.request.user.institution
|
||||
)
|
||||
description = self.object.description
|
||||
snapshot_uuid= self.object.snapshot_uuid
|
||||
|
||||
if request.user != self.object.user and not request.user.is_admin:
|
||||
messages.error(request, _("You do not have permission to delete this note."))
|
||||
return redirect(referer)
|
||||
|
||||
message = _("<Deleted> Note. Description: '{}'. ").format(description)
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=snapshot_uuid,
|
||||
event=message,
|
||||
user=request.user,
|
||||
institution=request.user.institution,
|
||||
)
|
||||
messages.warning(self.request, _("Note '{}' deleted successfully.").format(description))
|
||||
|
||||
self.object.delete()
|
||||
|
||||
return redirect(referer)
|
||||
|
|
5
admin/forms.py
Normal file
5
admin/forms.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django import forms
|
||||
|
||||
|
||||
class OrderingStateForm(forms.Form):
|
||||
ordering = forms.CharField()
|
233
admin/templates/states_panel.html
Normal file
233
admin/templates/states_panel.html
Normal file
|
@ -0,0 +1,233 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n django_bootstrap5 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ subtitle }}</h3>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button type="button" class="btn btn-green-admin" data-bs-toggle="modal" data-bs-target="#addStateModal">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
{% if state_definitions %}
|
||||
<table class="table table-hover table-bordered align-middle">
|
||||
<caption class="text-muted small">
|
||||
{% trans 'Move and drag state definitions to reorder' %}
|
||||
</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" width="1%" class="text-start">
|
||||
</th>
|
||||
<th scope="col" width="5%" class="text-center">
|
||||
#</th>
|
||||
<th scope="col">{% trans "State Definition" %}
|
||||
</th>
|
||||
<th scope="col" width="15%" class="text-center">{% trans "Actions" %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sortable_list">
|
||||
{% for state_definition in state_definitions %}
|
||||
<tr
|
||||
data-lookup="{{ state_definition.id }}"
|
||||
style="cursor: grab;"
|
||||
class="align-items-center">
|
||||
|
||||
<td class="">
|
||||
<i class="bi bi-grip-vertical" aria-hidden="true">
|
||||
</i>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<strong>{{ state_definition.order }} </strong>
|
||||
</td>
|
||||
<td class="font-monospace">
|
||||
{{ state_definition.state }}
|
||||
</td>
|
||||
|
||||
<!-- action buttons -->
|
||||
<td>
|
||||
<div class="btn-group float-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-info d-flex align-items-center"
|
||||
data-bs-toggle="modal" data-bs-target="#editStateModal{{ state_definition.id }}">
|
||||
<i class="bi bi-pencil me-1"></i>
|
||||
{% trans 'Edit' %}
|
||||
</button>
|
||||
<button
|
||||
type="button" class="btn btn-sm btn-outline-danger d-flex align-items-center"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteStateModal{{ state_definition.id }}" >
|
||||
<i class="bi bi-trash me-1"></i>
|
||||
{% trans 'Delete' %}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form id="orderingForm" method="post" action="{% url 'admin:update_state_order' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="orderingInput" name="ordering">
|
||||
<button id="saveOrderBtn" class="btn btn-success mt-5 float-start collapse" >{% trans "Update Order" %}</button>
|
||||
</form>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-primary text-center mt-5" role="alert">
|
||||
{% trans "No states found on current organization" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- add state definition Modal -->
|
||||
<div class="modal fade" id="addStateModal" tabindex="-1" aria-labelledby="addStateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addStateModalLabel">{% trans "Add State Definition" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<form method="post" action="{%url 'admin:add_state_definition'%}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="stateInput" class="form-label">{% trans "State" %}</label>
|
||||
<input type="text" class="form-control" id="stateInput" name="state" maxlength="50" required>
|
||||
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Add state definition" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit State Definition Modals -->
|
||||
{% for state_definition in state_definitions %}
|
||||
<div class="modal fade" id="editStateModal{{ state_definition.id }}" tabindex="-1" aria-labelledby="editStateModalLabel{{ state_definition.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{% url 'admin:edit_state_definition' state_definition.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editStateModalLabel{{ state_definition.id }}">
|
||||
{% trans "Edit State Definition" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning text-center" role="alert">
|
||||
{% trans "Existing devices with this state will not have their state names changed." %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editStateInput{{ state_definition.id }}" class="form-label">{% trans "State" %}</label>
|
||||
<input type="text" class="form-control" id="editStateInput{{ state_definition.id }}" name="state" maxlength="50" value="{{ state_definition.state }}" required>
|
||||
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted text-end">{% trans "Any changes in order will not be saved." %}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="submit" class="btn btn-green-admin">{% trans "Save Changes" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<!-- delete state definition Modal -->
|
||||
{% for state_definition in state_definitions %}
|
||||
<div class="modal fade" id="deleteStateModal{{ state_definition.id }}" tabindex="-1" aria-labelledby="deleteStateModalLabel{{ state_definition.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fw-bold" id="deleteStateModalLabel{{ state_definition.id }}">
|
||||
{% trans "Delete State Definition" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning text-center" role="alert">
|
||||
{% trans "Devices with a State of this description will not have their State altered" %}
|
||||
</div>
|
||||
<div class="d-flex align-items-center border rounded p-3 mt-3">
|
||||
<span class="badge bg-secondary me-3 display-6">{{ state_definition.order }}</span>
|
||||
<div>
|
||||
<p class="mb-0 fw-bold">{{ state_definition.state }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted text-end mt-3">{% trans "Any changes in order will not be saved." %}</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<form method="post" action="{% url 'admin:delete_state_definition' state_definition.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{% trans "Cancel" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<script>
|
||||
//following https://dev.to/nemecek_f/django-how-to-let-user-re-order-sort-table-of-content-with-drag-and-drop-3nlp
|
||||
const saveOrderingButton = document.getElementById('saveOrderBtn');
|
||||
const orderingForm = document.getElementById('orderingForm');
|
||||
const formInput = orderingForm.querySelector('#orderingInput');
|
||||
const sortable_table = document.getElementById('sortable_list');
|
||||
|
||||
const sortable = new Sortable(sortable_table, {
|
||||
animation: 150,
|
||||
swapThreshold: 0.10,
|
||||
onChange: () => {
|
||||
//TODO: change hide/show animation to a nicer one
|
||||
const collapse = new bootstrap.Collapse(saveOrderingButton, {
|
||||
toggle: false
|
||||
});
|
||||
collapse.show();
|
||||
}
|
||||
});
|
||||
|
||||
function saveOrdering() {
|
||||
const rows = sortable_table.querySelectorAll('tr');
|
||||
let ids = [];
|
||||
for (let row of rows) {
|
||||
ids.push(row.dataset.lookup);
|
||||
}
|
||||
formInput.value = ids.join(',');
|
||||
orderingForm.submit();
|
||||
}
|
||||
|
||||
saveOrderingButton.addEventListener('click', saveOrdering);
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -10,4 +10,9 @@ urlpatterns = [
|
|||
path("users/edit/<int:pk>", views.EditUserView.as_view(), name="edit_user"),
|
||||
path("users/delete/<int:pk>", views.DeleteUserView.as_view(), name="delete_user"),
|
||||
path("institution/<int:pk>", views.InstitutionView.as_view(), name="institution"),
|
||||
path("states/", views.StatesPanelView.as_view(), name="states_panel"),
|
||||
path("states/add", views.AddStateDefinitionView.as_view(), name="add_state_definition"),
|
||||
path("states/delete/<int:pk>", views.DeleteStateDefinitionView.as_view(), name='delete_state_definition'),
|
||||
path("states/update_order/", views.UpdateStateOrderView.as_view(), name='update_state_order'),
|
||||
path("states/edit/<int:pk>/", views.UpdateStateDefinitionView.as_view(), name='edit_state_definition'),
|
||||
]
|
||||
|
|
109
admin/views.py
109
admin/views.py
|
@ -1,16 +1,23 @@
|
|||
import logging
|
||||
from smtplib import SMTPException
|
||||
from django.contrib import messages
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.views.generic.base import TemplateView, ContextMixin
|
||||
from django.views.generic.edit import (
|
||||
CreateView,
|
||||
UpdateView,
|
||||
DeleteView,
|
||||
)
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError, transaction
|
||||
from dashboard.mixins import DashboardView, Http403
|
||||
from admin.forms import OrderingStateForm
|
||||
from user.models import User, Institution
|
||||
from admin.email import NotifyActivateUserByEmail
|
||||
from action.models import StateDefinition
|
||||
|
||||
|
||||
class AdminView(DashboardView):
|
||||
|
@ -124,3 +131,101 @@ class InstitutionView(AdminView, UpdateView):
|
|||
self.object = self.request.user.institution
|
||||
kwargs = super().get_form_kwargs()
|
||||
return kwargs
|
||||
|
||||
|
||||
class StateDefinitionContextMixin(ContextMixin):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
"state_definitions": StateDefinition.objects.filter(institution=self.request.user.institution).order_by('order'),
|
||||
"help_text": _('State definitions are the custom finite states that a device can be in.'),
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class StatesPanelView(AdminView, StateDefinitionContextMixin, TemplateView):
|
||||
template_name = "states_panel.html"
|
||||
title = _("States Panel")
|
||||
breadcrumb = _("admin / States Panel") + " /"
|
||||
|
||||
|
||||
class AddStateDefinitionView(AdminView, StateDefinitionContextMixin, CreateView):
|
||||
template_name = "states_panel.html"
|
||||
title = _("New State Definition")
|
||||
breadcrumb = "Admin / New state"
|
||||
success_url = reverse_lazy('admin:states_panel')
|
||||
model = StateDefinition
|
||||
fields = ('state',)
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.institution = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
try:
|
||||
response = super().form_valid(form)
|
||||
messages.success(self.request, _("State definition successfully added."))
|
||||
return response
|
||||
except IntegrityError:
|
||||
messages.error(self.request, _("State is already defined."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessMessageMixin, DeleteView):
|
||||
model = StateDefinition
|
||||
success_url = reverse_lazy('admin:states_panel')
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return f'State definition: {self.object.state}, has been deleted'
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
#only an admin of current institution can delete
|
||||
if not object.institution == self.request.user.institution:
|
||||
raise Http404
|
||||
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UpdateStateOrderView(AdminView, TemplateView):
|
||||
success_url = reverse_lazy('admin:states_panel')
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = OrderingStateForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
ordered_ids = form.cleaned_data["ordering"].split(',')
|
||||
|
||||
with transaction.atomic():
|
||||
current_order = 1
|
||||
_log = []
|
||||
for lookup_id in ordered_ids:
|
||||
state_definition = StateDefinition.objects.get(id=lookup_id)
|
||||
state_definition.order = current_order
|
||||
state_definition.save()
|
||||
_log.append(f"{state_definition.state} (ID: {lookup_id} -> Order: {current_order})")
|
||||
current_order += 1
|
||||
|
||||
messages.success(self.request, _("Order changed succesfuly."))
|
||||
return redirect(self.success_url)
|
||||
else:
|
||||
return Http404
|
||||
|
||||
|
||||
class UpdateStateDefinitionView(AdminView, UpdateView):
|
||||
model = StateDefinition
|
||||
template_name = 'states_panel.html'
|
||||
fields = ['state']
|
||||
pk_url_kwarg = 'pk'
|
||||
|
||||
def get_queryset(self):
|
||||
return StateDefinition.objects.filter(institution=self.request.user.institution)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, _("State definition updated successfully."))
|
||||
return reverse_lazy('admin:states_panel')
|
||||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(form)
|
||||
|
|
|
@ -7,7 +7,7 @@ app_name = 'api'
|
|||
|
||||
urlpatterns = [
|
||||
path('v1/snapshot/', views.NewSnapshotView.as_view(), name='new_snapshot'),
|
||||
path('v1/annotation/<str:pk>/', views.AddAnnotationView.as_view(), name='new_annotation'),
|
||||
path('v1/property/<str:pk>/', views.AddPropertyView.as_view(), name='new_property'),
|
||||
path('v1/device/<str:pk>/', views.DetailsDeviceView.as_view(), name='device'),
|
||||
path('v1/tokens/', views.TokenView.as_view(), name='tokens'),
|
||||
path('v1/tokens/new', views.TokenNewView.as_view(), name='new_token'),
|
||||
|
|
50
api/views.py
50
api/views.py
|
@ -21,7 +21,7 @@ from django.views.generic.edit import (
|
|||
from utils.save_snapshots import move_json, save_in_disk
|
||||
from django.views.generic.edit import View
|
||||
from dashboard.mixins import DashboardView
|
||||
from evidence.models import Annotation
|
||||
from evidence.models import SystemProperty, UserProperty
|
||||
from evidence.parse_details import ParseSnapshot
|
||||
from evidence.parse import Build
|
||||
from device.models import Device
|
||||
|
@ -85,21 +85,17 @@ class NewSnapshotView(ApiMixing):
|
|||
# except Exception:
|
||||
# return JsonResponse({'error': 'Invalid Snapshot'}, status=400)
|
||||
|
||||
ev_uuid = data.get("uuid")
|
||||
if data.get("credentialSubject"):
|
||||
ev_uuid = data["credentialSubject"].get("uuid")
|
||||
|
||||
if not ev_uuid:
|
||||
if not data.get("uuid"):
|
||||
txt = "error: the snapshot not have uuid"
|
||||
logger.error("%s", txt)
|
||||
return JsonResponse({'status': txt}, status=500)
|
||||
|
||||
exist_annotation = Annotation.objects.filter(
|
||||
uuid=ev_uuid
|
||||
exist_property = SystemProperty.objects.filter(
|
||||
uuid=data['uuid']
|
||||
).first()
|
||||
|
||||
if exist_annotation:
|
||||
txt = "error: the snapshot {} exist".format(ev_uuid)
|
||||
if exist_property:
|
||||
txt = "error: the snapshot {} exist".format(data['uuid'])
|
||||
logger.warning("%s", txt)
|
||||
return JsonResponse({'status': txt}, status=500)
|
||||
|
||||
|
@ -109,31 +105,30 @@ class NewSnapshotView(ApiMixing):
|
|||
except Exception as err:
|
||||
if settings.DEBUG:
|
||||
logger.exception("%s", err)
|
||||
snapshot_id = ev_uuid
|
||||
snapshot_id = data.get("uuid", "")
|
||||
txt = "It is not possible to parse snapshot: %s."
|
||||
logger.error(txt, snapshot_id)
|
||||
text = "fail: It is not possible to parse snapshot"
|
||||
return JsonResponse({'status': text}, status=500)
|
||||
|
||||
annotation = Annotation.objects.filter(
|
||||
uuid=ev_uuid,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
property = SystemProperty.objects.filter(
|
||||
uuid=data['uuid'],
|
||||
# TODO this is hardcoded, it should select the user preferred algorithm
|
||||
key="hidalgo1",
|
||||
owner=self.tk.owner.institution
|
||||
).first()
|
||||
|
||||
|
||||
if not annotation:
|
||||
logger.error("Error: No annotation for uuid: %s", ev_uuid)
|
||||
if not property:
|
||||
logger.error("Error: No property for uuid: %s", data["uuid"])
|
||||
return JsonResponse({'status': 'fail'}, status=500)
|
||||
|
||||
url_args = reverse_lazy("device:details", args=(annotation.value,))
|
||||
url_args = reverse_lazy("device:details", args=(property.value,))
|
||||
url = request.build_absolute_uri(url_args)
|
||||
|
||||
response = {
|
||||
"status": "success",
|
||||
"dhid": annotation.value[:6].upper(),
|
||||
"dhid": property.value[:6].upper(),
|
||||
"url": url,
|
||||
# TODO replace with public_url when available
|
||||
"public_url": url
|
||||
|
@ -259,22 +254,21 @@ class DetailsDeviceView(ApiMixing):
|
|||
"components": snapshot.get("components"),
|
||||
})
|
||||
|
||||
uuids = Annotation.objects.filter(
|
||||
uuids = SystemProperty.objects.filter(
|
||||
owner=self.tk.owner.institution,
|
||||
value=self.pk
|
||||
).values("uuid")
|
||||
|
||||
annotations = Annotation.objects.filter(
|
||||
properties = UserProperty.objects.filter(
|
||||
uuid__in=uuids,
|
||||
owner=self.tk.owner.institution,
|
||||
type = Annotation.Type.USER
|
||||
).values_list("key", "value")
|
||||
|
||||
data.update({"annotations": list(annotations)})
|
||||
data.update({"properties": list(properties)})
|
||||
return data
|
||||
|
||||
|
||||
class AddAnnotationView(ApiMixing):
|
||||
class AddPropertyView(ApiMixing):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
response = self.auth()
|
||||
|
@ -283,13 +277,12 @@ class AddAnnotationView(ApiMixing):
|
|||
|
||||
self.pk = kwargs['pk']
|
||||
institution = self.tk.owner.institution
|
||||
self.annotation = Annotation.objects.filter(
|
||||
self.property = SystemProperty.objects.filter(
|
||||
owner=institution,
|
||||
value=self.pk,
|
||||
type=Annotation.Type.SYSTEM
|
||||
).first()
|
||||
|
||||
if not self.annotation:
|
||||
if not self.property:
|
||||
return JsonResponse({}, status=404)
|
||||
|
||||
try:
|
||||
|
@ -300,10 +293,9 @@ class AddAnnotationView(ApiMixing):
|
|||
logger.error("Invalid Snapshot of user %s", self.tk.owner)
|
||||
return JsonResponse({'error': 'Invalid JSON'}, status=500)
|
||||
|
||||
Annotation.objects.create(
|
||||
uuid=self.annotation.uuid,
|
||||
UserProperty.objects.create(
|
||||
uuid=self.property.uuid,
|
||||
owner=self.tk.owner.institution,
|
||||
type = Annotation.Type.USER,
|
||||
key = key,
|
||||
value = value
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.core.exceptions import PermissionDenied
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic.base import TemplateView
|
||||
from device.models import Device
|
||||
from evidence.models import Annotation
|
||||
from evidence.models import SystemProperty
|
||||
from lot.models import LotTag
|
||||
|
||||
|
||||
|
@ -49,7 +49,7 @@ class DashboardView(LoginRequiredMixin):
|
|||
dev_ids = self.request.session.pop("devices", [])
|
||||
|
||||
self._devices = []
|
||||
for x in Annotation.objects.filter(value__in=dev_ids).filter(
|
||||
for x in SystemProperty.objects.filter(value__in=dev_ids).filter(
|
||||
owner=self.request.user.institution
|
||||
).distinct():
|
||||
self._devices.append(Device(id=x.value))
|
||||
|
|
2
dashboard/static/js/Sortable.min.js
vendored
Normal file
2
dashboard/static/js/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -18,6 +18,7 @@
|
|||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
|
||||
<script src="{% static 'js/Sortable.min.js' %}"></script>
|
||||
|
||||
<style>
|
||||
.bd-placeholder-img {
|
||||
|
@ -81,7 +82,7 @@
|
|||
<ul class="nav flex-column">
|
||||
{% if user.is_admin %}
|
||||
<li class="nav-item">
|
||||
<a class="admin {% if path in 'panel users' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()">
|
||||
<a class="admin {% if path in 'panel users states_panel' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()">
|
||||
<i class="bi bi-person-fill-gear icon_sidebar"></i>
|
||||
{% trans 'Admin' %}
|
||||
</a>
|
||||
|
@ -96,6 +97,11 @@
|
|||
{% trans 'Users' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if path == 'states' %} active2{% endif %}" href="{% url 'admin:states_panel' %}">
|
||||
{% trans 'States' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
@ -178,9 +184,15 @@
|
|||
{% endfor %}
|
||||
{% endblock messages %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
|
||||
<h1 class="h2">{{ title }}</h1>
|
||||
|
||||
<form method="post" action="{% url 'dashboard:search' %}">
|
||||
<h1 class="h2">{{ title }}
|
||||
{% if help_text %}
|
||||
<span class="ms-1" data-bs-toggle="tooltip" data-bs-placement="right" title="{{ help_text }}">
|
||||
<i class="fas fa-question-circle text-secondary h6 align-top"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="post" action="{% url 'dashboard:search' %}">
|
||||
{% csrf_token %}
|
||||
<div class="input-group rounded">
|
||||
<input type="search" name="search" class="form-control rounded" placeholder="Search your device..." aria-label="Search" aria-describedby="search-addon" />
|
||||
|
@ -221,4 +233,13 @@
|
|||
{% block extrascript %}{% endblock %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
<script>
|
||||
//If help_text is passed to the view as context, a hover-able help icon is displayed
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
{% trans 'Exports' %}
|
||||
</a>
|
||||
{% if lot %}
|
||||
<a href="{% url 'lot:annotations' object.id %}" type="button" class="btn btn-green-admin">
|
||||
<a href="{% url 'lot:properties' object.id %}" type="button" class="btn btn-green-admin">
|
||||
<i class="bi bi-tag"></i>
|
||||
{% trans 'Annotations' %}
|
||||
{% trans 'properties' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -69,11 +69,7 @@
|
|||
{{ dev.manufacturer }}
|
||||
</td>
|
||||
<td>
|
||||
{% if dev.version %}
|
||||
{{dev.version}} {{ dev.model }}
|
||||
{% else %}
|
||||
{{ dev.model }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.shortcuts import Http404
|
|||
from django.db.models import Q
|
||||
|
||||
from dashboard.mixins import InventaryMixin, DetailsMixin
|
||||
from evidence.models import Annotation
|
||||
from evidence.models import SystemProperty
|
||||
from evidence.xapian import search
|
||||
from device.models import Device
|
||||
from lot.models import Lot
|
||||
|
@ -74,7 +74,7 @@ class SearchView(InventaryMixin):
|
|||
|
||||
for x in matches:
|
||||
# devices.append(self.get_annotations(x))
|
||||
dev = self.get_annotations(x)
|
||||
dev = self.get_properties(x)
|
||||
if dev.id not in dev_id:
|
||||
devices.append(dev)
|
||||
dev_id.append(dev.id)
|
||||
|
@ -83,13 +83,10 @@ class SearchView(InventaryMixin):
|
|||
# TODO fix of pagination, the count is not correct
|
||||
return devices, count
|
||||
|
||||
def get_annotations(self, xp):
|
||||
snap = json.loads(xp.document.get_data())
|
||||
if snap.get("credentialSubject"):
|
||||
uuid = snap["credentialSubject"]["uuid"]
|
||||
else:
|
||||
uuid = snap["uuid"]
|
||||
return Device.get_annotation_from_uuid(uuid, self.request.user.institution)
|
||||
def get_properties(self, xp):
|
||||
snap = xp.document.get_data()
|
||||
uuid = json.loads(snap).get('uuid')
|
||||
return Device.get_properties_from_uuid(uuid, self.request.user.institution)
|
||||
|
||||
def search_hids(self, query, offset, limit):
|
||||
qry = Q()
|
||||
|
@ -98,8 +95,7 @@ class SearchView(InventaryMixin):
|
|||
if i:
|
||||
qry |= Q(value__startswith=i)
|
||||
|
||||
chids = Annotation.objects.filter(
|
||||
type=Annotation.Type.SYSTEM,
|
||||
chids = SystemProperty.objects.filter(
|
||||
owner=self.request.user.institution
|
||||
).filter(
|
||||
qry
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django import forms
|
||||
from utils.device import create_annotation, create_doc, create_index
|
||||
from utils.device import create_property, create_doc, create_index
|
||||
from utils.save_snapshots import move_json, save_in_disk
|
||||
|
||||
|
||||
|
@ -59,7 +59,7 @@ class BaseDeviceFormSet(forms.BaseFormSet):
|
|||
|
||||
path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
|
||||
create_index(doc, self.user)
|
||||
create_annotation(doc, user, commit=commit)
|
||||
create_property(doc, user, commit=commit)
|
||||
move_json(path_name, self.user.institution.name, place="placeholder")
|
||||
|
||||
return doc
|
||||
|
|
139
device/models.py
139
device/models.py
|
@ -1,8 +1,9 @@
|
|||
from django.db import models, connection
|
||||
|
||||
from utils.constants import ALGOS
|
||||
from evidence.models import Annotation, Evidence
|
||||
from evidence.models import SystemProperty, UserProperty, Evidence
|
||||
from lot.models import DeviceLot
|
||||
from action.models import State
|
||||
|
||||
|
||||
class Device:
|
||||
|
@ -25,12 +26,11 @@ class Device:
|
|||
def __init__(self, *args, **kwargs):
|
||||
# the id is the chid of the device
|
||||
self.id = kwargs["id"]
|
||||
self.uuid = kwargs.get("uuid")
|
||||
self.pk = self.id
|
||||
self.shortid = self.pk[:6].upper()
|
||||
self.algorithm = None
|
||||
self.owner = None
|
||||
self.annotations = []
|
||||
self.properties = []
|
||||
self.hids = []
|
||||
self.uuids = []
|
||||
self.evidences = []
|
||||
|
@ -39,61 +39,59 @@ class Device:
|
|||
self.get_last_evidence()
|
||||
|
||||
def initial(self):
|
||||
self.get_annotations()
|
||||
self.get_properties()
|
||||
self.get_uuids()
|
||||
self.get_hids()
|
||||
self.get_evidences()
|
||||
self.get_lots()
|
||||
|
||||
def get_annotations(self):
|
||||
if self.annotations:
|
||||
return self.annotations
|
||||
def get_properties(self):
|
||||
if self.properties:
|
||||
return self.properties
|
||||
|
||||
self.annotations = Annotation.objects.filter(
|
||||
type=Annotation.Type.SYSTEM,
|
||||
self.properties = SystemProperty.objects.filter(
|
||||
value=self.id
|
||||
).order_by("-created")
|
||||
|
||||
if self.annotations.count():
|
||||
self.algorithm = self.annotations[0].key
|
||||
self.owner = self.annotations[0].owner
|
||||
if self.properties.count():
|
||||
self.algorithm = self.properties[0].key
|
||||
self.owner = self.properties[0].owner
|
||||
|
||||
return self.annotations
|
||||
return self.properties
|
||||
|
||||
def get_user_annotations(self):
|
||||
def get_user_properties(self):
|
||||
if not self.uuids:
|
||||
self.get_uuids()
|
||||
|
||||
annotations = Annotation.objects.filter(
|
||||
user_properties = UserProperty.objects.filter(
|
||||
uuid__in=self.uuids,
|
||||
owner=self.owner,
|
||||
type=Annotation.Type.USER
|
||||
type=UserProperty.Type.USER,
|
||||
)
|
||||
return annotations
|
||||
return user_properties
|
||||
|
||||
def get_user_documents(self):
|
||||
if not self.uuids:
|
||||
self.get_uuids()
|
||||
|
||||
annotations = Annotation.objects.filter(
|
||||
properties = UserProperty.objects.filter(
|
||||
uuid__in=self.uuids,
|
||||
owner=self.owner,
|
||||
type=Annotation.Type.DOCUMENT
|
||||
type=UserProperty.Type.DOCUMENT
|
||||
)
|
||||
return annotations
|
||||
return properties
|
||||
|
||||
def get_uuids(self):
|
||||
for a in self.get_annotations():
|
||||
for a in self.get_properties():
|
||||
if a.uuid not in self.uuids:
|
||||
self.uuids.append(a.uuid)
|
||||
|
||||
def get_hids(self):
|
||||
annotations = self.get_annotations()
|
||||
properties = self.get_properties()
|
||||
|
||||
algos = list(ALGOS.keys())
|
||||
algos.append('CUSTOM_ID')
|
||||
self.hids = list(set(annotations.filter(
|
||||
type=Annotation.Type.SYSTEM,
|
||||
self.hids = list(set(properties.filter(
|
||||
key__in=algos,
|
||||
).values_list("value", flat=True)))
|
||||
|
||||
|
@ -104,19 +102,12 @@ class Device:
|
|||
self.evidences = [Evidence(u) for u in self.uuids]
|
||||
|
||||
def get_last_evidence(self):
|
||||
if self.last_evidence:
|
||||
properties = self.get_properties()
|
||||
if not properties.count():
|
||||
return
|
||||
property = properties.first()
|
||||
|
||||
if self.uuid:
|
||||
self.last_evidence = Evidence(self.uuid)
|
||||
return
|
||||
|
||||
annotations = self.get_annotations()
|
||||
if not annotations.count():
|
||||
return
|
||||
annotation = annotations.first()
|
||||
self.last_evidence = Evidence(annotation.uuid)
|
||||
self.uuid = annotation.uuid
|
||||
self.last_evidence = Evidence(property.uuid)
|
||||
|
||||
def is_eraseserver(self):
|
||||
if not self.uuids:
|
||||
|
@ -124,21 +115,24 @@ class Device:
|
|||
if not self.uuids:
|
||||
return False
|
||||
|
||||
annotation = Annotation.objects.filter(
|
||||
property = UserProperty.objects.filter(
|
||||
uuid__in=self.uuids,
|
||||
owner=self.owner,
|
||||
type=Annotation.Type.ERASE_SERVER
|
||||
type=UserProperty.Type.ERASE_SERVER
|
||||
).first()
|
||||
|
||||
if annotation:
|
||||
if property:
|
||||
return True
|
||||
return False
|
||||
|
||||
def last_uuid(self):
|
||||
if self.uuid:
|
||||
return self.uuid
|
||||
return self.uuids[0]
|
||||
|
||||
def get_current_state(self):
|
||||
uuid = self.last_uuid
|
||||
|
||||
return State.objects.filter(snapshot_uuid=uuid).order_by('-date').first()
|
||||
|
||||
def get_lots(self):
|
||||
self.lots = [
|
||||
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
|
||||
|
@ -147,7 +141,7 @@ class Device:
|
|||
def get_unassigned(cls, institution, offset=0, limit=None):
|
||||
|
||||
sql = """
|
||||
WITH RankedAnnotations AS (
|
||||
WITH RankedProperties AS (
|
||||
SELECT
|
||||
t1.value,
|
||||
t1.key,
|
||||
|
@ -160,34 +154,32 @@ class Device:
|
|||
ELSE 3
|
||||
END,
|
||||
t1.created DESC
|
||||
) AS row_num
|
||||
FROM evidence_annotation AS t1
|
||||
) AS row_num
|
||||
FROM evidence_systemproperty AS t1
|
||||
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
||||
WHERE t2.device_id IS NULL
|
||||
AND t1.owner_id = {institution}
|
||||
AND t1.type = {type}
|
||||
)
|
||||
SELECT DISTINCT
|
||||
value
|
||||
FROM
|
||||
RankedAnnotations
|
||||
RankedProperties
|
||||
WHERE
|
||||
row_num = 1
|
||||
""".format(
|
||||
institution=institution.id,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
)
|
||||
if limit:
|
||||
sql += " limit {} offset {}".format(int(limit), int(offset))
|
||||
|
||||
sql += ";"
|
||||
|
||||
annotations = []
|
||||
properties = []
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
annotations = cursor.fetchall()
|
||||
properties = cursor.fetchall()
|
||||
|
||||
devices = [cls(id=x[0]) for x in annotations]
|
||||
devices = [cls(id=x[0]) for x in properties]
|
||||
count = cls.get_unassigned_count(institution)
|
||||
return devices, count
|
||||
|
||||
|
@ -195,7 +187,7 @@ class Device:
|
|||
def get_unassigned_count(cls, institution):
|
||||
|
||||
sql = """
|
||||
WITH RankedAnnotations AS (
|
||||
WITH RankedProperties AS (
|
||||
SELECT
|
||||
t1.value,
|
||||
t1.key,
|
||||
|
@ -209,30 +201,28 @@ class Device:
|
|||
END,
|
||||
t1.created DESC
|
||||
) AS row_num
|
||||
FROM evidence_annotation AS t1
|
||||
FROM evidence_systemproperty AS t1
|
||||
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
||||
WHERE t2.device_id IS NULL
|
||||
AND t1.owner_id = {institution}
|
||||
AND t1.type = {type}
|
||||
)
|
||||
SELECT
|
||||
COUNT(DISTINCT value)
|
||||
FROM
|
||||
RankedAnnotations
|
||||
RankedProperties
|
||||
WHERE
|
||||
row_num = 1
|
||||
""".format(
|
||||
institution=institution.id,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
)
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
return cursor.fetchall()[0][0]
|
||||
|
||||
@classmethod
|
||||
def get_annotation_from_uuid(cls, uuid, institution):
|
||||
def get_properties_from_uuid(cls, uuid, institution):
|
||||
sql = """
|
||||
WITH RankedAnnotations AS (
|
||||
WITH RankedProperties AS (
|
||||
SELECT
|
||||
t1.value,
|
||||
t1.key,
|
||||
|
@ -246,72 +236,71 @@ class Device:
|
|||
END,
|
||||
t1.created DESC
|
||||
) AS row_num
|
||||
FROM evidence_annotation AS t1
|
||||
FROM evidence_systemproperty AS t1
|
||||
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
||||
WHERE t2.device_id IS NULL
|
||||
AND t1.owner_id = {institution}
|
||||
AND t1.type = {type}
|
||||
AND t1.uuid = '{uuid}'
|
||||
)
|
||||
SELECT DISTINCT
|
||||
value
|
||||
FROM
|
||||
RankedAnnotations
|
||||
RankedProperties
|
||||
WHERE
|
||||
row_num = 1;
|
||||
""".format(
|
||||
uuid=uuid.replace("-", ""),
|
||||
institution=institution.id,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
)
|
||||
|
||||
annotations = []
|
||||
properties = []
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
annotations = cursor.fetchall()
|
||||
properties = cursor.fetchall()
|
||||
|
||||
return cls(id=annotations[0][0])
|
||||
return cls(id=properties[0][0])
|
||||
|
||||
@property
|
||||
def is_websnapshot(self):
|
||||
self.get_last_evidence()
|
||||
if not self.last_evidence:
|
||||
self.get_last_evidence()
|
||||
return self.last_evidence.doc['type'] == "WebSnapshot"
|
||||
|
||||
@property
|
||||
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()
|
||||
|
||||
@property
|
||||
def manufacturer(self):
|
||||
self.get_last_evidence()
|
||||
if not self.last_evidence:
|
||||
self.get_last_evidence()
|
||||
return self.last_evidence.get_manufacturer()
|
||||
|
||||
@property
|
||||
def serial_number(self):
|
||||
self.get_last_evidence()
|
||||
if not self.last_evidence:
|
||||
self.get_last_evidence()
|
||||
return self.last_evidence.get_serial_number()
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
self.get_last_evidence()
|
||||
if self.last_evidence.doc['type'] == "WebSnapshot":
|
||||
return self.last_evidence.doc.get("device", {}).get("type", "")
|
||||
|
||||
if not self.last_evidence:
|
||||
self.get_last_evidence()
|
||||
return self.last_evidence.get_chassis()
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
self.get_last_evidence()
|
||||
if not self.last_evidence:
|
||||
self.get_last_evidence()
|
||||
return self.last_evidence.get_model()
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
def components(self):
|
||||
if not self.last_evidence:
|
||||
self.get_last_evidence()
|
||||
return self.last_evidence.get_version()
|
||||
|
||||
@property
|
||||
def components(self):
|
||||
self.get_last_evidence()
|
||||
return self.last_evidence.get_components()
|
||||
|
|
|
@ -2,11 +2,162 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ object.shortid }}</h3>
|
||||
|
||||
<div class="position-fixed" style="bottom: 2rem; right: 2rem; z-index: 9999; display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-warning d-flex align-items-center shadow" type="button"
|
||||
data-bs-toggle="offcanvas" data-bs-target="#notesOffcanvas" aria-controls="notesOffcanvas"
|
||||
data-bs-toggle="tooltip" data-bs-placement="left" title="{% trans 'View recent notes' %}">
|
||||
<i class="bi bi-journal-text me-1"></i>
|
||||
{% trans "Journal" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- side panel for latest notes -->
|
||||
|
||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="notesOffcanvas" aria-labelledby="notesOffcanvasLabel">
|
||||
<div class="offcanvas-header">
|
||||
<h5 id="notesOffcanvasLabel">{% trans "Latest Notes" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body" style="margin-bottom: 5rem;">
|
||||
{% for note in device_notes|slice:":4" %}
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<small class="text-muted">
|
||||
{{ note.date|timesince }} {% trans "ago" %}
|
||||
</small>
|
||||
|
||||
{% if user == note.user or user.is_admin %}
|
||||
<span class="badge bg-warning text-dark ms-2">{% trans "Editable" %}</span>
|
||||
</div>
|
||||
<blockquote
|
||||
class="blockquote mt-2 p-2 bg-light fst-italic"
|
||||
contenteditable="true"
|
||||
style="font-size: 1.2em!important"
|
||||
data-note-id="{{ note.id }}"
|
||||
title="{% trans 'Click to edit this note' %}"
|
||||
oninput="toggleSaveLink(this)">
|
||||
{% else %}
|
||||
</div>
|
||||
<blockquote style="font-size: 1.2em!important" class="blockquote mt-2 p-2 fst-italic">
|
||||
{% endif %}
|
||||
<p data-note-id="{{ note.id }}">
|
||||
{{ note.description }}
|
||||
</p>
|
||||
<footer class="blockquote-footer text-end mt-2" contenteditable="false">
|
||||
<small>{{ note.user.get_full_name|default:note.user.username }}</small>
|
||||
</footer>
|
||||
</blockquote>
|
||||
|
||||
{% if user == note.user or user.is_admin %}
|
||||
<div class="d-flex justify-content-end align-items-center">
|
||||
|
||||
<!-- update note button -->
|
||||
<form
|
||||
id="updateNoteForm{{ note.id }}"
|
||||
method="post"
|
||||
action="{% url 'action:update_note' note.id %}"
|
||||
class="d-inline"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="description" id="descriptionInput{{ note.id }}" value="">
|
||||
<a
|
||||
type="submit"
|
||||
id="saveLink{{ note.id }}"
|
||||
class="text-muted disabled me-4 border border-light rounded"
|
||||
style="pointer-events: none;"
|
||||
title="{% trans 'Save changes' %}"
|
||||
onclick="submitUpdatedNote('{{ note.id }}'); return false;"
|
||||
>
|
||||
<i class="fas fa-save px-1"></i>
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<!-- delete note button -->
|
||||
<button type="button" class="btn btn-link btn-outline-danger btn-sm text-danger" id="deleteIcon{{ note.id }}" title="{% trans 'Delete note' %}" data-bs-toggle="collapse" data-bs-target="#confirmDelete{{ note.id }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form class="d-inline" method="post" action="{% url 'action:delete_note' note.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="collapse mt-2" id="confirmDelete{{ note.id }}">
|
||||
<div class="card card-body border border-danger text-center">
|
||||
<p class="mb-2">{% trans 'Are you sure you want to delete this note?' %}</p>
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
onclick="submitDeleteForm({{ note.id }}); return false;"
|
||||
>
|
||||
{% trans 'Confirm delete' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>{% trans "No notes available." %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top bar buttons -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ object.shortid }}</h3>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<div class="btn-group" role="group" aria-label="Actions">
|
||||
|
||||
<!-- change state button -->
|
||||
{% if state_definitions %}
|
||||
<div class="dropdown ms-2">
|
||||
<a class="btn btn-green-admin dropdown-toggle" id="addStateDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% trans "Change state" %}
|
||||
{% if device_states %}
|
||||
({{ device_states.0.state }})
|
||||
{% else %}
|
||||
( {% trans "None" %} )
|
||||
{% endif %}
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="addStateDropdown" style="width: 100%;">
|
||||
{% for state in state_definitions %}
|
||||
<li style="width: 100%;">
|
||||
<form id="changeStateForm{{ state.id }}" method="post" action="{% url 'action:change_state' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="previous_state" value="{{ device_states.0.state|default:"nil" }}">
|
||||
<input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
|
||||
<input type="hidden" name="new_state" value="{{ state.state }}">
|
||||
|
||||
<a class="dropdown-item d-flex justify-content-between align-items-center" href="#" onclick="document.getElementById('changeStateForm{{ state.id }}').submit(); return false;">
|
||||
<span class="font-monospace">{{ state.state }}</span>
|
||||
<span class="badge bg-secondary rounded-pill-sm">{{ forloop.counter }}</span>
|
||||
</a>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<button class="btn btn-green-admin" type="button" disabled>
|
||||
<i class="bi bi-plus"></i> {% trans "Change state" %}
|
||||
{% if device_states %}
|
||||
({{ device_states.0.state }})
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Add note button -->
|
||||
<button class="btn btn-warning ms-2" type="button" data-bs-toggle="modal" data-bs-target="#addNoteModal">
|
||||
<i class="bi bi-sticky"></i> {% trans "Add a note" %}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
@ -15,7 +166,10 @@
|
|||
<a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">{% trans 'General details' %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#annotations" class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">{% trans 'User annotations' %}</a>
|
||||
<a href="#log" class="nav-link" data-bs-toggle="tab" data-bs-target="#log">{% trans 'Log' %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#user_properties" class="nav-link" data-bs-toggle="tab" data-bs-target="#user_properties">{% trans 'User properties' %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#documents" class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">{% trans 'Documents' %}</a>
|
||||
|
@ -29,238 +183,52 @@
|
|||
<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 %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content pt-4">
|
||||
|
||||
<div class="tab-content pt-2">
|
||||
<div class="tab-pane fade show active" id="details">
|
||||
<h5 class="card-title">{% trans 'Details' %}</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col-lg-3 col-md-4 label">Phid</div>
|
||||
<div class="col-lg-9 col-md-8">{{ object.id }}</div>
|
||||
</div>
|
||||
{% include 'tabs/general_details.html' %}
|
||||
|
||||
{% if object.is_eraseserver %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Is a erase server' %}
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8"></div>
|
||||
{% include 'tabs/log.html' %}
|
||||
|
||||
{% include 'tabs/user_properties.html' %}
|
||||
|
||||
{% include 'tabs/documents.html' %}
|
||||
|
||||
{% include 'tabs/lots.html' %}
|
||||
|
||||
{% include 'tabs/evidences.html' %}
|
||||
|
||||
<!-- Add a note popup -->
|
||||
<div class="modal fade" id="addNoteModal" tabindex="-1" aria-labelledby="addNoteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addNoteModalLabel">{% trans "Add a Note" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3 col-md-4 label">Type</div>
|
||||
<div class="col-lg-9 col-md-8">{{ object.type }}</div>
|
||||
</div>
|
||||
|
||||
{% if object.is_websnapshot and object.last_user_evidence %}
|
||||
{% for k, v in object.last_user_evidence %}
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3 col-md-4 label">{{ k }}</div>
|
||||
<div class="col-lg-9 col-md-8">{{ v|default:'' }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Manufacturer' %}
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8">{{ object.manufacturer|default:'' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Model' %}
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8">{{ object.model|default:'' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Version' %}
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8">{{ object.version|default:'' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Serial Number' %}
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8">{{ object.serial_number|default:'' }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Identifiers' %}
|
||||
</div>
|
||||
</div>
|
||||
{% for chid in object.hids %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">{{ chid|default:'' }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="annotations">
|
||||
<div class="btn-group mt-1 mb-3">
|
||||
<a href="{% url 'device:add_annotation' object.pk %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus"></i>
|
||||
{% trans 'Add new annotation' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title">{% trans 'Annotations' %}</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{% trans 'Key' %}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{% trans 'Value' %}
|
||||
</th>
|
||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
|
||||
{% trans 'Created on' %}
|
||||
</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in object.get_user_annotations %}
|
||||
<tr>
|
||||
<td>{{ a.key }}</td>
|
||||
<td>{{ a.value }}</td>
|
||||
<td>{{ a.created }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="documents">
|
||||
<div class="btn-group mt-1 mb-3">
|
||||
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus"></i>
|
||||
{% trans 'Add new document' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title">{% trans 'Documents' %}</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{% trans 'Key' %}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{% trans 'Value' %}
|
||||
</th>
|
||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
|
||||
{% trans 'Created on' %}
|
||||
</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in object.get_user_documents %}
|
||||
<tr>
|
||||
<td>{{ a.key }}</td>
|
||||
<td>{{ a.value }}</td>
|
||||
<td>{{ a.created }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="lots">
|
||||
{% for tag in lot_tags %}
|
||||
<h5 class="card-title">{{ tag }}</h5>
|
||||
{% for lot in object.lots %}
|
||||
{% if lot.type == tag %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}</a>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="{% url 'action:add_note' %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
|
||||
<label for="noteDescription" class="form-label">{% trans "Note" %}</label>
|
||||
<textarea class="form-control" id="noteDescription" name="note" placeholder="Max 250 characters" name="note" rows="3" required></textarea>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="components">
|
||||
<h5 class="card-title">{% trans 'Components last evidence' %}</h5>
|
||||
<div class="list-group col-6">
|
||||
{% for c in object.components %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ c.type }}</h5>
|
||||
<small class="text-muted">{{ evidence.created }}</small>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="submit" class="btn btn-green-admin">{% trans "Save Note" %}</button>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
{% for k, v in c.items %}
|
||||
{% if k not in 'actions,type' %}
|
||||
{{ k }}: {{ v }}<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="evidences">
|
||||
<h5 class="card-title">{% trans 'List of evidences' %}</h5>
|
||||
<div class="list-group col-6">
|
||||
{% for snap in object.evidences %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<small class="text-muted">{{ snap.created }}</small>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
|
||||
</p>
|
||||
</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 %}
|
||||
|
||||
{% block extrascript %}
|
||||
|
@ -268,12 +236,12 @@
|
|||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Obtener el hash de la URL (ejemplo: #components)
|
||||
const hash = window.location.hash
|
||||
|
||||
|
||||
// Verificar si hay un hash en la URL
|
||||
if (hash) {
|
||||
// Buscar el botón o enlace que corresponde al hash y activarlo
|
||||
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`)
|
||||
|
||||
|
||||
if (tabTrigger) {
|
||||
// Crear una instancia de tab de Bootstrap para activar el tab
|
||||
const tab = new bootstrap.Tab(tabTrigger)
|
||||
|
@ -281,5 +249,27 @@
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
//Enable save button on note if changes are made to it
|
||||
function toggleSaveLink(blockquoteElem) {
|
||||
const saveLink = document.getElementById("saveLink" + blockquoteElem.dataset.noteId);
|
||||
|
||||
saveLink.classList.remove("disabled", "text-muted", "border-light");
|
||||
saveLink.classList.add("text-success", "border-success");
|
||||
saveLink.style.pointerEvents = "auto";
|
||||
|
||||
}
|
||||
//updates note-update-form with new value from blockquote
|
||||
function submitUpdatedNote(noteId) {
|
||||
const noteParagraph = document.querySelector('p[data-note-id="' + noteId + '"]');
|
||||
const newText = noteParagraph.innerText.trim();
|
||||
const descriptionField = document.getElementById('descriptionInput' + noteId);
|
||||
descriptionField.value = newText;
|
||||
document.getElementById('updateNoteForm' + noteId).submit();
|
||||
}
|
||||
//simpler are u sure? confirmation message
|
||||
function submitDeleteForm(noteId) {
|
||||
document.getElementById('confirmDelete' + noteId).closest('form').submit();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
27
device/templates/tabs/components.html
Normal file
27
device/templates/tabs/components.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
|
||||
{% load i18n %}
|
||||
|
||||
<div class="tab-pane fade" id="components">
|
||||
<h5 class="card-title">{% trans 'Components last evidence' %}
|
||||
</h5>
|
||||
<div class="list-group col-6">
|
||||
{% for c in object.components %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ c.type }}
|
||||
</h5>
|
||||
<small class="text-muted">{{ evidence.created }}
|
||||
</small>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
{% for k, v in c.items %}
|
||||
{% if k not in 'actions,type' %}
|
||||
{{ k }}: {{ v }}
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
49
device/templates/tabs/documents.html
Normal file
49
device/templates/tabs/documents.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
|
||||
{% load i18n %}
|
||||
<div class="tab-pane fade" id="documents">
|
||||
<div class="btn-group mt-1 mb-3">
|
||||
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus">
|
||||
</i>
|
||||
{% trans 'Add new document' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title">{% trans 'Documents' %}
|
||||
</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{% trans 'Key' %}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{% trans 'Value' %}
|
||||
</th>
|
||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
|
||||
{% trans 'Created on' %}
|
||||
</th>
|
||||
<th>
|
||||
</th>
|
||||
<th>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in object.get_user_documents %}
|
||||
<tr>
|
||||
<td>{{ a.key }}
|
||||
</td>
|
||||
<td>{{ a.value }}
|
||||
</td>
|
||||
<td>{{ a.created }}
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
19
device/templates/tabs/evidences.html
Normal file
19
device/templates/tabs/evidences.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
{% load i18n %}
|
||||
|
||||
<div class="tab-pane fade" id="evidences">
|
||||
<h5 class="card-title">{% trans 'List of evidences' %}</h5>
|
||||
<div class="list-group col-6">
|
||||
{% for snap in object.evidences %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<small class="text-muted">{{ snap.created }}</small>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
74
device/templates/tabs/general_details.html
Normal file
74
device/templates/tabs/general_details.html
Normal file
|
@ -0,0 +1,74 @@
|
|||
|
||||
{% load i18n %}
|
||||
|
||||
<!-- Device Details -->
|
||||
<div class="tab-pane fade show active" id="details">
|
||||
<h5 class="card-title">{% trans 'Details' %}
|
||||
</h5>
|
||||
<hr>
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted fw-bold">{% trans 'Phid' %}
|
||||
</div>
|
||||
<div class="col-sm-8">{{ object.id }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object.is_eraseserver %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted fw-bold">{% trans 'Is an erase server' %}
|
||||
</div>
|
||||
<div class="col-sm-8">{% trans 'Yes' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted fw-bold">{% trans 'Type' %}
|
||||
</div>
|
||||
<div class="col-sm-8">{{ object.type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object.is_websnapshot and object.last_user_evidence %}
|
||||
{% for k, v in object.last_user_evidence.items %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted fw-bold">{{ k }}
|
||||
</div>
|
||||
<div class="col-sm-8">{{ v|default:'' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted fw-bold">{% trans 'Manufacturer' %}
|
||||
</div>
|
||||
<div class="col-sm-8">{{ object.manufacturer|default:'' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted fw-bold">{% trans 'Model' %}
|
||||
</div>
|
||||
<div class="col-sm-8">{{ object.model|default:'' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted fw-bold">{% trans 'Serial Number' %}
|
||||
</div>
|
||||
<div class="col-sm-8">{{ object.serial_number|default:'' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted fw-bold">{% trans 'Identifiers' %}
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
{% for chid in object.hids %}
|
||||
<div>{{ chid|default:'' }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
28
device/templates/tabs/log.html
Normal file
28
device/templates/tabs/log.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% load i18n %}
|
||||
|
||||
<div class="tab-pane fade" id="log">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover table-bordered bg-gradient">
|
||||
<thead >
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Date' %}</th>
|
||||
<th scope="col">{% trans 'Event' %}</th>
|
||||
<th scope="col">{% trans 'User' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in device_logs %}
|
||||
<tr>
|
||||
<td width="13%">{{ log.date|date:"M j, Y, H:i" }}</td>
|
||||
<td class="fst-italic">{{ log.event }}</td>
|
||||
<td>{{ log.user.get_full_name|default:log.user.username }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center">{% trans 'No logs recorded.' %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
19
device/templates/tabs/lots.html
Normal file
19
device/templates/tabs/lots.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
{% load i18n %}
|
||||
|
||||
<div class="tab-pane fade" id="lots">
|
||||
{% for tag in lot_tags %}
|
||||
<h5 class="card-title">{{ tag }}
|
||||
</h5>
|
||||
{% for lot in object.lots %}
|
||||
{% if lot.type == tag %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
131
device/templates/tabs/user_properties.html
Normal file
131
device/templates/tabs/user_properties.html
Normal file
|
@ -0,0 +1,131 @@
|
|||
|
||||
{% load i18n %}
|
||||
|
||||
<div class="tab-pane fade" id="user_properties">
|
||||
<div class="d-flex justify-content-end mt-1 mb-3">
|
||||
<a href="{% url 'device:add_user_property' object.pk %}"
|
||||
class="btn btn-green-admin d-flex align-items-center">
|
||||
<i class="bi bi-plus me-1"></i>
|
||||
{% trans 'New user property' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title">{% trans 'User properties' %}</h5>
|
||||
|
||||
<table class="table table-hover table-bordered table-responsive align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Key' %}</th>
|
||||
<th scope="col">{% trans 'Value' %}</th>
|
||||
<th scope="col" data-type="date" class="text-end" data-format="YYYY-MM-DD HH:mm">{% trans 'Created on' %}</th>
|
||||
<th scope="col" width="5%" class="text-end" title="{% trans 'Actions' %}"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in object.get_user_properties %}
|
||||
<tr>
|
||||
<td>{{ a.key }}
|
||||
</td>
|
||||
<td>{{ a.value }}
|
||||
</td>
|
||||
<td class="text-end">{{ a.created }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group ">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-info d-flex align-items-center" data-bs-toggle="modal"
|
||||
data-bs-target="#editModal{{ a.id }}" >
|
||||
<i class="bi bi-pencil me-1"></i>
|
||||
{% trans 'Edit' %}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger d-flex align-items-center"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal{{ a.id }}">
|
||||
<i class="bi bi-trash me-1"></i>
|
||||
{% trans 'Delete' %}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- pop up modal for delete confirmation -->
|
||||
{% for a in object.get_user_properties %}
|
||||
<div class="modal fade" id="deleteModal{{ a.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ a.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel{{ a.id }}">{% trans "Confirm Deletion" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<strong>{% trans "Key:" %}
|
||||
</strong> {{ a.key }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{% trans "Value:" %}
|
||||
</strong> {{ a.value }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{% trans "Created on:" %}
|
||||
</strong> {{ a.created }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}
|
||||
</button>
|
||||
<form method="post" action="{% url 'device:delete_user_property' a.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">{% trans "Delete" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- popup modals for edit button -->
|
||||
{% for a in object.get_user_properties %}
|
||||
<div class="modal fade" id="editModal{{ a.id }}" tabindex="-1" aria-labelledby="editModalLabel{{ a.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel{{ a.id }}">{% trans "Edit User Property" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editForm{{ a.id }}" method="post" action="{% url 'device:update_user_property' a.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="key" class="form-label">{% trans "Key" %}
|
||||
</label>
|
||||
<input type="text" class="form-control" id="key" name="key" value="{{ a.key }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">{% trans "Value" %}
|
||||
</label>
|
||||
<input type="text" class="form-control" id="value" name="value" value="{{ a.value }}">
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save changes" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
|
@ -7,7 +7,9 @@ urlpatterns = [
|
|||
path("add/", views.NewDeviceView.as_view(), name="add"),
|
||||
path("edit/<str:pk>/", views.EditDeviceView.as_view(), name="edit"),
|
||||
path("<str:pk>/", views.DetailsView.as_view(), name="details"),
|
||||
path("<str:pk>/annotation/add", views.AddAnnotationView.as_view(), name="add_annotation"),
|
||||
path("<str:pk>/user_property/add", views.AddUserPropertyView.as_view(), name="add_user_property"),
|
||||
path("user_property/<int:pk>/delete", views.DeleteUserPropertyView.as_view(), name="delete_user_property"),
|
||||
path("user_property/<int:pk>/update", views.UpdateUserPropertyView.as_view(), name="update_user_property"),
|
||||
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"),
|
||||
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
|
||||
|
||||
|
|
155
device/views.py
155
device/views.py
|
@ -1,22 +1,25 @@
|
|||
import json
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.conf import settings
|
||||
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import get_object_or_404, Http404
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.edit import (
|
||||
CreateView,
|
||||
UpdateView,
|
||||
FormView,
|
||||
DeleteView,
|
||||
)
|
||||
from django.views.generic.base import TemplateView
|
||||
from action.models import StateDefinition, State, DeviceLog, Note
|
||||
from dashboard.mixins import DashboardView, Http403
|
||||
from evidence.models import Annotation
|
||||
from evidence.models import UserProperty, SystemProperty
|
||||
from lot.models import LotTag
|
||||
from device.models import Device
|
||||
from device.forms import DeviceFormSet
|
||||
if settings.DPP:
|
||||
from dpp.models import Proof
|
||||
from dpp.api_dlt import PROOF_TYPE
|
||||
|
||||
|
||||
class NewDeviceView(DashboardView, FormView):
|
||||
|
@ -70,7 +73,7 @@ class EditDeviceView(DashboardView, UpdateView):
|
|||
title = _("Update Device")
|
||||
breadcrumb = "Device / Update Device"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
model = Annotation
|
||||
model = SystemProperty
|
||||
|
||||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
|
@ -88,7 +91,7 @@ class DetailsView(DashboardView, TemplateView):
|
|||
template_name = "details.html"
|
||||
title = _("Device")
|
||||
breadcrumb = "Device / Details"
|
||||
model = Annotation
|
||||
model = SystemProperty
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.pk = kwargs['pk']
|
||||
|
@ -104,17 +107,16 @@ class DetailsView(DashboardView, TemplateView):
|
|||
context = super().get_context_data(**kwargs)
|
||||
self.object.initial()
|
||||
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
|
||||
dpps = []
|
||||
if settings.DPP:
|
||||
dpps = Proof.objects.filter(
|
||||
uuid__in=self.object.uuids,
|
||||
type=PROOF_TYPE["IssueDPP"]
|
||||
)
|
||||
last_evidence= self.object.get_last_evidence(),
|
||||
uuid=self.object.last_uuid()
|
||||
context.update({
|
||||
'object': self.object,
|
||||
'snapshot': self.object.get_last_evidence(),
|
||||
'snapshot': last_evidence,
|
||||
'lot_tags': lot_tags,
|
||||
'dpps': dpps,
|
||||
"state_definitions": StateDefinition.objects.filter(institution=self.request.user.institution).order_by('order'),
|
||||
"device_states": State.objects.filter(snapshot_uuid=uuid).order_by('-date'),
|
||||
"device_logs": DeviceLog.objects.filter(snapshot_uuid=uuid).order_by('-date'),
|
||||
"device_notes": Note.objects.filter(snapshot_uuid=uuid).order_by('-date'),
|
||||
})
|
||||
return context
|
||||
|
||||
|
@ -175,65 +177,136 @@ class PublicDeviceWebView(TemplateView):
|
|||
return JsonResponse(device_data)
|
||||
|
||||
|
||||
class AddAnnotationView(DashboardView, CreateView):
|
||||
template_name = "new_annotation.html"
|
||||
title = _("New annotation")
|
||||
breadcrumb = "Device / New annotation"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
model = Annotation
|
||||
class AddUserPropertyView(DashboardView, CreateView):
|
||||
template_name = "new_user_property.html"
|
||||
title = _("New User Property")
|
||||
breadcrumb = "Device / New Property"
|
||||
model = UserProperty
|
||||
fields = ("key", "value")
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
form.instance.uuid = self.annotation.uuid
|
||||
form.instance.type = Annotation.Type.USER
|
||||
form.instance.uuid = self.property.uuid
|
||||
form.instance.type = UserProperty.Type.USER
|
||||
|
||||
message = _("<Created> UserProperty: {}: {}".format(form.instance.key, form.instance.value))
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=form.instance.uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution
|
||||
)
|
||||
|
||||
messages.success(self.request, _("User property successfully added."))
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
institution = self.request.user.institution
|
||||
self.annotation = Annotation.objects.filter(
|
||||
owner=institution,
|
||||
value=pk,
|
||||
type=Annotation.Type.SYSTEM
|
||||
).first()
|
||||
self.property = get_object_or_404(SystemProperty, owner=institution, value=pk)
|
||||
|
||||
if not self.annotation:
|
||||
raise Http404
|
||||
return super().get_form_kwargs()
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('device:details', args=[self.kwargs.get('pk')])
|
||||
|
||||
self.success_url = reverse_lazy('device:details', args=[pk])
|
||||
kwargs = super().get_form_kwargs()
|
||||
return kwargs
|
||||
|
||||
class UpdateUserPropertyView(DashboardView, UpdateView):
|
||||
template_name = "new_user_property.html"
|
||||
title = _("Update User Property")
|
||||
breadcrumb = "Device / Update Property"
|
||||
model = UserProperty
|
||||
fields = ("key", "value")
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
institution = self.request.user.institution
|
||||
return UserProperty.objects.filter(pk=pk, owner=institution)
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
old_instance = self.get_object()
|
||||
old_key = old_instance.key
|
||||
old_value = old_instance.value
|
||||
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
form.instance.type = UserProperty.Type.USER
|
||||
|
||||
new_key = form.cleaned_data['key']
|
||||
new_value = form.cleaned_data['value']
|
||||
|
||||
message = _("<Updated> UserProperty: {}: {} to {}: {}".format(old_key, old_value, new_key, new_value))
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=form.instance.uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution
|
||||
)
|
||||
|
||||
messages.success(self.request, _("User property updated successfully."))
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
|
||||
|
||||
|
||||
class DeleteUserPropertyView(DashboardView, DeleteView):
|
||||
model = UserProperty
|
||||
|
||||
def get_queryset(self):
|
||||
return UserProperty.objects.filter(owner=self.request.user.institution)
|
||||
|
||||
#using post() method because delete() method from DeleteView has some issues with messages framework
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.object.delete()
|
||||
|
||||
message = _("<Deleted> User Property: {}:{}".format(self.object.key, self.object.value ))
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=self.object.uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution
|
||||
)
|
||||
|
||||
messages.info(self.request, _("User property deleted successfully."))
|
||||
|
||||
return self.handle_success()
|
||||
|
||||
def handle_success(self):
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
|
||||
|
||||
class AddDocumentView(DashboardView, CreateView):
|
||||
template_name = "new_annotation.html"
|
||||
template_name = "new_user_property.html"
|
||||
title = _("New Document")
|
||||
breadcrumb = "Device / New document"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
model = Annotation
|
||||
model = UserProperty
|
||||
fields = ("key", "value")
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
form.instance.uuid = self.annotation.uuid
|
||||
form.instance.type = Annotation.Type.DOCUMENT
|
||||
form.instance.uuid = self.property.uuid
|
||||
form.instance.type = UserProperty.Type.DOCUMENT
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
institution = self.request.user.institution
|
||||
self.annotation = Annotation.objects.filter(
|
||||
self.property = SystemProperty.objects.filter(
|
||||
owner=institution,
|
||||
value=pk,
|
||||
type=Annotation.Type.SYSTEM
|
||||
).first()
|
||||
|
||||
if not self.annotation:
|
||||
if not self.property:
|
||||
raise Http404
|
||||
|
||||
self.success_url = reverse_lazy('device:details', args=[pk])
|
||||
|
|
|
@ -65,6 +65,7 @@ ENABLE_EMAIL = config("ENABLE_EMAIL", default=True, cast=bool)
|
|||
|
||||
EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
@ -91,11 +92,6 @@ INSTALLED_APPS = [
|
|||
"api",
|
||||
]
|
||||
|
||||
DPP = config("DPP", default=False, cast=bool)
|
||||
|
||||
if DPP:
|
||||
INSTALLED_APPS.extend(["dpp", "did"])
|
||||
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
|
@ -215,6 +211,10 @@ LOGGING = {
|
|||
'()': CustomFormatter,
|
||||
'format': '%(levelname)s %(asctime)s %(message)s'
|
||||
},
|
||||
'verbose': {
|
||||
'format': '{levelname} {asctime} {module} {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
|
@ -237,16 +237,10 @@ LOGGING = {
|
|||
"handlers": ["console"],
|
||||
"level": "ERROR",
|
||||
"propagate": False,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
SNAPSHOT_PATH="/tmp/"
|
||||
DATA_UPLOAD_MAX_NUMBER_FILES = 1000
|
||||
COMMIT = config('COMMIT', default='')
|
||||
|
||||
# DLT SETTINGS
|
||||
TOKEN_DLT = config("API_DLT_TOKEN", default=None)
|
||||
API_DLT = config("API_DLT", default=None)
|
||||
API_RESOLVER = config("API_RESOLVER", default=None)
|
||||
ID_FEDERATED = config("ID_FEDERATED", default=None)
|
||||
|
|
|
@ -14,7 +14,7 @@ Including another URLconf
|
|||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -22,15 +22,10 @@ urlpatterns = [
|
|||
path("", include("login.urls")),
|
||||
path("dashboard/", include("dashboard.urls")),
|
||||
path("evidence/", include("evidence.urls")),
|
||||
path('action/', include('action.urls')),
|
||||
path("device/", include("device.urls")),
|
||||
path("admin/", include("admin.urls")),
|
||||
path("user/", include("user.urls")),
|
||||
path("lot/", include("lot.urls")),
|
||||
path('api/', include('api.urls')),
|
||||
]
|
||||
|
||||
if settings.DPP:
|
||||
urlpatterns.extend([
|
||||
path('dpp/', include('dpp.urls')),
|
||||
path('did/', include('did.urls')),
|
||||
])
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DidConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "did"
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -1,41 +0,0 @@
|
|||
dpp_tmpl = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/credentials/v2",
|
||||
"https://test.uncefact.org/vocabulary/untp/dpp/0.5.0/"
|
||||
],
|
||||
"type": [
|
||||
"DigitalProductPassport",
|
||||
"VerifiableCredential"
|
||||
],
|
||||
"id": "https://example.ereuse.org/credentials/2a423366-a0d6-4855-ba65-2e0c926d09b0",
|
||||
"issuer": {
|
||||
"type": [
|
||||
"CredentialIssuer"
|
||||
],
|
||||
"id": "did:web:r1.identifiers.ereuse.org:did-registry:z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ#z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ",
|
||||
"name": "Refurbisher One"
|
||||
},
|
||||
"validFrom": "2024-11-15T12:00:00",
|
||||
"validUntil": "2034-11-15T12:00:00",
|
||||
"credentialSubject": {
|
||||
"type": [
|
||||
"Product"
|
||||
],
|
||||
"id": "https://id.ereuse.org/01/09520123456788/21/12345",
|
||||
"name": "Refurbished XYZ Lenovo laptop item",
|
||||
"registeredId": "09520123456788.21.12345",
|
||||
"description": "XYZ Lenovo laptop refurbished by Refurbisher One",
|
||||
"data": ""
|
||||
},
|
||||
"credentialSchema": {
|
||||
"id": "https://idhub.pangea.org/vc_schemas/dpp.json",
|
||||
"type": "FullJsonSchemaValidator2021",
|
||||
"proof": {
|
||||
"type": "Ed25519Signature2018",
|
||||
"proofPurpose": "assertionMethod",
|
||||
"verificationMethod": "did:web:r1.identifiers.ereuse.org:did-registry:z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ#z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ",
|
||||
"created": "2024-12-03T15:33:42Z",
|
||||
"jws": "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..rBPqbOcZCXB7GAnq1XIfV9Jvw4MKXlHff7qZkRfgwQ0Hnd9Ujt5s1xT4O0K6VESzWvdP2mOvMvu780fVNfraBQ"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,8 +0,0 @@
|
|||
from django.urls import path
|
||||
from did import views
|
||||
|
||||
app_name = 'did'
|
||||
|
||||
urlpatterns = [
|
||||
path("<str:pk>", views.PublicDeviceWebView.as_view(), name="device_web"),
|
||||
]
|
263
did/views.py
263
did/views.py
|
@ -1,263 +0,0 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
from django.http import JsonResponse, Http404
|
||||
from django.views.generic.base import TemplateView
|
||||
from device.models import Device
|
||||
from evidence.parse import Build
|
||||
from dpp.api_dlt import ALGORITHM
|
||||
from dpp.models import Proof
|
||||
from dpp.api_dlt import PROOF_TYPE
|
||||
from did.template_credential import dpp_tmpl
|
||||
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
class PublicDeviceWebView(TemplateView):
|
||||
template_name = "device_did.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.pk = kwargs['pk']
|
||||
chid = self.pk.split(":")[0]
|
||||
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:
|
||||
raise Http404
|
||||
|
||||
if self.request.headers.get('Accept') == 'application/json':
|
||||
return self.get_json_response()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.context = super().get_context_data(**kwargs)
|
||||
self.object.initial()
|
||||
roles = [("Operator", "Operator")]
|
||||
role = "Operator"
|
||||
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:
|
||||
self.get_manuals()
|
||||
return self.context
|
||||
|
||||
@property
|
||||
def public_fields(self):
|
||||
return {
|
||||
'id': self.object.id,
|
||||
'shortid': self.object.shortid,
|
||||
'uuids': self.object.uuids,
|
||||
'hids': self.object.hids,
|
||||
'components': self.remove_serial_number_from(self.object.components),
|
||||
}
|
||||
|
||||
@property
|
||||
def authenticated_fields(self):
|
||||
return {
|
||||
'serial_number': self.object.serial_number,
|
||||
'components': self.object.components,
|
||||
}
|
||||
|
||||
def remove_serial_number_from(self, components):
|
||||
for component in components:
|
||||
if 'serial_number' in component:
|
||||
del component['SerialNumber']
|
||||
return components
|
||||
|
||||
def get_device_data(self):
|
||||
data = self.public_fields
|
||||
if self.request.user.is_authenticated:
|
||||
data.update(self.authenticated_fields)
|
||||
return data
|
||||
|
||||
def get_json_response(self):
|
||||
device_data = self.get_result()
|
||||
# device_data = self.get_device_data()
|
||||
response = JsonResponse(device_data)
|
||||
response["Access-Control-Allow-Origin"] = "*"
|
||||
return response
|
||||
|
||||
def get_result(self):
|
||||
|
||||
if len(self.pk.split(":")) > 1:
|
||||
return self.build_from_dpp()
|
||||
else:
|
||||
return self.build_from_chid()
|
||||
|
||||
def build_from_dpp(self):
|
||||
data = {
|
||||
'document': {},
|
||||
'dpp': self.pk,
|
||||
'algorithm': ALGORITHM,
|
||||
'components': [],
|
||||
'manufacturer DPP': '',
|
||||
'device': {},
|
||||
}
|
||||
dev = Build(self.object.last_evidence.doc, None, check=True)
|
||||
doc = dev.get_phid()
|
||||
data['document'] = json.dumps(doc)
|
||||
data['device'] = dev.device
|
||||
data['components'] = dev.components
|
||||
|
||||
self.object.get_evidences()
|
||||
last_dpp = Proof.objects.filter(
|
||||
uuid__in=self.object.uuids, type=PROOF_TYPE['IssueDPP']
|
||||
).order_by("-timestamp").first()
|
||||
|
||||
key = self.pk
|
||||
if last_dpp:
|
||||
key = last_dpp.signature
|
||||
|
||||
url = "https://{}/did/{}".format(
|
||||
self.request.get_host(),
|
||||
key
|
||||
)
|
||||
data['url_last'] = url
|
||||
tmpl = dpp_tmpl.copy()
|
||||
tmpl["credentialSubject"]["data"] = data
|
||||
return tmpl
|
||||
|
||||
def build_from_chid(self):
|
||||
dpps = []
|
||||
self.object.initial()
|
||||
for d in self.object.evidences:
|
||||
d.get_doc()
|
||||
dev = Build(d.doc, None, check=True)
|
||||
doc = dev.get_phid()
|
||||
ev = json.dumps(doc)
|
||||
phid = dev.get_signature(doc)
|
||||
dpp = "{}:{}".format(self.pk, phid)
|
||||
rr = {
|
||||
'dpp': dpp,
|
||||
'document': ev,
|
||||
'algorithm': ALGORITHM,
|
||||
'manufacturer DPP': '',
|
||||
'device': dev.device,
|
||||
'components': dev.components
|
||||
}
|
||||
|
||||
tmpl = dpp_tmpl.copy()
|
||||
tmpl["credentialSubject"]["data"] = rr
|
||||
|
||||
dpps.append(tmpl)
|
||||
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, {})
|
|
@ -9,7 +9,6 @@ services:
|
|||
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN}
|
||||
- DEMO=${DEMO:-false}
|
||||
- PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-}
|
||||
- DPP=${DPP:-false}
|
||||
volumes:
|
||||
- .:/opt/devicehub-django
|
||||
ports:
|
||||
|
|
|
@ -20,9 +20,7 @@ main() {
|
|||
echo "WARNING: .env was not there, .env.example was copied, this only happens once"
|
||||
fi
|
||||
# remove old database
|
||||
rm -vfr ./db/*
|
||||
# deactivate configured flag
|
||||
rm -vfr ./already_configured
|
||||
sudo rm -vfr ./db/*
|
||||
docker compose down -v
|
||||
docker compose build
|
||||
docker compose up ${detach_arg:-}
|
||||
|
|
|
@ -7,14 +7,8 @@ RUN apt update && \
|
|||
git \
|
||||
sqlite3 \
|
||||
jq \
|
||||
time \
|
||||
vim \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# TODO I don't like this, but the whole ereuse-dpp works with user 1000 because of the volume mapping
|
||||
# thanks https://stackoverflow.com/questions/70520205/docker-non-root-user-best-practices-for-python-images
|
||||
RUN adduser --home /opt/devicehub-django -u 1000 app
|
||||
|
||||
WORKDIR /opt/devicehub-django
|
||||
|
||||
# reduce size (python specifics) -> src https://stackoverflow.com/questions/74616667/removing-pip-cache-after-installing-dependencies-in-docker-image
|
||||
|
@ -28,18 +22,15 @@ compile = no
|
|||
no-cache-dir = True
|
||||
END
|
||||
|
||||
# upgrade pip, which might fail on lxc, then remove the "corrupted file"
|
||||
RUN python -m pip install --upgrade pip || (rm -rf /usr/local/lib/python3.11/site-packages/pip-*.dist-info && python -m pip install --upgrade pip)
|
||||
|
||||
COPY ./requirements.txt /opt/devicehub-django
|
||||
RUN pip install -r requirements.txt
|
||||
# TODO hardcoded, is ignored in requirements.txt
|
||||
RUN pip install -i https://test.pypi.org/simple/ ereuseapitest==0.0.14
|
||||
|
||||
# TODO Is there a better way?
|
||||
# Set PYTHONPATH to include the directory with the xapian module
|
||||
ENV PYTHONPATH="${PYTHONPATH}:/usr/lib/python3/dist-packages"
|
||||
|
||||
COPY docker/devicehub-django.entrypoint.sh /
|
||||
|
||||
RUN chown -R app:app /opt/devicehub-django
|
||||
|
||||
USER app
|
||||
ENTRYPOINT sh /devicehub-django.entrypoint.sh
|
||||
|
|
|
@ -5,149 +5,6 @@ set -u
|
|||
# DEBUG
|
||||
set -x
|
||||
|
||||
# TODO there is a conflict between two shared vars
|
||||
# 1. from the original docker compose devicehub-teal
|
||||
# 2. from the new docker compose that integrates all dpp services
|
||||
wait_for_dpp_shared() {
|
||||
while true; do
|
||||
# specially ensure VERAMO_API_CRED_FILE is not empty,
|
||||
# it takes some time to get data in
|
||||
OPERATOR_TOKEN_FILE='operator-token.txt'
|
||||
if [ -f "/shared/${OPERATOR_TOKEN_FILE}" ] && \
|
||||
[ -f "/shared/create_user_operator_finished" ]; then
|
||||
sleep 5
|
||||
echo "Files ready to process."
|
||||
break
|
||||
else
|
||||
echo "Waiting for file in shared: ${OPERATOR_TOKEN_FILE}"
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 3. Generate an environment .env file.
|
||||
# TODO cargar via shared
|
||||
gen_env_vars() {
|
||||
INIT_ORG="${INIT_ORG:-example-org}"
|
||||
INIT_USER="${INIT_USER:-user@example.org}"
|
||||
INIT_PASSWD="${INIT_PASSWD:-1234}"
|
||||
ADMIN='True'
|
||||
PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}"
|
||||
# specific dpp env vars
|
||||
if [ "${DPP:-}" = 'true' ]; then
|
||||
# fill env vars in this docker entrypoint
|
||||
wait_for_dpp_shared
|
||||
export API_DLT='http://api_connector:3010'
|
||||
export API_DLT_TOKEN="$(cat "/shared/${OPERATOR_TOKEN_FILE}")"
|
||||
export API_RESOLVER='http://id_index_api:3012'
|
||||
# TODO hardcoded
|
||||
export ID_FEDERATED='DH1'
|
||||
# propagate to .env
|
||||
dpp_env_vars="$(cat <<END
|
||||
API_DLT=${API_DLT}
|
||||
API_DLT_TOKEN=${API_DLT_TOKEN}
|
||||
API_RESOLVER=${API_RESOLVER}
|
||||
ID_FEDERATED=${ID_FEDERATED}
|
||||
END
|
||||
)"
|
||||
# generate config using env vars from docker
|
||||
# TODO rethink if this is needed because now this is django, not flask
|
||||
cat > .env <<END
|
||||
${dpp_env_vars:-}
|
||||
END
|
||||
fi
|
||||
}
|
||||
|
||||
handle_federated_id() {
|
||||
|
||||
# devicehub host and id federated checker
|
||||
|
||||
# //getAll queries are not accepted by this service, so we remove them
|
||||
EXPECTED_ID_FEDERATED="$(curl -s "${API_RESOLVER%/}/getAll" \
|
||||
| jq -r '.url | to_entries | .[] | select(.value == "'"${DEVICEHUB_HOST}"'") | .key' \
|
||||
| head -n 1)"
|
||||
|
||||
# if is a new DEVICEHUB_HOST, then register it
|
||||
if [ -z "${EXPECTED_ID_FEDERATED}" ]; then
|
||||
# TODO better docker compose run command
|
||||
cmd="docker compose run --entrypoint= devicehub flask dlt_insert_members ${DEVICEHUB_HOST}"
|
||||
big_error "No FEDERATED ID maybe you should run \`${cmd}\`"
|
||||
fi
|
||||
|
||||
# if not new DEVICEHUB_HOST, then check consistency
|
||||
|
||||
# if there is already an ID in the DLT, it should match with my internal ID
|
||||
if [ ! "${EXPECTED_ID_FEDERATED}" = "${ID_FEDERATED}" ]; then
|
||||
|
||||
big_error "ID_FEDERATED should be ${EXPECTED_ID_FEDERATED} instead of ${ID_FEDERATED}"
|
||||
fi
|
||||
|
||||
# not needed, but reserved
|
||||
# EXPECTED_DEVICEHUB_HOST="$(curl -s "${API_RESOLVER%/}/getAll" \
|
||||
# | jq -r '.url | to_entries | .[] | select(.key == "'"${ID_FEDERATED}"'") | .value' \
|
||||
# | head -n 1)"
|
||||
# if [ ! "${EXPECTED_DEVICEHUB_HOST}" = "${DEVICEHUB_HOST}" ]; then
|
||||
# big_error "ERROR: DEVICEHUB_HOST should be ${EXPECTED_DEVICEHUB_HOST} instead of ${DEVICEHUB_HOST}"
|
||||
# fi
|
||||
|
||||
}
|
||||
|
||||
config_dpp_part1() {
|
||||
# 12. Add a new server to the 'api resolver'
|
||||
if [ "${ID_SERVICE:-}" ]; then
|
||||
handle_federated_id
|
||||
else
|
||||
# TODO when this runs more than one time per service, this is a problem, but for the docker-reset.sh workflow, that's fine
|
||||
# TODO put this in already_configured
|
||||
# TODO hardcoded http proto and port
|
||||
./manage.py dlt_insert_members "http://${DOMAIN}:8000"
|
||||
fi
|
||||
|
||||
# 13. Do a rsync api resolve
|
||||
./manage.py dlt_rsync_members
|
||||
|
||||
# 14. Register a new user to the DLT
|
||||
DATASET_FILE='/tmp/dataset.json'
|
||||
cat > "${DATASET_FILE}" <<END
|
||||
{
|
||||
"email": "${INIT_USER}",
|
||||
"password": "${INIT_PASSWD}",
|
||||
"api_token": "${API_DLT_TOKEN}"
|
||||
}
|
||||
END
|
||||
./manage.py dlt_register_user "${DATASET_FILE}"
|
||||
}
|
||||
|
||||
config_phase() {
|
||||
# TODO review this flag file
|
||||
init_flagfile="${program_dir}/already_configured"
|
||||
if [ ! -f "${init_flagfile}" ]; then
|
||||
|
||||
# non DL user (only for the inventory)
|
||||
./manage.py add_institution "${INIT_ORG}"
|
||||
# TODO: one error on add_user, and you don't add user anymore
|
||||
./manage.py add_user "${INIT_ORG}" "${INIT_USER}" "${INIT_PASSWD}" "${ADMIN}" "${PREDEFINED_TOKEN}"
|
||||
|
||||
if [ "${DPP:-}" = 'true' ]; then
|
||||
# 12, 13, 14
|
||||
config_dpp_part1
|
||||
|
||||
# cleanup other spnapshots and copy dlt/dpp snapshots
|
||||
# TODO make this better
|
||||
rm example/snapshots/*
|
||||
cp example/dpp-snapshots/*.json example/snapshots/
|
||||
fi
|
||||
|
||||
# # 15. Add inventory snapshots for user "${INIT_USER}".
|
||||
if [ "${DEMO:-}" = 'true' ]; then
|
||||
/usr/bin/time ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
|
||||
fi
|
||||
|
||||
# remain next command as the last operation for this if conditional
|
||||
touch "${init_flagfile}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_app_is_there() {
|
||||
if [ ! -f "./manage.py" ]; then
|
||||
usage
|
||||
|
@ -156,7 +13,7 @@ check_app_is_there() {
|
|||
|
||||
deploy() {
|
||||
# TODO this is weird, find better workaround
|
||||
git config --global --add safe.directory "${program_dir}"
|
||||
git config --global --add safe.directory /opt/devicehub-django
|
||||
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
|
||||
|
||||
if [ "${DEBUG:-}" = 'true' ]; then
|
||||
|
@ -174,7 +31,18 @@ deploy() {
|
|||
# inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc
|
||||
echo "INFO detected NEW deployment"
|
||||
./manage.py migrate
|
||||
config_phase
|
||||
INIT_ORG="${INIT_ORG:-example-org}"
|
||||
INIT_USER="${INIT_USER:-user@example.org}"
|
||||
INIT_PASSWD="${INIT_PASSWD:-1234}"
|
||||
ADMIN='True'
|
||||
PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}"
|
||||
./manage.py add_institution "${INIT_ORG}"
|
||||
# TODO: one error on add_user, and you don't add user anymore
|
||||
./manage.py add_user "${INIT_ORG}" "${INIT_USER}" "${INIT_PASSWD}" "${ADMIN}" "${PREDEFINED_TOKEN}"
|
||||
|
||||
if [ "${DEMO:-}" = 'true' ]; then
|
||||
./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
@ -202,7 +70,6 @@ runserver() {
|
|||
main() {
|
||||
program_dir='/opt/devicehub-django'
|
||||
cd "${program_dir}"
|
||||
gen_env_vars
|
||||
deploy
|
||||
runserver
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
166
dpp/api_dlt.py
166
dpp/api_dlt.py
|
@ -1,166 +0,0 @@
|
|||
import json
|
||||
import time
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from ereuseapi.methods import API
|
||||
|
||||
from dpp.models import Proof, UserDpp
|
||||
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
# """The code of the status response of api dlt."""
|
||||
STATUS_CODE = {
|
||||
"Success": 201,
|
||||
"Notwork": 400
|
||||
}
|
||||
|
||||
|
||||
ALGORITHM = "sha3-256"
|
||||
|
||||
|
||||
PROOF_TYPE = {
|
||||
'Register': 'Register',
|
||||
'IssueDPP': 'IssueDPP',
|
||||
'proof_of_recycling': 'proof_of_recycling',
|
||||
'Erase': 'Erase',
|
||||
'EWaste': 'EWaste',
|
||||
}
|
||||
|
||||
|
||||
def connect_api(user):
|
||||
|
||||
dp = UserDpp.objects.filter(user=user).first()
|
||||
if not dp:
|
||||
return
|
||||
|
||||
api_dlt = settings.API_DLT
|
||||
token_dlt = dp.api_keys_dlt
|
||||
|
||||
if not api_dlt or not token_dlt:
|
||||
logger.error("NOT POSSIBLE CONNECT WITH API DLT!!!")
|
||||
return
|
||||
|
||||
return API(api_dlt, token_dlt, "ethereum")
|
||||
|
||||
|
||||
def register_dlt(api, chid, phid, proof_type=None):
|
||||
if proof_type:
|
||||
return api.generate_proof(
|
||||
chid,
|
||||
ALGORITHM,
|
||||
phid,
|
||||
proof_type,
|
||||
settings.ID_FEDERATED
|
||||
)
|
||||
|
||||
return api.register_device(
|
||||
chid,
|
||||
ALGORITHM,
|
||||
phid,
|
||||
settings.ID_FEDERATED
|
||||
)
|
||||
|
||||
|
||||
def issuer_dpp_dlt(api, dpp):
|
||||
phid = dpp.split(":")[1]
|
||||
|
||||
return api.issue_passport(
|
||||
dpp,
|
||||
ALGORITHM,
|
||||
phid,
|
||||
settings.ID_FEDERATED
|
||||
)
|
||||
|
||||
|
||||
|
||||
def save_proof(signature, ev_uuid, result, proof_type, user):
|
||||
if result['Status'] == STATUS_CODE.get("Success"):
|
||||
timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
|
||||
|
||||
if not timestamp:
|
||||
return
|
||||
|
||||
logger.debug("timestamp: %s", timestamp)
|
||||
d = {
|
||||
"type": proof_type,
|
||||
"timestamp": timestamp,
|
||||
"issuer": user.institution,
|
||||
"user": user,
|
||||
"uuid": ev_uuid,
|
||||
"signature": signature,
|
||||
}
|
||||
Proof.objects.create(**d)
|
||||
|
||||
|
||||
def register_device_dlt(chid, phid, ev_uuid, user):
|
||||
cny_a = 1
|
||||
while cny_a:
|
||||
api = connect_api(user)
|
||||
if not api:
|
||||
cny_a = 0
|
||||
return
|
||||
|
||||
result = register_dlt(api, chid, phid)
|
||||
try:
|
||||
assert result['Status'] == STATUS_CODE.get("Success")
|
||||
assert result['Data']['data']['timestamp']
|
||||
cny_a = 0
|
||||
except Exception:
|
||||
if result.get("Data") != "Device already exists":
|
||||
logger.error("API return: %s", result)
|
||||
time.sleep(10)
|
||||
else:
|
||||
cny_a = 0
|
||||
|
||||
save_proof(phid, ev_uuid, result, PROOF_TYPE['Register'], user)
|
||||
|
||||
|
||||
# TODO is neccesary?
|
||||
if settings.ID_FEDERATED:
|
||||
cny = 1
|
||||
while cny:
|
||||
try:
|
||||
api.add_service(
|
||||
chid,
|
||||
'DeviceHub',
|
||||
settings.ID_FEDERATED,
|
||||
'Inventory service',
|
||||
'Inv',
|
||||
)
|
||||
cny = 0
|
||||
except Exception:
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
def register_passport_dlt(chid, phid, ev_uuid, user):
|
||||
token_dlt = settings.TOKEN_DLT
|
||||
api_dlt = settings.API_DLT
|
||||
if not token_dlt or not api_dlt:
|
||||
return
|
||||
|
||||
dpp = "{chid}:{phid}".format(chid=chid, phid=phid)
|
||||
if Proof.objects.filter(signature=dpp, type=PROOF_TYPE['IssueDPP']).exists():
|
||||
return
|
||||
|
||||
cny_a = 1
|
||||
while cny_a:
|
||||
try:
|
||||
api = connect_api(user)
|
||||
if not api:
|
||||
cny_a = 0
|
||||
return
|
||||
|
||||
result = issuer_dpp_dlt(api, dpp)
|
||||
cny_a = 0
|
||||
except Exception as err:
|
||||
logger.error("ERROR API issue passport return: %s", err)
|
||||
time.sleep(10)
|
||||
|
||||
if result['Status'] is not STATUS_CODE.get("Success"):
|
||||
logger.error("ERROR API issue passport return: %s", result)
|
||||
return
|
||||
|
||||
save_proof(phid, ev_uuid, result, PROOF_TYPE['IssueDPP'], user)
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DppConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "dpp"
|
|
@ -1,35 +0,0 @@
|
|||
import logging
|
||||
import requests
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from user.models import Institution
|
||||
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Insert a new Institution in DLT"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('domain', type=str, help='institution')
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
domain = kwargs.get("domain")
|
||||
api = settings.API_RESOLVER
|
||||
if not api:
|
||||
logger.error("you need set the var API_RESOLVER")
|
||||
return
|
||||
|
||||
if "http" not in domain:
|
||||
logger.error("you need put https:// in %s", domain)
|
||||
return
|
||||
|
||||
api = api.strip("/")
|
||||
domain = domain.strip("/")
|
||||
|
||||
data = {"url": domain}
|
||||
url = api + '/registerURL'
|
||||
res = requests.post(url, json=data)
|
||||
print(res.json())
|
|
@ -1,72 +0,0 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
from ereuseapi.methods import API
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from user.models import User, Institution
|
||||
from dpp.models import UserDpp
|
||||
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Insert users than are in Dlt with params: path of data set file"
|
||||
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('dataset_file', type=str, help='institution')
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
dataset_file = kwargs.get("dataset_file")
|
||||
self.api_dlt = settings.API_DLT
|
||||
self.institution = Institution.objects.filter().first()
|
||||
if not self.api_dlt:
|
||||
logger.error("you need set the var API_DLT")
|
||||
return
|
||||
|
||||
self.api_dlt = self.api_dlt.strip("/")
|
||||
|
||||
with open(dataset_file) as f:
|
||||
dataset = json.loads(f.read())
|
||||
|
||||
self.add_user(dataset)
|
||||
|
||||
def add_user(self, data):
|
||||
email = data.get("email")
|
||||
password = data.get("password")
|
||||
api_token = data.get("api_token")
|
||||
# ethereum = {"data": {"api_token": api_token}}
|
||||
# data_eth = json.dumps(ethereum)
|
||||
data_eth = json.dumps(api_token)
|
||||
# TODO encrypt in the future
|
||||
# api_keys_dlt = encrypt(password, data_eth)
|
||||
api_keys_dlt = data_eth.strip('"').strip("'")
|
||||
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
if not user:
|
||||
user = User.objects.create(
|
||||
email=email,
|
||||
password=password,
|
||||
institution = self.institution
|
||||
)
|
||||
|
||||
roles = []
|
||||
token_dlt = api_token
|
||||
api = API(self.api_dlt, token_dlt, "ethereum")
|
||||
result = api.check_user_roles()
|
||||
|
||||
if result.get('Status') == 200:
|
||||
if 'Success' in result.get('Data', {}).get('status'):
|
||||
rols = result.get('Data', {}).get('data', {})
|
||||
roles = [(k, k) for k, v in rols.items() if v]
|
||||
|
||||
roles_dlt = json.dumps(roles)
|
||||
|
||||
UserDpp.objects.create(
|
||||
roles_dlt=roles_dlt,
|
||||
api_keys_dlt=api_keys_dlt,
|
||||
user=user
|
||||
)
|
|
@ -1,47 +0,0 @@
|
|||
import logging
|
||||
import requests
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from dpp.models import MemberFederated
|
||||
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Synchronize members of DLT"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
api = settings.API_RESOLVER
|
||||
if not api:
|
||||
logger.error("you need set the var API_RESOLVER")
|
||||
return
|
||||
|
||||
|
||||
api = api.strip("/")
|
||||
|
||||
url = api + '/getAll'
|
||||
res = requests.get(url)
|
||||
if res.status_code != 200:
|
||||
return "Error, {}".format(res.text)
|
||||
response = res.json()
|
||||
members = response['url']
|
||||
counter = members.pop('counter')
|
||||
if counter <= MemberFederated.objects.count():
|
||||
logger.info("Synchronize members of DLT -> All Ok")
|
||||
return "All ok"
|
||||
|
||||
for k, v in members.items():
|
||||
id = self.clean_id(k)
|
||||
member = MemberFederated.objects.filter(dlt_id_provider=id).first()
|
||||
if member:
|
||||
if member.domain != v:
|
||||
member.domain = v
|
||||
member.save()
|
||||
continue
|
||||
MemberFederated.objects.create(dlt_id_provider=id, domain=v)
|
||||
return res.text
|
||||
|
||||
def clean_id(self, id):
|
||||
return int(id.split('DH')[-1])
|
|
@ -1,52 +0,0 @@
|
|||
# Generated by Django 5.0.6 on 2024-11-18 14:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("user", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Proof",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("timestamp", models.IntegerField()),
|
||||
("uuid", models.UUIDField()),
|
||||
("signature", models.CharField(max_length=256)),
|
||||
("type", models.CharField(max_length=256)),
|
||||
(
|
||||
"issuer",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,25 +0,0 @@
|
|||
# Generated by Django 5.0.6 on 2024-11-19 19:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("dpp", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="MemberFederated",
|
||||
fields=[
|
||||
(
|
||||
"dlt_id_provider",
|
||||
models.IntegerField(primary_key=True, serialize=False),
|
||||
),
|
||||
("domain", models.CharField(max_length=256)),
|
||||
("client_id", models.CharField(max_length=256)),
|
||||
("client_secret", models.CharField(max_length=256)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,60 +0,0 @@
|
|||
# Generated by Django 5.0.6 on 2024-11-20 10:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("dpp", "0002_memberfederated"),
|
||||
("user", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="memberfederated",
|
||||
name="institution",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="memberfederated",
|
||||
name="client_id",
|
||||
field=models.CharField(max_length=256, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="memberfederated",
|
||||
name="client_secret",
|
||||
field=models.CharField(max_length=256, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserDpp",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("roles_dlt", models.TextField()),
|
||||
("api_keys_dlt", models.TextField()),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,32 +0,0 @@
|
|||
from django.db import models
|
||||
from user.models import User, Institution
|
||||
from utils.constants import STR_EXTEND_SIZE
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Proof(models.Model):
|
||||
## The signature can be a phid or dpp depending of type of Proof
|
||||
timestamp = models.IntegerField()
|
||||
uuid = models.UUIDField()
|
||||
signature = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||
type = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||
issuer = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class MemberFederated(models.Model):
|
||||
dlt_id_provider = models.IntegerField(primary_key=True)
|
||||
domain = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||
# This client_id and client_secret is used for connected to this domain as
|
||||
# a client and this domain then is the server of auth
|
||||
client_id = models.CharField(max_length=STR_EXTEND_SIZE, null=True)
|
||||
client_secret = models.CharField(max_length=STR_EXTEND_SIZE, null=True)
|
||||
institution = models.ForeignKey(
|
||||
Institution, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class UserDpp(models.Model):
|
||||
roles_dlt = models.TextField()
|
||||
api_keys_dlt = models.TextField()
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,8 +0,0 @@
|
|||
from django.urls import path
|
||||
from dpp import views
|
||||
|
||||
app_name = 'dpp'
|
||||
|
||||
urlpatterns = [
|
||||
path("<int:proof_id>/", views.ProofView.as_view(), name="proof"),
|
||||
]
|
40
dpp/views.py
40
dpp/views.py
|
@ -1,40 +0,0 @@
|
|||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
from django.views.generic.edit import View
|
||||
from django.http import JsonResponse
|
||||
|
||||
from dpp.api_dlt import ALGORITHM
|
||||
from evidence.models import Evidence
|
||||
from evidence.parse import Build
|
||||
from dpp.models import Proof
|
||||
|
||||
|
||||
class ProofView(View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
timestamp = kwargs.get("proof_id")
|
||||
proof = Proof.objects.filter(timestamp=timestamp).first()
|
||||
if not proof:
|
||||
return JsonResponse({}, status=404)
|
||||
|
||||
ev = Evidence(proof.uuid)
|
||||
if not ev.doc:
|
||||
return JsonResponse({}, status=404)
|
||||
|
||||
dev = Build(ev.doc, None, check=True)
|
||||
doc = dev.get_phid()
|
||||
|
||||
data = {
|
||||
"algorithm": ALGORITHM,
|
||||
"document": json.dumps(doc)
|
||||
}
|
||||
|
||||
d = {
|
||||
'@context': ['https://ereuse.org/proof0.json'],
|
||||
'data': data,
|
||||
}
|
||||
response = JsonResponse(d, status=200)
|
||||
response["Access-Control-Allow-Origin"] = "*"
|
||||
return response
|
|
@ -4,12 +4,13 @@ import pandas as pd
|
|||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from utils.device import create_annotation, create_doc, create_index
|
||||
from utils.device import create_property, create_doc, create_index
|
||||
from utils.forms import MultipleFileField
|
||||
from device.models import Device
|
||||
from evidence.parse import Build
|
||||
from evidence.models import Annotation
|
||||
from evidence.models import SystemProperty, UserProperty
|
||||
from utils.save_snapshots import move_json, save_in_disk
|
||||
from action.models import DeviceLog
|
||||
|
||||
|
||||
class UploadForm(forms.Form):
|
||||
|
@ -29,17 +30,17 @@ class UploadForm(forms.Form):
|
|||
|
||||
try:
|
||||
file_json = json.loads(file_data)
|
||||
snap = Build(file_json, None, check=True)
|
||||
exist_annotation = Annotation.objects.filter(
|
||||
uuid=snap.uuid
|
||||
Build(file_json, None, check=True)
|
||||
exists_property = SystemProperty.objects.filter(
|
||||
uuid=file_json['uuid']
|
||||
).first()
|
||||
|
||||
if exist_annotation:
|
||||
raise ValidationError(
|
||||
|
||||
if exists_property:
|
||||
raise ValidationError(
|
||||
_("The snapshot already exists"),
|
||||
code="duplicate_snapshot",
|
||||
)
|
||||
|
||||
|
||||
#Catch any error and display it as Validation Error so the Form handles it
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
|
@ -68,9 +69,8 @@ class UserTagForm(forms.Form):
|
|||
self.pk = None
|
||||
self.uuid = kwargs.pop('uuid', None)
|
||||
self.user = kwargs.pop('user')
|
||||
instance = Annotation.objects.filter(
|
||||
instance = SystemProperty.objects.filter(
|
||||
uuid=self.uuid,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
key='CUSTOM_ID',
|
||||
owner=self.user.institution
|
||||
).first()
|
||||
|
@ -86,9 +86,8 @@ class UserTagForm(forms.Form):
|
|||
if not data:
|
||||
return False
|
||||
self.tag = data
|
||||
self.instance = Annotation.objects.filter(
|
||||
self.instance = SystemProperty.objects.filter(
|
||||
uuid=self.uuid,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
key='CUSTOM_ID',
|
||||
owner=self.user.institution
|
||||
).first()
|
||||
|
@ -100,20 +99,31 @@ class UserTagForm(forms.Form):
|
|||
return
|
||||
|
||||
if self.instance:
|
||||
old_value = self.instance.value
|
||||
if not self.tag:
|
||||
message =_("<Deleted> Evidence Tag. Old Value: '{}'").format(old_value)
|
||||
self.instance.delete()
|
||||
self.instance.value = self.tag
|
||||
self.instance.save()
|
||||
return
|
||||
|
||||
Annotation.objects.create(
|
||||
uuid=self.uuid,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
key='CUSTOM_ID',
|
||||
value=self.tag,
|
||||
owner=self.user.institution,
|
||||
user=self.user
|
||||
)
|
||||
else:
|
||||
self.instance.value = self.tag
|
||||
self.instance.save()
|
||||
if old_value != self.tag:
|
||||
message=_("<Updated> Evidence Tag. Old Value: '{}'. New Value: '{}'").format(old_value, self.tag)
|
||||
else:
|
||||
message =_("<Created> Evidence Tag. Value: '{}'").format(self.tag)
|
||||
SystemProperty.objects.create(
|
||||
uuid=self.uuid,
|
||||
key='CUSTOM_ID',
|
||||
value=self.tag,
|
||||
owner=self.user.institution,
|
||||
user=self.user
|
||||
)
|
||||
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=self.uuid,
|
||||
event= message,
|
||||
user=self.user,
|
||||
institution=self.user.institution
|
||||
)
|
||||
|
||||
|
||||
class ImportForm(forms.Form):
|
||||
|
@ -164,8 +174,8 @@ class ImportForm(forms.Form):
|
|||
table = []
|
||||
for row in self.rows:
|
||||
doc = create_doc(row)
|
||||
annotation = create_annotation(doc, self.user)
|
||||
table.append((doc, annotation))
|
||||
property = create_property(doc, self.user)
|
||||
table.append((doc, property))
|
||||
|
||||
if commit:
|
||||
for doc, cred in table:
|
||||
|
@ -186,9 +196,9 @@ class EraseServerForm(forms.Form):
|
|||
self.pk = None
|
||||
self.uuid = kwargs.pop('uuid', None)
|
||||
self.user = kwargs.pop('user')
|
||||
instance = Annotation.objects.filter(
|
||||
instance = UserProperty.objects.filter(
|
||||
uuid=self.uuid,
|
||||
type=Annotation.Type.ERASE_SERVER,
|
||||
type=UserProperty.Type.ERASE_SERVER,
|
||||
key='ERASE_SERVER',
|
||||
owner=self.user.institution
|
||||
).first()
|
||||
|
@ -201,9 +211,9 @@ class EraseServerForm(forms.Form):
|
|||
|
||||
def clean(self):
|
||||
self.erase_server = self.cleaned_data.get('erase_server', False)
|
||||
self.instance = Annotation.objects.filter(
|
||||
self.instance = UserProperty.objects.filter(
|
||||
uuid=self.uuid,
|
||||
type=Annotation.Type.ERASE_SERVER,
|
||||
type=UserProperty.Type.ERASE_SERVER,
|
||||
key='ERASE_SERVER',
|
||||
owner=self.user.institution
|
||||
).first()
|
||||
|
@ -221,10 +231,10 @@ class EraseServerForm(forms.Form):
|
|||
|
||||
if self.instance:
|
||||
return
|
||||
|
||||
Annotation.objects.create(
|
||||
|
||||
UserProperty.objects.create(
|
||||
uuid=self.uuid,
|
||||
type=Annotation.Type.ERASE_SERVER,
|
||||
type=UserProperty.Type.ERASE_SERVER,
|
||||
key='ERASE_SERVER',
|
||||
value=self.erase_server,
|
||||
owner=self.user.institution,
|
||||
|
|
|
@ -5,7 +5,7 @@ import logging
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
|
||||
from utils.device import create_annotation, create_doc, create_index
|
||||
from utils.device import create_property, create_doc, create_index
|
||||
from user.models import Institution
|
||||
from evidence.parse import Build
|
||||
|
||||
|
@ -70,7 +70,7 @@ class Command(BaseCommand):
|
|||
def build_placeholder(self, s, user, f_path):
|
||||
try:
|
||||
create_index(s, user)
|
||||
create_annotation(s, user, commit=True)
|
||||
create_property(s, user, commit=True)
|
||||
except Exception as err:
|
||||
txt = "In placeholder %s \n%s"
|
||||
logger.warning(txt, f_path, err)
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
# Generated by Django 5.0.6 on 2024-12-10 19:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("evidence", "0002_alter_annotation_type"),
|
||||
("user", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SystemProperty",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("key", models.CharField(max_length=256)),
|
||||
("value", models.CharField(max_length=256)),
|
||||
("uuid", models.UUIDField()),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserProperty",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("key", models.CharField(max_length=256)),
|
||||
("value", models.CharField(max_length=256)),
|
||||
("uuid", models.UUIDField()),
|
||||
(
|
||||
"type",
|
||||
models.SmallIntegerField(
|
||||
choices=[(1, "User"), (2, "Document"), (3, "EraseServer")],
|
||||
default=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="Annotation",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="systemproperty",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("key", "uuid"), name="system_unique_type_key_uuid"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="userproperty",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("key", "uuid", "type"), name="user_unique_type_key_uuid"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 5.0.6 on 2024-12-18 12:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("evidence", "0003_systemproperty_userproperty_delete_annotation_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="userproperty",
|
||||
name="user_unique_type_key_uuid",
|
||||
),
|
||||
]
|
|
@ -1,38 +1,50 @@
|
|||
import json
|
||||
import hashlib
|
||||
|
||||
from dmidecode import DMIParse
|
||||
from django.db import models
|
||||
|
||||
|
||||
from django.db.models import Q
|
||||
from utils.constants import STR_EXTEND_SIZE, CHASSIS_DH
|
||||
from evidence.xapian import search
|
||||
from evidence.parse_details import ParseSnapshot, get_inxi, get_inxi_key
|
||||
from evidence.parse_details import ParseSnapshot
|
||||
from user.models import User, Institution
|
||||
|
||||
|
||||
class Annotation(models.Model):
|
||||
class Type(models.IntegerChoices):
|
||||
SYSTEM = 0, "System"
|
||||
USER = 1, "User"
|
||||
DOCUMENT = 2, "Document"
|
||||
ERASE_SERVER = 3, "EraseServer"
|
||||
|
||||
class Property(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
uuid = models.UUIDField()
|
||||
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
type = models.SmallIntegerField(choices=Type)
|
||||
key = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||
value = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||
|
||||
class Meta:
|
||||
#Only for shared behaviour, it is not a table
|
||||
abstract = True
|
||||
|
||||
|
||||
class SystemProperty(Property):
|
||||
uuid = models.UUIDField()
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["type", "key", "uuid"], name="unique_type_key_uuid")
|
||||
fields=["key", "uuid"], name="system_unique_type_key_uuid")
|
||||
]
|
||||
|
||||
|
||||
class UserProperty(Property):
|
||||
uuid = models.UUIDField()
|
||||
|
||||
class Type(models.IntegerChoices):
|
||||
USER = 1, "User"
|
||||
DOCUMENT = 2, "Document"
|
||||
ERASE_SERVER = 3, "EraseServer"
|
||||
|
||||
type = models.SmallIntegerField(choices=Type, default=Type.USER)
|
||||
|
||||
|
||||
class Evidence:
|
||||
def __init__(self, uuid):
|
||||
self.uuid = uuid
|
||||
|
@ -40,39 +52,29 @@ class Evidence:
|
|||
self.doc = None
|
||||
self.created = None
|
||||
self.dmi = None
|
||||
self.inxi = None
|
||||
self.annotations = []
|
||||
self.properties = []
|
||||
self.components = []
|
||||
self.default = "n/a"
|
||||
|
||||
self.get_owner()
|
||||
self.get_time()
|
||||
|
||||
def get_annotations(self):
|
||||
self.annotations = Annotation.objects.filter(
|
||||
def get_properties(self):
|
||||
self.properties = SystemProperty.objects.filter(
|
||||
uuid=self.uuid
|
||||
).order_by("created")
|
||||
|
||||
def get_owner(self):
|
||||
if not self.annotations:
|
||||
self.get_annotations()
|
||||
a = self.annotations.first()
|
||||
if not self.properties:
|
||||
self.get_properties()
|
||||
a = self.properties.first()
|
||||
if a:
|
||||
self.owner = a.owner
|
||||
|
||||
def get_phid(self):
|
||||
if not self.doc:
|
||||
self.get_doc()
|
||||
|
||||
return hashlib.sha3_256(json.dumps(self.doc)).hexdigest()
|
||||
|
||||
def get_doc(self):
|
||||
self.doc = {}
|
||||
self.inxi = None
|
||||
|
||||
if not self.owner:
|
||||
self.get_owner()
|
||||
|
||||
qry = 'uuid:"{}"'.format(self.uuid)
|
||||
matches = search(self.owner, qry, limit=1)
|
||||
if matches and matches.size() < 0:
|
||||
|
@ -81,36 +83,9 @@ class Evidence:
|
|||
for xa in matches:
|
||||
self.doc = json.loads(xa.document.get_data())
|
||||
|
||||
if self.is_legacy():
|
||||
return
|
||||
|
||||
if self.doc.get("credentialSubject"):
|
||||
for ev in self.doc["evidence"]:
|
||||
if "dmidecode" == ev.get("operation"):
|
||||
dmidecode_raw = ev["output"]
|
||||
if "inxi" == ev.get("operation"):
|
||||
self.inxi = ev["output"]
|
||||
else:
|
||||
if not self.is_legacy():
|
||||
dmidecode_raw = self.doc["data"]["dmidecode"]
|
||||
inxi_raw = self.doc["data"]["inxi"]
|
||||
self.dmi = DMIParse(dmidecode_raw)
|
||||
try:
|
||||
self.inxi = json.loads(inxi_raw)
|
||||
except Exception:
|
||||
pass
|
||||
if self.inxi:
|
||||
try:
|
||||
machine = get_inxi_key(self.inxi, 'Machine')
|
||||
for m in machine:
|
||||
system = get_inxi(m, "System")
|
||||
if system:
|
||||
self.device_manufacturer = system
|
||||
self.device_model = get_inxi(m, "product")
|
||||
self.device_serial_number = get_inxi(m, "serial")
|
||||
self.device_chassis = get_inxi(m, "Type")
|
||||
self.device_version = get_inxi(m, "v")
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def get_time(self):
|
||||
if not self.doc:
|
||||
|
@ -118,7 +93,7 @@ class Evidence:
|
|||
self.created = self.doc.get("endTime")
|
||||
|
||||
if not self.created:
|
||||
self.created = self.annotations.last().created
|
||||
self.created = self.properties.last().created
|
||||
|
||||
def get_components(self):
|
||||
if self.is_legacy():
|
||||
|
@ -136,9 +111,6 @@ class Evidence:
|
|||
if self.is_legacy():
|
||||
return self.doc['device']['manufacturer']
|
||||
|
||||
if self.inxi:
|
||||
return self.device_manufacturer
|
||||
|
||||
return self.dmi.manufacturer().strip()
|
||||
|
||||
def get_model(self):
|
||||
|
@ -151,18 +123,12 @@ class Evidence:
|
|||
if self.is_legacy():
|
||||
return self.doc['device']['model']
|
||||
|
||||
if self.inxi:
|
||||
return self.device_model
|
||||
|
||||
return self.dmi.model().strip()
|
||||
|
||||
def get_chassis(self):
|
||||
if self.is_legacy():
|
||||
return self.doc['device']['model']
|
||||
|
||||
if self.inxi:
|
||||
return self.device_chassis
|
||||
|
||||
chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual')
|
||||
lower_type = chassis.lower()
|
||||
|
||||
|
@ -174,23 +140,12 @@ class Evidence:
|
|||
def get_serial_number(self):
|
||||
if self.is_legacy():
|
||||
return self.doc['device']['serialNumber']
|
||||
|
||||
if self.inxi:
|
||||
return self.device_serial_number
|
||||
|
||||
return self.dmi.serial_number().strip()
|
||||
|
||||
def get_version(self):
|
||||
if self.inxi:
|
||||
return self.device_version
|
||||
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, user):
|
||||
return Annotation.objects.filter(
|
||||
return SystemProperty.objects.filter(
|
||||
owner=user.institution,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
key="hidalgo1",
|
||||
).order_by("-created").values_list("uuid", "created").distinct()
|
||||
|
||||
|
@ -199,9 +154,6 @@ class Evidence:
|
|||
self.components = snapshot['components']
|
||||
|
||||
def is_legacy(self):
|
||||
if self.doc.get("credentialSubject"):
|
||||
return False
|
||||
|
||||
return self.doc.get("software") != "workbench-script"
|
||||
|
||||
def is_web_snapshot(self):
|
||||
|
|
|
@ -3,66 +3,58 @@ import hashlib
|
|||
import logging
|
||||
|
||||
from dmidecode import DMIParse
|
||||
from evidence.parse_details import ParseSnapshot
|
||||
from json_repair import repair_json
|
||||
from evidence.parse_details import get_lshw_child
|
||||
|
||||
from evidence.models import Annotation
|
||||
from evidence.models import SystemProperty
|
||||
from evidence.xapian import index
|
||||
from evidence.parse_details import get_inxi_key, get_inxi
|
||||
from django.conf import settings
|
||||
from utils.constants import CHASSIS_DH
|
||||
|
||||
if settings.DPP:
|
||||
from dpp.api_dlt import register_device_dlt, register_passport_dlt
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
def get_mac(lshw):
|
||||
try:
|
||||
if type(lshw) is dict:
|
||||
hw = lshw
|
||||
else:
|
||||
hw = json.loads(lshw)
|
||||
except json.decoder.JSONDecodeError:
|
||||
hw = json.loads(repair_json(lshw))
|
||||
|
||||
def get_mac(inxi):
|
||||
nets = get_inxi_key(inxi, "Network")
|
||||
networks = [(nets[i], nets[i + 1]) for i in range(0, len(nets) - 1, 2)]
|
||||
nets = []
|
||||
get_lshw_child(hw, nets, 'network')
|
||||
|
||||
nets_sorted = sorted(nets, key=lambda x: x['businfo'])
|
||||
|
||||
if nets_sorted:
|
||||
mac = nets_sorted[0]['serial']
|
||||
logger.debug("The snapshot has the following MAC: %s" , mac)
|
||||
return mac
|
||||
|
||||
for n, iface in networks:
|
||||
if get_inxi(n, "port"):
|
||||
return get_inxi(iface, 'mac')
|
||||
|
||||
|
||||
class Build:
|
||||
def __init__(self, evidence_json, user, check=False):
|
||||
self.evidence = evidence_json.copy()
|
||||
self.json = evidence_json.copy()
|
||||
|
||||
if evidence_json.get("credentialSubject"):
|
||||
self.json.update(evidence_json["credentialSubject"])
|
||||
if evidence_json.get("evidence"):
|
||||
self.json["data"] = {}
|
||||
for ev in evidence_json["evidence"]:
|
||||
k = ev.get("operation")
|
||||
if not k:
|
||||
continue
|
||||
self.json["data"][k] = ev.get("output")
|
||||
|
||||
self.json = evidence_json
|
||||
self.uuid = self.json['uuid']
|
||||
self.user = user
|
||||
self.hid = None
|
||||
self.chid = None
|
||||
self.phid = self.get_signature(self.json)
|
||||
self.generate_chids()
|
||||
|
||||
if check:
|
||||
return
|
||||
|
||||
self.index()
|
||||
self.create_annotations()
|
||||
if settings.DPP:
|
||||
self.register_device_dlt()
|
||||
self.create_properties()
|
||||
|
||||
def index(self):
|
||||
snap = json.dumps(self.evidence)
|
||||
snap = json.dumps(self.json)
|
||||
index(self.user.institution, self.uuid, snap)
|
||||
|
||||
def generate_chids(self):
|
||||
self.algorithms = {
|
||||
'hidalgo1': self.get_hid_14(),
|
||||
'legacy_dpp': self.get_chid_dpp(),
|
||||
}
|
||||
|
||||
def get_hid_14(self):
|
||||
|
@ -77,107 +69,61 @@ class Build:
|
|||
sku = device.get("sku", '')
|
||||
hid = f"{manufacturer}{model}{chassis}{serial_number}{sku}"
|
||||
|
||||
self.chid = hashlib.sha3_256(hid.encode()).hexdigest()
|
||||
return self.chid
|
||||
|
||||
def get_chid_dpp(self):
|
||||
if self.json.get("software") == "workbench-script":
|
||||
device = ParseSnapshot(self.json).device
|
||||
else:
|
||||
device = self.json['device']
|
||||
return hashlib.sha3_256(hid.encode()).hexdigest()
|
||||
|
||||
hid = self.get_id_hw_dpp(device)
|
||||
self.chid = hashlib.sha3_256(hid.encode("utf-8")).hexdigest()
|
||||
return self.chid
|
||||
|
||||
def get_id_hw_dpp(self, d):
|
||||
manufacturer = d.get("manufacturer", '')
|
||||
model = d.get("model", '')
|
||||
chassis = d.get("chassis", '')
|
||||
serial_number = d.get("serialNumber", '')
|
||||
sku = d.get("sku", '')
|
||||
typ = d.get("type", '')
|
||||
version = d.get("version", '')
|
||||
|
||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}{typ}{version}"
|
||||
|
||||
def get_phid(self):
|
||||
if self.json.get("software") == "workbench-script":
|
||||
data = ParseSnapshot(self.json)
|
||||
self.device = data.device
|
||||
self.components = data.components
|
||||
else:
|
||||
self.device = self.json.get("device")
|
||||
self.components = self.json.get("components", [])
|
||||
|
||||
self.device.pop("actions", None)
|
||||
for c in self.components:
|
||||
c.pop("actions", None)
|
||||
|
||||
device = self.get_id_hw_dpp(self.device)
|
||||
components = sorted(self.components, key=lambda x: x.get("type"))
|
||||
doc = [("computer", device)]
|
||||
|
||||
for c in components:
|
||||
doc.append((c.get("type"), self.get_id_hw_dpp(c)))
|
||||
|
||||
return doc
|
||||
|
||||
def create_annotations(self):
|
||||
annotation = Annotation.objects.filter(
|
||||
def create_properties(self):
|
||||
property = SystemProperty.objects.filter(
|
||||
uuid=self.uuid,
|
||||
owner=self.user.institution,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
)
|
||||
|
||||
if annotation:
|
||||
txt = "Warning: Snapshot %s already registered (annotation exists)"
|
||||
if property:
|
||||
txt = "Warning: Snapshot %s already registered (property exists)"
|
||||
logger.warning(txt, self.uuid)
|
||||
return
|
||||
|
||||
for k, v in self.algorithms.items():
|
||||
Annotation.objects.create(
|
||||
SystemProperty.objects.create(
|
||||
uuid=self.uuid,
|
||||
owner=self.user.institution,
|
||||
user=self.user,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
key=k,
|
||||
value=v
|
||||
)
|
||||
|
||||
def get_chassis_dh(self):
|
||||
chassis = self.get_chassis()
|
||||
lower_type = chassis.lower()
|
||||
for k, v in CHASSIS_DH.items():
|
||||
if lower_type in v:
|
||||
return k
|
||||
return self.default
|
||||
|
||||
def get_sku(self):
|
||||
return self.dmi.get("System")[0].get("SKU Number", "n/a").strip()
|
||||
|
||||
def get_chassis(self):
|
||||
return self.dmi.get("Chassis")[0].get("Type", '_virtual')
|
||||
|
||||
def get_hid(self, snapshot):
|
||||
try:
|
||||
self.inxi = self.json["data"]["inxi"]
|
||||
if isinstance(self.inxi, str):
|
||||
self.inxi = json.loads(self.inxi)
|
||||
except Exception:
|
||||
logger.error("No inxi in snapshot %s", self.uuid)
|
||||
return ""
|
||||
dmidecode_raw = snapshot["data"]["dmidecode"]
|
||||
self.dmi = DMIParse(dmidecode_raw)
|
||||
|
||||
machine = get_inxi_key(self.inxi, 'Machine')
|
||||
for m in machine:
|
||||
system = get_inxi(m, "System")
|
||||
if system:
|
||||
manufacturer = system
|
||||
model = get_inxi(m, "product")
|
||||
serial_number = get_inxi(m, "serial")
|
||||
chassis = get_inxi(m, "Type")
|
||||
else:
|
||||
sku = get_inxi(m, "part-nu")
|
||||
manufacturer = self.dmi.manufacturer().strip()
|
||||
model = self.dmi.model().strip()
|
||||
chassis = self.get_chassis_dh()
|
||||
serial_number = self.dmi.serial_number()
|
||||
sku = self.get_sku()
|
||||
|
||||
mac = get_mac(self.inxi) or ""
|
||||
if not snapshot["data"].get('lshw'):
|
||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}"
|
||||
|
||||
lshw = snapshot["data"]["lshw"]
|
||||
# mac = get_mac2(hwinfo_raw) or ""
|
||||
mac = get_mac(lshw) or ""
|
||||
if not mac:
|
||||
txt = "Could not retrieve MAC address in snapshot %s"
|
||||
logger.warning(txt, snapshot['uuid'])
|
||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}"
|
||||
|
||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}"
|
||||
|
||||
def get_signature(self, doc):
|
||||
return hashlib.sha3_256(json.dumps(doc).encode()).hexdigest()
|
||||
|
||||
def register_device_dlt(self):
|
||||
chid = self.algorithms.get('legacy_dpp')
|
||||
phid = self.get_signature(self.get_phid())
|
||||
register_device_dlt(chid, phid, self.uuid, self.user)
|
||||
register_passport_dlt(chid, phid, self.uuid, self.user)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import re
|
||||
import json
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
from datetime import datetime
|
||||
from dmidecode import DMIParse
|
||||
from json_repair import repair_json
|
||||
|
||||
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
|
||||
|
||||
|
@ -12,345 +12,322 @@ from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
|
|||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
def get_inxi_key(inxi, component):
|
||||
for n in inxi:
|
||||
for k, v in n.items():
|
||||
if component in k:
|
||||
return v
|
||||
|
||||
|
||||
def get_inxi(n, name):
|
||||
for k, v in n.items():
|
||||
if f"#{name}" in k:
|
||||
return v
|
||||
|
||||
return ""
|
||||
def get_lshw_child(child, nets, component):
|
||||
if child.get('id') == component:
|
||||
nets.append(child)
|
||||
if child.get('children'):
|
||||
[get_lshw_child(x, nets, component) for x in child['children']]
|
||||
|
||||
|
||||
class ParseSnapshot:
|
||||
def __init__(self, snapshot, default="n/a"):
|
||||
self.default = default
|
||||
self.dmidecode_raw = snapshot.get("data", {}).get("dmidecode", "{}")
|
||||
self.smart_raw = snapshot.get("data", {}).get("smartctl", [])
|
||||
self.inxi_raw = snapshot.get("data", {}).get("inxi", "") or ""
|
||||
for ev in snapshot.get("evidence", []):
|
||||
if "dmidecode" == ev.get("operation"):
|
||||
self.dmidecode_raw = ev["output"]
|
||||
if "inxi" == ev.get("operation"):
|
||||
self.inxi_raw = ev["output"]
|
||||
if "smartctl" == ev.get("operation"):
|
||||
self.smart_raw = ev["output"]
|
||||
data = snapshot
|
||||
if snapshot.get("credentialSubject"):
|
||||
data = snapshot["credentialSubject"]
|
||||
|
||||
self.dmidecode_raw = snapshot["data"].get("dmidecode", "{}")
|
||||
self.smart_raw = snapshot["data"].get("disks", [])
|
||||
self.hwinfo_raw = snapshot["data"].get("hwinfo", "")
|
||||
self.lshw_raw = snapshot["data"].get("lshw", {}) or {}
|
||||
self.lscpi_raw = snapshot["data"].get("lspci", "")
|
||||
self.device = {"actions": []}
|
||||
self.components = []
|
||||
self.monitors = []
|
||||
|
||||
self.dmi = DMIParse(self.dmidecode_raw)
|
||||
self.smart = self.loads(self.smart_raw)
|
||||
self.inxi = self.loads(self.inxi_raw)
|
||||
self.lshw = self.loads(self.lshw_raw)
|
||||
self.hwinfo = self.parse_hwinfo()
|
||||
|
||||
self.set_computer()
|
||||
self.get_hwinfo_monitors()
|
||||
self.set_components()
|
||||
self.snapshot_json = {
|
||||
"type": "Snapshot",
|
||||
"device": self.device,
|
||||
"software": data["software"],
|
||||
"software": snapshot["software"],
|
||||
"components": self.components,
|
||||
"uuid": data['uuid'],
|
||||
"endTime": data["timestamp"],
|
||||
"uuid": snapshot['uuid'],
|
||||
"version": snapshot['version'],
|
||||
"endTime": snapshot["timestamp"],
|
||||
"elapsed": 1,
|
||||
}
|
||||
|
||||
def set_computer(self):
|
||||
machine = get_inxi_key(self.inxi, 'Machine') or []
|
||||
for m in machine:
|
||||
system = get_inxi(m, "System")
|
||||
if system:
|
||||
self.device['manufacturer'] = system
|
||||
self.device['model'] = get_inxi(m, "product")
|
||||
self.device['serialNumber'] = get_inxi(m, "serial")
|
||||
self.device['type'] = get_inxi(m, "Type")
|
||||
self.device['chassis'] = self.device['type']
|
||||
self.device['version'] = get_inxi(m, "v")
|
||||
else:
|
||||
self.device['system_uuid'] = get_inxi(m, "uuid")
|
||||
self.device['sku'] = get_inxi(m, "part-nu")
|
||||
self.device['manufacturer'] = self.dmi.manufacturer().strip()
|
||||
self.device['model'] = self.dmi.model().strip()
|
||||
self.device['serialNumber'] = self.dmi.serial_number()
|
||||
self.device['type'] = self.get_type()
|
||||
self.device['sku'] = self.get_sku()
|
||||
self.device['version'] = self.get_version()
|
||||
self.device['system_uuid'] = self.get_uuid()
|
||||
self.device['family'] = self.get_family()
|
||||
self.device['chassis'] = self.get_chassis_dh()
|
||||
|
||||
def set_components(self):
|
||||
self.get_mother_board()
|
||||
self.get_cpu()
|
||||
self.get_ram()
|
||||
self.get_mother_board()
|
||||
self.get_graphic()
|
||||
self.get_display()
|
||||
self.get_networks()
|
||||
self.get_sound_card()
|
||||
self.get_data_storage()
|
||||
self.get_battery()
|
||||
|
||||
def get_mother_board(self):
|
||||
machine = get_inxi_key(self.inxi, 'Machine') or []
|
||||
mb = {"type": "Motherboard",}
|
||||
for m in machine:
|
||||
bios_date = get_inxi(m, "date")
|
||||
if not bios_date:
|
||||
continue
|
||||
mb["manufacturer"] = get_inxi(m, "Mobo")
|
||||
mb["model"] = get_inxi(m, "model")
|
||||
mb["serialNumber"] = get_inxi(m, "serial")
|
||||
mb["version"] = get_inxi(m, "v")
|
||||
mb["biosDate"] = bios_date
|
||||
mb["biosVersion"] = self.get_bios_version()
|
||||
mb["firewire"]: self.get_firmware_num()
|
||||
mb["pcmcia"]: self.get_pcmcia_num()
|
||||
mb["serial"]: self.get_serial_num()
|
||||
mb["usb"]: self.get_usb_num()
|
||||
|
||||
self.get_ram_slots(mb)
|
||||
|
||||
self.components.append(mb)
|
||||
|
||||
def get_ram_slots(self, mb):
|
||||
memory = get_inxi_key(self.inxi, 'Memory') or []
|
||||
for m in memory:
|
||||
slots = get_inxi(m, "slots")
|
||||
if not slots:
|
||||
continue
|
||||
mb["slots"] = slots
|
||||
mb["ramSlots"] = get_inxi(m, "modules")
|
||||
mb["ramMaxSize"] = get_inxi(m, "capacity")
|
||||
|
||||
self.get_display()
|
||||
self.get_sound_card()
|
||||
self.get_networks()
|
||||
|
||||
def get_cpu(self):
|
||||
cpu = get_inxi_key(self.inxi, 'CPU') or []
|
||||
cp = {"type": "Processor"}
|
||||
vulnerabilities = []
|
||||
for c in cpu:
|
||||
base = get_inxi(c, "model")
|
||||
if base:
|
||||
cp["model"] = get_inxi(c, "model")
|
||||
cp["arch"] = get_inxi(c, "arch")
|
||||
cp["bits"] = get_inxi(c, "bits")
|
||||
cp["gen"] = get_inxi(c, "gen")
|
||||
cp["family"] = get_inxi(c, "family")
|
||||
cp["date"] = get_inxi(c, "built")
|
||||
continue
|
||||
des = get_inxi(c, "L1")
|
||||
if des:
|
||||
cp["L1"] = des
|
||||
cp["L2"] = get_inxi(c, "L2")
|
||||
cp["L3"] = get_inxi(c, "L3")
|
||||
cp["cpus"] = get_inxi(c, "cpus")
|
||||
cp["cores"] = get_inxi(c, "cores")
|
||||
cp["threads"] = get_inxi(c, "threads")
|
||||
continue
|
||||
bogo = get_inxi(c, "bogomips")
|
||||
if bogo:
|
||||
cp["bogomips"] = bogo
|
||||
cp["base/boost"] = get_inxi(c, "base/boost")
|
||||
cp["min/max"] = get_inxi(c, "min/max")
|
||||
cp["ext-clock"] = get_inxi(c, "ext-clock")
|
||||
cp["volts"] = get_inxi(c, "volts")
|
||||
continue
|
||||
ctype = get_inxi(c, "Type")
|
||||
if ctype:
|
||||
v = {"Type": ctype}
|
||||
status = get_inxi(c, "status")
|
||||
if status:
|
||||
v["status"] = status
|
||||
mitigation = get_inxi(c, "mitigation")
|
||||
if mitigation:
|
||||
v["mitigation"] = mitigation
|
||||
vulnerabilities.append(v)
|
||||
|
||||
self.components.append(cp)
|
||||
|
||||
|
||||
def get_ram(self):
|
||||
memory = get_inxi_key(self.inxi, 'Memory') or []
|
||||
mem = {"type": "RamModule"}
|
||||
|
||||
for m in memory:
|
||||
base = get_inxi(m, "System RAM")
|
||||
if base:
|
||||
mem["size"] = get_inxi(m, "total")
|
||||
slot = get_inxi(m, "manufacturer")
|
||||
if slot:
|
||||
mem["manufacturer"] = slot
|
||||
mem["model"] = get_inxi(m, "part-no")
|
||||
mem["serialNumber"] = get_inxi(m, "serial")
|
||||
mem["speed"] = get_inxi(m, "speed")
|
||||
mem["bits"] = get_inxi(m, "data")
|
||||
mem["interface"] = get_inxi(m, "type")
|
||||
module = get_inxi(m, "modules")
|
||||
if module:
|
||||
mem["modules"] = module
|
||||
|
||||
self.components.append(mem)
|
||||
|
||||
def get_graphic(self):
|
||||
graphics = get_inxi_key(self.inxi, 'Graphics') or []
|
||||
|
||||
for c in graphics:
|
||||
if not get_inxi(c, "Device") or not get_inxi(c, "vendor"):
|
||||
continue
|
||||
|
||||
for cpu in self.dmi.get('Processor'):
|
||||
serial = cpu.get('Serial Number')
|
||||
if serial == 'Not Specified' or not serial:
|
||||
serial = cpu.get('ID').replace(' ', '')
|
||||
self.components.append(
|
||||
{
|
||||
"type": "GraphicCard",
|
||||
"memory": self.get_memory_video(c),
|
||||
"manufacturer": get_inxi(c, "vendor"),
|
||||
"model": get_inxi(c, "Device"),
|
||||
"arch": get_inxi(c, "arch"),
|
||||
"serialNumber": get_inxi(c, "serial"),
|
||||
"integrated": True if get_inxi(c, "port") else False
|
||||
"actions": [],
|
||||
"type": "Processor",
|
||||
"speed": self.get_cpu_speed(cpu),
|
||||
"cores": int(cpu.get('Core Count', 1)),
|
||||
"model": cpu.get('Version'),
|
||||
"threads": int(cpu.get('Thread Count', 1)),
|
||||
"manufacturer": cpu.get('Manufacturer'),
|
||||
"serialNumber": serial,
|
||||
"brand": cpu.get('Family'),
|
||||
"address": self.get_cpu_address(cpu),
|
||||
"bogomips": self.get_bogomips(),
|
||||
}
|
||||
)
|
||||
|
||||
def get_battery(self):
|
||||
bats = get_inxi_key(self.inxi, 'Battery') or []
|
||||
for b in bats:
|
||||
def get_ram(self):
|
||||
for ram in self.dmi.get("Memory Device"):
|
||||
if ram.get('size') == 'No Module Installed':
|
||||
continue
|
||||
if not ram.get("Speed"):
|
||||
continue
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"type": "Battery",
|
||||
"model": get_inxi(b, "model"),
|
||||
"serialNumber": get_inxi(b, "serial"),
|
||||
"condition": get_inxi(b, "condition"),
|
||||
"cycles": get_inxi(b, "cycles"),
|
||||
"volts": get_inxi(b, "volts")
|
||||
"actions": [],
|
||||
"type": "RamModule",
|
||||
"size": self.get_ram_size(ram),
|
||||
"speed": self.get_ram_speed(ram),
|
||||
"manufacturer": ram.get("Manufacturer", self.default),
|
||||
"serialNumber": ram.get("Serial Number", self.default),
|
||||
"interface": ram.get("Type", "DDR"),
|
||||
"format": ram.get("Form Factor", "DIMM"),
|
||||
"model": ram.get("Part Number", self.default),
|
||||
}
|
||||
)
|
||||
|
||||
def get_mother_board(self):
|
||||
for moder_board in self.dmi.get("Baseboard"):
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "Motherboard",
|
||||
"version": moder_board.get("Version"),
|
||||
"serialNumber": moder_board.get("Serial Number", "").strip(),
|
||||
"manufacturer": moder_board.get("Manufacturer", "").strip(),
|
||||
"biosDate": self.get_bios_date(),
|
||||
"ramMaxSize": self.get_max_ram_size(),
|
||||
"ramSlots": len(self.dmi.get("Memory Device")),
|
||||
"slots": self.get_ram_slots(),
|
||||
"model": moder_board.get("Product Name", "").strip(),
|
||||
"firewire": self.get_firmware_num(),
|
||||
"pcmcia": self.get_pcmcia_num(),
|
||||
"serial": self.get_serial_num(),
|
||||
"usb": self.get_usb_num(),
|
||||
}
|
||||
)
|
||||
|
||||
def get_graphic(self):
|
||||
displays = []
|
||||
get_lshw_child(self.lshw, displays, 'display')
|
||||
|
||||
for c in displays:
|
||||
if not c['configuration'].get('driver', None):
|
||||
continue
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "GraphicCard",
|
||||
"memory": self.get_memory_video(c),
|
||||
"manufacturer": c.get("vendor", self.default),
|
||||
"model": c.get("product", self.default),
|
||||
"serialNumber": c.get("serial", self.default),
|
||||
}
|
||||
)
|
||||
|
||||
def get_memory_video(self, c):
|
||||
memory = get_inxi_key(self.inxi, 'Memory') or []
|
||||
|
||||
for m in memory:
|
||||
igpu = get_inxi(m, "igpu")
|
||||
agpu = get_inxi(m, "agpu")
|
||||
ngpu = get_inxi(m, "ngpu")
|
||||
gpu = get_inxi(m, "gpu")
|
||||
if igpu or agpu or gpu or ngpu:
|
||||
return igpu or agpu or gpu or ngpu
|
||||
|
||||
return self.default
|
||||
# get info of lspci
|
||||
# pci_id = c['businfo'].split('@')[1]
|
||||
# lspci.get(pci_id) | grep size
|
||||
# lspci -v -s 00:02.0
|
||||
return None
|
||||
|
||||
def get_data_storage(self):
|
||||
hdds= get_inxi_key(self.inxi, 'Drives') or []
|
||||
for d in hdds:
|
||||
usb = get_inxi(d, "type")
|
||||
if usb == "USB":
|
||||
for sm in self.smart:
|
||||
if sm.get('smartctl', {}).get('exit_status') == 1:
|
||||
continue
|
||||
model = sm.get('model_name')
|
||||
manufacturer = None
|
||||
hours = sm.get("power_on_time", {}).get("hours", 0)
|
||||
if model and len(model.split(" ")) > 1:
|
||||
mm = model.split(" ")
|
||||
model = mm[-1]
|
||||
manufacturer = " ".join(mm[:-1])
|
||||
|
||||
serial = get_inxi(d, "serial")
|
||||
if serial:
|
||||
hd = {
|
||||
"type": "Storage",
|
||||
"manufacturer": get_inxi(d, "vendor"),
|
||||
"model": get_inxi(d, "model"),
|
||||
"serialNumber": get_inxi(d, "serial"),
|
||||
"size": get_inxi(d, "size"),
|
||||
"speed": get_inxi(d, "speed"),
|
||||
"interface": get_inxi(d, "tech"),
|
||||
"firmware": get_inxi(d, "fw-rev")
|
||||
self.components.append(
|
||||
{
|
||||
"actions": self.sanitize(sm),
|
||||
"type": self.get_data_storage_type(sm),
|
||||
"model": model,
|
||||
"manufacturer": manufacturer,
|
||||
"serialNumber": sm.get('serial_number'),
|
||||
"size": self.get_data_storage_size(sm),
|
||||
"variant": sm.get("firmware_version"),
|
||||
"interface": self.get_data_storage_interface(sm),
|
||||
"hours": hours,
|
||||
}
|
||||
rpm = get_inxi(d, "rpm")
|
||||
if rpm:
|
||||
hd["rpm"] = rpm
|
||||
|
||||
family = get_inxi(d, "family")
|
||||
if family:
|
||||
hd["family"] = family
|
||||
|
||||
sata = get_inxi(d, "sata")
|
||||
if sata:
|
||||
hd["sata"] = sata
|
||||
|
||||
continue
|
||||
|
||||
|
||||
cycles = get_inxi(d, "cycles")
|
||||
if cycles:
|
||||
hd['cycles'] = cycles
|
||||
hd["health"] = get_inxi(d, "health")
|
||||
hd["time of used"] = get_inxi(d, "on")
|
||||
hd["read used"] = get_inxi(d, "read-units")
|
||||
hd["written used"] = get_inxi(d, "written-units")
|
||||
|
||||
self.components.append(hd)
|
||||
continue
|
||||
|
||||
hd = {}
|
||||
)
|
||||
|
||||
def sanitize(self, action):
|
||||
return []
|
||||
|
||||
def get_bogomips(self):
|
||||
if not self.hwinfo:
|
||||
return self.default
|
||||
|
||||
bogomips = 0
|
||||
for row in self.hwinfo:
|
||||
for cel in row:
|
||||
if 'BogoMips' in cel:
|
||||
try:
|
||||
bogomips += float(cel.split(":")[-1])
|
||||
except:
|
||||
pass
|
||||
return bogomips
|
||||
|
||||
def get_networks(self):
|
||||
nets = get_inxi_key(self.inxi, "Network") or []
|
||||
networks = [(nets[i], nets[i + 1]) for i in range(0, len(nets) - 1, 2)]
|
||||
|
||||
for n, iface in networks:
|
||||
model = get_inxi(n, "Device")
|
||||
if not model:
|
||||
continue
|
||||
|
||||
interface = ''
|
||||
for k in n.keys():
|
||||
if "port" in k:
|
||||
interface = "Integrated"
|
||||
if "pcie" in k:
|
||||
interface = "PciExpress"
|
||||
if get_inxi(n, "type") == "USB":
|
||||
interface = "USB"
|
||||
|
||||
networks = []
|
||||
get_lshw_child(self.lshw, networks, 'network')
|
||||
|
||||
for c in networks:
|
||||
capacity = c.get('capacity')
|
||||
wireless = bool(c.get('configuration', {}).get('wireless', False))
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "NetworkAdapter",
|
||||
"model": model,
|
||||
"manufacturer": get_inxi(n, 'vendor'),
|
||||
"serialNumber": get_inxi(iface, 'mac'),
|
||||
"speed": get_inxi(n, "speed"),
|
||||
"interface": interface,
|
||||
"model": c.get('product'),
|
||||
"manufacturer": c.get('vendor'),
|
||||
"serialNumber": c.get('serial'),
|
||||
"speed": capacity,
|
||||
"variant": c.get('version', 1),
|
||||
"wireless": wireless or False,
|
||||
"integrated": "PCI:0000:00" in c.get("businfo", ""),
|
||||
}
|
||||
)
|
||||
|
||||
def get_sound_card(self):
|
||||
audio = get_inxi_key(self.inxi, "Audio") or []
|
||||
|
||||
for c in audio:
|
||||
model = get_inxi(c, "Device")
|
||||
if not model:
|
||||
continue
|
||||
|
||||
multimedias = []
|
||||
get_lshw_child(self.lshw, multimedias, 'multimedia')
|
||||
|
||||
for c in multimedias:
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "SoundCard",
|
||||
"model": model,
|
||||
"manufacturer": get_inxi(c, 'vendor'),
|
||||
"serialNumber": get_inxi(c, 'serial'),
|
||||
"model": c.get('product'),
|
||||
"manufacturer": c.get('vendor'),
|
||||
"serialNumber": c.get('serial'),
|
||||
}
|
||||
)
|
||||
|
||||
def get_display(self):
|
||||
graphics = get_inxi_key(self.inxi, "Graphics") or []
|
||||
for c in graphics:
|
||||
if not get_inxi(c, "Monitor"):
|
||||
continue
|
||||
def get_display(self): # noqa: C901
|
||||
TECHS = 'CRT', 'TFT', 'LED', 'PDP', 'LCD', 'OLED', 'AMOLED'
|
||||
|
||||
for c in self.monitors:
|
||||
resolution_width, resolution_height = (None,) * 2
|
||||
refresh, serial, model, manufacturer, size = (None,) * 5
|
||||
year, week, production_date = (None,) * 3
|
||||
|
||||
for x in c:
|
||||
if "Vendor: " in x:
|
||||
manufacturer = x.split('Vendor: ')[-1].strip()
|
||||
if "Model: " in x:
|
||||
model = x.split('Model: ')[-1].strip()
|
||||
if "Serial ID: " in x:
|
||||
serial = x.split('Serial ID: ')[-1].strip()
|
||||
if " Resolution: " in x:
|
||||
rs = x.split(' Resolution: ')[-1].strip()
|
||||
if 'x' in rs:
|
||||
resolution_width, resolution_height = [
|
||||
int(r) for r in rs.split('x')
|
||||
]
|
||||
if "Frequencies: " in x:
|
||||
try:
|
||||
refresh = int(float(x.split(',')[-1].strip()[:-3]))
|
||||
except Exception:
|
||||
pass
|
||||
if 'Year of Manufacture' in x:
|
||||
year = x.split(': ')[1]
|
||||
|
||||
if 'Week of Manufacture' in x:
|
||||
week = x.split(': ')[1]
|
||||
|
||||
if "Size: " in x:
|
||||
size = self.get_size_monitor(x)
|
||||
technology = next((t for t in TECHS if t in c[0]), None)
|
||||
|
||||
if year and week:
|
||||
d = '{} {} 0'.format(year, week)
|
||||
production_date = datetime.strptime(d, '%Y %W %w').isoformat()
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "Display",
|
||||
"model": get_inxi(c, "model"),
|
||||
"manufacturer": get_inxi(c, "vendor"),
|
||||
"serialNumber": get_inxi(c, "serial"),
|
||||
'size': get_inxi(c, "size"),
|
||||
'diagonal': get_inxi(c, "diag"),
|
||||
'resolution': get_inxi(c, "res"),
|
||||
"date": get_inxi(c, "built"),
|
||||
'ratio': get_inxi(c, "ratio"),
|
||||
"model": model,
|
||||
"manufacturer": manufacturer,
|
||||
"serialNumber": serial,
|
||||
'size': size,
|
||||
'resolutionWidth': resolution_width,
|
||||
'resolutionHeight': resolution_height,
|
||||
"productionDate": production_date,
|
||||
'technology': technology,
|
||||
'refreshRate': refresh,
|
||||
}
|
||||
)
|
||||
|
||||
def get_hwinfo_monitors(self):
|
||||
for c in self.hwinfo:
|
||||
monitor = None
|
||||
external = None
|
||||
for x in c:
|
||||
if 'Hardware Class: monitor' in x:
|
||||
monitor = c
|
||||
if 'Driver Info' in x:
|
||||
external = c
|
||||
|
||||
if monitor and not external:
|
||||
self.monitors.append(c)
|
||||
|
||||
def get_size_monitor(self, x):
|
||||
i = 1 / 25.4
|
||||
t = x.split('Size: ')[-1].strip()
|
||||
tt = t.split('mm')
|
||||
if not tt:
|
||||
return 0
|
||||
sizes = tt[0].strip()
|
||||
if 'x' not in sizes:
|
||||
return 0
|
||||
w, h = [int(x) for x in sizes.split('x')]
|
||||
return "{:.2f}".format(np.sqrt(w**2 + h**2) * i)
|
||||
|
||||
def get_cpu_address(self, cpu):
|
||||
default = 64
|
||||
for ch in self.lshw.get('children', []):
|
||||
for c in ch.get('children', []):
|
||||
if c['class'] == 'processor':
|
||||
return c.get('width', default)
|
||||
return default
|
||||
|
||||
def get_usb_num(self):
|
||||
return len(
|
||||
[
|
||||
|
@ -387,13 +364,133 @@ class ParseSnapshot:
|
|||
]
|
||||
)
|
||||
|
||||
def get_bios_version(self):
|
||||
return self.dmi.get("BIOS")[0].get("BIOS Revision", '1')
|
||||
def get_bios_date(self):
|
||||
return self.dmi.get("BIOS")[0].get("Release Date", self.default)
|
||||
|
||||
def get_firmware(self):
|
||||
return self.dmi.get("BIOS")[0].get("Firmware Revision", '1')
|
||||
|
||||
def get_max_ram_size(self):
|
||||
size = 0
|
||||
for slot in self.dmi.get("Physical Memory Array"):
|
||||
capacity = slot.get("Maximum Capacity", '0').split(" ")[0]
|
||||
size += int(capacity)
|
||||
|
||||
return size
|
||||
|
||||
def get_ram_slots(self):
|
||||
slots = 0
|
||||
for x in self.dmi.get("Physical Memory Array"):
|
||||
slots += int(x.get("Number Of Devices", 0))
|
||||
return slots
|
||||
|
||||
def get_ram_size(self, ram):
|
||||
memory = ram.get("Size", "0")
|
||||
return memory
|
||||
|
||||
def get_ram_speed(self, ram):
|
||||
size = ram.get("Speed", "0")
|
||||
return size
|
||||
|
||||
def get_cpu_speed(self, cpu):
|
||||
speed = cpu.get('Max Speed', "0")
|
||||
return speed
|
||||
|
||||
def get_sku(self):
|
||||
return self.dmi.get("System")[0].get("SKU Number", self.default).strip()
|
||||
|
||||
def get_version(self):
|
||||
return self.dmi.get("System")[0].get("Version", self.default).strip()
|
||||
|
||||
def get_uuid(self):
|
||||
return self.dmi.get("System")[0].get("UUID", '').strip()
|
||||
|
||||
def get_family(self):
|
||||
return self.dmi.get("System")[0].get("Family", '')
|
||||
|
||||
def get_chassis(self):
|
||||
return self.dmi.get("Chassis")[0].get("Type", '_virtual')
|
||||
|
||||
def get_type(self):
|
||||
chassis_type = self.get_chassis()
|
||||
return self.translation_to_devicehub(chassis_type)
|
||||
|
||||
def translation_to_devicehub(self, original_type):
|
||||
lower_type = original_type.lower()
|
||||
CHASSIS_TYPE = {
|
||||
'Desktop': [
|
||||
'desktop',
|
||||
'low-profile',
|
||||
'tower',
|
||||
'docking',
|
||||
'all-in-one',
|
||||
'pizzabox',
|
||||
'mini-tower',
|
||||
'space-saving',
|
||||
'lunchbox',
|
||||
'mini',
|
||||
'stick',
|
||||
],
|
||||
'Laptop': [
|
||||
'portable',
|
||||
'laptop',
|
||||
'convertible',
|
||||
'tablet',
|
||||
'detachable',
|
||||
'notebook',
|
||||
'handheld',
|
||||
'sub-notebook',
|
||||
],
|
||||
'Server': ['server'],
|
||||
'Computer': ['_virtual'],
|
||||
}
|
||||
for k, v in CHASSIS_TYPE.items():
|
||||
if lower_type in v:
|
||||
return k
|
||||
return self.default
|
||||
|
||||
def get_chassis_dh(self):
|
||||
chassis = self.get_chassis()
|
||||
lower_type = chassis.lower()
|
||||
for k, v in CHASSIS_DH.items():
|
||||
if lower_type in v:
|
||||
return k
|
||||
return self.default
|
||||
|
||||
def get_data_storage_type(self, x):
|
||||
# TODO @cayop add more SSDS types
|
||||
SSDS = ["nvme"]
|
||||
SSD = 'SolidStateDrive'
|
||||
HDD = 'HardDrive'
|
||||
type_dev = x.get('device', {}).get('type')
|
||||
trim = x.get('trim', {}).get("supported") in [True, "true"]
|
||||
return SSD if type_dev in SSDS or trim else HDD
|
||||
|
||||
def get_data_storage_interface(self, x):
|
||||
interface = x.get('device', {}).get('protocol', 'ATA')
|
||||
if interface.upper() in DATASTORAGEINTERFACE:
|
||||
return interface.upper()
|
||||
|
||||
txt = "Sid: {}, interface {} is not in DataStorageInterface Enum".format(
|
||||
self.sid, interface
|
||||
)
|
||||
self.errors("{}".format(err))
|
||||
|
||||
def get_data_storage_size(self, x):
|
||||
return x.get('user_capacity', {}).get('bytes')
|
||||
|
||||
def parse_hwinfo(self):
|
||||
hw_blocks = self.hwinfo_raw.split("\n\n")
|
||||
return [x.split("\n") for x in hw_blocks]
|
||||
|
||||
def loads(self, x):
|
||||
if isinstance(x, str):
|
||||
try:
|
||||
return json.loads(x)
|
||||
try:
|
||||
hw = json.loads(x)
|
||||
except json.decoder.JSONDecodeError:
|
||||
hw = json.loads(repair_json(x))
|
||||
return hw
|
||||
except Exception as ss:
|
||||
logger.warning("%s", ss)
|
||||
return {}
|
||||
|
@ -405,3 +502,4 @@ class ParseSnapshot:
|
|||
|
||||
logger.error(txt)
|
||||
self._errors.append("%s", txt)
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for snap in object.annotations %}
|
||||
{% for snap in object.properties %}
|
||||
<tbody>
|
||||
{% if snap.type == 0 %}
|
||||
<tr>
|
||||
|
@ -94,7 +94,7 @@
|
|||
</div>
|
||||
{% if form.tag.value %}
|
||||
<div class="col-1">
|
||||
<a class="btn btn-yellow" href="{% url 'evidence:delete_annotation' form.pk %}">{% translate "Delete" %}</a>
|
||||
<a class="btn btn-yellow" href="{% url 'evidence:delete_tag' form.pk %}">{% translate "Delete" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -20,5 +20,5 @@ urlpatterns = [
|
|||
path("<uuid:pk>", views.EvidenceView.as_view(), name="details"),
|
||||
path("<uuid:pk>/eraseserver", views.EraseServerView.as_view(), name="erase_server"),
|
||||
path("<uuid:pk>/download", views.DownloadEvidenceView.as_view(), name="download"),
|
||||
path('annotation/<int:pk>/del', views.AnnotationDeleteView.as_view(), name='delete_annotation'),
|
||||
path("tag/<str:pk>/delete", views.DeleteEvidenceTagView.as_view(), name="delete_tag"),
|
||||
]
|
||||
|
|
|
@ -12,8 +12,9 @@ from django.views.generic.edit import (
|
|||
FormView,
|
||||
)
|
||||
|
||||
from action.models import DeviceLog
|
||||
from dashboard.mixins import DashboardView, Http403
|
||||
from evidence.models import Evidence, Annotation
|
||||
from evidence.models import SystemProperty, UserProperty, Evidence
|
||||
from evidence.forms import (
|
||||
UploadForm,
|
||||
UserTagForm,
|
||||
|
@ -95,7 +96,7 @@ class EvidenceView(DashboardView, FormView):
|
|||
if self.object.owner != self.request.user.institution:
|
||||
raise Http403
|
||||
|
||||
self.object.get_annotations()
|
||||
self.object.get_properties()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -137,37 +138,10 @@ class DownloadEvidenceView(DashboardView, TemplateView):
|
|||
evidence.get_doc()
|
||||
data = json.dumps(evidence.doc)
|
||||
response = HttpResponse(data, content_type="application/json")
|
||||
response['Content-Disposition'] = 'attachment; filename={}'.format("evidence.json")
|
||||
response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json")
|
||||
return response
|
||||
|
||||
|
||||
class AnnotationDeleteView(DashboardView, DeleteView):
|
||||
model = Annotation
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.pk = kwargs['pk']
|
||||
|
||||
try:
|
||||
referer = self.request.META["HTTP_REFERER"]
|
||||
path_referer = urlparse(referer).path
|
||||
resolver_match = resolve(path_referer)
|
||||
url_name = resolver_match.view_name
|
||||
kwargs_view = resolver_match.kwargs
|
||||
except:
|
||||
# if is not possible resolve the reference path return 404
|
||||
raise Http404
|
||||
|
||||
self.object = get_object_or_404(
|
||||
self.model,
|
||||
pk=self.pk,
|
||||
owner=self.request.user.institution
|
||||
)
|
||||
self.object.delete()
|
||||
|
||||
|
||||
return redirect(url_name, **kwargs_view)
|
||||
|
||||
|
||||
class EraseServerView(DashboardView, FormView):
|
||||
template_name = "ev_eraseserver.html"
|
||||
section = "evidences"
|
||||
|
@ -182,7 +156,7 @@ class EraseServerView(DashboardView, FormView):
|
|||
if self.object.owner != self.request.user.institution:
|
||||
raise Http403
|
||||
|
||||
self.object.get_annotations()
|
||||
self.object.get_properties()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -211,3 +185,35 @@ class EraseServerView(DashboardView, FormView):
|
|||
def get_success_url(self):
|
||||
success_url = reverse_lazy('evidence:details', args=[self.pk])
|
||||
return success_url
|
||||
|
||||
|
||||
class DeleteEvidenceTagView(DashboardView, DeleteView):
|
||||
model = SystemProperty
|
||||
|
||||
def get_queryset(self):
|
||||
# only those with 'CUSTOM_ID'
|
||||
return SystemProperty.objects.filter(owner=self.request.user.institution, key='CUSTOM_ID')
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
message = _("<Deleted> Evidence Tag: {}").format(self.object.value)
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=self.object.uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution
|
||||
)
|
||||
self.object.delete()
|
||||
|
||||
messages.info(self.request, _("Evicende Tag deleted successfully."))
|
||||
return self.handle_success()
|
||||
|
||||
def handle_success(self):
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.META.get(
|
||||
'HTTP_REFERER',
|
||||
reverse_lazy('evidence:details', args=[self.object.uuid])
|
||||
)
|
||||
|
|
|
@ -22,14 +22,10 @@ def search(institution, qs, offset=0, limit=10):
|
|||
qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
|
||||
qp.add_prefix("uuid", "uuid")
|
||||
query = qp.parse_query(qs)
|
||||
if institution:
|
||||
institution_term = "U{}".format(institution.id)
|
||||
final_query = xapian.Query(
|
||||
xapian.Query.OP_AND, query, xapian.Query(institution_term)
|
||||
)
|
||||
else:
|
||||
final_query = xapian.Query(query)
|
||||
|
||||
institution_term = "U{}".format(institution.id)
|
||||
final_query = xapian.Query(
|
||||
xapian.Query.OP_AND, query, xapian.Query(institution_term)
|
||||
)
|
||||
enquire = xapian.Enquire(database)
|
||||
enquire.set_query(final_query)
|
||||
matches = enquire.get_mset(offset, limit)
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
{"closed": true, "components": [{"actions": [], "manufacturer": "Intel Corporation", "model": "82579LM Gigabit Network Connection", "serialNumber": "00:11:11:11:11:00", "speed": 1000.0, "type": "NetworkAdapter", "variant": "04", "wireless": false}, {"actions": [], "manufacturer": "Intel Corporation", "model": "7 Series/C216 Chipset Family High Definition Audio Controller", "serialNumber": null, "type": "SoundCard"}, {"actions": [], "format": "DIMM", "interface": "DDR3", "manufacturer": "Micron", "model": "16KTF51264AZ", "serialNumber": "AAAAAAAA", "size": 4096.0, "speed": 1600.0, "type": "RamModule"}, {"actions": [{"endTime": "2022-10-11T13:45:31.239555+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.623967+00:00", "steps": [{"endTime": "2021-10-11T11:05:28.090897+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.624163+00:00", "type": "StepZero"}, {"endTime": "2021-10-11T13:45:31.239402+00:00", "severity": "Info", "startTime": "2021-10-11T11:05:28.091255+00:00", "type": "StepRandom"}], "type": "EraseSectors"}, {"assessment": true, "commandTimeout": 30, "currentPendingSectorCount": 0, "elapsed": 60, "length": "Short", "lifetime": 18720, "offlineUncorrectable": 0, "powerCycleCount": 2147, "reallocatedSectorCount": 0, "reportedUncorrectableErrors": 0, "severity": "Info", "status": "Completed without error", "type": "TestDataStorage"}, {"elapsed": 11, "readSpeed": 119.0, "type": "BenchmarkDataStorage", "writeSpeed": 32.7}], "interface": "ATA", "manufacturer": "Seagate", "model": "ST3500418AS", "serialNumber": "AAAAAAAA", "size": 500000.0, "type": "HardDrive", "variant": "CC46"}, {"actions": [{"elapsed": 0, "rate": 25540.36, "type": "BenchmarkProcessor"}, {"elapsed": 8, "rate": 7.6939, "type": "BenchmarkProcessorSysbench"}], "address": 64, "brand": "Core i5", "cores": 4, "generation": 3, "manufacturer": "Intel Corp.", "model": "Intel Core i5-3470 CPU @ 3.20GHz", "serialNumber": null, "speed": 1.6242180000000002, "threads": 4, "type": "Processor"}, {"actions": [], "manufacturer": "Intel Corporation", "memory": null, "model": "Xeon E3-1200 v2/3rd Gen Core processor Graphics Controller", "serialNumber": null, "type": "GraphicCard"}, {"actions": [], "biosDate": "2012-08-07T00:00:00", "firewire": 0, "manufacturer": "LENOVO", "model": "MAHOBAY", "pcmcia": 0, "ramMaxSize": 32, "ramSlots": 4, "serial": 1, "serialNumber": null, "slots": 4, "type": "Motherboard", "usb": 3, "version": "9SKT39AUS"}], "device": {"actions": [{"elapsed": 1, "rate": 0.6507, "type": "BenchmarkRamSysbench"}], "chassis": "Tower", "manufacturer": "LENOVO", "model": "3227A2G", "serialNumber": "AAAAAAAA", "sku": "LENOVO_MT_3227", "type": "Desktop", "version": "ThinkCentre M92P"}, "elapsed": 187302510, "endTime": "2016-11-03T17:17:01.116554+00:00", "software": "Workbench", "type": "Snapshot", "uuid": "ae913de1-e639-476a-ad9b-78eabbe4628b", "version": "11.0b11"}
|
|
@ -1 +0,0 @@
|
|||
{"closed": true, "components": [{"actions": [], "manufacturer": "Intel Corporation", "model": "82579LM Gigabit Network Connection", "serialNumber": "00:11:11:11:11:00", "speed": 1000.0, "type": "NetworkAdapter", "variant": "04", "wireless": false}, {"actions": [], "manufacturer": "Intel Corporation", "model": "7 Series/C216 Chipset Family High Definition Audio Controller", "serialNumber": null, "type": "SoundCard"}, {"actions": [], "format": "DIMM", "interface": "DDR3", "manufacturer": "Micron", "model": "16KTF51264AZ", "serialNumber": "AAAAAAAA", "size": 4096.0, "speed": 1600.0, "type": "RamModule"}, {"actions": [{"endTime": "2022-10-11T13:45:31.239555+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.623967+00:00", "steps": [{"endTime": "2021-10-11T11:05:28.090897+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.624163+00:00", "type": "StepZero"}, {"endTime": "2021-10-11T13:45:31.239402+00:00", "severity": "Info", "startTime": "2021-10-11T11:05:28.091255+00:00", "type": "StepRandom"}], "type": "EraseSectors"}, {"assessment": true, "commandTimeout": 30, "currentPendingSectorCount": 0, "elapsed": 60, "length": "Short", "lifetime": 18720, "offlineUncorrectable": 0, "powerCycleCount": 2147, "reallocatedSectorCount": 0, "reportedUncorrectableErrors": 0, "severity": "Info", "status": "Completed without error", "type": "TestDataStorage"}, {"elapsed": 11, "readSpeed": 119.0, "type": "BenchmarkDataStorage", "writeSpeed": 32.7}], "interface": "ATA", "manufacturer": "Seagate", "model": "ST3500418AS", "serialNumber": "AAAAAAAA", "size": 500000.0, "type": "HardDrive", "variant": "CC46"}, {"actions": [{"elapsed": 0, "rate": 25540.36, "type": "BenchmarkProcessor"}, {"elapsed": 8, "rate": 7.6939, "type": "BenchmarkProcessorSysbench"}], "address": 64, "brand": "Core i5", "cores": 4, "generation": 3, "manufacturer": "Intel Corp.", "model": "Intel Core i5-3470 CPU @ 3.20GHz", "serialNumber": null, "speed": 1.6242180000000002, "threads": 4, "type": "Processor"}, {"actions": [], "manufacturer": "Intel Corporation", "memory": null, "model": "Xeon E3-1200 v2/3rd Gen Core processor Graphics Controller", "serialNumber": null, "type": "GraphicCard"}, {"actions": [], "biosDate": "2012-08-07T00:00:00", "firewire": 0, "manufacturer": "LENOVO", "model": "MAHOBAY", "pcmcia": 0, "ramMaxSize": 32, "ramSlots": 4, "serial": 1, "serialNumber": null, "slots": 4, "type": "Motherboard", "usb": 3, "version": "9SKT39AUS"}], "device": {"actions": [{"elapsed": 1, "rate": 0.6507, "type": "BenchmarkRamSysbench"}], "chassis": "Tower", "manufacturer": "LENOVO", "model": "3227A2G", "serialNumber": "AAAAAAAAD", "sku": "LENOVO_MT_3227", "type": "Desktop", "version": "ThinkCentre M92P"}, "elapsed": 187302510, "endTime": "2016-11-03T17:17:01.116554+00:00", "software": "Workbench", "type": "Snapshot", "uuid": "ae913de1-e639-476a-ad9b-78eabbe4625b", "version": "11.0b11"}
|
|
@ -0,0 +1,77 @@
|
|||
# Generated by Django 5.0.6 on 2024-12-10 19:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("lot", "0002_alter_lot_closed"),
|
||||
("user", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LotProperty",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("key", models.CharField(max_length=256)),
|
||||
("value", models.CharField(max_length=256)),
|
||||
(
|
||||
"type",
|
||||
models.SmallIntegerField(
|
||||
choices=[
|
||||
(0, "System"),
|
||||
(1, "User"),
|
||||
(2, "Document"),
|
||||
(3, "EraseServer"),
|
||||
],
|
||||
default=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"lot",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="lot.lot"
|
||||
),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="user.institution",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="LotAnnotation",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lotproperty",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("key", "lot", "type"), name="lot_unique_type_key_lot"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 5.0.6 on 2024-12-18 12:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("lot", "0003_lotproperty_delete_lotannotation_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="lotproperty",
|
||||
name="lot_unique_type_key_lot",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="lotproperty",
|
||||
name="type",
|
||||
field=models.SmallIntegerField(
|
||||
choices=[(0, "System"), (1, "User"), (2, "Document")], default=1
|
||||
),
|
||||
),
|
||||
]
|
|
@ -6,9 +6,9 @@ from utils.constants import (
|
|||
STR_EXTEND_SIZE,
|
||||
)
|
||||
|
||||
from user.models import User, Institution
|
||||
from user.models import User, Institution
|
||||
from evidence.models import Property
|
||||
# from device.models import Device
|
||||
# from evidence.models import Annotation
|
||||
|
||||
|
||||
class LotTag(models.Model):
|
||||
|
@ -45,17 +45,12 @@ class Lot(models.Model):
|
|||
for d in DeviceLot.objects.filter(lot=self, device_id=v):
|
||||
d.delete()
|
||||
|
||||
class LotProperty (Property):
|
||||
lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
|
||||
|
||||
class LotAnnotation(models.Model):
|
||||
class Type(models.IntegerChoices):
|
||||
SYSTEM= 0, "System"
|
||||
SYSTEM = 0, "System"
|
||||
USER = 1, "User"
|
||||
DOCUMENT = 2, "Document"
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
|
||||
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
type = models.SmallIntegerField(choices=Type)
|
||||
key = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||
value = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||
type = models.SmallIntegerField(choices=Type.choices, default=Type.USER)
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Lot {{ lot.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="tab-pane fade show active" id="details">
|
||||
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
|
||||
<a href="{% url 'lot:add_annotation' lot.pk %}" class="btn btn-primary">
|
||||
|
||||
<i class="bi bi-plus"></i>
|
||||
Add new annotation
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title mt-2">Annotations</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Key</th>
|
||||
<th scope="col">Value</th>
|
||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Created on</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in annotations %}
|
||||
<tr>
|
||||
<td>{{ a.key }}</td>
|
||||
<td>{{ a.value }}</td>
|
||||
<td>{{ a.created }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
105
lot/templates/properties.html
Normal file
105
lot/templates/properties.html
Normal file
|
@ -0,0 +1,105 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Lot {{ lot.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="tab-pane fade show active" id="details">
|
||||
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
|
||||
<a href="{% url 'lot:add_property' lot.pk %}" class="btn btn-primary">
|
||||
|
||||
<i class="bi bi-plus"></i>
|
||||
Add new lot Property
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title mt-2">Properties</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Key</th>
|
||||
<th scope="col">Value</th>
|
||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Created on</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in properties %}
|
||||
<tr>
|
||||
<td>{{ a.key }}</td>
|
||||
<td>{{ a.value }}</td>
|
||||
<td>{{ a.created }}</td>
|
||||
<td class="text-center">
|
||||
<a href="#" class="text-info" data-bs-toggle="modal" data-bs-target="#editPropertyModal{{ a.id }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="#" class="text-danger" data-bs-toggle="modal" data-bs-target="#deletePropertyModal{{ a.id }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<div class="modal fade" id="editPropertyModal{{ a.id }}" tabindex="-1" aria-labelledby="editPropertyModalLabel{{ a.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editPropertyModalLabel{{ a.id }}">{% trans "Edit Property" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="{% url 'lot:update_property' a.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="propertyKey{{ a.id }}" class="form-label">{% trans "Key" %}</label>
|
||||
<input type="text" class="form-control" id="propertyKey{{ a.id }}" name="key" value="{{ a.key }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="propertyValue{{ a.id }}" class="form-label">{% trans "Value" %}</label>
|
||||
<input type="text" class="form-control" id="propertyValue{{ a.id }}" name="value" value="{{ a.value }}" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save changes" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="deletePropertyModal{{ a.id }}" tabindex="-1" aria-labelledby="deletePropertyModalLabel{{ a.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deletePropertyModalLabel{{ a.id }}">{% trans "Delete Property" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{% trans "Are you sure you want to delete this property?" %}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<form method="post" action="{% url 'lot:delete_property' a.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -12,6 +12,8 @@ urlpatterns = [
|
|||
path("tag/<int:pk>/", views.LotsTagsView.as_view(), name="tag"),
|
||||
path("<int:pk>/document/", views.LotDocumentsView.as_view(), name="documents"),
|
||||
path("<int:pk>/document/add", views.LotAddDocumentView.as_view(), name="add_document"),
|
||||
path("<int:pk>/annotation", views.LotAnnotationsView.as_view(), name="annotations"),
|
||||
path("<int:pk>/annotation/add", views.LotAddAnnotationView.as_view(), name="add_annotation"),
|
||||
path("<int:pk>/property", views.LotPropertiesView.as_view(), name="properties"),
|
||||
path("<int:pk>/property/add", views.AddLotPropertyView.as_view(), name="add_property"),
|
||||
path("<int:pk>/property/update", views.UpdateLotPropertyView.as_view(), name="update_property"),
|
||||
path("<int:pk>/property/delete", views.DeleteLotPropertyView.as_view(), name="delete_property"),
|
||||
]
|
||||
|
|
104
lot/views.py
104
lot/views.py
|
@ -1,5 +1,6 @@
|
|||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.edit import (
|
||||
|
@ -9,10 +10,9 @@ from django.views.generic.edit import (
|
|||
FormView,
|
||||
)
|
||||
from dashboard.mixins import DashboardView
|
||||
from lot.models import Lot, LotTag, LotAnnotation
|
||||
from lot.models import Lot, LotTag, LotProperty
|
||||
from lot.forms import LotsForm
|
||||
|
||||
|
||||
class NewLotView(DashboardView, CreateView):
|
||||
template_name = "new_lot.html"
|
||||
title = _("New lot")
|
||||
|
@ -143,18 +143,18 @@ class LotsTagsView(DashboardView, TemplateView):
|
|||
|
||||
|
||||
class LotAddDocumentView(DashboardView, CreateView):
|
||||
template_name = "new_annotation.html"
|
||||
template_name = "new_property.html"
|
||||
title = _("New Document")
|
||||
breadcrumb = "Device / New document"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
model = LotAnnotation
|
||||
model = LotProperty
|
||||
fields = ("key", "value")
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
form.instance.lot = self.lot
|
||||
form.instance.type = LotAnnotation.Type.DOCUMENT
|
||||
form.instance.type = LotProperty.Type.DOCUMENT
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
|
@ -169,16 +169,16 @@ class LotAddDocumentView(DashboardView, CreateView):
|
|||
class LotDocumentsView(DashboardView, TemplateView):
|
||||
template_name = "documents.html"
|
||||
title = _("New Document")
|
||||
breadcrumb = "Device / New document"
|
||||
breadcrumb = "Devicce / New document"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.pk = kwargs.get('pk')
|
||||
context = super().get_context_data(**kwargs)
|
||||
lot = get_object_or_404(Lot, owner=self.request.user.institution, id=self.pk)
|
||||
documents = LotAnnotation.objects.filter(
|
||||
documents = LotProperty.objects.filter(
|
||||
lot=lot,
|
||||
owner=self.request.user.institution,
|
||||
type=LotAnnotation.Type.DOCUMENT,
|
||||
type=LotProperty.Type.DOCUMENT,
|
||||
)
|
||||
context.update({
|
||||
'lot': lot,
|
||||
|
@ -189,48 +189,106 @@ class LotDocumentsView(DashboardView, TemplateView):
|
|||
return context
|
||||
|
||||
|
||||
class LotAnnotationsView(DashboardView, TemplateView):
|
||||
template_name = "annotations.html"
|
||||
title = _("New Annotation")
|
||||
breadcrumb = "Device / New annotation"
|
||||
class LotPropertiesView(DashboardView, TemplateView):
|
||||
template_name = "properties.html"
|
||||
title = _("New Lot Property")
|
||||
breadcrumb = "Lot / New property"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.pk = kwargs.get('pk')
|
||||
context = super().get_context_data(**kwargs)
|
||||
lot = get_object_or_404(Lot, owner=self.request.user.institution, id=self.pk)
|
||||
annotations = LotAnnotation.objects.filter(
|
||||
properties = LotProperty.objects.filter(
|
||||
lot=lot,
|
||||
owner=self.request.user.institution,
|
||||
type=LotAnnotation.Type.USER,
|
||||
type=LotProperty.Type.USER,
|
||||
)
|
||||
context.update({
|
||||
'lot': lot,
|
||||
'annotations': annotations,
|
||||
'properties': properties,
|
||||
'title': self.title,
|
||||
'breadcrumb': self.breadcrumb
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class LotAddAnnotationView(DashboardView, CreateView):
|
||||
template_name = "new_annotation.html"
|
||||
title = _("New Annotation")
|
||||
breadcrumb = "Device / New annotation"
|
||||
class AddLotPropertyView(DashboardView, CreateView):
|
||||
template_name = "new_property.html"
|
||||
title = _("New Lot Property")
|
||||
breadcrumb = "Device / New property"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
model = LotAnnotation
|
||||
model = LotProperty
|
||||
fields = ("key", "value")
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
form.instance.lot = self.lot
|
||||
form.instance.type = LotAnnotation.Type.USER
|
||||
form.instance.type = LotProperty.Type.USER
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
self.lot = get_object_or_404(Lot, pk=pk, owner=self.request.user.institution)
|
||||
self.success_url = reverse_lazy('lot:annotations', args=[pk])
|
||||
self.success_url = reverse_lazy('lot:properties', args=[pk])
|
||||
kwargs = super().get_form_kwargs()
|
||||
return kwargs
|
||||
|
||||
|
||||
class UpdateLotPropertyView(DashboardView, UpdateView):
|
||||
template_name = "properties.html"
|
||||
title = _("Update lot Property")
|
||||
breadcrumb = "Lot / Update Property"
|
||||
model = LotProperty
|
||||
fields = ("key", "value")
|
||||
|
||||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
lot_property = get_object_or_404(LotProperty, pk=pk, owner=self.request.user.institution)
|
||||
|
||||
if not lot_property:
|
||||
raise Http404
|
||||
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['instance'] = lot_property
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
old_key= self.object.key
|
||||
old_value = self.object.value
|
||||
new_key = form.cleaned_data['key']
|
||||
new_value = form.cleaned_data['value']
|
||||
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
form.instance.type = LotProperty.Type.USER
|
||||
response = super().form_valid(form)
|
||||
|
||||
messages.success(self.request, _("Lot property updated successfully."))
|
||||
return response
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
|
||||
|
||||
|
||||
class DeleteLotPropertyView(DashboardView, DeleteView):
|
||||
model = LotProperty
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.pk = kwargs['pk']
|
||||
referer = request.META.get('HTTP_REFERER')
|
||||
if not referer:
|
||||
raise Http404("No referer header found")
|
||||
|
||||
self.object = get_object_or_404(
|
||||
self.model,
|
||||
pk=self.pk,
|
||||
owner=self.request.user.institution
|
||||
)
|
||||
old_value = self.object.key
|
||||
self.object.delete()
|
||||
messages.success(self.request, _("Lot property deleted successfully."))
|
||||
|
||||
# Redirect back to the original URL
|
||||
return redirect(referer)
|
||||
|
|
|
@ -11,7 +11,3 @@ xlrd==2.0.1
|
|||
odfpy==1.4.1
|
||||
pytz==2024.2
|
||||
json-repair==0.30.0
|
||||
setuptools==65.5.1
|
||||
requests==2.32.3
|
||||
wheel==0.45.1
|
||||
|
||||
|
|
|
@ -17,19 +17,8 @@ HID_ALGO1 = [
|
|||
"sku"
|
||||
]
|
||||
|
||||
LEGACY_DPP = [
|
||||
"manufacturer",
|
||||
"model",
|
||||
"chassis",
|
||||
"serialNumber",
|
||||
"sku",
|
||||
"type",
|
||||
"version"
|
||||
]
|
||||
|
||||
ALGOS = {
|
||||
"hidalgo1": HID_ALGO1,
|
||||
"legacy_dpp": LEGACY_DPP
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
|||
|
||||
from django.core.exceptions import ValidationError
|
||||
from evidence.xapian import index
|
||||
from evidence.models import Annotation
|
||||
from evidence.models import SystemProperty
|
||||
from device.models import Device
|
||||
|
||||
|
||||
|
@ -68,7 +68,7 @@ def create_doc(data):
|
|||
return doc
|
||||
|
||||
|
||||
def create_annotation(doc, user, commit=False):
|
||||
def create_property(doc, user, commit=False):
|
||||
if not doc or not doc.get('uuid') or not doc.get("CUSTOMER_ID"):
|
||||
return []
|
||||
|
||||
|
@ -76,25 +76,23 @@ def create_annotation(doc, user, commit=False):
|
|||
'uuid': doc['uuid'],
|
||||
'owner': user.institution,
|
||||
'user': user,
|
||||
'type': Annotation.Type.SYSTEM,
|
||||
'key': 'CUSTOMER_ID',
|
||||
'value': doc['CUSTOMER_ID'],
|
||||
}
|
||||
if commit:
|
||||
annotation = Annotation.objects.filter(
|
||||
property = SystemProperty.objects.filter(
|
||||
uuid=doc["uuid"],
|
||||
owner=user.institution,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
)
|
||||
|
||||
if annotation:
|
||||
txt = "Warning: Snapshot %s already registered (annotation exists)"
|
||||
if property:
|
||||
txt = "Warning: Snapshot %s already registered (system property exists)"
|
||||
logger.warning(txt, doc["uuid"])
|
||||
return annotation
|
||||
return property
|
||||
|
||||
return Annotation.objects.create(**data)
|
||||
return SystemProperty.objects.create(**data)
|
||||
|
||||
return Annotation(**data)
|
||||
return SystemProperty(**data)
|
||||
|
||||
|
||||
def create_index(doc, user):
|
||||
|
|
|
@ -18,28 +18,20 @@ class CustomFormatter(logging.Formatter):
|
|||
color = PURPLE
|
||||
else:
|
||||
color = RESET
|
||||
|
||||
|
||||
record.levelname = f"{color}{record.levelname}{RESET}"
|
||||
|
||||
if record.args:
|
||||
try:
|
||||
record.msg = record.msg % record.args
|
||||
record.args = ()
|
||||
except (TypeError, ValueError):
|
||||
record.msg = f"{color}{record.msg}{RESET}"
|
||||
|
||||
# Highlight the final formatted message
|
||||
record.msg = self.highlight_message(record.msg, color)
|
||||
|
||||
# pedro says: I discovered that trace is provided anyway with
|
||||
# this commented (reason: strange None msgs)
|
||||
# is this needed?
|
||||
### provide trace when DEBUG config
|
||||
#if settings.DEBUG:
|
||||
# import traceback
|
||||
# print(traceback.format_exc())
|
||||
record.msg = self.highlight_args(record.msg, record.args, color)
|
||||
record.args = ()
|
||||
|
||||
# provide trace when DEBUG config
|
||||
if settings.DEBUG:
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
return super().format(record)
|
||||
|
||||
def highlight_message(self, message, color):
|
||||
return f"{color}{message}{RESET}"
|
||||
def highlight_args(self, message, args, color):
|
||||
highlighted_args = tuple(f"{color}{arg}{RESET}" for arg in args)
|
||||
return message % highlighted_args
|
||||
|
|
|
@ -19,10 +19,7 @@ def move_json(path_name, user, place="snapshots"):
|
|||
|
||||
|
||||
def save_in_disk(data, user, place="snapshots"):
|
||||
uuid = data.get("uuid")
|
||||
if data.get("credentialSubject"):
|
||||
uuid = data["credentialSubject"].get("uuid")
|
||||
|
||||
uuid = data.get('uuid', '')
|
||||
now = datetime.now()
|
||||
year = now.year
|
||||
month = now.month
|
||||
|
|
Loading…
Reference in a new issue