Compare commits

..

107 commits

Author SHA1 Message Date
Cayo Puigdefabregas 80232f0bd7 fix base template 2025-01-15 16:52:16 +01:00
Cayo Puigdefabregas 3ba17aab85 split long line 2025-01-15 13:26:56 +01:00
Cayo Puigdefabregas 083c45e78b add version 2025-01-15 13:23:36 +01:00
Cayo Puigdefabregas ef9687b0a5 replace waring for yellow in details 2025-01-15 13:13:16 +01:00
Thomas Nahuel Rusiecki be554eb526 deleting obsolete if statement 2025-01-15 13:09:37 +01:00
Thomas Nahuel Rusiecki 88f535da5c renamed property variable to prop 2025-01-15 13:09:36 +01:00
Thomas Nahuel Rusiecki 38a2fd50dc added missing components tab 2025-01-15 13:08:43 +01:00
Thomas Nahuel Rusiecki dd262428ed added logging for evidence tag changes 2025-01-15 13:08:43 +01:00
Thomas Nahuel Rusiecki 8b7add4d0d text size adjustment and displaying none states 2025-01-15 13:08:43 +01:00
Thomas Nahuel Rusiecki 37432edd3a more contrast on save note button 2025-01-15 13:08:43 +01:00
Thomas Nahuel Rusiecki 00433d662e states and notes view refactoring 2025-01-15 13:08:43 +01:00
Thomas Nahuel Rusiecki 824c17ab0a userproperties views refactoring 2025-01-15 13:08:42 +01:00
Thomas Nahuel Rusiecki 841a9ee599 better bootstrap tables 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 28e17e3519 view changes 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 90aeda2cf7 better representiation of delete/edit notes 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 0903f87201 edit update cannot be blank 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki f7eec1b0e6 adding remove button for notes 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 615b55326f notes now can be updated 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki ffe134ba03 deleted undo state view 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 62269f1c3c added a sidebar notes display 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki ce9b92f982 changes to state defiinitions list 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki f85c5668be better success message and removed devicelog var 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 2d0474fcbb normalized deviceLog messages 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 7dc65dcf23 command for adding default states 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 1ff10ca42b erased old logger 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki b1d8b4f1cd minor cosmetic changes 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 75f80c799c state definition list changes and disabled logging 2025-01-15 12:47:54 +01:00
Thomas Nahuel Rusiecki 3be08d97e3 default value for no state 2025-01-15 12:47:53 +01:00
Thomas Nahuel Rusiecki 143fdab105 adding orm migrations 2025-01-15 12:46:31 +01:00
Thomas Nahuel Rusiecki 80fd301709 log list now shows log table 2025-01-15 12:46:31 +01:00
Thomas Nahuel Rusiecki 7959d0d688 added logging for states, user properties and notes 2025-01-15 12:46:30 +01:00
Thomas Nahuel Rusiecki 879c89c722 simpler state change and action input 2025-01-15 12:46:05 +01:00
Thomas Nahuel Rusiecki 32c2b2a9fd current state table erased and spacing fix 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 0695f61db0 deleted unique constraint on userproperty 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 6e7390e010 notes and log models added 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki c20fcda811 device tag bugfix 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 57b03ee711 lotproperties delete and update added 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 14d2900e6b added action migration 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 0efd11f5a6 now check for state on delete modal 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki e32470d1c3 cosmetic changes to statesdefinitions list 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 1eef253b88 change state delete to undo last state 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 1cb7135416 help icon added and new icon on add button 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 281fb6fffa minor changes to states view 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 4e2880f094 statedefinitions update and delete popup changes 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 92df63f7d5 changes to state definitions list 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki a41f30e36e statedefinitions edit popup added and url changes 2025-01-15 12:45:01 +01:00
Thomas Nahuel Rusiecki 1b3581b28e state button rework and warning if same state 2025-01-15 12:45:00 +01:00
Thomas Nahuel Rusiecki 126f12aa03 current state helper function added 2025-01-15 12:44:37 +01:00
Thomas Nahuel Rusiecki f8858b8aa2 Device-details html modularized into several files 2025-01-15 12:44:35 +01:00
Thomas Nahuel Rusiecki f2f41dfbb1 statedefinitions list updated 2025-01-15 12:34:07 +01:00
Thomas Nahuel Rusiecki 405a3f8b29 updated logging for states 2025-01-15 12:34:07 +01:00
Thomas Nahuel Rusiecki f29f908b4c statedefinitions delete now uses correct id 2025-01-15 12:34:07 +01:00
Thomas Nahuel Rusiecki 6ffff595f2 logging for statedefinitions 2025-01-15 12:34:07 +01:00
Thomas Nahuel Rusiecki e71318843e better delete modal 2025-01-15 12:34:07 +01:00
Thomas Nahuel Rusiecki 563f893bf3 erase_server type now userproperty 2025-01-15 12:34:05 +01:00
Thomas Nahuel Rusiecki d4cb8d92cb deleted obsolete field type for sysproperties 2025-01-15 12:33:39 +01:00
Thomas Nahuel Rusiecki 2e12a0673d updated views for new model structure 2025-01-15 12:33:05 +01:00
Thomas Nahuel Rusiecki d7f7352931 more property model refactoring 2025-01-15 12:33:05 +01:00
Thomas Nahuel Rusiecki f4d2b6f297 property models refactor 2025-01-15 12:33:05 +01:00
Thomas Nahuel Rusiecki 48c818c3d1 added logging for user property creation 2025-01-15 12:33:05 +01:00
Thomas Nahuel Rusiecki 685de7650c made log var a env variable 2025-01-15 12:33:05 +01:00
Thomas Nahuel Rusiecki 4fb7157222 added loggin for state actions 2025-01-15 12:33:05 +01:00
Thomas Nahuel Rusiecki 227be2d159 added state delete 2025-01-15 12:33:05 +01:00
Thomas Nahuel Rusiecki ef66b9ca9f changed tab to log and added logging 2025-01-15 12:33:04 +01:00
Thomas Nahuel Rusiecki 53be1c7e1a changed state visual and ordering 2025-01-15 12:32:45 +01:00
Thomas Nahuel Rusiecki 2a1a51ffdb minor fix for correct tab handling 2025-01-15 12:32:23 +01:00
Thomas Nahuel Rusiecki de64c1a989 added current state to device details 2025-01-15 12:31:55 +01:00
Thomas Nahuel Rusiecki db3772f0f9 added view and url for new action 2025-01-15 12:30:03 +01:00
Thomas Nahuel Rusiecki 59e99f5ee6 state institution now nullable 2025-01-15 12:30:03 +01:00
Thomas Nahuel Rusiecki d9ae4c61ab condition checking and renaming 2025-01-15 12:30:03 +01:00
Thomas Nahuel Rusiecki ec67da3d49 stylish new popup for state change 2025-01-15 12:30:01 +01:00
Thomas Nahuel Rusiecki 963f91201d added Sortable js dependency for tables 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 5e6620c64f added form for state definition order update 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki aea96fc708 minor cosmetic changes 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 52d4ea116d changed delete button to icon 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 110aa3b7f7 send button hidden until changes are made 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 7c40b36f5f sortable list now updates order of definitions 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 10651dd70b added Sortable js for state definitions list 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki a07a12ed39 added model level constraints 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki e9533542b8 modals for state definition deletion 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki ca0d7116f7 delete view added and some refactorig 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki f6b68f9856 added add state definition view 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 756de1b7f0 changed constrain on model 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 809d69c0e7 fixed models confusion 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki bbc856cafc added model constraints 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 4561c5fb72 added admin state definition panel 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 0c09da7f8a initial orm state models 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 4cbc523bd8 centered popup modals 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 3675b388b0 disabled lotPoperty field 2025-01-15 12:29:30 +01:00
Thomas Nahuel Rusiecki 9fde49d917 added logging for device operations 2025-01-15 12:29:29 +01:00
Thomas Nahuel Rusiecki 032b2f4f4d renaming lotAnnotations to Lotproperty 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki fa347e6f99 renaming of annotation to property 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki 5e855009f9 change edit view to modal popup 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki 90c2d6b8fd added userproperty update view 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki 0763ae6a62 added user_property 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki 6fdd63f8a5 model constraints changed and moved url to /device 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki 12ba239230 fixed search and moved delete user property class 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki 0e8ed18f05 renaming to new Property tables 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki d5597494fa details view changed to now use properties 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki e4ef818e12 fixed user_properties list not working 2025-01-15 12:29:06 +01:00
Thomas Nahuel Rusiecki 1654182064 renaming to property 2025-01-15 12:29:05 +01:00
Thomas Nahuel Rusiecki e5780676ca changed imports 2025-01-15 12:28:48 +01:00
Thomas Nahuel Rusiecki 1abbe4600a fixed self inflicted recursion 2025-01-15 12:28:48 +01:00
Thomas Nahuel Rusiecki 35d8611336 renaming annotation to variable 2025-01-15 12:28:46 +01:00
Thomas Nahuel Rusiecki ddd8a16597 variables and function semantic renaming 2025-01-15 12:27:41 +01:00
Thomas Nahuel Rusiecki 86a1bb6252 split into system and user properties 2025-01-15 12:08:33 +01:00
Thomas Nahuel Rusiecki bd81342d94 annotations renaming on views 2025-01-15 12:08:33 +01:00
95 changed files with 1536 additions and 2956 deletions

View file

@ -1,3 +0,0 @@
db
.git
already_configured

View file

@ -1,17 +0,0 @@
# repository editor configuration normalization
# @see http://editorconfig.org/
# This is the top-most .editorconfig file; do not search in parent directories.
root = true
# All files.
[*]
end_of_line = LF
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.sh]
indent_size = 8

View file

@ -1,18 +1,8 @@
####
# DEV OPTIONS
####
DEV_DOCKER_ALWAYS_BUILD=false
####
# DEVICEHUB
####
DEVICEHUB_DOMAIN=localhost
DEVICEHUB_PORT=8001
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
STATIC_ROOT=/tmp/static/
@ -26,48 +16,6 @@ EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_FILE_PATH="/tmp/app-messages"
ENABLE_EMAIL=false
PREDEFINED_TOKEN='5018dd65-9abd-4a62-8896-80f34ac66150'
DEVICEHUB_ALLOWED_HOSTS=${DEVICEHUB_DOMAIN},${DEVICEHUB_DOMAIN}:${DEVICEHUB_PORT},127.0.0.1,127.0.0.1:${DEVICEHUB_PORT}
# TODO review these vars
#SNAPSHOTS_DIR=/path/to/TODO
#EVIDENCES_DIR=/path/to/TODO
#DEMO_IDHUB_DOMAIN='idhub.example.org'
####
# IDHUB
####
IDHUB_ENABLED=false
IDHUB_DOMAIN=localhost
IDHUB_PORT=9001
IDHUB_ALLOWED_HOSTS=${IDHUB_DOMAIN},${IDHUB_DOMAIN}:${IDHUB_PORT},127.0.0.1,127.0.0.1:${IDHUB_PORT}
IDHUB_TIME_ZONE='Europe/Madrid'
#IDHUB_SECRET_KEY='uncomment-it-and-fill-this'
# enable dev flags when DEVELOPMENT deployment
# adapt to your domain in a production/reverse proxy env
IDHUB_CSRF_TRUSTED_ORIGINS='https://idhub.example.org'
# fill this section with your email credentials
IDHUB_DEFAULT_FROM_EMAIL="user@example.org"
IDHUB_EMAIL_HOST="smtp.example.org"
IDHUB_EMAIL_HOST_USER="smtp_user"
IDHUB_EMAIL_HOST_PASSWORD="smtp_passwd"
IDHUB_EMAIL_PORT=25
IDHUB_EMAIL_USE_TLS=True
IDHUB_EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
# replace with production data
# this is used when IDHUB_DEPLOYMENT is not equal to DEVELOPMENT
IDHUB_ADMIN_USER='admin'
IDHUB_ADMIN_PASSWD='admin'
IDHUB_ADMIN_EMAIL='admin@example.org'
# this option needs to be set to 'n' to be able to make work idhub in docker
# by default it is set to 'y' to facilitate idhub dev when outside docker
IDHUB_SYNC_ORG_DEV='n'
# TODO that is only for testing
IDHUB_ENABLE_EMAIL=false
IDHUB_ENABLE_2FACTOR_AUTH=false
IDHUB_ENABLE_DOMAIN_CHECKER=false
IDHUB_PREDEFINED_TOKEN='27f944ce-3d58-4f48-b068-e4aa95f97c95'

5
.gitignore vendored
View file

@ -1,7 +1,4 @@
db.sqlite3
env/
__pycache__/
.env
# the following could be autogenerated by devicehub
db.sqlite3
example/snapshots/snapshot_workbench-script_verifiable-credential.json

View file

@ -14,7 +14,7 @@ class State(models.Model):
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)
@ -22,7 +22,6 @@ class State(models.Model):
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)
@ -42,7 +41,7 @@ class StateDefinition(models.Model):
self.order = (max_order or 0) + 1
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
institution = self.institution
order = self.order

View file

@ -2,7 +2,6 @@ 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.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import DeleteView, CreateView, UpdateView, FormView
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -10,7 +9,7 @@ from action.models import State, StateDefinition, Note, DeviceLog
from device.models import Device
class ChangeStateView(LoginRequiredMixin, FormView):
class ChangeStateView(FormView):
form_class = ChangeStateForm
def form_valid(self, form):
@ -43,7 +42,7 @@ class ChangeStateView(LoginRequiredMixin, FormView):
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
class AddNoteView(LoginRequiredMixin, FormView):
class AddNoteView(FormView):
form_class = AddNoteForm
def form_valid(self, form):
@ -74,7 +73,7 @@ class AddNoteView(LoginRequiredMixin, FormView):
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
class UpdateNoteView(LoginRequiredMixin, UpdateView):
class UpdateNoteView(UpdateView):
model = Note
fields = ['description']
pk_url_kwarg = 'pk'
@ -94,19 +93,19 @@ class UpdateNoteView(LoginRequiredMixin, UpdateView):
)
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(LoginRequiredMixin, View):
class DeleteNoteView(View):
model = Note
def post(self, request, *args, **kwargs):

View file

@ -1,173 +0,0 @@
{% 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="#addLotTagModal">
{% trans "Add" %}
</button>
</div>
</div>
<div class="row mt-4">
<div class="col">
{% if lot_tags_edit %}
<table class="table table-hover table-bordered align-middle">
<thead class="table-light">
<tr>
<th scope="col">{% trans "Lot Group Name" %}
</th>
<th scope="col" width="15%" class="text-center">{% trans "Actions" %}
</th>
</tr>
</thead>
<tbody id="sortable_list">
{% for tag in lot_tags_edit %}
<tr>
<td class="font-monospace">
{{ tag.name }}
</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="#editLotTagModal{{ tag.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="#deleteLotTagModal{{ tag.id }}" >
<i class="bi bi-trash me-1"></i>
{% trans 'Delete' %}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-primary text-center mt-5" role="alert">
{% trans "No Lot Groups found on current organization" %}
</div>
{% endif %}
</div>
</div>
<!-- add lot tag Modal -->
<div class="modal fade" id="addLotTagModal" tabindex="-1" aria-labelledby="addLoTagModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addLotTagModalLabel">{% trans "Add Lot Group" %}</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_lot_tag' %}">
{% csrf_token %}
<div class="mb-3">
<label for="lotTagInput" class="form-label">{% trans "Tag" %}</label>
<input type="text" class="form-control" id="lotTagInput" name="name" 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 Lot tag" %}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit Lot Group Modals -->
{% for tag in lot_tags_edit %}
<div class="modal fade" id="editLotTagModal{{ tag.id }}" tabindex="-1" aria-labelledby="editLotTagModalLabel{{ tag.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{% url 'admin:edit_lot_tag' tag.id %}">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title" id="editLotTagModalLabel{{ tag.id }}">
{% trans "Edit Lot Group" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="editLotTagInput{{ tag.id }}" class="form-label">{% trans "Tag" %}</label>
<input type="text" class="form-control" id="editLotTagInput{{ tag.id }}" name="name" maxlength="50" value="{{ tag.name }}" required>
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
</div>
</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 lot tag definition Modal -->
{% for tag in lot_tags_edit %}
<div class="modal fade" id="deleteLotTagModal{{ tag.id }}" tabindex="-1" aria-labelledby="deleteLotTagModalLabel{{ tag.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="deleteLotTagModalLabel{{ tag.id }}">
{% trans "Delete Lot Group" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
</div>
<div class="modal-body">
{% if tag.lot_set.first %}
<div class="alert alert-warning text-center" role="alert">
{% trans "Failed to remove Lot Group, it is not empty" %}
</div>
{% endif %}
<div class="d-flex align-items-center border rounded p-3 mt-3">
<div>
<p class="mb-0 fw-bold">{{ tag.name }}</p>
</div>
</div>
</div>
<div class="modal-footer">
<form method="post" action="{% url 'admin:delete_lot_tag' tag.pk %}">
{% csrf_token %}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans "Cancel" %}
</button>
{% if tag.lot_set.first %}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans "Delete" %}
</button>
{% else %}
<button type="submit" class="btn btn-danger">
{% trans "Delete" %}
</button>
{% endif %}
</form>
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}

View file

@ -15,8 +15,4 @@ urlpatterns = [
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'),
path("lot/", views.LotTagPanelView.as_view(), name="tag_panel"),
path("lot/add", views.AddLotTagView.as_view(), name="add_lot_tag"),
path("lot/delete/<int:pk>", views.DeleteLotTagView.as_view(), name='delete_lot_tag'),
path("lot/edit/<int:pk>/", views.UpdateLotTagView.as_view(), name='edit_lot_tag'),
]

View file

@ -18,7 +18,6 @@ from admin.forms import OrderingStateForm
from user.models import User, Institution
from admin.email import NotifyActivateUserByEmail
from action.models import StateDefinition
from lot.models import LotTag
class AdminView(DashboardView):
@ -26,7 +25,7 @@ class AdminView(DashboardView):
response = super().get(*args, **kwargs)
if not self.request.user.is_admin:
raise Http403
return response
class PanelView(AdminView, TemplateView):
@ -112,100 +111,7 @@ class EditUserView(AdminView, UpdateView):
kwargs = super().get_form_kwargs()
return kwargs
class LotTagPanelView(AdminView, TemplateView):
template_name = "lot_tag_panel.html"
title = _("Lot Groups Panel")
breadcrumb = _("admin / Lot Groups Panel")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
lot_tags = LotTag.objects.filter(
owner=self.request.user.institution
)
context.update({"lot_tags_edit": lot_tags})
return context
class AddLotTagView(AdminView, CreateView):
template_name = "lot_tag_panel.html"
title = _("New lot group Definition")
breadcrumb = "Admin / New lot tag"
success_url = reverse_lazy('admin:tag_panel')
model = LotTag
fields = ('name',)
def form_valid(self, form):
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
name = form.instance.name
if LotTag.objects.filter(name=name).first():
msg = _(f"The name '{name}' exist.")
messages.error(self.request, msg)
return redirect(self.success_url)
response = super().form_valid(form)
messages.success(self.request, _("Lot Group successfully added."))
return response
class DeleteLotTagView(AdminView, DeleteView):
model = LotTag
success_url = reverse_lazy('admin:tag_panel')
def post(self, request, *args, **kwargs):
pk = kwargs.get('pk')
self.object = get_object_or_404(
self.model,
owner=self.request.user.institution,
pk=pk
)
if self.object.lot_set.first():
msg = _('This group have lots. Impossible to delete.')
messages.warning(self.request, msg)
return redirect(reverse_lazy('admin:tag_panel'))
if self.object.inbox:
msg = f"The lot group '{self.object.name}'"
msg += " is INBOX, so it cannot be deleted, only renamed."
messages.error(self.request, msg)
return redirect(self.success_url)
response = super().delete(request, *args, **kwargs)
msg = _('Lot Group has been deleted.')
messages.success(self.request, msg)
return response
class UpdateLotTagView(AdminView, UpdateView):
model = LotTag
template_name = 'lot_tag_panel.html'
fields = ['name']
success_url = reverse_lazy('admin:tag_panel')
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
self.object = get_object_or_404(
self.model,
owner=self.request.user.institution,
pk=pk
)
return super().get_form_kwargs()
def form_valid(self, form):
name = form.instance.name
if LotTag.objects.filter(name=name).first():
msg = _(f"The name '{name}' exist.")
messages.error(self.request, msg)
return redirect(self.success_url)
response = super().form_valid(form)
msg = _("Lot Group updated successfully.")
messages.success(self.request, msg)
return response
class InstitutionView(AdminView, UpdateView):
template_name = "institution.html"
title = _("Edit institution")
@ -218,8 +124,7 @@ class InstitutionView(AdminView, UpdateView):
"logo",
"location",
"responsable_person",
"supervisor_person",
"algorithm"
"supervisor_person"
)
def get_form_kwargs(self):
@ -263,9 +168,8 @@ class AddStateDefinitionView(AdminView, StateDefinitionContextMixin, CreateView)
messages.error(self.request, _("State is already defined."))
return self.form_invalid(form)
def form_invalid(self, form):
super().form_invalid(form)
return redirect(self.success_url)
def form_invalid(self, form):
return super().form_invalid(form)
class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessMessageMixin, DeleteView):
@ -275,11 +179,14 @@ class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessM
def get_success_message(self, cleaned_data):
return f'State definition: {self.object.state}, has been deleted'
def form_valid(self, form):
if not self.object.institution == self.request.user.institution:
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().form_valid(form)
return super().delete(request, *args, **kwargs)
class UpdateStateOrderView(AdminView, TemplateView):
@ -311,21 +218,14 @@ class UpdateStateDefinitionView(AdminView, UpdateView):
model = StateDefinition
template_name = 'states_panel.html'
fields = ['state']
success_url = reverse_lazy('admin:states_panel')
pk_url_kwarg = 'pk'
def get_queryset(self):
return StateDefinition.objects.filter(institution=self.request.user.institution)
def form_valid(self, form):
try:
response = super().form_valid(form)
messages.success(self.request, _("State definition updated successfully."))
return response
except IntegrityError:
messages.error(self.request, _("State is already defined."))
return self.form_invalid(form)
def get_success_url(self):
messages.success(self.request, _("State definition updated successfully."))
return reverse_lazy('admin:states_panel')
def form_invalid(self, form):
super().form_invalid(form)
return redirect(self.get_success_url())
def form_valid(self, form):
return super().form_valid(form)

View file

@ -90,7 +90,7 @@ class NewSnapshotView(ApiMixing):
ev_uuid = data["credentialSubject"].get("uuid")
if not ev_uuid:
txt = "error: the snapshot does not have an uuid"
txt = "error: the snapshot not have uuid"
logger.error("%s", txt)
return JsonResponse({'status': txt}, status=500)
@ -118,7 +118,7 @@ class NewSnapshotView(ApiMixing):
prop = SystemProperty.objects.filter(
uuid=ev_uuid,
# TODO this is hardcoded, it should select the user preferred algorithm
key="ereuse24",
key="hidalgo1",
owner=self.tk.owner.institution
).first()
@ -127,12 +127,12 @@ class NewSnapshotView(ApiMixing):
logger.error("Error: No property for uuid: %s", ev_uuid)
return JsonResponse({'status': 'fail'}, status=500)
url_args = reverse_lazy("device:details", args=(prop.value,))
url_args = reverse_lazy("device:details", args=(property.value,))
url = request.build_absolute_uri(url_args)
response = {
"status": "success",
"dhid": prop.value[:6].upper(),
"dhid": property.value[:6].upper(),
"url": url,
# TODO replace with public_url when available
"public_url": url

View file

@ -8,7 +8,6 @@ from django.views.generic.base import TemplateView
from device.models import Device
from evidence.models import SystemProperty
from lot.models import LotTag
from action.models import StateDefinition
class Http403(PermissionDenied):
@ -33,9 +32,6 @@ class DashboardView(LoginRequiredMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
lot_tags = LotTag.objects.filter(
owner=self.request.user.institution,
)
context.update({
"commit_id": settings.COMMIT,
'title': self.title,
@ -45,7 +41,7 @@ class DashboardView(LoginRequiredMixin):
'section': self.section,
'path': resolve(self.request.path).url_name,
'user': self.request.user,
'lot_tags': lot_tags
'lot_tags': LotTag.objects.filter(owner=self.request.user.institution)
})
return context

View file

@ -82,11 +82,11 @@
<ul class="nav flex-column">
{% if user.is_admin %}
<li class="nav-item">
<a class="admin {% if path in 'panel users states_panel tag_panel edit_user delete_user new_user institution' %}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 edit_user delete_user new_user institution' %}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>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'panel users tag_panel states_panel' %}expanded{% else %}collapse{% endif %}" id="ul_admin" data-bs-parent="#sidebarMenu">
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'panel institution users edit_user new_user delete_user states_panel' %}expanded{% else %}collapse{% endif %}" id="ul_admin" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path in 'panel institution' %} active2{% endif %}" href="{% url 'admin:panel' %}">
{% trans 'Panel' %}
@ -100,43 +100,33 @@
<li class="nav-item">
<a class="nav-link{% if path == 'states_panel' %} active2{% endif %}" href="{% url 'admin:states_panel' %}">
{% trans 'States' %}
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if path == 'tag_panel' %} active2{% endif %}" href="{% url 'admin:tag_panel' %}">
{% trans 'Lot Groups' %}
</a>
</li>
</ul>
</li>
{% endif %}
<li class="nav-item">
<a class="admin {% if path in 'all_device' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_device" aria-expanded="false" aria-controls="ul_lots" href="javascript:void()">
<a class="admin {% if path == 'unassigned_devices' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_devices" aria-expanded="false" aria-controls="ul_devices" href="javascript:void()">
<i class="bi bi-laptop icon_sidebar"></i>
{% trans 'Device' %}
{% trans 'Devices' %}
</a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'all_device' %}expanded{% else %}collapse{% endif %}" id="ul_device" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path == 'all_device' %} active2{% endif %}" href="{% url 'dashboard:all_device' %}">
{% trans 'All' %}
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path == 'unassigned_devices' %}expanded{% else %}collapse{% endif %}" id="ul_devices" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path == 'unassigned_devices' %} active2{% endif %}" href="{% url 'dashboard:unassigned_devices' %}">
{% trans 'Unassigned devices' %}
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="admin {% if path == 'tags' or path == 'lot' or path in 'unassigned dashboard' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_lots" aria-expanded="false" aria-controls="ul_lots" href="javascript:void()">
<a class="admin {% if path == 'tag' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_lots" aria-expanded="false" aria-controls="ul_lots" href="javascript:void()">
<i class="bi bi-database icon_sidebar"></i>
{% trans 'Lots' %}
</a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path == 'tags' or path == 'lot' or path in 'unassigned dashboard' %}expanded{% else %}collapse{% endif %}" id="ul_lots" data-bs-parent="#sidebarMenu">
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path == 'tag' %}expanded{% else %}collapse{% endif %}" id="ul_lots" data-bs-parent="#sidebarMenu">
{% for tag in lot_tags %}
<li class="nav-items">
{% if tag.inbox %}
<a class="nav-link{% if path == 'unassigned' %} active2{% endif %}" href="{% url 'dashboard:unassigned' %}">
{% else %}
<a class="nav-link{% if path == 'tags' %} active2{% endif %}" href="{% url 'lot:tags' tag.id %}">
{% endif %}
<a class="nav-link{% if path == 'tag' %} active2{% endif %}" href="{% url 'lot:tag' tag.id %}">
{{ tag.name }}
</a>
</li>
@ -144,29 +134,37 @@
</ul>
</li>
<li class="nav-item">
<a class="admin {% if path in 'upload list import add' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_evidences" aria-expanded="false" aria-controls="ul_evidences" href="javascript:void()">
<a class="admin {% if path in 'upload list' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_evidences" aria-expanded="false" aria-controls="ul_evidences" href="javascript:void()">
<i class="bi bi-usb-drive icon_sidebar"></i>
{% trans 'Evidences' %}
</a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'upload list import add' %}expanded{% else %}collapse{% endif %}" id="ul_evidences" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path == 'list' %} active2{% endif %}" href="{% url 'evidence:list' %}">
{% trans 'List of evidences' %}
</a>
</li>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'upload list' %}expanded{% else %}collapse{% endif %}" id="ul_evidences" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path == 'upload' %} active2{% endif %}" href="{% url 'evidence:upload' %}">
{% trans 'Upload with JSON file' %}
{% trans 'Upload one' %}
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if path == 'list' %} active2{% endif %}" href="{% url 'evidence:list' %}">
{% trans 'Old evidences' %}
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="admin {% if path in 'import add' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_placeholders" aria-expanded="false" aria-controls="ul_placeholders" href="javascript:void()">
<i class="bi-menu-button-wide icon_sidebar"></i>
{% trans 'Placeholders' %}
</a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'import add' %}expanded{% else %}collapse{% endif %}" id="ul_placeholders" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path == 'import' %} active2{% endif %}" href="{% url 'evidence:import' %}">
{% trans 'Upload with Spreadsheet' %}
{% trans 'Upload Spreadsheet' %}
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if path == 'add' %} active2{% endif %}" href="{% url 'device:add' %}">
{% trans 'Upload with Web Form' %}
{% trans 'Create one' %}
</a>
</li>
</ul>
@ -194,10 +192,10 @@
{% endif %}
</h1>
<form method="get" action="{% url 'dashboard:search' %}">
<form method="post" action="{% url 'dashboard:search' %}">
{% csrf_token %}
<div class="input-group rounded">
<input type="search" name="search" class="form-control rounded" {% if search %}value="{{ search }}" {% endif %}placeholder="Search your device..." aria-label="Search" aria-describedby="search-addon" />
<input type="search" name="search" class="form-control rounded" placeholder="Search your device..." aria-label="Search" aria-describedby="search-addon" />
<span class="input-group-text border-0" id="search-addon">
<i class="fas fa-search"></i>
</span>

View file

@ -4,12 +4,12 @@
<ul class="pagination">
{% if page_number > 1 %}
<li class="previous">
<a type="button" class="btn btn-grey border border-dark" href="?page=1&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
<a type="button" class="btn btn-grey border border-dark" href="?page=1&limit={{ limit }}">
&laquo;
</a>
</li>
<li class="previous">
<a type="button" class="btn btn-grey border border-dark" href="?page={{ page_number|add:-1 }}&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
<a type="button" class="btn btn-grey border border-dark" href="?page={{ page_number|add:-1 }}&limit={{ limit }}">
{% trans 'Previous' %}
</a>
</li>
@ -24,7 +24,7 @@
{% if p == page_number or p == "..." %}
href="#">
{% else %}
href="?page={{ p }}&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
href="?page={{ p }}&limit={{ limit }}">
{% endif %}
{{ p }}
</a>
@ -34,12 +34,12 @@
{% if page_number < total_pages %}
<li class="previous">
<a type="button" class="btn btn-grey border border-dark" href="?page={{ page_number|add:+1 }}&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
<a type="button" class="btn btn-grey border border-dark" href="?page={{ page_number|add:+1 }}&limit={{ limit }}">
{% trans 'Next' %}
</a>
</li>
<li class="previous">
<a type="button" class="btn btn-grey border border-dark" href="?page={{ total_pages }}&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
<a type="button" class="btn btn-grey border border-dark" href="?page={{ total_pages }}&limit={{ limit }}">
&raquo;
</a>
</li>

View file

@ -9,6 +9,12 @@
<h3>{{ subtitle }}</h3>
</div>
<div class="col text-center">
{% if lot %}
<a href="{% url 'lot:documents' object.id %}" type="button" class="btn btn-green-admin">
<i class="bi bi-folder2"></i>
{% trans 'Documents' %}
</a>
{% endif %}
<a href="{# url 'dashboard:exports' object.id #}" type="button" class="btn btn-green-admin">
<i class="bi bi-reply"></i>
{% trans 'Exports' %}
@ -25,9 +31,6 @@
<div class="dataTable-container">
<form method="post">
{% csrf_token %}
Lot actions: <button class="btn btn-green-admin" type="submit" name="url" value="{% url 'lot:add_devices' %}">Add</button> <button class="btn btn-green-admin" type="submit" value="{% url 'lot:del_devices' %}" name="url">Remove</button>
<table class="table">
<thead>
<tr>
@ -46,9 +49,6 @@
<th scope="col" data-sortable="">
model
</th>
<th scope="col" data-sortable="">
updated
</th>
</tr>
</thead>
{% for dev in devices %}
@ -75,18 +75,16 @@
{{ dev.model }}
{% endif %}
</td>
<td>
{{ dev.updated }}
</td>
</tr>
</tbody>
{% endfor %}
</table>
<button class="btn btn-green-admin" type="submit" value="{% url 'lot:del_devices' %}" name="url">Remove</button> <button class="btn btn-green-admin" type="submit" name="url" value="{% url 'lot:add_devices' %}">add</button>
</form>
</div>
<div class="row mt-3">
<div class="col">
{% render_pagination page total_pages limit search %}
{% render_pagination page total_pages limit %}
</div>
</div>
{% endblock %}

View file

@ -3,7 +3,7 @@ from django import template
register = template.Library()
@register.inclusion_tag('pagination.html')
def render_pagination(page_number, total_pages, limit=10, search=None):
def render_pagination(page_number, total_pages, limit=10):
"""
Template tag for render pagination
@ -16,6 +16,5 @@ def render_pagination(page_number, total_pages, limit=10, search=None):
return {
'page_number': page_number,
'total_pages': total_pages,
'limit': limit,
"search": search,
'limit': limit
}

View file

@ -1,11 +1,10 @@
from django.urls import path
from dashboard import views
from dashboard import views
app_name = 'dashboard'
urlpatterns = [
path("", views.UnassignedDevicesView.as_view(), name="unassigned"),
path("all", views.AllDevicesView.as_view(), name="all_device"),
path("", views.UnassignedDevicesView.as_view(), name="unassigned_devices"),
path("<int:pk>/", views.LotDashboardView.as_view(), name="lot"),
path("search", views.SearchView.as_view(), name="search"),
]

View file

@ -22,16 +22,6 @@ class UnassignedDevicesView(InventaryMixin):
return Device.get_unassigned(self.request.user.institution, offset, limit)
class AllDevicesView(InventaryMixin):
template_name = "unassigned_devices.html"
section = "All"
title = _("All Devices")
breadcrumb = "Devices / All Devices"
def get_devices(self, user, offset, limit):
return Device.get_all(self.request.user.institution, offset, limit)
class LotDashboardView(InventaryMixin, DetailsMixin):
template_name = "unassigned_devices.html"
section = "dashboard_lot"
@ -52,18 +42,8 @@ class LotDashboardView(InventaryMixin, DetailsMixin):
"device_id", flat=True
).distinct()
props = SystemProperty.objects.filter(
owner=self.request.user.institution,
value__in=chids
).order_by("-created")
chids_ordered = []
for x in props:
if x.value not in chids_ordered:
chids_ordered.append(x.value)
chids_page = chids_ordered[offset:offset+limit]
return [Device(id=x) for x in chids_page], len(chids_ordered)
chids_page = chids[offset:offset+limit]
return [Device(id=x) for x in chids_page], chids.count()
class SearchView(InventaryMixin):
@ -72,20 +52,9 @@ class SearchView(InventaryMixin):
title = _("Search Devices")
breadcrumb = "Devices / Search Devices"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
search_params = self.request.GET.urlencode(),
search = self.request.GET.get("search")
if search:
context.update({
'search_params': search_params,
'search': search
})
return context
def get_devices(self, user, offset, limit):
query = dict(self.request.GET).get("search")
post = dict(self.request.POST)
query = post.get("search")
if not query:
return [], 0
@ -96,12 +65,6 @@ class SearchView(InventaryMixin):
offset,
limit
)
count = search(
self.request.user.institution,
query[0],
0,
9999
).size()
if not matches or not matches.size():
return self.search_hids(query, offset, limit)
@ -116,6 +79,7 @@ class SearchView(InventaryMixin):
devices.append(dev)
dev_id.append(dev.id)
count = matches.size()
# TODO fix of pagination, the count is not correct
return devices, count

View file

@ -71,6 +71,17 @@ class Device:
)
return user_properties
def get_user_documents(self):
if not self.uuids:
self.get_uuids()
user_properties = UserProperty.objects.filter(
uuid__in=self.uuids,
owner=self.owner,
type=UserProperty.Type.DOCUMENT
)
return user_properties
def get_uuids(self):
for a in self.get_properties():
if a.uuid not in self.uuids:
@ -136,91 +147,6 @@ class Device:
self.lots = [
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
@classmethod
def get_all(cls, institution, offset=0, limit=None):
sql = """
WITH RankedProperties AS (
SELECT
t1.value,
t1.key,
t1.created,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = '{algorithm}' THEN 2
END,
t1.created DESC
) AS row_num
FROM evidence_systemproperty AS t1
WHERE t1.owner_id = {institution}
AND t1.key IN ('CUSTOM_ID', '{algorithm}')
)
SELECT DISTINCT
value
FROM
RankedProperties
WHERE
row_num = 1
ORDER BY created DESC
""".format(
institution=institution.id,
algorithm=institution.algorithm,
)
if limit:
sql += " limit {} offset {}".format(int(limit), int(offset))
sql += ";"
annotations = []
with connection.cursor() as cursor:
cursor.execute(sql)
annotations = cursor.fetchall()
devices = [cls(id=x[0]) for x in annotations]
count = cls.get_all_count(institution)
return devices, count
@classmethod
def get_all_count(cls, institution):
sql = """
WITH RankedProperties AS (
SELECT
t1.value,
t1.key,
t1.created,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = '{algorithm}' THEN 2
END,
t1.created DESC
) AS row_num
FROM evidence_systemproperty AS t1
WHERE t1.owner_id = {institution}
AND t1.key IN ('CUSTOM_ID', '{algorithm}')
)
SELECT
COUNT(DISTINCT value)
FROM
RankedProperties
WHERE
row_num = 1
ORDER BY created DESC
""".format(
institution=institution.id,
algorithm=institution.algorithm
)
with connection.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()[0][0]
@classmethod
def get_unassigned(cls, institution, offset=0, limit=None):
@ -229,21 +155,20 @@ class Device:
SELECT
t1.value,
t1.key,
t1.created,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = '{algorithm}' THEN 2
WHEN t1.key = 'hidalgo1' THEN 2
ELSE 3
END,
t1.created DESC
) AS row_num
) 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.key IN ('CUSTOM_ID', '{algorithm}')
)
SELECT DISTINCT
value
@ -251,10 +176,8 @@ class Device:
RankedProperties
WHERE
row_num = 1
ORDER BY created DESC
""".format(
institution=institution.id,
algorithm=institution.algorithm
)
if limit:
sql += " limit {} offset {}".format(int(limit), int(offset))
@ -278,13 +201,13 @@ class Device:
SELECT
t1.value,
t1.key,
t1.created,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = '{algorithm}' THEN 2
WHEN t1.key = 'hidalgo1' THEN 2
ELSE 3
END,
t1.created DESC
) AS row_num
@ -292,7 +215,6 @@ class Device:
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.key IN ('CUSTOM_ID', '{algorithm}')
)
SELECT
COUNT(DISTINCT value)
@ -300,10 +222,8 @@ class Device:
RankedProperties
WHERE
row_num = 1
ORDER BY created DESC
""".format(
institution=institution.id,
algorithm=institution.algorithm
)
with connection.cursor() as cursor:
cursor.execute(sql)
@ -316,32 +236,31 @@ class Device:
SELECT
t1.value,
t1.key,
t1.created,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = '{algorithm}' THEN 2
WHEN t1.key = 'hidalgo1' THEN 2
ELSE 3
END,
t1.created DESC
) AS row_num
FROM evidence_systemproperty AS t1
WHERE t1.owner_id = {institution}
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.uuid = '{uuid}'
AND t1.key IN ('CUSTOM_ID', '{algorithm}')
)
SELECT DISTINCT
value
FROM
RankedProperties
WHERE
row_num = 1
ORDER BY created DESC
row_num = 1;
""".format(
uuid=uuid.replace("-", ""),
institution=institution.id,
algorithm=institution.algorithm,
)
properties = []
@ -366,12 +285,6 @@ class Device:
self.get_last_evidence()
return self.last_evidence.get_manufacturer()
@property
def updated(self):
"""get timestamp from last evidence created"""
self.get_last_evidence()
return self.last_evidence.get_time_created()
@property
def serial_number(self):
self.get_last_evidence()
@ -392,15 +305,11 @@ class Device:
@property
def version(self):
self.get_last_evidence()
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()
@property
def did_document(self):
self.get_last_evidence()
return self.last_evidence.get_did_document()

View file

@ -2,6 +2,108 @@
{% load i18n %}
{% block content %}
<div class="position-fixed" style="bottom: 2rem; right: 2rem; z-index: 9999; display: flex; gap: 0.5rem;">
<button class="btn btn-yellow 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-yellow 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">
@ -64,7 +166,13 @@
<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="#user_properties" class="nav-link" data-bs-toggle="tab" data-bs-target="#user_properties">{% trans 'Properties' %}</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>
</li>
<li class="nav-item">
<a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">{% trans 'Lots' %}</a>
@ -83,9 +191,6 @@
<li class="nav-item">
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
</li>
<li class="nav-item">
<a href="#log" class="nav-link" data-bs-toggle="tab" data-bs-target="#log">{% trans 'Log' %}</a>
</li>
</ul>
</div>
</div>
@ -97,14 +202,14 @@
{% include 'tabs/user_properties.html' %}
{% include 'tabs/documents.html' %}
{% include 'tabs/lots.html' %}
{% include 'tabs/components.html' %}
{% include 'tabs/evidences.html' %}
{% include 'tabs/dpps.html' %}
<!-- Add a note popup -->
<div class="modal fade" id="addNoteModal" tabindex="-1" aria-labelledby="addNoteModalLabel" aria-hidden="true">
<div class="modal-dialog">
@ -151,5 +256,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

@ -78,7 +78,7 @@
{% endfor %}
</div>
<div class="container">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>

View file

@ -36,7 +36,7 @@
{% endfor %}
</div>
<div class="container">
<a class="btn btn-grey" href="{% url 'device:details' pk %}#user_properties">{% translate "Cancel" %}</a>
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>

View file

@ -0,0 +1,251 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col">
<h3>{{ object.pk }}</h3>
</div>
</div>
<div class="row">
<div class="col">
<div class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items">
<a class="nav-link" href="{% url 'device:details' device.pk %}">General details</a>
</li>
<li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#physicalproperties">Physical properties</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">Documents</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">Lots</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#status">Status</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#components">Components</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#traceabiliy">Traceability log</button>
</li>
<li class="nav-items">
<a class="nav-link" href="">Web</a>
</li>
</div>
</div>
</div>
<div class="tab-content pt-2">
<div class="tab-pane fade profile-overview" id="details">
<h5 class="card-title">Details</h5>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label ">
(<a href="/inventory/device/edit/4W8D3/">Edit Device</a>)
</div>
<div class="col-lg-9 col-md-8">
{%if object.hid %}Snapshot{% else %}Placeholder{% endif %}
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label ">Phid</div>
<div class="col-lg-9 col-md-8">{{ object.id }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label ">Id device internal</div>
<div class="col-lg-9 col-md-8"></div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label ">Type</div>
<div class="col-lg-9 col-md-8">{{ object.type }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Manufacturer</div>
<div class="col-lg-9 col-md-8">{{ object.manufacturer|default:"" }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Model</div>
<div class="col-lg-9 col-md-8">{{ object.model|default:"" }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Part Number</div>
<div class="col-lg-9 col-md-8">{{ object.part_number|default:"" }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Serial Number</div>
<div class="col-lg-9 col-md-8">{{ object.serial_number|default:"" }}</div>
</div>
</div>
<div class="tab-pane fade show active" id="physicalproperties">
<h5 class="card-title">Physical Properties</h5>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label ">
(<a href="{% url 'device:physical_edit' object.pk %}">Edit Physical Properties</a>)
</div>
</div>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label ">
{% load django_bootstrap5 %}
<form role="form" method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
<div class="message">
{% for field, error in form.errors.items %}
{{ error }}<br />
{% endfor %}
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
</div>
</div>
{% endif %}
{% bootstrap_form form %}
<div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'device:details' device.pk %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="lots">
<h5 class="card-title">Incoming Lots</h5>
<div class="row">
</div>
<h5 class="card-title">Outgoing Lots</h5>
<div class="row">
</div>
<h5 class="card-title">Temporary Lots</h5>
<div class="row">
</div>
</div>
<div class="tab-pane fade profile-overview" id="documents">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="/inventory/device/4W8D3/document/add/" class="btn btn-primary">
<i class="bi bi-plus"></i>
Add new document
<span class="caret"></span>
</a>
</div>
<h5 class="card-title">Documents</h5>
<table class="table">
<thead>
<tr>
<th scope="col">File</th>
<th scope="col">Type</th>
<th scope="col">Description</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Uploaded on</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="tab-pane fade profile-overview" id="status">
<h5 class="card-title">Status Details</h5>
<div class="row">
<div class="col-lg-3 col-md-4 label">Physical State</div>
<div class="col-lg-9 col-md-8">
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Lifecycle State</div>
<div class="col-lg-9 col-md-8">
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Allocated State</div>
<div class="col-lg-9 col-md-8">
</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="traceability">
<h5 class="card-title">Traceability log Details</h5>
<div class="list-group col-6">
<div class="list-group-item d-flex justify-content-between align-items-center">
Snapshot ✓
<small class="text-muted">14:07 23-06-2024</small>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
EraseCrypto ✓
<small class="text-muted">14:07 23-06-2024</small>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
EraseCrypto ✓
<small class="text-muted">14:07 23-06-2024</small>
</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="components">
<h5 class="card-title">Components Snapshot</h5>
<div class="list-group col-6">
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Motherboard</h5>
<small class="text-muted">14:07 23-06-2024</small>
</div>
<p class="mb-1">
hp<br />
890e<br />
</p>
<small class="text-muted">
</small>
</div>
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">NetworkAdapter</h5>
<small class="text-muted">14:07 23-06-2024</small>
</div>
<p class="mb-1">
realtek semiconductor co., ltd.<br />
rtl8852ae 802.11ax pcie wireless network adapter<br />
</p>
<small class="text-muted">
</small>
</div>
</div>
</div>
</div>
{% endblock %}

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

@ -1,18 +0,0 @@
{% load i18n %}
<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.2.timestamp }}</small>
<span>{{ d.2.type }}</span>
</div>
<p class="mb-1">
<a href="{% url 'did:device_web' d.0 %}">{{ d.1 }}...</a>
</p>
</div>
{% endfor %}
</div>
</div>

View file

@ -1,34 +1,19 @@
{% load i18n %}
<div class="tab-pane fade" id="evidences">
<div class="tab-pane fade" id="evidences">
<h5 class="card-title">{% trans 'List of evidences' %}</h5>
<div class="list-group col">
<table class="table">
<thead>
<tr>
<th scope="col">uuid</th>
<th scope="col">Did Document</th>
<th scope="col">{% trans "Date" %}</th>
</tr>
</thead>
<tbody>
{% for snap in object.evidences %}
<tr>
<td>
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
</td>
<td>
{% if snap.did_document %}
<a href="{{ snap.did_document }}" target="_blank">DID</a>
{% endif %}
</td>
<td>
<small class="text-muted">{{ snap.created }}</small>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<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

@ -82,7 +82,7 @@
<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' object.id a.id %}">
<form method="post" action="{% url 'device:delete_user_property' a.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger">{% trans "Delete" %}
</button>
@ -105,7 +105,7 @@
</button>
</div>
<div class="modal-body">
<form id="editForm{{ a.id }}" method="post" action="{% url 'device:update_user_property' object.id a.id %}">
<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" %}

View file

@ -7,11 +7,10 @@ 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>/user_property/add",
views.AddUserPropertyView.as_view(), name="add_user_property"),
path("<str:device_id>/user_property/<int:pk>/delete",
views.DeleteUserPropertyView.as_view(), name="delete_user_property"),
path("<str:device_id>/user_property/<int:pk>/update",
views.UpdateUserPropertyView.as_view(), name="update_user_property"),
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,9 +1,7 @@
import json
import logging
from django.http import JsonResponse
from django.conf import settings
from django.db import IntegrityError
from django.urls import reverse_lazy
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, Http404
@ -26,21 +24,11 @@ if settings.DPP:
from dpp.api_dlt import PROOF_TYPE
class DeviceLogMixin(DashboardView):
def log_registry(self, _uuid, msg):
DeviceLog.objects.create(
snapshot_uuid=_uuid,
event=msg,
user=self.request.user,
institution=self.request.user.institution
)
class NewDeviceView(DashboardView, FormView):
template_name = "new_device.html"
title = _("New Device")
breadcrumb = "Device / New Device"
success_url = reverse_lazy('dashboard:unassigned')
success_url = reverse_lazy('dashboard:unassigned_devices')
form_class = DeviceFormSet
def form_valid(self, form):
@ -53,6 +41,35 @@ class NewDeviceView(DashboardView, FormView):
return response
# class AddToLotView(DashboardView, FormView):
# template_name = "list_lots.html"
# title = _("Add to lots")
# breadcrumb = "lot / add to lots"
# success_url = reverse_lazy('dashboard:unassigned_devices')
# form_class = LotsForm
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# lots = Lot.objects.filter(owner=self.request.user)
# lot_tags = LotTag.objects.filter(owner=self.request.user)
# context.update({
# 'lots': lots,
# 'lot_tags':lot_tags,
# })
# return context
# def get_form(self):
# form = super().get_form()
# form.fields["lots"].queryset = Lot.objects.filter(owner=self.request.user)
# return form
# def form_valid(self, form):
# form.devices = self.get_session_devices()
# form.save()
# response = super().form_valid(form)
# return response
class EditDeviceView(DashboardView, UpdateView):
template_name = "new_device.html"
title = _("Update Device")
@ -94,32 +111,24 @@ class DetailsView(DashboardView, TemplateView):
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
dpps = []
if settings.DPP:
_dpps = Proof.objects.filter(
dpps = Proof.objects.filter(
uuid__in=self.object.uuids,
type=PROOF_TYPE["IssueDPP"]
)
for x in _dpps:
dpp = "{}:{}".format(self.pk, x.signature)
dpps.append((dpp, x.signature[:10], x))
last_evidence = self.object.get_last_evidence()
uuids = self.object.uuids
last_evidence= self.object.get_last_evidence(),
uuid=self.object.last_uuid()
state_definitions = StateDefinition.objects.filter(
institution=self.request.user.institution
).order_by('order')
device_states = State.objects.filter(snapshot_uuid__in=uuids).order_by('-date')
device_logs = DeviceLog.objects.filter(
snapshot_uuid__in=uuids).order_by('-date')
device_notes = Note.objects.filter(snapshot_uuid__in=uuids).order_by('-date')
context.update({
'object': self.object,
'snapshot': last_evidence,
'lot_tags': lot_tags,
'dpps': dpps,
"state_definitions": state_definitions,
"device_states": device_states,
"device_logs": device_logs,
"device_notes": device_notes,
"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
@ -180,7 +189,7 @@ class PublicDeviceWebView(TemplateView):
return JsonResponse(device_data)
class AddUserPropertyView(DeviceLogMixin, CreateView):
class AddUserPropertyView(DashboardView, CreateView):
template_name = "new_user_property.html"
title = _("New User Property")
breadcrumb = "Device / New Property"
@ -193,107 +202,125 @@ class AddUserPropertyView(DeviceLogMixin, CreateView):
form.instance.uuid = self.property.uuid
form.instance.type = UserProperty.Type.USER
try:
response = super().form_valid(form)
messages.success(self.request, _("Property successfully added."))
log_message = _("<Created> UserProperty: {}: {}".format(
form.instance.key,
form.instance.value
))
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
)
self.log_registry(form.instance.uuid, log_message)
return response
except IntegrityError:
messages.error(self.request, _("Property is already defined."))
return self.form_invalid(form)
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.property = SystemProperty.objects.filter(
owner=institution, value=pk).first()
if not self.property:
raise Http404
self.property = get_object_or_404(SystemProperty, owner=institution, value=pk)
return super().get_form_kwargs()
def get_success_url(self):
pk = self.kwargs.get('pk')
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['pk'] = self.kwargs.get('pk')
return context
return reverse_lazy('device:details', args=[self.kwargs.get('pk')])
class UpdateUserPropertyView(DeviceLogMixin, UpdateView):
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_form_kwargs(self):
def get_queryset(self):
pk = self.kwargs.get('pk')
institution = self.request.user.institution
self.object = get_object_or_404(UserProperty, owner=institution, pk=pk)
self.old_key = self.object.key
self.old_value = self.object.value
return super().get_form_kwargs()
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']
try:
super().form_valid(form)
messages.success(self.request, _("Property updated successfully."))
log_message = _("<Updated> UserProperty: {}: {} to {}: {}".format(
self.old_key,
self.old_value,
new_key,
new_value
))
self.log_registry(form.instance.uuid, log_message)
# return response
return redirect(self.get_success_url())
except IntegrityError:
messages.error(self.request, _("Property is already defined."))
return self.form_invalid(form)
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
)
def form_invalid(self, form):
super().form_invalid(form)
return redirect(self.get_success_url())
messages.success(self.request, _("User property updated successfully."))
return super().form_valid(form)
def get_success_url(self):
pk = self.kwargs.get('device_id')
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
class DeleteUserPropertyView(DeviceLogMixin, DeleteView):
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
#using post() method because delete() method from DeleteView has some issues with messages framework
def post(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
institution = self.request.user.institution
self.object = get_object_or_404(UserProperty, owner=institution, pk=pk)
self.object = self.get_object()
self.object.delete()
msg = _("<Deleted> User Property: {}:{}".format(
self.object.key,
self.object.value
))
self.log_registry(self.object.uuid, msg)
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):
pk = self.kwargs.get('device_id')
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
class AddDocumentView(DashboardView, CreateView):
template_name = "new_user_property.html"
title = _("New Document")
breadcrumb = "Device / New document"
success_url = reverse_lazy('dashboard:unassigned_devices')
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.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.property = SystemProperty.objects.filter(
owner=institution,
value=pk,
).first()
if not self.property:
raise Http404
self.success_url = reverse_lazy('device:details', args=[pk])
kwargs = super().get_form_kwargs()
return kwargs

View file

@ -78,13 +78,16 @@ INSTALLED_APPS = [
'django_extensions',
'django_bootstrap5',
'django_tables2',
"rest_framework",
"login",
"user",
"device",
"evidence",
"lot",
"dashboard",
"action",
"tag",
"lot",
"documents",
"dashboard",
"admin",
"api",
]

View file

@ -106,10 +106,10 @@ class PublicDeviceWebView(TemplateView):
'device': {},
}
dev = Build(self.object.last_evidence.doc, None, check=True)
doc = dev.build.get_doc()
doc = dev.get_phid()
data['document'] = json.dumps(doc)
data['device'] = dev.build.device
data['components'] = dev.build.components
data['device'] = dev.device
data['components'] = dev.components
self.object.get_evidences()
last_dpp = Proof.objects.filter(
@ -118,7 +118,7 @@ class PublicDeviceWebView(TemplateView):
key = self.pk
if last_dpp:
key += ":"+last_dpp.signature
key = last_dpp.signature
url = "https://{}/did/{}".format(
self.request.get_host(),
@ -135,17 +135,17 @@ class PublicDeviceWebView(TemplateView):
for d in self.object.evidences:
d.get_doc()
dev = Build(d.doc, None, check=True)
doc = dev.build.get_doc()
doc = dev.get_phid()
ev = json.dumps(doc)
phid = dev.sign(ev)
phid = dev.get_signature(doc)
dpp = "{}:{}".format(self.pk, phid)
rr = {
'dpp': dpp,
'document': ev,
'algorithm': ALGORITHM,
'manufacturer DPP': '',
'device': dev.build.device,
'components': dev.build.components
'device': dev.device,
'components': dev.components
}
tmpl = dpp_tmpl.copy()

View file

@ -1,59 +1,17 @@
services:
devicehub-django:
init: true
image: farga.pangea.org/ereuse/devicehub-django:latest
build:
context: .
dockerfile: docker/devicehub-django.Dockerfile
environment:
- DEBUG=${DEBUG:-false}
- DOMAIN=${DEVICEHUB_DOMAIN:-localhost}
- PORT=${DEVICEHUB_PORT:-8000}
- ALLOWED_HOSTS=${DEVICEHUB_ALLOWED_HOSTS:-$DEVICEHUB_DOMAIN}
- DOMAIN=${DOMAIN:-localhost}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN}
- DEMO=${DEMO:-false}
- DEMO_IDHUB_DOMAIN=${DEMO_IDHUB_DOMAIN:-}
- DEMO_IDHUB_PREDEFINED_TOKEN=${IDHUB_PREDEFINED_TOKEN:-}
- PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-}
- DPP=${DPP:-false}
# TODO manage volumes dev vs prod
volumes:
- .:/opt/devicehub-django
ports:
- ${DEVICEHUB_PORT:-8000}:${DEVICEHUB_PORT:-8000}
- 8000:8000
# TODO add database service for idhub, meanwhile sqlite
idhub:
# https://docs.docker.com/compose/how-tos/profiles/
profiles: [idhub]
init: true
image: farga.pangea.org/ereuse/idhub:latest
environment:
- DOMAIN=${IDHUB_DOMAIN:-localhost}
- ALLOWED_HOSTS=${IDHUB_ALLOWED_HOSTS:-$IDHUB_DOMAIN}
- DEBUG=true
- DEMO=${DEMO:-false}
- INITIAL_ADMIN_EMAIL=${IDHUB_ADMIN_EMAIL}
- INITIAL_ADMIN_PASSWORD=${IDHUB_ADMIN_PASSWD}
- CREATE_TEST_USERS=true
- ENABLE_EMAIL=${IDHUB_ENABLE_EMAIL:-true}
- ENABLE_2FACTOR_AUTH=${IDHUB_ENABLE_2FACTOR_AUTH:-true}
- ENABLE_DOMAIN_CHECKER=${IDHUB_ENABLE_DOMAIN_CHECKER:-true}
- PREDEFINED_TOKEN=${IDHUB_PREDEFINED_TOKEN:-}
- SECRET_KEY=${IDHUB_SECRET_KEY:-publicsecretisnotsecureVtmKBfxpVV47PpBCF2Nzz2H6qnbd}
- STATIC_ROOT=${IDHUB_STATIC_ROOT:-/static/}
- MEDIA_ROOT=${IDHUB_MEDIA_ROOT:-/media/}
- PORT=${IDHUB_PORT:-9001}
- DEFAULT_FROM_EMAIL=${IDHUB_DEFAULT_FROM_EMAIL}
- EMAIL_HOST=${IDHUB_EMAIL_HOST}
- EMAIL_HOST_USER=${IDHUB_EMAIL_HOST_USER}
- EMAIL_HOST_PASSWORD=${IDHUB_EMAIL_HOST_PASSWORD}
- EMAIL_PORT=${IDHUB_EMAIL_PORT}
- EMAIL_USE_TLS=${IDHUB_EMAIL_USE_TLS}
- EMAIL_BACKEND=${IDHUB_EMAIL_BACKEND}
- SUPPORTED_CREDENTIALS=['Snapshot']
- SYNC_ORG_DEV=${IDHUB_SYNC_ORG_DEV}
ports:
- 9001:9001
# TODO add database service for idhub, meanwhile sqlite

View file

@ -19,24 +19,12 @@ main() {
cp -v .env.example .env
echo "WARNING: .env was not there, .env.example was copied, this only happens once"
fi
# load vars
. ./.env
if [ "${IDHUB_ENABLED:-}" = 'true' ]; then
export COMPOSE_PROFILES='idhub'
fi
# remove old database
rm -vfr ./db/*
# deactivate configured flag
rm -vfr ./already_configured
docker compose down -v
if [ "${DEV_DOCKER_ALWAYS_BUILD:-}" = 'true' ]; then
docker compose pull --ignore-buildable
docker compose build
else
docker compose pull
fi
docker compose build
docker compose up ${detach_arg:-}
}

View file

@ -6,7 +6,6 @@ RUN apt update && \
python3-xapian \
git \
sqlite3 \
curl \
jq \
time \
vim \
@ -38,7 +37,6 @@ RUN pip install -i https://test.pypi.org/simple/ ereuseapitest==0.0.14
# Set PYTHONPATH to include the directory with the xapian module
ENV PYTHONPATH="${PYTHONPATH}:/usr/lib/python3/dist-packages"
COPY . .
COPY docker/devicehub-django.entrypoint.sh /
RUN chown -R app:app /opt/devicehub-django

View file

@ -42,6 +42,19 @@ gen_env_vars() {
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
}
@ -105,54 +118,8 @@ END
./manage.py dlt_register_user "${DATASET_FILE}"
}
# wait until idhub api is prepared to received requests
wait_idhub() {
echo "Start waiting idhub API"
while true; do
result="$(curl -s "${url}" \
| jq -r .error \
|| echo "Reported errors, idhub API is still not ready")"
if [ "${result}" = "Invalid request method" ]; then
break
sleep 2
else
echo "Waiting idhub API"
sleep 3
fi
done
}
demo__send_to_sign_credential() {
filepath="${1}"
# hashlib.sha3_256 of PREDEFINED_TOKEN for idhub
DEMO_IDHUB_PREDEFINED_TOKEN="${DEMO_IDHUB_PREDEFINED_TOKEN:-}"
auth_header="Authorization: Bearer ${DEMO_IDHUB_PREDEFINED_TOKEN}"
json_header='Content-Type: application/json'
curl -s -X POST \
-H "${json_header}" \
-H "${auth_header}" \
-d @"${filepath}" \
"${url}" \
| jq -r .data
}
run_demo() {
if [ "${DEMO_IDHUB_DOMAIN:-}" ]; then
DEMO_IDHUB_DOMAIN="${DEMO_IDHUB_DOMAIN:-}"
# this demo only works with FQDN domain (with no ports)
url="https://${DEMO_IDHUB_DOMAIN}/webhook/sign/"
wait_idhub
demo__send_to_sign_credential \
'example/demo-snapshots-vc/snapshot_pre-verifiable-credential.json' \
> 'example/snapshots/snapshot_workbench-script_verifiable-credential.json'
fi
./manage.py create_default_states "${INIT_ORG}"
/usr/bin/time ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
}
config_phase() {
# TODO review this flag file
# TODO review this flag file
init_flagfile="${program_dir}/already_configured"
if [ ! -f "${init_flagfile}" ]; then
@ -165,7 +132,7 @@ config_phase() {
# 12, 13, 14
config_dpp_part1
# cleanup other snapshots and copy dlt/dpp snapshots
# cleanup other spnapshots and copy dlt/dpp snapshots
# TODO make this better
rm example/snapshots/*
cp example/dpp-snapshots/*.json example/snapshots/
@ -173,7 +140,7 @@ config_phase() {
# # 15. Add inventory snapshots for user "${INIT_USER}".
if [ "${DEMO:-}" = 'true' ]; then
run_demo
/usr/bin/time ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
fi
# remain next command as the last operation for this if conditional
@ -188,11 +155,9 @@ check_app_is_there() {
}
deploy() {
if [ -d /opt/devicehub-django/.git ]; then
# TODO this is weird, find better workaround
git config --global --add safe.directory "${program_dir}"
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
fi
# TODO this is weird, find better workaround
git config --global --add safe.directory "${program_dir}"
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
if [ "${DEBUG:-}" = 'true' ]; then
./manage.py print_settings
@ -208,9 +173,6 @@ deploy() {
# move the migrate thing in docker entrypoint
# inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc
echo "INFO detected NEW deployment"
if [ ! -d "${program_dir}/db/" ]; then
mkdir -p "${program_dir}/db/"
fi
./manage.py migrate
config_phase
fi

0
documents/__init__.py Normal file
View file

3
documents/admin.py Normal file
View file

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

6
documents/apps.py Normal file
View file

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

View file

3
documents/models.py Normal file
View file

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

3
documents/tests.py Normal file
View file

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

3
documents/views.py Normal file
View file

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

View file

@ -12,7 +12,7 @@ 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()
@ -22,9 +22,9 @@ class ProofView(View):
ev = Evidence(proof.uuid)
if not ev.doc:
return JsonResponse({}, status=404)
dev = Build(ev.doc, None, check=True)
doc = dev.build.get_doc()
doc = dev.get_phid()
data = {
"algorithm": ALGORITHM,

View file

@ -58,9 +58,7 @@ class UploadForm(forms.Form):
for ev in self.evidences:
path_name = save_in_disk(ev[1], user.institution.name)
build = Build
file_json = ev[1]
build(file_json, user)
Build(ev[1], user)
move_json(path_name, user.institution.name)

View file

@ -1,74 +0,0 @@
import json
import logging
from dmidecode import DMIParse
from json_repair import repair_json
from evidence.mixin_parse import BuildMix
from evidence.legacy_parse_details import get_lshw_child, ParseSnapshot
from utils.constants import CHASSIS_DH
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))
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
class Build(BuildMix):
# This parse is for get info from snapshots created with
# workbench-script but builded for send to devicehub-teal
def get_details(self):
dmidecode_raw = self.json["data"]["dmidecode"]
self.dmi = DMIParse(dmidecode_raw)
self.manufacturer = self.dmi.manufacturer().strip()
self.model = self.dmi.model().strip()
self.chassis = self.get_chassis_dh()
self.serial_number = self.dmi.serial_number()
self.sku = self.get_sku()
self.type = self.chassis
self.version = self.get_version()
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_version(self):
return self.dmi.get("System")[0].get("Verson", '_virtual')
def _get_components(self):
data = ParseSnapshot(self.json)
self.device = data.device
self.components = data.components
self.device.pop("actions", None)
for c in self.components:
c.pop("actions", None)

View file

@ -1,503 +0,0 @@
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
logger = logging.getLogger('django')
def get_lshw_child(child, nets, component):
try:
if child.get('id') == component:
nets.append(child)
if child.get('children'):
[get_lshw_child(x, nets, component) for x in child['children']]
except Exception:
return []
class ParseSnapshot:
def __init__(self, snapshot, default="n/a"):
self.default = default
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.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": snapshot["software"],
"components": self.components,
"uuid": snapshot['uuid'],
"version": snapshot['version'],
"endTime": snapshot["timestamp"],
"elapsed": 1,
}
def set_computer(self):
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_cpu()
self.get_ram()
self.get_mother_board()
self.get_graphic()
self.get_data_storage()
self.get_display()
self.get_sound_card()
self.get_networks()
def get_cpu(self):
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(
{
"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_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(
{
"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": "",
"manufacturer": c.get("vendor", self.default),
"model": c.get("product", self.default),
"serialNumber": c.get("serial", self.default),
}
)
def get_data_storage(self):
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])
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,
}
)
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 Exception:
pass
return bogomips
def get_networks(self):
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": 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):
multimedias = []
get_lshw_child(self.lshw, multimedias, 'multimedia')
for c in multimedias:
self.components.append(
{
"actions": [],
"type": "SoundCard",
"model": c.get('product'),
"manufacturer": c.get('vendor'),
"serialNumber": c.get('serial'),
}
)
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": 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
try:
for ch in self.lshw.get('children', []):
for c in ch.get('children', []):
if c['class'] == 'processor':
return c.get('width', default)
except:
return default
return default
def get_usb_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "USB" in u.get("Port Type", "").upper()
]
)
def get_serial_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "SERIAL" in u.get("Port Type", "").upper()
]
)
def get_firmware_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "FIRMWARE" in u.get("Port Type", "").upper()
]
)
def get_pcmcia_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "PCMCIA" in u.get("Port Type", "").upper()
]
)
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(txt))
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:
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 {}
return x
def errors(self, txt=None):
if not txt:
return self._errors
logger.error(txt)
self._errors.append("%s", txt)

View file

@ -4,6 +4,7 @@ import logging
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.conf import settings
from utils.save_snapshots import move_json, save_in_disk
from evidence.parse import Build

View file

@ -1,19 +0,0 @@
# Generated by Django 5.0.6 on 2025-01-29 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("evidence", "0004_remove_userproperty_user_unique_type_key_uuid"),
]
operations = [
migrations.AlterField(
model_name="userproperty",
name="type",
field=models.SmallIntegerField(
choices=[(1, "User"), (2, "EraseServer")], default=1
),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 5.0.6 on 2025-01-30 17:52
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evidence', '0005_alter_userproperty_type'),
('user', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddConstraint(
model_name='userproperty',
constraint=models.UniqueConstraint(fields=('key', 'uuid'), name='userproperty_unique_type_key_uuid'),
),
]

View file

@ -1,64 +0,0 @@
import logging
from django.conf import settings
from utils.constants import ALGOS
logger = logging.getLogger('django')
class BuildMix:
def __init__(self, evidence_json):
self.json = evidence_json
self.uuid = self.json.get('uuid')
self.manufacturer = ""
self.model = ""
self.serial_number = ""
self.chassis = ""
self.sku = ""
self.mac = ""
self.type = ""
self.version = ""
self.default = ""
self.algorithms = {}
if not self.uuid:
logger.error("snapshot without UUID. Software {}".format(self.json.get("software")))
return
self.get_details()
self.generate_chids()
def get_hid(self, algo):
algorithm = ALGOS.get(algo, [])
hid = ""
for f in algorithm:
if hasattr(self, f):
hid += getattr(self, f) or ''
return hid
def generate_chids(self):
self.algorithms = {}
for k in ALGOS.keys():
if not settings.DPP and k == 'ereuse22':
continue
self.algorithms[k] = self.get_hid(k)
def get_doc(self):
self._get_components()
components = sorted(self.components, key=lambda x: x.get("type"))
device = self.algorithms.get('ereuse22')
doc = [("computer", device)]
for c in components:
doc.append((c.get("type"), self.get_id_hw_dpp(c)))
return doc
def get_id_hw_dpp(self, d):
algorithm = ALGOS.get("ereuse22", [])
hid = ""
for f in algorithm:
hid += d.get(f, '')
return hid

View file

@ -8,8 +8,7 @@ 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
from evidence.normal_parse_details import get_inxi, get_inxi_key
from evidence.parse_details import ParseSnapshot, get_inxi, get_inxi_key
from user.models import User, Institution
@ -37,20 +36,15 @@ class SystemProperty(Property):
class UserProperty(Property):
uuid = models.UUIDField()
class Type(models.IntegerChoices):
USER = 1, "User"
ERASE_SERVER = 2, "EraseServer"
DOCUMENT = 2, "Document"
ERASE_SERVER = 3, "EraseServer"
uuid = models.UUIDField()
type = models.SmallIntegerField(choices=Type, default=Type.USER)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["key", "uuid"], name="userproperty_unique_type_key_uuid")
]
class Evidence:
def __init__(self, uuid):
@ -111,7 +105,7 @@ class Evidence:
self.inxi = ev["output"]
else:
dmidecode_raw = self.doc["data"]["dmidecode"]
inxi_raw = self.doc.get("data", {}).get("inxi")
inxi_raw = self.doc["data"]["inxi"]
self.dmi = DMIParse(dmidecode_raw)
try:
self.inxi = json.loads(inxi_raw)
@ -135,11 +129,9 @@ class Evidence:
if not self.doc:
self.get_doc()
self.created = self.doc.get("endTime")
if not self.created:
self.created = self.get_time_created()
def get_time_created(self):
return self.properties.last().created.isoformat()
if not self.created:
self.created = self.properties.last().created
def get_components(self):
if self.is_legacy():
@ -155,7 +147,7 @@ class Evidence:
return list(self.doc.get('kv').values())[0]
if self.is_legacy():
return self.doc.get('device', {}).get('manufacturer', '')
return self.doc['device']['manufacturer']
if self.inxi:
return self.device_manufacturer
@ -170,7 +162,7 @@ class Evidence:
return list(self.doc.get('kv').values())[1]
if self.is_legacy():
return self.doc.get('device', {}).get('model', '')
return self.doc['device']['model']
if self.inxi:
return self.device_model
@ -179,7 +171,7 @@ class Evidence:
def get_chassis(self):
if self.is_legacy():
return self.doc.get('device', {}).get('model', '')
return self.doc['device']['model']
if self.inxi:
return self.device_chassis
@ -194,7 +186,7 @@ class Evidence:
def get_serial_number(self):
if self.is_legacy():
return self.doc.get('device', {}).get('serialNumber', '')
return self.doc['device']['serialNumber']
if self.inxi:
return self.device_serial_number
@ -211,11 +203,12 @@ class Evidence:
def get_all(cls, user):
return SystemProperty.objects.filter(
owner=user.institution,
key="ereuse24",
key="hidalgo1",
).order_by("-created").values_list("uuid", "created").distinct()
def set_components(self):
self.components = ParseSnapshot(self.doc).components
snapshot = ParseSnapshot(self.doc).snapshot_json
self.components = snapshot['components']
def is_legacy(self):
if self.doc.get("credentialSubject"):
@ -225,14 +218,3 @@ class Evidence:
def is_web_snapshot(self):
return self.doc.get("type") == "WebSnapshot"
def did_document(self):
if not self.doc.get("credentialSubject"):
return ''
did = self.doc.get('issuer')
if not "did:web" in did:
return ''
return "https://{}/did.json".format(
did.split("did:web:")[1].replace(":", "/")
)

View file

@ -1,76 +0,0 @@
import json
import logging
from evidence.mixin_parse import BuildMix
from evidence.normal_parse_details import get_inxi_key, get_inxi, ParseSnapshot
logger = logging.getLogger('django')
def get_mac(inxi):
nets = get_inxi_key(inxi, "Network")
n_nets = len(nets) - 1
for i in range(0, n_nets):
if i + 1 > n_nets:
break
n = nets[i]
iface = nets[i + 1]
if get_inxi(n, "port"):
return get_inxi(iface, 'mac')
class Build(BuildMix):
def get_details(self):
self.from_credential()
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 ""
machine = get_inxi_key(self.inxi, 'Machine')
for m in machine:
system = get_inxi(m, "System")
if system:
self.manufacturer = system
self.model = get_inxi(m, "product")
self.serial_number = get_inxi(m, "serial")
self.type = get_inxi(m, "Type")
self.chassis = self.type
self.version = get_inxi(m, "v")
else:
self.sku = get_inxi(m, "part-nu")
self.mac = get_mac(self.inxi) or ""
if not self.mac:
txt = "Could not retrieve MAC address in snapshot %s"
logger.warning(txt, self.uuid)
def from_credential(self):
if not self.json.get("credentialSubject"):
return
self.uuid = self.json.get("credentialSubject", {}).get("uuid")
self.json.update(self.json["credentialSubject"])
if self.json.get("evidence"):
self.json["data"] = {}
for ev in self.json["evidence"]:
k = ev.get("operation")
if not k:
continue
self.json["data"][k] = ev.get("output")
def _get_components(self):
data = ParseSnapshot(self.json)
self.device = data.device
self.components = data.components
self.device.pop("actions", None)
for c in self.components:
c.pop("actions", None)

View file

@ -1,410 +0,0 @@
import json
import logging
from dmidecode import DMIParse
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 ""
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.device = {"actions": []}
self.components = []
self.dmi = DMIParse(self.dmidecode_raw)
self.smart = self.loads(self.smart_raw)
self.inxi = self.loads(self.inxi_raw)
self.set_computer()
self.set_components()
self.snapshot_json = {
"type": "Snapshot",
"device": self.device,
"software": data["software"],
"components": self.components,
"uuid": data['uuid'],
"endTime": data["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")
def set_components(self):
self.get_mother_board()
self.get_cpu()
self.get_ram()
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")
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
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
}
)
def get_battery(self):
bats = get_inxi_key(self.inxi, 'Battery') or []
for b in bats:
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")
}
)
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
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":
continue
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")
}
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_networks(self):
nets = get_inxi_key(self.inxi, "Network") or []
for i in range(0, len(nets)-1):
n = nets[i]
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"
manufacturer = get_inxi(n, "manufacturer")
speed = get_inxi(n, "speed")
mac = ""
if len(nets) > i+1:
iface = nets[i+1]
mac = get_inxi(iface, 'mac')
if not speed:
speed = get_inxi(iface, "speed")
self.components.append({
"type": "NetworkAdapter",
"model": model,
"manufacturer": manufacturer,
"serialNumber": mac,
"speed": speed,
"interface": interface,
})
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
self.components.append(
{
"type": "SoundCard",
"model": model,
"manufacturer": get_inxi(c, 'vendor'),
"serialNumber": get_inxi(c, 'serial'),
}
)
def get_display(self):
graphics = get_inxi_key(self.inxi, "Graphics") or []
for c in graphics:
if not get_inxi(c, "Monitor"):
continue
self.components.append(
{
"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"),
}
)
def get_usb_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "USB" in u.get("Port Type", "").upper()
]
)
def get_serial_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "SERIAL" in u.get("Port Type", "").upper()
]
)
def get_firmware_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "FIRMWARE" in u.get("Port Type", "").upper()
]
)
def get_pcmcia_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "PCMCIA" in u.get("Port Type", "").upper()
]
)
def get_bios_version(self):
return self.dmi.get("BIOS")[0].get("BIOS Revision", '1')
def loads(self, x):
if isinstance(x, str):
try:
return json.loads(x)
except Exception as ss:
logger.warning("%s", ss)
return {}
return x
def errors(self, txt=None):
if not txt:
return self._errors
logger.error(txt)
self._errors.append("%s", txt)

View file

@ -1,28 +0,0 @@
import logging
from evidence.mixin_parse import BuildMix
logger = logging.getLogger('django')
class Build(BuildMix):
# This parse is for get info from snapshots created with old workbench
# normaly is worbench 11
def get_details(self):
self.device = self.json.get('device', {})
self.manufacturer = self.device.get("manufacturer", '')
self.model = self.device.get("model", '')
self.chassis = self.device.get("chassis", '')
self.serial_number = self.device.get("serialNumber", '')
self.sku = self.device.get("sku", '')
self.type = self.device.get("type", '')
self.version = self.device.get("version", '')
def _get_components(self):
self.components = self.json.get("components", [])
self.device.pop("actions", None)
for c in self.components:
c.pop("actions", None)

View file

@ -1,13 +0,0 @@
import logging
logger = logging.getLogger('django')
class ParseSnapshot:
def __init__(self, snapshot, default="n/a"):
self.default = default
self.snapshot_json = snapshot
self.device = snapshot.get("device")
self.components = snapshot.get("components")

View file

@ -2,14 +2,12 @@ import json
import hashlib
import logging
from evidence import legacy_parse
from evidence import old_parse
from evidence import normal_parse
from dmidecode import DMIParse
from evidence.parse_details import ParseSnapshot
from evidence.models import SystemProperty
from evidence.xapian import index
from evidence.normal_parse_details import get_inxi_key, get_inxi
from evidence.parse_details import get_inxi_key, get_inxi
from django.conf import settings
if settings.DPP:
@ -26,40 +24,34 @@ def get_mac(inxi):
if get_inxi(n, "port"):
return get_inxi(iface, 'mac')
class Build:
def __init__(self, evidence_json, user, check=False):
"""
This Build do the save in xapian as document, in Annotations and do
register in dlt if is configured for that.
We have 4 cases for parser diferents snapshots than come from workbench.
1) worbench 11 is old_parse.
2) legacy is the worbench-script when create a snapshot for devicehub-teal
3) some snapshots come as a credential. In this case is parsed as normal_parse
4) normal snapshot from worbench-script is the most basic and is parsed as normal_parse
"""
self.evidence = evidence_json.copy()
self.uuid = self.evidence.get('uuid')
self.user = user
self.json = evidence_json.copy()
if evidence_json.get("credentialSubject"):
self.build = normal_parse.Build(evidence_json)
self.uuid = evidence_json.get("credentialSubject", {}).get("uuid")
elif evidence_json.get("software") != "workbench-script":
self.build = old_parse.Build(evidence_json)
elif evidence_json.get("data",{}).get("lshw"):
self.build = legacy_parse.Build(evidence_json)
else:
self.build = normal_parse.Build(evidence_json)
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.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
if not self.build.uuid:
return
self.index()
self.create_annotations()
self.create_properties()
if settings.DPP:
self.register_device_dlt()
@ -67,32 +59,123 @@ class Build:
snap = json.dumps(self.evidence)
index(self.user.institution, self.uuid, snap)
def create_annotations(self):
prop = SystemProperty.objects.filter(
def generate_chids(self):
self.algorithms = {
'hidalgo1': self.get_hid_14(),
'legacy_dpp': self.get_chid_dpp(),
}
def get_hid_14(self):
if self.json.get("software") == "workbench-script":
hid = self.get_hid(self.json)
else:
device = self.json['device']
manufacturer = device.get("manufacturer", '')
model = device.get("model", '')
chassis = device.get("chassis", '')
serial_number = device.get("serialNumber", '')
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']
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_properties(self):
property = SystemProperty.objects.filter(
uuid=self.uuid,
owner=self.user.institution,
)
if prop:
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.build.algorithms.items():
for k, v in self.algorithms.items():
SystemProperty.objects.create(
uuid=self.uuid,
owner=self.user.institution,
user=self.user,
key=k,
value=self.sign(v)
value=v
)
def sign(self, doc):
return hashlib.sha3_256(doc.encode()).hexdigest()
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 ""
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")
mac = get_mac(self.inxi) 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):
legacy_dpp = self.build.algorithms.get('ereuse22')
chid = self.sign(legacy_dpp)
phid = self.sign(json.dumps(self.build.get_doc()))
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,38 +1,407 @@
import re
import json
import logging
import numpy as np
from evidence import (
legacy_parse_details,
normal_parse_details,
old_parse_details
)
from datetime import datetime
from dmidecode import DMIParse
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 ""
class ParseSnapshot:
def __init__(self, snapshot, default="n/a"):
if snapshot.get("credentialSubject"):
self.build = normal_parse_details.ParseSnapshot(
snapshot,
default=default
)
elif snapshot.get("software") != "workbench-script":
self.build = old_parse_details.ParseSnapshot(
snapshot,
default=default
)
elif snapshot.get("data",{}).get("lshw"):
self.build = legacy_parse_details.ParseSnapshot(
snapshot,
default=default
)
else:
self.build = normal_parse_details.ParseSnapshot(
snapshot,
default=default
)
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.default = default
self.device = self.build.snapshot_json.get("device")
self.components = self.build.snapshot_json.get("components")
self.device = {"actions": []}
self.components = []
self.dmi = DMIParse(self.dmidecode_raw)
self.smart = self.loads(self.smart_raw)
self.inxi = self.loads(self.inxi_raw)
self.set_computer()
self.set_components()
self.snapshot_json = {
"type": "Snapshot",
"device": self.device,
"software": data["software"],
"components": self.components,
"uuid": data['uuid'],
"endTime": data["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")
def set_components(self):
self.get_mother_board()
self.get_cpu()
self.get_ram()
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")
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
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
}
)
def get_battery(self):
bats = get_inxi_key(self.inxi, 'Battery') or []
for b in bats:
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")
}
)
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
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":
continue
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")
}
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_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"
self.components.append(
{
"type": "NetworkAdapter",
"model": model,
"manufacturer": get_inxi(n, 'vendor'),
"serialNumber": get_inxi(iface, 'mac'),
"speed": get_inxi(n, "speed"),
"interface": interface,
}
)
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
self.components.append(
{
"type": "SoundCard",
"model": model,
"manufacturer": get_inxi(c, 'vendor'),
"serialNumber": get_inxi(c, 'serial'),
}
)
def get_display(self):
graphics = get_inxi_key(self.inxi, "Graphics") or []
for c in graphics:
if not get_inxi(c, "Monitor"):
continue
self.components.append(
{
"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"),
}
)
def get_usb_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "USB" in u.get("Port Type", "").upper()
]
)
def get_serial_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "SERIAL" in u.get("Port Type", "").upper()
]
)
def get_firmware_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "FIRMWARE" in u.get("Port Type", "").upper()
]
)
def get_pcmcia_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "PCMCIA" in u.get("Port Type", "").upper()
]
)
def get_bios_version(self):
return self.dmi.get("BIOS")[0].get("BIOS Revision", '1')
def loads(self, x):
if isinstance(x, str):
try:
return json.loads(x)
except Exception as ss:
logger.warning("%s", ss)
return {}
return x
def errors(self, txt=None):
if not txt:
return self._errors
logger.error(txt)
self._errors.append("%s", txt)

43
evidence/serializers.py Normal file
View file

@ -0,0 +1,43 @@
from rest_framework import serializers
from evidence.models import EvidenceJson
import json
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
from evidence.parse import Parse
class EvidenceSerializer(serializers.ModelSerializer):
class Meta:
model = EvidenceJson
fields = ['id', 'title', 'content']
@csrf_exempt
def webhook_verify(request):
if request.method == 'POST':
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return JsonResponse({'error': 'Invalid authorization'}, status=401)
token = auth_header.split(' ')[1]
tk = Token.objects.filter(token=token).first()
if not tk:
return JsonResponse({'error': 'Invalid authorization'}, status=401)
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
try:
device = Parse(data)
except Exception:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
if not device:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
return JsonResponse({"result": "Ok"}, status=200)
return JsonResponse({'error': 'Invalid request method'}, status=400)

View file

@ -11,8 +11,8 @@
<!-- override invalid-feedback class -->
<style>
.invalid-feedback {
color: #670000;
font-size: 1rem;
color: #670000;
font-size: 1rem;
}
</style>
@ -22,7 +22,7 @@
{% bootstrap_form form alert_error_type="none" error_css_class="alert alert-danger alert-icon alert-icon-border" %}
<div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>

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

@ -17,18 +17,18 @@ class LoginView(auth_views.LoginView):
template_name = 'login.html'
extra_context = {
'title': _('Login'),
'success_url': reverse_lazy('dashboard:unassigned'),
'success_url': reverse_lazy('dashboard:unassigned_devices'),
'commit_id': settings.COMMIT,
}
def get(self, request, *args, **kwargs):
self.extra_context['success_url'] = request.GET.get(
'next',
reverse_lazy('dashboard:unassigned')
reverse_lazy('dashboard:unassigned_devices')
)
if not self.request.user.is_anonymous:
return redirect(reverse_lazy('dashboard:unassigned'))
return redirect(reverse_lazy('dashboard:unassigned_devices'))
return super().get(request, *args, **kwargs)
def form_valid(self, form):
@ -72,3 +72,4 @@ class PasswordResetView(auth_views.PasswordResetView):
except Exception as err:
logger.error(err)
return HttpResponseRedirect(self.success_url)

View file

@ -1,19 +0,0 @@
# Generated by Django 5.0.6 on 2025-01-29 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("lot", "0004_remove_lotproperty_lot_unique_type_key_lot_and_more"),
]
operations = [
migrations.AlterField(
model_name="lotproperty",
name="type",
field=models.SmallIntegerField(
choices=[(0, "System"), (1, "User")], default=1
),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 5.0.6 on 2025-01-31 10:33
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lot', '0005_alter_lotproperty_type'),
('user', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddConstraint(
model_name='lotproperty',
constraint=models.UniqueConstraint(fields=('key', 'lot'), name='property_unique_type_key_lot'),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 5.0.6 on 2025-02-17 10:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lot', '0006_lotproperty_property_unique_type_key_lot'),
]
operations = [
migrations.AddField(
model_name='lottag',
name='inbox',
field=models.BooleanField(default=False),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 5.0.6 on 2025-02-25 12:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('lot', '0007_lottag_inbox'),
]
operations = [
migrations.RenameField(
model_name='lot',
old_name='closed',
new_name='archived',
),
]

View file

@ -6,8 +6,8 @@ from utils.constants import (
STR_EXTEND_SIZE,
)
from user.models import User, Institution
from evidence.models import Property
from user.models import User, Institution
from evidence.models import Property
# from device.models import Device
@ -15,7 +15,6 @@ class LotTag(models.Model):
name = models.CharField(max_length=STR_SIZE, blank=False, null=False)
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
inbox = models.BooleanField(default=False)
def __str__(self):
return self.name
@ -32,7 +31,7 @@ class Lot(models.Model):
name = models.CharField(max_length=STR_SIZE, blank=True, null=True)
code = models.CharField(max_length=STR_SIZE, blank=True, null=True)
description = models.CharField(max_length=STR_SIZE, blank=True, null=True)
archived = models.BooleanField(default=False)
closed = models.BooleanField(default=False)
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.ForeignKey(LotTag, on_delete=models.CASCADE)
@ -41,22 +40,17 @@ class Lot(models.Model):
if DeviceLot.objects.filter(lot=self, device_id=v).exists():
return
DeviceLot.objects.create(lot=self, device_id=v)
def remove(self, v):
for d in DeviceLot.objects.filter(lot=self, device_id=v):
d.delete()
class LotProperty(Property):
class LotProperty (Property):
lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
class Type(models.IntegerChoices):
SYSTEM = 0, "System"
USER = 1, "User"
DOCUMENT = 2, "Document"
type = models.SmallIntegerField(choices=Type.choices, default=Type.USER)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["key", "lot"], name="property_unique_type_key_lot")
]

View file

@ -30,7 +30,7 @@
{% endif %}
{% bootstrap_form form %}
<div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Delete' %}" />
</div>

View file

@ -0,0 +1,48 @@
{% 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_document' lot.pk %}" class="btn btn-primary">
<i class="bi bi-plus"></i>
Add new document
<span class="caret"></span>
</a>
</div>
<h5 class="card-title mt-2">Documents</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 documents %}
<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

@ -11,26 +11,20 @@
<form method="post">
{% csrf_token %}
<table class="table">
<thead>
<tr>
<th>Select</th>
<th>Lot Group / Lot</th>
</tr>
</thead>
<tbody>
{% for tag in lot_tags %}
{% for lot in lots %}
{% if lot.type == tag %}
<tr>
<td><input type="checkbox" name="lots" value="{{ lot.id }}" /></td>
<td>{{ tag }}/{{ lot.name }}</td>
</tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody>
</table>
{% for tag in lot_tags %}
<div class="row">
<div class="col-lg-3 col-md-4 label ">{{ tag }}</div>
</div>
{% for lot in lots %}
{% if lot.type == tag %}
<div class="row">
<div class="col-lg-3 col-md-4 label "><input type="checkbox" name="lots" value="{{ lot.id }}" /></div>
<div class="col-lg-3 col-md-4 label ">{{ lot.name }}</div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
<button class="btn btn-green-admin" type="submit">Save</button>
</form>

View file

@ -7,13 +7,13 @@
<h3>{{ subtitle }}</h3>
</div>
<div class="col text-center">
{% if show_archived %}
<a href="?show_archived=false" class="btn btn-green-admin">
{% trans 'Show active lots' %}
{% if show_closed %}
<a href="?show_closed=false" class="btn btn-green-admin">
{% trans 'Hide closed lots' %}
</a>
{% else %}
<a href="?show_archived=true" class="btn btn-green-admin">
{% trans 'Show archived lots' %}
<a href="?show_closed=true" class="btn btn-green-admin">
{% trans 'Show closed lots' %}
</a>
{% endif %}

View file

@ -24,7 +24,7 @@
{% endif %}
{% bootstrap_form form %}
<div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>

View file

@ -36,7 +36,7 @@
{% endfor %}
</div>
<div class="container">
<a class="btn btn-grey" href="{% url 'lot:properties' lot_id %}">{% translate "Cancel" %}</a>
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>

View file

@ -10,8 +10,8 @@
<div class="row">
<div class="tab-pane fade show active" id="details">
<div class="d-flex justify-content-end mt-1 mb-3">
<a href="{% url 'lot:add_property' lot.pk %}" class="btn btn-green-admin d-flex align-items-center">
<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
@ -20,35 +20,34 @@
</div>
<h5 class="card-title mt-2">Properties</h5>
<table class="table table-hover table-bordered table-responsive align-middle">
<thead class="table-light">
<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" class="text-end" data-format="YYYY-MM-DD HH:mm">{% trans 'Created on' %}</th>
<th scope="col" width="5%" class="text-end"></th>
</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 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="#editPropertyModal{{ 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="#deletePropertyModal{{ a.id }}">
<i class="bi bi-trash me-1"></i>
{% trans 'Delete' %}
</button>
</div>
</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">
@ -76,7 +75,7 @@
</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">
@ -98,7 +97,7 @@
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>

View file

@ -9,7 +9,9 @@ urlpatterns = [
path("edit/<int:pk>/", views.EditLotView.as_view(), name="edit"),
path("add/devices/", views.AddToLotView.as_view(), name="add_devices"),
path("del/devices/", views.DelToLotView.as_view(), name="del_devices"),
path("group/<int:pk>/", views.LotsTagsView.as_view(), name="tags"),
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>/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"),

View file

@ -1,4 +1,3 @@
from django.db import IntegrityError
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404, redirect, Http404
from django.contrib import messages
@ -18,24 +17,16 @@ class NewLotView(DashboardView, CreateView):
template_name = "new_lot.html"
title = _("New lot")
breadcrumb = "lot / New lot"
success_url = reverse_lazy('dashboard:unassigned')
success_url = reverse_lazy('dashboard:unassigned_devices')
model = Lot
fields = (
"type",
"name",
"code",
"description",
"archived",
"closed",
)
def get_form(self):
form = super().get_form()
form.fields["type"].queryset = LotTag.objects.filter(
owner=self.request.user.institution,
inbox=False
)
return form
def form_valid(self, form):
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
@ -47,14 +38,14 @@ class DeleteLotView(DashboardView, DeleteView):
template_name = "delete_lot.html"
title = _("Delete lot")
breadcrumb = "lot / Delete lot"
success_url = reverse_lazy('dashboard:unassigned')
success_url = reverse_lazy('dashboard:unassigned_devices')
model = Lot
fields = (
"type",
"name",
"code",
"description",
"archived",
"closed",
)
def form_valid(self, form):
@ -66,14 +57,14 @@ class EditLotView(DashboardView, UpdateView):
template_name = "new_lot.html"
title = _("Edit lot")
breadcrumb = "Lot / Edit lot"
success_url = reverse_lazy('dashboard:unassigned')
success_url = reverse_lazy('dashboard:unassigned_devices')
model = Lot
fields = (
"type",
"name",
"code",
"description",
"archived",
"closed",
)
def get_form_kwargs(self):
@ -87,20 +78,12 @@ class EditLotView(DashboardView, UpdateView):
kwargs = super().get_form_kwargs()
return kwargs
def get_form(self):
form = super().get_form()
form.fields["type"].queryset = LotTag.objects.filter(
owner=self.request.user.institution,
inbox=False
)
return form
class AddToLotView(DashboardView, FormView):
template_name = "list_lots.html"
title = _("Add to lots")
breadcrumb = "lot / add to lots"
success_url = reverse_lazy('dashboard:unassigned')
success_url = reverse_lazy('dashboard:unassigned_devices')
form_class = LotsForm
def get_context_data(self, **kwargs):
@ -115,8 +98,7 @@ class AddToLotView(DashboardView, FormView):
def get_form(self):
form = super().get_form()
form.fields["lots"].queryset = Lot.objects.filter(
owner=self.request.user.institution)
form.fields["lots"].queryset = Lot.objects.filter(owner=self.request.user.institution)
return form
def form_valid(self, form):
@ -141,7 +123,7 @@ class LotsTagsView(DashboardView, TemplateView):
template_name = "lots.html"
title = _("lots")
breadcrumb = _("lots") + " /"
success_url = reverse_lazy('dashboard:unassigned')
success_url = reverse_lazy('dashboard:unassigned_devices')
def get_context_data(self, **kwargs):
self.pk = kwargs.get('pk')
@ -149,15 +131,60 @@ class LotsTagsView(DashboardView, TemplateView):
tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk)
self.title += " {}".format(tag.name)
self.breadcrumb += " {}".format(tag.name)
show_archived = self.request.GET.get('show_archived', 'false') == 'true'
lots = Lot.objects.filter(owner=self.request.user.institution).filter(
type=tag, archived=show_archived
)
show_closed = self.request.GET.get('show_closed', 'false') == 'true'
lots = Lot.objects.filter(owner=self.request.user.institution).filter(type=tag, closed=show_closed)
context.update({
'lots': lots,
'title': self.title,
'breadcrumb': self.breadcrumb,
'show_archived': show_archived
'show_closed': show_closed
})
return context
class LotAddDocumentView(DashboardView, CreateView):
template_name = "new_property.html"
title = _("New Document")
breadcrumb = "Device / New document"
success_url = reverse_lazy('dashboard:unassigned_devices')
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 = LotProperty.Type.DOCUMENT
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:documents', args=[pk])
kwargs = super().get_form_kwargs()
return kwargs
class LotDocumentsView(DashboardView, TemplateView):
template_name = "documents.html"
title = _("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 = LotProperty.objects.filter(
lot=lot,
owner=self.request.user.institution,
type=LotProperty.Type.DOCUMENT,
)
context.update({
'lot': lot,
'documents': documents,
'title': self.title,
'breadcrumb': self.breadcrumb
})
return context
@ -198,13 +225,8 @@ class AddLotPropertyView(DashboardView, CreateView):
form.instance.user = self.request.user
form.instance.lot = self.lot
form.instance.type = LotProperty.Type.USER
try:
response = super().form_valid(form)
messages.success(self.request, _("Property successfully added."))
return response
except IntegrityError:
messages.error(self.request, _("Property is already defined."))
return self.form_invalid(form)
response = super().form_valid(form)
return response
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
@ -213,11 +235,6 @@ class AddLotPropertyView(DashboardView, CreateView):
kwargs = super().get_form_kwargs()
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['lot_id'] = self.lot.id
return context
class UpdateLotPropertyView(DashboardView, UpdateView):
template_name = "properties.html"
@ -228,33 +245,31 @@ class UpdateLotPropertyView(DashboardView, UpdateView):
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
lot_property = get_object_or_404(
LotProperty,
pk=pk,
owner=self.request.user.institution
)
lot_property = get_object_or_404(LotProperty, pk=pk, owner=self.request.user.institution)
if not lot_property:
raise Http404
lot_pk = lot_property.lot.pk
self.success_url = reverse_lazy('lot:properties', args=[lot_pk])
kwargs = super().get_form_kwargs()
kwargs['instance'] = lot_property
return kwargs
def form_valid(self, form):
try:
response = super().form_valid(form)
messages.success(self.request, _("Property updated successfully."))
return response
except IntegrityError:
messages.error(self.request, _("Property is already defined."))
return self.form_invalid(form)
old_key= self.object.key
old_value = self.object.value
new_key = form.cleaned_data['key']
new_value = form.cleaned_data['value']
def form_invalid(self, form):
super().form_invalid(form)
return redirect(self.get_success_url())
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):
@ -262,15 +277,18 @@ class DeleteLotPropertyView(DashboardView, DeleteView):
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
)
lot_pk = self.object.lot.pk
old_value = self.object.key
self.object.delete()
messages.success(self.request, _("Lot property deleted successfully."))
self.success_url = reverse_lazy('lot:properties', args=[lot_pk])
# Redirect back to the original URL
return redirect(self.success_url)
return redirect(referer)

0
tag/__init__.py Normal file
View file

3
tag/admin.py Normal file
View file

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

6
tag/apps.py Normal file
View file

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

View file

3
tag/models.py Normal file
View file

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

3
tag/tests.py Normal file
View file

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

3
tag/views.py Normal file
View file

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

View file

@ -1,8 +0,0 @@
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
tests-examples
example.spec.ts

View file

@ -1,97 +0,0 @@
{
"name": "end-to-end",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "end-to-end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.49.1",
"@types/node": "^22.10.7"
}
},
"node_modules/@playwright/test": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@types/node": {
"version": "22.10.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
}
}
}

View file

@ -1,15 +0,0 @@
{
"name": "end-to-end",
"version": "1.0.0",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"description": "",
"devDependencies": {
"@playwright/test": "^1.49.1",
"@types/node": "^22.10.7"
}
}

View file

@ -1,77 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View file

@ -1,21 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: AGPL-3.0-or-later
set -e
set -u
# DEBUG
set -x
main() {
cd "$(dirname "${0}")"
browser="${browser:-firefox}"
project="${project:-firefox}"
headed="${headed:---headed}"
npx playwright test --project "${project}" "${headed}"
}
main "${@}"
# written in emacs
# -*- mode: shell-script; -*-

View file

@ -1,237 +0,0 @@
import { test, expect } from '@playwright/test';
// TODO after the tests, put again demo.ereuse.org as default
const TEST_SITE = process.env.TEST_SITE || 'https://lab1.ereuse.org'
const TEST_USER = process.env.TEST_USER || 'user@example.org'
const TEST_PASSWD = process.env.TEST_PASSWD || '1234'
async function login(page, date, time) {
await page.goto(TEST_SITE);
await page.getByPlaceholder('Email address').click();
await page.getByPlaceholder('Email address').fill(TEST_USER);
await page.getByPlaceholder('Password').fill(TEST_PASSWD);
await page.getByPlaceholder('Password').press('Enter');
}
// when introducing a new test, use only temporarily to just enable that test
//
//test.only('NEW example', async ({ page }) => {
// await login(page);
// test.setTimeout(0)
// await page.pause();
//});
test('Evidence: create and destroy tag (custom id)', async ({ page }) => {
await login(page);
await page.goto(`${TEST_SITE}/evidence/`);
await page.locator('table a').first().click();
await page.getByRole('link', { name: 'Tag' }).click();
// create tag
await page.getByPlaceholder('Tag').click();
await page.getByPlaceholder('Tag').fill('test');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('alert')).toContainText('Tag mytag has been added.');
// delete tag
await page.getByRole('link', { name: 'Tag' }).click();
await page.getByRole('link', { name: 'Delete' }).click();
await expect(page.getByRole('alert')).toContainText('Tag mytag has been deleted.');
});
test('Property: create key-value, edit key, edit value, delete property property', async ({ page }) => {
const last_log = '#log tr:nth-child(1) td:nth-child(2)'
await login(page);
// assuming after login, we are in devices page, and there, there is a table with devices
await page.locator('table a').first().click();
// new property; key: init1, value: 1
await page.getByRole('link', { name: 'User properties' }).click();
await page.getByRole('link', { name: ' New user property' }).click();
await page.getByPlaceholder('Key').click();
await page.getByPlaceholder('Key').fill('init1');
await page.getByPlaceholder('Key').press('Tab');
await page.getByPlaceholder('Value').fill('1');
await page.getByRole('button', { name: 'Save' }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('User property init1 has been added.');
await page.getByRole('link', { name: 'Log' }).click();
await expect(page.locator(last_log)).toContainText('<Created> UserProperty: init1: 1');
// edit property; key: init2, value: 1
await page.getByRole('link', { name: 'User properties' }).click();
await page.getByRole('button', { name: ' Edit' }).first().click();
await page.getByLabel('Key').click();
await page.getByLabel('Key').fill('init2');
await page.getByRole('button', { name: 'Save changes' }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('User property init2 has been updated.');
await page.getByRole('link', { name: 'Log' }).click();
await expect(page.locator(last_log)).toContainText('<Updated> UserProperty: init1: 1 to init2: 1');
// edit property; key: init2, value: 2
await page.getByRole('link', { name: 'User properties' }).click();
await page.getByRole('button', { name: ' Edit' }).first().click();
await page.getByLabel('Value').fill('2');
await page.getByRole('button', { name: 'Save changes' }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('User property init2 has been updated.');
await page.getByRole('link', { name: 'Log' }).click();
await expect(page.locator(last_log)).toContainText('<Updated> UserProperty: init2: 1 to init2: 2');
// delete property; key: init2, value: 2
await page.getByRole('link', { name: 'User properties' }).click();
await page.getByRole('button', { name: ' Delete' }).click();
await page.getByRole('button', { name: 'Delete', exact: true }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('User property init2 has been updated.');
await page.getByRole('link', { name: 'Log' }).click();
await expect(page.locator(last_log)).toContainText('<Deleted> User Property: init2:2');
});
test('Property: duplication tests', async ({ page }) => {
await login(page);
// assuming after login, we are in devices page, and there, there is a table with devices
await page.locator('table a').first().click();
// new property; key: uniq1, value: 1
await page.getByRole('link', { name: 'User properties' }).click();
await page.getByRole('link', { name: ' New user property' }).click();
await page.getByPlaceholder('Key').click();
await page.getByPlaceholder('Key').fill('uniq1');
await page.getByPlaceholder('Key').press('Tab');
await page.getByPlaceholder('Value').fill('1');
await page.getByRole('button', { name: 'Save' }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('User property uniq1 has been added.');
// new property (duplicate); key: uniq1, value: 1
await page.getByRole('link', { name: 'User properties' }).click();
await page.getByRole('link', { name: ' New user property' }).click();
await page.getByPlaceholder('Key').click();
await page.getByPlaceholder('Key').fill('uniq1');
await page.getByPlaceholder('Key').press('Tab');
await page.getByPlaceholder('Value').fill('1');
await page.getByRole('button', { name: 'Save' }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('User property uniq1 already exists.');
// delete property; key: uniq1, value: 1
await page.getByRole('link', { name: 'User properties' }).click();
await page.getByRole('button', { name: ' Delete' }).first().click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('User property uniq1 deleted has been.');
});
test.only('States: duplication tests', async ({ page }) => {
await login(page);
await page.getByRole('link', { name: ' Admin' }).click();
await page.getByRole('link', { name: 'States' }).click();
// create state: TEST_STATE
await page.getByRole('button', { name: 'Add' }).click();
await page.getByRole('textbox', { name: 'State' }).click();
await page.getByRole('textbox', { name: 'State' }).fill('TEST_STATE');
await page.getByRole('button', { name: 'Add state definition' }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('State definition TEST_STATE has been added.');
// create state (duplicate): TEST_STATE
await page.getByRole('button', { name: 'Add' }).click();
await page.getByRole('textbox', { name: 'State' }).click();
await page.getByRole('textbox', { name: 'State' }).fill('TEST_STATE');
await page.getByRole('button', { name: 'Add state definition' }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('State definition TEST_STATE is already defined.');
// edit state: TEST_STATE -> TEST_STATE_EDIT
await page.getByRole('row', { name: 'TEST_STATE  Edit  Delete' }).getByRole('button').first().click();
await page.getByRole('textbox', { name: 'State' }).fill('TEST_STATE_EDIT');
await page.getByRole('button', { name: 'Save Changes' }).click();
// create state: TEST_STATE
await page.getByRole('button', { name: 'Add' }).click();
await page.getByRole('textbox', { name: 'State' }).click();
await page.getByRole('textbox', { name: 'State' }).fill('TEST_STATE');
await page.getByRole('button', { name: 'Add state definition' }).click();
// you edit state, and target name already exists
// TODO uncomment. "Cannot create key that already exists (UNIQUE constraint)"
// edit state (duplicated during edit): TEST_STATE_EDIT -> TEST_STATE
//await page.getByRole('row', { name: 'TEST_STATE  Edit  Delete' }).getByRole('button').first().click();
//await page.getByRole('textbox', { name: 'State' }).fill('TEST_STATE_EDIT');
//await page.getByRole('button', { name: 'Save Changes' }).click();
// delete state: TEST_STATE_EDIT
await page.getByRole('row', { name: 'TEST_STATE_EDIT  Edit  Delete' }).getByRole('button').nth(1).click();
await page.getByRole('button', { name: 'Delete', exact: true }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('State definition TEST_STATE has been deleted.');
// delete state: TEST_STATE
await page.getByRole('row', { name: 'TEST_STATE  Edit  Delete' }).getByRole('button').nth(1).click();
await page.getByRole('button', { name: 'Delete', exact: true }).click();
// TODO uncomment
//await expect(page.getByRole('alert')).toContainText('State definition TEST_STATE has been deleted.');
});
test('Lot: duplication tests', async ({ page }) => {
await login(page);
// add lot
await page.getByRole('link', { name: ' Lots' }).click();
await page.getByRole('link', { name: 'Entrada' }).click();
await page.getByRole('link', { name: ' Add new lot' }).click();
await page.getByLabel('Type').selectOption('1');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('testlot');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Code').fill('testlot');
await page.getByPlaceholder('Code').press('Tab');
await page.getByPlaceholder('Description').fill('testlot');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('alert')).toContainText('Lot testlot has been added.');
// add (duplicate) lot
await page.getByRole('link', { name: ' Lots' }).click();
await page.getByRole('link', { name: 'Entrada' }).click();
await page.getByRole('link', { name: ' Add new lot' }).click();
await page.getByLabel('Type').selectOption('1');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('testlot');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Code').fill('testlot');
await page.getByPlaceholder('Code').press('Tab');
await page.getByPlaceholder('Description').fill('testlot');
await page.getByPlaceholder('Description').press('Enter');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('alert')).toContainText('Lot testlot is already defined.');
// delete lot
await page.getByRole('link', { name: ' Lots' }).click();
await page.getByRole('link', { name: 'Entrada' }).click();
await page.getByRole('link', { name: '' }).nth(3).click();
await page.getByRole('link', { name: 'Cancel' }).click();
await page.getByRole('link', { name: ' Lots' }).click();
await page.getByRole('link', { name: 'Entrada' }).click();
await page.getByRole('link', { name: '' }).first().click();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByRole('alert')).toContainText('Lot testlot has been deleted.');
});
// TODO falta probar la parte de notas
// falta vista https://lab1.ereuse.org/dashboard/ con columna de state actual; si no hay None pero con un estilo diferente (cursiva y gris?)
//test('Bug 4: Missing logs for actions', async ({ page }) => {
// await login(page);
// await page.goto(`${TEST_SITE}/device/7b769bd6e9191d5ff163fa4a206b9220dad10c47b45d210d3d4d31d586f6a4b6/#log`);
// // Add your assertions and steps to test if logs are missing
//});
//
//test('Bug 6: Log note is not visible', async ({ page }) => {
// await login(page);
// // Add the specific URL or steps for testing log note visibility
//});

View file

@ -1,7 +1,6 @@
from django.core.management.base import BaseCommand
from user.models import Institution
from lot.models import LotTag, Lot
from lot.models import LotTag
class Command(BaseCommand):
help = "Create a new Institution"
@ -12,14 +11,8 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
self.institution = Institution.objects.create(name=kwargs['name'])
self.create_lot_tags()
self.create_lots()
def create_lot_tags(self):
LotTag.objects.create(
inbox=True,
name="Inbox",
owner=self.institution
)
tags = [
"Entrada",
"Salida",
@ -30,59 +23,3 @@ class Command(BaseCommand):
name=tag,
owner=self.institution
)
def create_lots(self):
for g in LotTag.objects.all():
if g.name == "Entrada":
Lot.objects.create(
name="donante-orgA",
owner=self.institution,
archived=True,
type=g
)
Lot.objects.create(
name="donante-orgB",
owner=self.institution,
type=g
)
Lot.objects.create(
name="donante-orgC",
owner=self.institution,
type=g
)
if g.name == "Salida":
Lot.objects.create(
name="beneficiario-org1",
owner=self.institution,
type=g
)
Lot.objects.create(
name="beneficiario-org2",
owner=self.institution,
archived=True,
type=g
)
Lot.objects.create(
name="beneficiario-org3",
owner=self.institution,
type=g
)
if g.name == "Temporal":
Lot.objects.create(
name="palet1",
owner=self.institution,
type=g
)
Lot.objects.create(
name="palet2",
owner=self.institution,
type=g
)
Lot.objects.create(
name="palet3",
owner=self.institution,
archived=True,
type=g
)

View file

@ -1,18 +0,0 @@
# Generated by Django 5.0.6 on 2025-02-18 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='institution',
name='algorithm',
field=models.CharField(choices=[('ereuse24', 'ereuse24'), ('ereuse22', 'ereuse22')], default='ereuse24', max_length=30),
),
]

View file

@ -1,10 +1,8 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
from utils.constants import ALGOS
ALGORITHMS = [(x, x) for x in ALGOS.keys()]
# Create your models here.
class Institution(models.Model):
@ -29,7 +27,6 @@ class Institution(models.Model):
blank=True,
null=True
)
algorithm = models.CharField(max_length=30, choices=ALGORITHMS, default='ereuse24')
class UserManager(BaseUserManager):

View file

@ -9,28 +9,27 @@ STR_EXTEND_SIZE = 256
# Algorithms for build hids
EREUSE24 = [
HID_ALGO1 = [
"manufacturer",
"model",
"chassis",
"serial_number",
"mac"
"serialNumber",
"sku"
]
# EREUSE22 is used for build the chid of DPP
EREUSE22 = [
LEGACY_DPP = [
"manufacturer",
"model",
"chassis",
"serial_number",
"serialNumber",
"sku",
"type",
"version"
]
ALGOS = {
"ereuse24": EREUSE24,
"ereuse22": EREUSE22
"hidalgo1": HID_ALGO1,
"legacy_dpp": LEGACY_DPP
}