Compare commits

..

101 commits

Author SHA1 Message Date
Thomas Nahuel Rusiecki 743e8d314a added logging for evidence tag changes 2025-01-08 17:11:33 -03:00
Thomas Nahuel Rusiecki e81c072bd4 text size adjustment and displaying none states 2025-01-08 15:36:42 -03:00
Thomas Nahuel Rusiecki 67461b3edd more contrast on save note button 2025-01-08 14:36:23 -03:00
Thomas Nahuel Rusiecki 3d81000176 states and notes view refactoring 2025-01-08 14:36:06 -03:00
Thomas Nahuel Rusiecki 344baab1ce userproperties views refactoring 2025-01-08 04:31:28 -03:00
Thomas Nahuel Rusiecki ca4dd434e1 better bootstrap tables 2025-01-08 01:49:22 -03:00
Thomas Nahuel Rusiecki 3c714d818a view changes 2025-01-08 01:29:20 -03:00
Thomas Nahuel Rusiecki f3c04cdba8 better representiation of delete/edit notes 2025-01-07 21:13:28 -03:00
Thomas Nahuel Rusiecki e58a83ddc2 edit update cannot be blank 2025-01-04 02:24:33 -03:00
Thomas Nahuel Rusiecki 988c04f3be adding remove button for notes 2025-01-04 01:54:20 -03:00
Thomas Nahuel Rusiecki e4a24370e0 notes now can be updated 2025-01-03 03:24:08 -03:00
Thomas Nahuel Rusiecki 6355d48556 deleted undo state view 2025-01-03 02:33:26 -03:00
Thomas Nahuel Rusiecki 199286ad45 added a sidebar notes display 2024-12-30 15:42:29 -03:00
Thomas Nahuel Rusiecki 574a7c018f changes to state defiinitions list 2024-12-30 15:27:29 -03:00
Thomas Nahuel Rusiecki 214ff03b38 better success message and removed devicelog var 2024-12-30 15:25:57 -03:00
Thomas Nahuel Rusiecki 60b1e394e5 normalized deviceLog messages 2024-12-20 16:11:59 -03:00
Thomas Nahuel Rusiecki 5783a00318 command for adding default states 2024-12-20 15:25:38 -03:00
Thomas Nahuel Rusiecki 5201aabe2b erased old logger 2024-12-20 15:22:06 -03:00
Thomas Nahuel Rusiecki 21e30b4ac0 minor cosmetic changes 2024-12-20 14:41:17 -03:00
Thomas Nahuel Rusiecki 4ff884272c state definition list changes and disabled logging 2024-12-20 14:40:37 -03:00
Thomas Nahuel Rusiecki e42fb17877 default value for no state 2024-12-20 14:38:58 -03:00
Thomas Nahuel Rusiecki f14deca149 adding orm migrations 2024-12-19 20:28:00 -03:00
Thomas Nahuel Rusiecki 5972417422 log list now shows log table 2024-12-19 20:17:33 -03:00
Thomas Nahuel Rusiecki 7d3b448d2a added logging for states, user properties and notes 2024-12-19 20:09:29 -03:00
Thomas Nahuel Rusiecki 12711c2d5f simpler state change and action input 2024-12-19 18:17:09 -03:00
Thomas Nahuel Rusiecki 2ee6f4d515 current state table erased and spacing fix 2024-12-18 13:15:09 -03:00
Thomas Nahuel Rusiecki 2d7fd1df31 deleted unique constraint on userproperty 2024-12-18 12:59:36 -03:00
Thomas Nahuel Rusiecki 31d90a6e98 notes and log models added 2024-12-18 12:57:36 -03:00
Thomas Nahuel Rusiecki ea5d2a743f device tag bugfix 2024-12-18 10:05:29 -03:00
Thomas Nahuel Rusiecki 479974ebf4 lotproperties delete and update added 2024-12-18 09:55:00 -03:00
Thomas Nahuel Rusiecki 0191c5d1d1 added action migration 2024-12-17 13:35:17 -03:00
Thomas Nahuel Rusiecki 7cbfd3223d now check for state on delete modal 2024-12-16 17:50:31 -03:00
Thomas Nahuel Rusiecki 0217ed6d04 cosmetic changes to statesdefinitions list 2024-12-16 16:20:15 -03:00
Thomas Nahuel Rusiecki 92024a96c1 change state delete to undo last state 2024-12-16 15:21:09 -03:00
Thomas Nahuel Rusiecki a971889fb1 help icon added and new icon on add button 2024-12-14 16:41:18 -03:00
Thomas Nahuel Rusiecki f3bd562fcb minor changes to states view 2024-12-14 16:02:17 -03:00
Thomas Nahuel Rusiecki 8aa65f466c statedefinitions update and delete popup changes 2024-12-13 17:06:26 -03:00
Thomas Nahuel Rusiecki 4b0638c2b9 changes to state definitions list 2024-12-13 16:30:38 -03:00
Thomas Nahuel Rusiecki 883db4ea09 statedefinitions edit popup added and url changes 2024-12-13 16:06:42 -03:00
Thomas Nahuel Rusiecki d005d323f4 state button rework and warning if same state 2024-12-12 18:18:24 -03:00
Thomas Nahuel Rusiecki 92ce6d89b2 current state helper function added 2024-12-12 17:01:21 -03:00
Thomas Nahuel Rusiecki 7c1e0c1804 Device-details html modularized into several files 2024-12-12 16:46:39 -03:00
Thomas Nahuel Rusiecki 6e952e8643 statedefinitions list updated 2024-12-12 16:10:07 -03:00
Thomas Nahuel Rusiecki 3aae9cc87f updated logging for states 2024-12-11 17:35:55 -03:00
Thomas Nahuel Rusiecki 852bf75846 statedefinitions delete now uses correct id 2024-12-11 17:18:58 -03:00
Thomas Nahuel Rusiecki 814f28bbd7 logging for statedefinitions 2024-12-11 17:18:46 -03:00
Thomas Nahuel Rusiecki 2d84b71cb2 better delete modal 2024-12-11 15:48:00 -03:00
Thomas Nahuel Rusiecki f1f2964785 Merge branch 'rework/properties' into feature/states 2024-12-11 15:03:28 -03:00
Thomas Nahuel Rusiecki a38475136f erase_server type now userproperty 2024-12-10 16:54:58 -03:00
Thomas Nahuel Rusiecki b1460d39af deleted obsolete field type for sysproperties 2024-12-10 16:54:41 -03:00
Thomas Nahuel Rusiecki 443c37b1e2 updated views for new model structure 2024-12-10 16:53:07 -03:00
Thomas Nahuel Rusiecki fe5abd0299 more property model refactoring 2024-12-10 16:52:29 -03:00
Thomas Nahuel Rusiecki 643cc44824 property models refactor 2024-12-09 19:00:47 -03:00
Thomas Nahuel Rusiecki 26b9b2e4d9 added logging for user property creation 2024-12-09 18:38:09 -03:00
Thomas Nahuel Rusiecki 0c837416e1 made log var a env variable 2024-12-09 18:37:53 -03:00
Thomas Nahuel Rusiecki d305448e63 added loggin for state actions 2024-12-09 17:26:31 -03:00
Thomas Nahuel Rusiecki 56ed49ec4c added state delete 2024-12-06 20:04:23 -03:00
Thomas Nahuel Rusiecki 3ad5699c46 changed tab to log and added logging 2024-12-06 19:48:31 -03:00
Thomas Nahuel Rusiecki ac1d5a3ab0 changed state visual and ordering 2024-12-06 19:19:49 -03:00
Thomas Nahuel Rusiecki b5d818fdf7 minor fix for correct tab handling 2024-12-06 18:21:46 -03:00
Thomas Nahuel Rusiecki b3132e5582 added current state to device details 2024-12-06 17:48:06 -03:00
Thomas Nahuel Rusiecki a642be2211 added view and url for new action 2024-12-05 23:09:17 -03:00
Thomas Nahuel Rusiecki dcc95788c3 state institution now nullable 2024-12-05 21:30:31 -03:00
Thomas Nahuel Rusiecki f9674b4ad8 condition checking and renaming 2024-12-05 21:30:06 -03:00
Thomas Nahuel Rusiecki 6d909d5883 stylish new popup for state change 2024-12-05 18:31:30 -03:00
Thomas Nahuel Rusiecki 5b781d3930 added Sortable js dependency for tables 2024-12-04 04:41:41 -03:00
Thomas Nahuel Rusiecki c5dae459f4 added form for state definition order update 2024-12-04 03:06:01 -03:00
Thomas Nahuel Rusiecki 1746139228 minor cosmetic changes 2024-12-04 01:24:18 -03:00
Thomas Nahuel Rusiecki 2cfb892bf7 changed delete button to icon 2024-12-03 20:10:21 -03:00
Thomas Nahuel Rusiecki 051f8f3356 send button hidden until changes are made 2024-12-03 20:00:50 -03:00
Thomas Nahuel Rusiecki aedd8801ce sortable list now updates order of definitions 2024-12-03 19:32:21 -03:00
Thomas Nahuel Rusiecki 26398e3ac1 added Sortable js for state definitions list 2024-12-03 15:01:16 -03:00
Thomas Nahuel Rusiecki dc2418f61b added model level constraints 2024-12-03 15:00:26 -03:00
Thomas Nahuel Rusiecki b3c268a49e modals for state definition deletion 2024-12-02 16:56:23 -03:00
Thomas Nahuel Rusiecki 8dc5bf1435 delete view added and some refactorig 2024-12-02 16:55:59 -03:00
Thomas Nahuel Rusiecki 4d8f9eac9b added add state definition view 2024-12-01 19:47:40 -03:00
Thomas Nahuel Rusiecki 175b6d93dc changed constrain on model 2024-12-01 19:46:04 -03:00
Thomas Nahuel Rusiecki 22d4cea560 fixed models confusion 2024-12-01 17:54:22 -03:00
Thomas Nahuel Rusiecki e35e6414aa added model constraints 2024-12-01 17:47:10 -03:00
Thomas Nahuel Rusiecki a072224645 added admin state definition panel 2024-12-01 02:58:22 -03:00
Thomas Nahuel Rusiecki 5e492e0210 initial orm state models 2024-11-30 04:38:19 -03:00
Thomas Nahuel Rusiecki 18d9eef6ef centered popup modals 2024-11-21 10:45:18 -03:00
Thomas Nahuel Rusiecki b77120a4c3 disabled lotPoperty field 2024-11-16 15:27:21 -03:00
Thomas Nahuel Rusiecki db1d38c3d1 added logging for device operations 2024-11-16 15:26:45 -03:00
Thomas Nahuel Rusiecki 95fb09bd69 renaming lotAnnotations to Lotproperty 2024-11-16 12:31:02 -03:00
Thomas Nahuel Rusiecki aff13a04eb renaming of annotation to property 2024-11-15 20:13:39 -03:00
Thomas Nahuel Rusiecki e93cccaf2d change edit view to modal popup 2024-11-15 19:38:33 -03:00
Thomas Nahuel Rusiecki 0bd6f8d69f added userproperty update view 2024-11-15 19:11:05 -03:00
Thomas Nahuel Rusiecki ca047f4e8b added user_property 2024-11-15 18:16:45 -03:00
Thomas Nahuel Rusiecki 04099d31a6 model constraints changed and moved url to /device 2024-11-15 18:15:44 -03:00
Thomas Nahuel Rusiecki 10f2f6dc16 fixed search and moved delete user property class 2024-11-14 14:16:45 -03:00
Thomas Nahuel Rusiecki caa2be3a41 renaming to new Property tables 2024-11-14 13:08:37 -03:00
Thomas Nahuel Rusiecki 7d18f4797f details view changed to now use properties 2024-11-14 12:00:03 -03:00
Thomas Nahuel Rusiecki 3350966bb3 fixed user_properties list not working 2024-11-13 21:14:23 -03:00
Thomas Nahuel Rusiecki 2a9eca74eb renaming to property 2024-11-13 21:05:54 -03:00
Thomas Nahuel Rusiecki bfe922bed6 changed imports 2024-11-13 21:01:00 -03:00
Thomas Nahuel Rusiecki 9a45988875 fixed self inflicted recursion 2024-11-13 20:29:07 -03:00
Thomas Nahuel Rusiecki 4c69906f24 renaming annotation to variable 2024-11-13 19:16:33 -03:00
Thomas Nahuel Rusiecki 296fab8cf6 variables and function semantic renaming 2024-11-12 14:04:59 -03:00
Thomas Nahuel Rusiecki 41926ea85a split into system and user properties 2024-11-12 12:22:09 -03:00
Thomas Nahuel Rusiecki 1732cc7a9c annotations renaming on views 2024-11-12 11:47:32 -03:00
93 changed files with 2635 additions and 2638 deletions

View file

@ -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
View 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'}),
)

View 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}'))

View 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"
),
),
]

View 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"],
},
),
]

View file

@ -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}"

View file

@ -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'),
]

View file

@ -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
View file

@ -0,0 +1,5 @@
from django import forms
class OrderingStateForm(forms.Form):
ordering = forms.CharField()

View 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 %}

View file

@ -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'),
]

View file

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

View file

@ -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'),

View file

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

View file

@ -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

File diff suppressed because one or more lines are too long

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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 %}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 %}

View file

@ -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"),

View file

@ -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])

View file

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

View file

@ -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')),
])

View file

View file

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

View file

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

View file

@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View file

@ -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"
}
}
}

View file

@ -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>
&copy;{% 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>

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -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"),
]

View file

@ -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, {})

View file

@ -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:

View file

@ -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:-}

View file

@ -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

View file

@ -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
}

View file

View file

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

View file

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

View file

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

View file

@ -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())

View file

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

View file

@ -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])

View file

@ -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,
),
),
],
),
]

View file

@ -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)),
],
),
]

View file

@ -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,
),
),
],
),
]

View file

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

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -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"),
]

View file

@ -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

View file

@ -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,

View file

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

View file

@ -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"
),
),
]

View file

@ -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",
),
]

View file

@ -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):

View file

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

View file

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

View file

@ -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>

View file

@ -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"),
]

View file

@ -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])
)

View file

@ -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

View file

@ -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"}

View file

@ -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"}

View file

@ -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"
),
),
]

View file

@ -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
),
),
]

View file

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

View file

@ -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 %}

View 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 %}

View file

@ -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"),
]

View file

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

View file

@ -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

View file

@ -17,19 +17,8 @@ HID_ALGO1 = [
"sku"
]
LEGACY_DPP = [
"manufacturer",
"model",
"chassis",
"serialNumber",
"sku",
"type",
"version"
]
ALGOS = {
"hidalgo1": HID_ALGO1,
"legacy_dpp": LEGACY_DPP
}

View file

@ -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):

View file

@ -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

View file

@ -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