Compare commits
196 commits
feature/st
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
1e388b2c26 | ||
|
82ea17969d | ||
|
1250d47553 | ||
|
deffdc03f6 | ||
|
abf9f31beb | ||
|
0ba3f6fad6 | ||
|
7bd0c4a563 | ||
|
29f08a633b | ||
|
6e50fe654c | ||
|
705442e54f | ||
|
ea6db98c0e | ||
|
8b644da29a | ||
|
afced90f19 | ||
|
e0b48248f0 | ||
|
beda2b893c | ||
|
bef3fb6cf9 | ||
|
c35c3fcdbc | ||
|
782536aec6 | ||
|
f7468f90e5 | ||
|
461dc747eb | ||
|
8a1dc1945a | ||
|
9b183062ad | ||
|
85a3575728 | ||
|
d994d302a0 | ||
|
5f2a5c4f71 | ||
|
9d18f113cf | ||
|
31276f50f1 | ||
|
331699d922 | ||
|
65c698e6c2 | ||
|
8c244acd00 | ||
|
15ce739c15 | ||
|
0a73da50ce | ||
|
509c54e5d8 | ||
|
72f359df87 | ||
|
bb8be7cc09 | ||
|
427e80f8b3 | ||
|
42f5cf7e36 | ||
|
73a582aeb3 | ||
|
28479fb871 | ||
|
0983af929a | ||
|
49cce3daa9 | ||
|
e23fed5c13 | ||
|
f54dce0979 | ||
|
8edfaa1bc5 | ||
|
74d48c173b | ||
|
f91818e515 | ||
|
8d37932aa0 | ||
|
0903f53f8b | ||
|
ccaa8834dc | ||
|
bd1efe3adc | ||
|
35a753d244 | ||
|
976ce43e6e | ||
|
c540bb7f7f | ||
|
7145e721f1 | ||
|
3db94ee82b | ||
|
5cf51df952 | ||
|
e7d958c550 | ||
|
2e932b9725 | ||
|
a4fb574d07 | ||
|
a52ce5c889 | ||
|
f9b93d4790 | ||
|
00df679156 | ||
|
0259c2be2c | ||
|
a496045f4d | ||
|
73bb3b4751 | ||
|
0db033e2dd | ||
|
b58cfc9ab1 | ||
|
1cb09fb19f | ||
|
9565288eee | ||
|
8b9ea1cf10 | ||
|
ad497e6299 | ||
|
c383e692c3 | ||
|
4da9961eea | ||
|
c0c4e29fdc | ||
|
0a50f75ca4 | ||
|
8136684b91 | ||
|
67d7621509 | ||
|
86a7d9f733 | ||
|
a06cf9f4da | ||
|
a0596f618b | ||
|
afb7cf8d6e | ||
|
df386136ce | ||
|
b1b9f7e100 | ||
|
8f0b8771a7 | ||
|
11813db7f6 | ||
|
86c2a26130 | ||
|
6b7fd09777 | ||
|
643e6c1f45 | ||
|
2837d3e560 | ||
|
3bed441d10 | ||
|
0fb3df0155 | ||
|
3e2a5f03bf | ||
|
1f4515781a | ||
|
a1381f68fa | ||
|
fcc93955c4 | ||
|
455448aea2 | ||
|
afcb6feb22 | ||
|
0ca4a83f97 | ||
|
c93915f285 | ||
|
2b0f0b8d08 | ||
|
18ec5d74c9 | ||
|
63c427e3eb | ||
|
5a5dfc3319 | ||
|
519648226b | ||
|
24f5508462 | ||
|
0d8ec72d7c | ||
|
d75cd75c86 | ||
|
85013a340e | ||
|
24753b1004 | ||
|
2dfe313076 | ||
|
3db4374cd0 | ||
|
cf48000ef4 | ||
|
30826afd45 | ||
|
c530454054 | ||
|
6e88f77c1b | ||
|
70175be472 | ||
|
dbd837b079 | ||
|
7a55501c34 | ||
|
d92dec28bb | ||
|
3467a483e7 | ||
|
5a448755c2 | ||
|
f3f18e0962 | ||
|
080ff4f668 | ||
|
ec9ae644b9 | ||
|
b5c57aa4d2 | ||
|
68d8ff33a7 | ||
|
3cf6af0c25 | ||
|
4d38e75bba | ||
|
5c7db7d60d | ||
|
4045baac9f | ||
|
0e3bba0569 | ||
|
012e25d086 | ||
|
cfa4b9a291 | ||
|
9a9970336b | ||
|
2f23ed7e88 | ||
|
7470dc5de2 | ||
|
57954d66e2 | ||
|
ede5d6a6c5 | ||
|
62ccf46194 | ||
|
3b0735faec | ||
|
3670fda2a3 | ||
|
6d44aa855b | ||
|
4485517221 | ||
|
b9ffd788a1 | ||
|
bad82965e9 | ||
|
eecaef7cff | ||
|
fb6c243ee8 | ||
|
ff9a78ed23 | ||
|
5fcd9ce7ca | ||
|
539f5b5bb7 | ||
|
5e2c8f2328 | ||
|
2451e843ac | ||
|
acbe2f6a75 | ||
|
2bb9e8d035 | ||
|
35622ff9c7 | ||
|
ff928a381b | ||
|
09bed0a904 | ||
|
9807cb56aa | ||
|
e0e4fd862a | ||
|
b4f0909199 | ||
|
db59b099f5 | ||
|
963858263a | ||
|
4bc423a979 | ||
|
4d9f588ad7 | ||
|
42b13eec84 | ||
|
fcefddb5a0 | ||
|
8991faa423 | ||
|
9df611293a | ||
|
9c27a30399 | ||
|
13d325105f | ||
|
09c3f96185 | ||
|
dad8e40ee8 | ||
|
f7cd7bc3f2 | ||
|
1f0a9a60ce | ||
|
01b7267dd8 | ||
|
91c03cb990 | ||
|
9dafc51210 | ||
|
7f16552762 | ||
|
829fb6e2a1 | ||
|
02a69e6994 | ||
|
2ff630f212 | ||
|
d7d6fb7bc6 | ||
|
096704935d | ||
|
0485604512 | ||
|
40b0617a72 | ||
|
b1c4a2cec9 | ||
|
9503a9a8b4 | ||
|
9247f11c27 | ||
|
56d8aadf83 | ||
|
601da538bf | ||
|
754820f631 | ||
|
01db6a3e8e | ||
|
9817d4eb45 | ||
|
0d60afaa35 | ||
|
918cf73506 | ||
|
9d963e7f54 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
db
|
||||
.git
|
||||
already_configured
|
17
.editorconfig
Normal file
17
.editorconfig
Normal file
|
@ -0,0 +1,17 @@
|
|||
# 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
|
56
.env.example
56
.env.example
|
@ -1,8 +1,18 @@
|
|||
DOMAIN=localhost
|
||||
####
|
||||
# DEV OPTIONS
|
||||
####
|
||||
|
||||
DEV_DOCKER_ALWAYS_BUILD=false
|
||||
|
||||
####
|
||||
# DEVICEHUB
|
||||
####
|
||||
|
||||
DEVICEHUB_DOMAIN=localhost
|
||||
DEVICEHUB_PORT=8001
|
||||
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/
|
||||
|
@ -16,6 +26,48 @@ 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
5
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
db.sqlite3
|
||||
env/
|
||||
__pycache__/
|
||||
.env
|
||||
|
||||
# the following could be autogenerated by devicehub
|
||||
db.sqlite3
|
||||
example/snapshots/snapshot_workbench-script_verifiable-credential.json
|
||||
|
|
|
@ -22,6 +22,7 @@ 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)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ 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 _
|
||||
|
@ -9,7 +10,7 @@ from action.models import State, StateDefinition, Note, DeviceLog
|
|||
from device.models import Device
|
||||
|
||||
|
||||
class ChangeStateView(FormView):
|
||||
class ChangeStateView(LoginRequiredMixin, FormView):
|
||||
form_class = ChangeStateForm
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -42,7 +43,7 @@ class ChangeStateView(FormView):
|
|||
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
|
||||
|
||||
|
||||
class AddNoteView(FormView):
|
||||
class AddNoteView(LoginRequiredMixin, FormView):
|
||||
form_class = AddNoteForm
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -73,7 +74,7 @@ class AddNoteView(FormView):
|
|||
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
|
||||
|
||||
|
||||
class UpdateNoteView(UpdateView):
|
||||
class UpdateNoteView(LoginRequiredMixin, UpdateView):
|
||||
model = Note
|
||||
fields = ['description']
|
||||
pk_url_kwarg = 'pk'
|
||||
|
@ -105,7 +106,7 @@ class UpdateNoteView(UpdateView):
|
|||
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details'))
|
||||
|
||||
|
||||
class DeleteNoteView(View):
|
||||
class DeleteNoteView(LoginRequiredMixin, View):
|
||||
model = Note
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
|
173
admin/templates/lot_tag_panel.html
Normal file
173
admin/templates/lot_tag_panel.html
Normal file
|
@ -0,0 +1,173 @@
|
|||
{% 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 %}
|
|
@ -15,4 +15,8 @@ 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'),
|
||||
]
|
||||
|
|
126
admin/views.py
126
admin/views.py
|
@ -18,6 +18,7 @@ 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):
|
||||
|
@ -112,6 +113,99 @@ class EditUserView(AdminView, UpdateView):
|
|||
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")
|
||||
|
@ -124,7 +218,8 @@ class InstitutionView(AdminView, UpdateView):
|
|||
"logo",
|
||||
"location",
|
||||
"responsable_person",
|
||||
"supervisor_person"
|
||||
"supervisor_person",
|
||||
"algorithm"
|
||||
)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
|
@ -169,7 +264,8 @@ class AddStateDefinitionView(AdminView, StateDefinitionContextMixin, CreateView)
|
|||
return self.form_invalid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
return super().form_invalid(form)
|
||||
super().form_invalid(form)
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessMessageMixin, DeleteView):
|
||||
|
@ -179,14 +275,11 @@ class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessM
|
|||
def get_success_message(self, cleaned_data):
|
||||
return f'State definition: {self.object.state}, has been deleted'
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
#only an admin of current institution can delete
|
||||
if not object.institution == self.request.user.institution:
|
||||
def form_valid(self, form):
|
||||
if not self.object.institution == self.request.user.institution:
|
||||
raise Http404
|
||||
|
||||
return super().delete(request, *args, **kwargs)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UpdateStateOrderView(AdminView, TemplateView):
|
||||
|
@ -218,14 +311,21 @@ 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 get_success_url(self):
|
||||
messages.success(self.request, _("State definition updated successfully."))
|
||||
return reverse_lazy('admin:states_panel')
|
||||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(form)
|
||||
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 form_invalid(self, form):
|
||||
super().form_invalid(form)
|
||||
return redirect(self.get_success_url())
|
||||
|
|
|
@ -90,7 +90,7 @@ class NewSnapshotView(ApiMixing):
|
|||
ev_uuid = data["credentialSubject"].get("uuid")
|
||||
|
||||
if not ev_uuid:
|
||||
txt = "error: the snapshot not have uuid"
|
||||
txt = "error: the snapshot does not have an 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="hidalgo1",
|
||||
key="ereuse24",
|
||||
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=(property.value,))
|
||||
url_args = reverse_lazy("device:details", args=(prop.value,))
|
||||
url = request.build_absolute_uri(url_args)
|
||||
|
||||
response = {
|
||||
"status": "success",
|
||||
"dhid": property.value[:6].upper(),
|
||||
"dhid": prop.value[:6].upper(),
|
||||
"url": url,
|
||||
# TODO replace with public_url when available
|
||||
"public_url": url
|
||||
|
|
|
@ -8,6 +8,7 @@ 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):
|
||||
|
@ -32,6 +33,9 @@ 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,
|
||||
|
@ -41,7 +45,7 @@ class DashboardView(LoginRequiredMixin):
|
|||
'section': self.section,
|
||||
'path': resolve(self.request.path).url_name,
|
||||
'user': self.request.user,
|
||||
'lot_tags': LotTag.objects.filter(owner=self.request.user.institution)
|
||||
'lot_tags': lot_tags
|
||||
})
|
||||
return context
|
||||
|
||||
|
|
|
@ -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 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 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()">
|
||||
<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 institution users edit_user new_user delete_user 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 users tag_panel 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,33 +100,43 @@
|
|||
<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 == '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()">
|
||||
<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()">
|
||||
<i class="bi bi-laptop icon_sidebar"></i>
|
||||
{% trans 'Devices' %}
|
||||
{% trans 'Device' %}
|
||||
</a>
|
||||
<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' %}
|
||||
<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' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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()">
|
||||
<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()">
|
||||
<i class="bi bi-database icon_sidebar"></i>
|
||||
{% trans 'Lots' %}
|
||||
</a>
|
||||
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path == 'tag' %}expanded{% else %}collapse{% endif %}" id="ul_lots" data-bs-parent="#sidebarMenu">
|
||||
<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">
|
||||
|
||||
{% for tag in lot_tags %}
|
||||
<li class="nav-items">
|
||||
<a class="nav-link{% if path == 'tag' %} active2{% endif %}" href="{% url 'lot:tag' tag.id %}">
|
||||
{% 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 %}
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -134,37 +144,29 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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()">
|
||||
<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()">
|
||||
<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' %}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 one' %}
|
||||
</a>
|
||||
</li>
|
||||
<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 'Old evidences' %}
|
||||
{% trans 'List of evidences' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if path == 'upload' %} active2{% endif %}" href="{% url 'evidence:upload' %}">
|
||||
{% trans 'Upload with JSON file' %}
|
||||
</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 Spreadsheet' %}
|
||||
{% trans 'Upload with Spreadsheet' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if path == 'add' %} active2{% endif %}" href="{% url 'device:add' %}">
|
||||
{% trans 'Create one' %}
|
||||
{% trans 'Upload with Web Form' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -192,10 +194,10 @@
|
|||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="post" action="{% url 'dashboard:search' %}">
|
||||
<form method="get" action="{% url 'dashboard:search' %}">
|
||||
{% csrf_token %}
|
||||
<div class="input-group rounded">
|
||||
<input type="search" name="search" class="form-control rounded" placeholder="Search your device..." aria-label="Search" aria-describedby="search-addon" />
|
||||
<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" />
|
||||
<span class="input-group-text border-0" id="search-addon">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
|
|
|
@ -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 }}">
|
||||
<a type="button" class="btn btn-grey border border-dark" href="?page=1&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
|
||||
«
|
||||
</a>
|
||||
</li>
|
||||
<li class="previous">
|
||||
<a type="button" class="btn btn-grey border border-dark" href="?page={{ page_number|add:-1 }}&limit={{ limit }}">
|
||||
<a type="button" class="btn btn-grey border border-dark" href="?page={{ page_number|add:-1 }}&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
|
||||
{% trans 'Previous' %}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -24,7 +24,7 @@
|
|||
{% if p == page_number or p == "..." %}
|
||||
href="#">
|
||||
{% else %}
|
||||
href="?page={{ p }}&limit={{ limit }}">
|
||||
href="?page={{ p }}&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
|
||||
{% 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 }}">
|
||||
<a type="button" class="btn btn-grey border border-dark" href="?page={{ page_number|add:+1 }}&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
|
||||
{% trans 'Next' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="previous">
|
||||
<a type="button" class="btn btn-grey border border-dark" href="?page={{ total_pages }}&limit={{ limit }}">
|
||||
<a type="button" class="btn btn-grey border border-dark" href="?page={{ total_pages }}&limit={{ limit }}{% if search %}&search={{ search }}{% endif %}">
|
||||
»
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -9,12 +9,6 @@
|
|||
<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' %}
|
||||
|
@ -31,6 +25,9 @@
|
|||
<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>
|
||||
|
@ -49,6 +46,9 @@
|
|||
<th scope="col" data-sortable="">
|
||||
model
|
||||
</th>
|
||||
<th scope="col" data-sortable="">
|
||||
updated
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for dev in devices %}
|
||||
|
@ -75,16 +75,18 @@
|
|||
{{ 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 %}
|
||||
{% render_pagination page total_pages limit search %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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):
|
||||
def render_pagination(page_number, total_pages, limit=10, search=None):
|
||||
"""
|
||||
Template tag for render pagination
|
||||
|
||||
|
@ -16,5 +16,6 @@ def render_pagination(page_number, total_pages, limit=10):
|
|||
return {
|
||||
'page_number': page_number,
|
||||
'total_pages': total_pages,
|
||||
'limit': limit
|
||||
'limit': limit,
|
||||
"search": search,
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ from dashboard import views
|
|||
app_name = 'dashboard'
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.UnassignedDevicesView.as_view(), name="unassigned_devices"),
|
||||
path("", views.UnassignedDevicesView.as_view(), name="unassigned"),
|
||||
path("all", views.AllDevicesView.as_view(), name="all_device"),
|
||||
path("<int:pk>/", views.LotDashboardView.as_view(), name="lot"),
|
||||
path("search", views.SearchView.as_view(), name="search"),
|
||||
]
|
||||
|
|
|
@ -22,6 +22,16 @@ 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"
|
||||
|
@ -42,8 +52,18 @@ class LotDashboardView(InventaryMixin, DetailsMixin):
|
|||
"device_id", flat=True
|
||||
).distinct()
|
||||
|
||||
chids_page = chids[offset:offset+limit]
|
||||
return [Device(id=x) for x in chids_page], chids.count()
|
||||
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)
|
||||
|
||||
|
||||
class SearchView(InventaryMixin):
|
||||
|
@ -52,9 +72,20 @@ 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):
|
||||
post = dict(self.request.POST)
|
||||
query = post.get("search")
|
||||
query = dict(self.request.GET).get("search")
|
||||
|
||||
if not query:
|
||||
return [], 0
|
||||
|
@ -65,6 +96,12 @@ 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)
|
||||
|
@ -79,7 +116,6 @@ 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
|
||||
|
||||
|
|
147
device/models.py
147
device/models.py
|
@ -71,17 +71,6 @@ 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:
|
||||
|
@ -148,27 +137,25 @@ class Device:
|
|||
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
|
||||
|
||||
@classmethod
|
||||
def get_unassigned(cls, institution, offset=0, limit=None):
|
||||
|
||||
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 = 'hidalgo1' THEN 2
|
||||
ELSE 3
|
||||
WHEN t1.key = '{algorithm}' THEN 2
|
||||
END,
|
||||
t1.created DESC
|
||||
) 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}
|
||||
WHERE t1.owner_id = {institution}
|
||||
AND t1.key IN ('CUSTOM_ID', '{algorithm}')
|
||||
)
|
||||
SELECT DISTINCT
|
||||
value
|
||||
|
@ -176,8 +163,98 @@ 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))
|
||||
|
||||
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):
|
||||
|
||||
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
|
||||
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
|
||||
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))
|
||||
|
@ -201,13 +278,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 = 'hidalgo1' THEN 2
|
||||
ELSE 3
|
||||
WHEN t1.key = '{algorithm}' THEN 2
|
||||
END,
|
||||
t1.created DESC
|
||||
) AS row_num
|
||||
|
@ -215,6 +292,7 @@ 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)
|
||||
|
@ -222,8 +300,10 @@ 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)
|
||||
|
@ -236,31 +316,32 @@ 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 = 'hidalgo1' THEN 2
|
||||
ELSE 3
|
||||
WHEN t1.key = '{algorithm}' THEN 2
|
||||
END,
|
||||
t1.created DESC
|
||||
) 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}
|
||||
WHERE t1.owner_id = {institution}
|
||||
AND t1.uuid = '{uuid}'
|
||||
AND t1.key IN ('CUSTOM_ID', '{algorithm}')
|
||||
)
|
||||
SELECT DISTINCT
|
||||
value
|
||||
FROM
|
||||
RankedProperties
|
||||
WHERE
|
||||
row_num = 1;
|
||||
row_num = 1
|
||||
ORDER BY created DESC
|
||||
""".format(
|
||||
uuid=uuid.replace("-", ""),
|
||||
institution=institution.id,
|
||||
algorithm=institution.algorithm,
|
||||
)
|
||||
|
||||
properties = []
|
||||
|
@ -285,6 +366,12 @@ 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()
|
||||
|
@ -305,11 +392,15 @@ class Device:
|
|||
|
||||
@property
|
||||
def version(self):
|
||||
if not self.last_evidence:
|
||||
self.get_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()
|
||||
|
|
|
@ -2,108 +2,6 @@
|
|||
{% 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">
|
||||
|
@ -166,13 +64,7 @@
|
|||
<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="#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>
|
||||
<a href="#user_properties" class="nav-link" data-bs-toggle="tab" data-bs-target="#user_properties">{% trans 'Properties' %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">{% trans 'Lots' %}</a>
|
||||
|
@ -191,6 +83,9 @@
|
|||
<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>
|
||||
|
@ -202,14 +97,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">
|
||||
|
@ -256,27 +151,5 @@
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
//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 %}
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
<div class="container">
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
<div class="container">
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
|
||||
<a class="btn btn-grey" href="{% url 'device:details' pk %}#user_properties">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,251 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,49 +0,0 @@
|
|||
|
||||
{% 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>
|
18
device/templates/tabs/dpps.html
Normal file
18
device/templates/tabs/dpps.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% 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>
|
|
@ -1,19 +1,34 @@
|
|||
|
||||
{% 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-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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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' a.id %}">
|
||||
<form method="post" action="{% url 'device:delete_user_property' object.id 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' a.id %}">
|
||||
<form id="editForm{{ a.id }}" method="post" action="{% url 'device:update_user_property' object.id a.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="key" class="form-label">{% trans "Key" %}
|
||||
|
|
|
@ -7,10 +7,11 @@ 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("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>/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>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
|
||||
|
||||
]
|
||||
|
|
207
device/views.py
207
device/views.py
|
@ -1,7 +1,9 @@
|
|||
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
|
||||
|
@ -24,11 +26,21 @@ 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_devices')
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
form_class = DeviceFormSet
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -41,35 +53,6 @@ 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")
|
||||
|
@ -111,24 +94,32 @@ 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"]
|
||||
)
|
||||
last_evidence= self.object.get_last_evidence(),
|
||||
uuid=self.object.last_uuid()
|
||||
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
|
||||
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": 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'),
|
||||
"device_states": device_states,
|
||||
"device_logs": device_logs,
|
||||
"device_notes": device_notes,
|
||||
})
|
||||
return context
|
||||
|
||||
|
@ -189,7 +180,7 @@ class PublicDeviceWebView(TemplateView):
|
|||
return JsonResponse(device_data)
|
||||
|
||||
|
||||
class AddUserPropertyView(DashboardView, CreateView):
|
||||
class AddUserPropertyView(DeviceLogMixin, CreateView):
|
||||
template_name = "new_user_property.html"
|
||||
title = _("New User Property")
|
||||
breadcrumb = "Device / New Property"
|
||||
|
@ -202,125 +193,107 @@ class AddUserPropertyView(DashboardView, CreateView):
|
|||
form.instance.uuid = self.property.uuid
|
||||
form.instance.type = UserProperty.Type.USER
|
||||
|
||||
message = _("<Created> UserProperty: {}: {}".format(form.instance.key, form.instance.value))
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=form.instance.uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution
|
||||
)
|
||||
try:
|
||||
response = super().form_valid(form)
|
||||
messages.success(self.request, _("Property successfully added."))
|
||||
log_message = _("<Created> UserProperty: {}: {}".format(
|
||||
form.instance.key,
|
||||
form.instance.value
|
||||
))
|
||||
|
||||
messages.success(self.request, _("User property successfully added."))
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
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)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
institution = self.request.user.institution
|
||||
self.property = get_object_or_404(SystemProperty, owner=institution, value=pk)
|
||||
self.property = SystemProperty.objects.filter(
|
||||
owner=institution, value=pk).first()
|
||||
if not self.property:
|
||||
raise Http404
|
||||
|
||||
return super().get_form_kwargs()
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('device:details', args=[self.kwargs.get('pk')])
|
||||
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
|
||||
|
||||
|
||||
class UpdateUserPropertyView(DashboardView, UpdateView):
|
||||
class UpdateUserPropertyView(DeviceLogMixin, UpdateView):
|
||||
template_name = "new_user_property.html"
|
||||
title = _("Update User Property")
|
||||
breadcrumb = "Device / Update Property"
|
||||
model = UserProperty
|
||||
fields = ("key", "value")
|
||||
|
||||
def get_queryset(self):
|
||||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
institution = self.request.user.institution
|
||||
return UserProperty.objects.filter(pk=pk, owner=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()
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
old_instance = self.get_object()
|
||||
old_key = old_instance.key
|
||||
old_value = old_instance.value
|
||||
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
form.instance.type = UserProperty.Type.USER
|
||||
|
||||
new_key = form.cleaned_data['key']
|
||||
new_value = form.cleaned_data['value']
|
||||
|
||||
message = _("<Updated> UserProperty: {}: {} to {}: {}".format(old_key, old_value, new_key, new_value))
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=form.instance.uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution
|
||||
)
|
||||
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)
|
||||
|
||||
messages.success(self.request, _("User property updated successfully."))
|
||||
return super().form_valid(form)
|
||||
def form_invalid(self, form):
|
||||
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', args=[self.object.pk]))
|
||||
pk = self.kwargs.get('device_id')
|
||||
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
|
||||
|
||||
|
||||
class DeleteUserPropertyView(DashboardView, DeleteView):
|
||||
class DeleteUserPropertyView(DeviceLogMixin, 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):
|
||||
self.object = self.get_object()
|
||||
pk = self.kwargs.get('pk')
|
||||
institution = self.request.user.institution
|
||||
self.object = get_object_or_404(UserProperty, owner=institution, pk=pk)
|
||||
self.object.delete()
|
||||
|
||||
message = _("<Deleted> User Property: {}:{}".format(self.object.key, self.object.value ))
|
||||
DeviceLog.objects.create(
|
||||
snapshot_uuid=self.object.uuid,
|
||||
event=message,
|
||||
user=self.request.user,
|
||||
institution=self.request.user.institution
|
||||
)
|
||||
|
||||
msg = _("<Deleted> User Property: {}:{}".format(
|
||||
self.object.key,
|
||||
self.object.value
|
||||
))
|
||||
self.log_registry(self.object.uuid, msg)
|
||||
messages.info(self.request, _("User property deleted successfully."))
|
||||
|
||||
return self.handle_success()
|
||||
|
||||
def handle_success(self):
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
|
||||
|
||||
class AddDocumentView(DashboardView, CreateView):
|
||||
template_name = "new_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
|
||||
pk = self.kwargs.get('device_id')
|
||||
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
|
||||
|
|
|
@ -78,16 +78,13 @@ INSTALLED_APPS = [
|
|||
'django_extensions',
|
||||
'django_bootstrap5',
|
||||
'django_tables2',
|
||||
"rest_framework",
|
||||
"login",
|
||||
"user",
|
||||
"device",
|
||||
"evidence",
|
||||
"action",
|
||||
"tag",
|
||||
"lot",
|
||||
"documents",
|
||||
"dashboard",
|
||||
"action",
|
||||
"admin",
|
||||
"api",
|
||||
]
|
||||
|
|
16
did/views.py
16
did/views.py
|
@ -106,10 +106,10 @@ class PublicDeviceWebView(TemplateView):
|
|||
'device': {},
|
||||
}
|
||||
dev = Build(self.object.last_evidence.doc, None, check=True)
|
||||
doc = dev.get_phid()
|
||||
doc = dev.build.get_doc()
|
||||
data['document'] = json.dumps(doc)
|
||||
data['device'] = dev.device
|
||||
data['components'] = dev.components
|
||||
data['device'] = dev.build.device
|
||||
data['components'] = dev.build.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.get_phid()
|
||||
doc = dev.build.get_doc()
|
||||
ev = json.dumps(doc)
|
||||
phid = dev.get_signature(doc)
|
||||
phid = dev.sign(ev)
|
||||
dpp = "{}:{}".format(self.pk, phid)
|
||||
rr = {
|
||||
'dpp': dpp,
|
||||
'document': ev,
|
||||
'algorithm': ALGORITHM,
|
||||
'manufacturer DPP': '',
|
||||
'device': dev.device,
|
||||
'components': dev.components
|
||||
'device': dev.build.device,
|
||||
'components': dev.build.components
|
||||
}
|
||||
|
||||
tmpl = dpp_tmpl.copy()
|
||||
|
|
|
@ -1,17 +1,59 @@
|
|||
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=${DOMAIN:-localhost}
|
||||
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN}
|
||||
- DOMAIN=${DEVICEHUB_DOMAIN:-localhost}
|
||||
- PORT=${DEVICEHUB_PORT:-8000}
|
||||
- ALLOWED_HOSTS=${DEVICEHUB_ALLOWED_HOSTS:-$DEVICEHUB_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:
|
||||
- 8000:8000
|
||||
- ${DEVICEHUB_PORT:-8000}:${DEVICEHUB_PORT:-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
|
||||
|
|
|
@ -19,12 +19,24 @@ 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
|
||||
docker compose build
|
||||
if [ "${DEV_DOCKER_ALWAYS_BUILD:-}" = 'true' ]; then
|
||||
docker compose pull --ignore-buildable
|
||||
docker compose build
|
||||
else
|
||||
docker compose pull
|
||||
fi
|
||||
docker compose up ${detach_arg:-}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ RUN apt update && \
|
|||
python3-xapian \
|
||||
git \
|
||||
sqlite3 \
|
||||
curl \
|
||||
jq \
|
||||
time \
|
||||
vim \
|
||||
|
@ -37,6 +38,7 @@ 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
|
||||
|
|
|
@ -42,19 +42,6 @@ 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
|
||||
}
|
||||
|
||||
|
@ -118,8 +105,54 @@ 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
|
||||
|
||||
|
@ -132,7 +165,7 @@ config_phase() {
|
|||
# 12, 13, 14
|
||||
config_dpp_part1
|
||||
|
||||
# cleanup other spnapshots and copy dlt/dpp snapshots
|
||||
# cleanup other snapshots and copy dlt/dpp snapshots
|
||||
# TODO make this better
|
||||
rm example/snapshots/*
|
||||
cp example/dpp-snapshots/*.json example/snapshots/
|
||||
|
@ -140,7 +173,7 @@ config_phase() {
|
|||
|
||||
# # 15. Add inventory snapshots for user "${INIT_USER}".
|
||||
if [ "${DEMO:-}" = 'true' ]; then
|
||||
/usr/bin/time ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
|
||||
run_demo
|
||||
fi
|
||||
|
||||
# remain next command as the last operation for this if conditional
|
||||
|
@ -155,9 +188,11 @@ check_app_is_there() {
|
|||
}
|
||||
|
||||
deploy() {
|
||||
# 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 [ -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
|
||||
|
||||
if [ "${DEBUG:-}" = 'true' ]; then
|
||||
./manage.py print_settings
|
||||
|
@ -173,6 +208,9 @@ 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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DocumentsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "documents"
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
|
@ -24,7 +24,7 @@ class ProofView(View):
|
|||
return JsonResponse({}, status=404)
|
||||
|
||||
dev = Build(ev.doc, None, check=True)
|
||||
doc = dev.get_phid()
|
||||
doc = dev.build.get_doc()
|
||||
|
||||
data = {
|
||||
"algorithm": ALGORITHM,
|
||||
|
|
|
@ -58,7 +58,9 @@ class UploadForm(forms.Form):
|
|||
|
||||
for ev in self.evidences:
|
||||
path_name = save_in_disk(ev[1], user.institution.name)
|
||||
Build(ev[1], user)
|
||||
build = Build
|
||||
file_json = ev[1]
|
||||
build(file_json, user)
|
||||
move_json(path_name, user.institution.name)
|
||||
|
||||
|
||||
|
|
74
evidence/legacy_parse.py
Normal file
74
evidence/legacy_parse.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
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)
|
503
evidence/legacy_parse_details.py
Normal file
503
evidence/legacy_parse_details.py
Normal file
|
@ -0,0 +1,503 @@
|
|||
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)
|
|
@ -4,7 +4,6 @@ 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
|
||||
|
|
19
evidence/migrations/0005_alter_userproperty_type.py
Normal file
19
evidence/migrations/0005_alter_userproperty_type.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# 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
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# 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'),
|
||||
),
|
||||
]
|
64
evidence/mixin_parse.py
Normal file
64
evidence/mixin_parse.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
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
|
|
@ -8,7 +8,8 @@ from django.db import models
|
|||
from django.db.models import Q
|
||||
from utils.constants import STR_EXTEND_SIZE, CHASSIS_DH
|
||||
from evidence.xapian import search
|
||||
from evidence.parse_details import ParseSnapshot, get_inxi, get_inxi_key
|
||||
from evidence.parse_details import ParseSnapshot
|
||||
from evidence.normal_parse_details import get_inxi, get_inxi_key
|
||||
from user.models import User, Institution
|
||||
|
||||
|
||||
|
@ -36,15 +37,20 @@ class SystemProperty(Property):
|
|||
|
||||
|
||||
class UserProperty(Property):
|
||||
uuid = models.UUIDField()
|
||||
|
||||
class Type(models.IntegerChoices):
|
||||
USER = 1, "User"
|
||||
DOCUMENT = 2, "Document"
|
||||
ERASE_SERVER = 3, "EraseServer"
|
||||
ERASE_SERVER = 2, "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):
|
||||
|
@ -105,7 +111,7 @@ class Evidence:
|
|||
self.inxi = ev["output"]
|
||||
else:
|
||||
dmidecode_raw = self.doc["data"]["dmidecode"]
|
||||
inxi_raw = self.doc["data"]["inxi"]
|
||||
inxi_raw = self.doc.get("data", {}).get("inxi")
|
||||
self.dmi = DMIParse(dmidecode_raw)
|
||||
try:
|
||||
self.inxi = json.loads(inxi_raw)
|
||||
|
@ -129,9 +135,11 @@ class Evidence:
|
|||
if not self.doc:
|
||||
self.get_doc()
|
||||
self.created = self.doc.get("endTime")
|
||||
|
||||
if not self.created:
|
||||
self.created = self.properties.last().created
|
||||
self.created = self.get_time_created()
|
||||
|
||||
def get_time_created(self):
|
||||
return self.properties.last().created.isoformat()
|
||||
|
||||
def get_components(self):
|
||||
if self.is_legacy():
|
||||
|
@ -147,7 +155,7 @@ class Evidence:
|
|||
return list(self.doc.get('kv').values())[0]
|
||||
|
||||
if self.is_legacy():
|
||||
return self.doc['device']['manufacturer']
|
||||
return self.doc.get('device', {}).get('manufacturer', '')
|
||||
|
||||
if self.inxi:
|
||||
return self.device_manufacturer
|
||||
|
@ -162,7 +170,7 @@ class Evidence:
|
|||
return list(self.doc.get('kv').values())[1]
|
||||
|
||||
if self.is_legacy():
|
||||
return self.doc['device']['model']
|
||||
return self.doc.get('device', {}).get('model', '')
|
||||
|
||||
if self.inxi:
|
||||
return self.device_model
|
||||
|
@ -171,7 +179,7 @@ class Evidence:
|
|||
|
||||
def get_chassis(self):
|
||||
if self.is_legacy():
|
||||
return self.doc['device']['model']
|
||||
return self.doc.get('device', {}).get('model', '')
|
||||
|
||||
if self.inxi:
|
||||
return self.device_chassis
|
||||
|
@ -186,7 +194,7 @@ class Evidence:
|
|||
|
||||
def get_serial_number(self):
|
||||
if self.is_legacy():
|
||||
return self.doc['device']['serialNumber']
|
||||
return self.doc.get('device', {}).get('serialNumber', '')
|
||||
|
||||
if self.inxi:
|
||||
return self.device_serial_number
|
||||
|
@ -203,12 +211,11 @@ class Evidence:
|
|||
def get_all(cls, user):
|
||||
return SystemProperty.objects.filter(
|
||||
owner=user.institution,
|
||||
key="hidalgo1",
|
||||
key="ereuse24",
|
||||
).order_by("-created").values_list("uuid", "created").distinct()
|
||||
|
||||
def set_components(self):
|
||||
snapshot = ParseSnapshot(self.doc).snapshot_json
|
||||
self.components = snapshot['components']
|
||||
self.components = ParseSnapshot(self.doc).components
|
||||
|
||||
def is_legacy(self):
|
||||
if self.doc.get("credentialSubject"):
|
||||
|
@ -218,3 +225,14 @@ 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(":", "/")
|
||||
)
|
||||
|
|
76
evidence/normal_parse.py
Normal file
76
evidence/normal_parse.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
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)
|
410
evidence/normal_parse_details.py
Normal file
410
evidence/normal_parse_details.py
Normal file
|
@ -0,0 +1,410 @@
|
|||
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)
|
28
evidence/old_parse.py
Normal file
28
evidence/old_parse.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
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)
|
13
evidence/old_parse_details.py
Normal file
13
evidence/old_parse_details.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
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")
|
|
@ -2,12 +2,14 @@ import json
|
|||
import hashlib
|
||||
import logging
|
||||
|
||||
from dmidecode import DMIParse
|
||||
from evidence import legacy_parse
|
||||
from evidence import old_parse
|
||||
from evidence import normal_parse
|
||||
from evidence.parse_details import ParseSnapshot
|
||||
|
||||
from evidence.models import SystemProperty
|
||||
from evidence.xapian import index
|
||||
from evidence.parse_details import get_inxi_key, get_inxi
|
||||
from evidence.normal_parse_details import get_inxi_key, get_inxi
|
||||
from django.conf import settings
|
||||
|
||||
if settings.DPP:
|
||||
|
@ -24,34 +26,40 @@ 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.json = evidence_json.copy()
|
||||
self.uuid = self.evidence.get('uuid')
|
||||
self.user = user
|
||||
|
||||
if evidence_json.get("credentialSubject"):
|
||||
self.json.update(evidence_json["credentialSubject"])
|
||||
if evidence_json.get("evidence"):
|
||||
self.json["data"] = {}
|
||||
for ev in evidence_json["evidence"]:
|
||||
k = ev.get("operation")
|
||||
if not k:
|
||||
continue
|
||||
self.json["data"][k] = ev.get("output")
|
||||
|
||||
self.uuid = self.json['uuid']
|
||||
self.user = user
|
||||
self.hid = None
|
||||
self.chid = None
|
||||
self.phid = self.get_signature(self.json)
|
||||
self.generate_chids()
|
||||
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)
|
||||
|
||||
if check:
|
||||
return
|
||||
|
||||
if not self.build.uuid:
|
||||
return
|
||||
|
||||
self.index()
|
||||
self.create_properties()
|
||||
self.create_annotations()
|
||||
if settings.DPP:
|
||||
self.register_device_dlt()
|
||||
|
||||
|
@ -59,123 +67,32 @@ class Build:
|
|||
snap = json.dumps(self.evidence)
|
||||
index(self.user.institution, self.uuid, snap)
|
||||
|
||||
def generate_chids(self):
|
||||
self.algorithms = {
|
||||
'hidalgo1': self.get_hid_14(),
|
||||
'legacy_dpp': self.get_chid_dpp(),
|
||||
}
|
||||
|
||||
def get_hid_14(self):
|
||||
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(
|
||||
def create_annotations(self):
|
||||
prop = SystemProperty.objects.filter(
|
||||
uuid=self.uuid,
|
||||
owner=self.user.institution,
|
||||
)
|
||||
|
||||
if property:
|
||||
txt = "Warning: Snapshot %s already registered (property exists)"
|
||||
if prop:
|
||||
txt = "Warning: Snapshot %s already registered (annotation exists)"
|
||||
logger.warning(txt, self.uuid)
|
||||
return
|
||||
|
||||
for k, v in self.algorithms.items():
|
||||
for k, v in self.build.algorithms.items():
|
||||
SystemProperty.objects.create(
|
||||
uuid=self.uuid,
|
||||
owner=self.user.institution,
|
||||
user=self.user,
|
||||
key=k,
|
||||
value=v
|
||||
value=self.sign(v)
|
||||
)
|
||||
|
||||
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 sign(self, doc):
|
||||
return hashlib.sha3_256(doc.encode()).hexdigest()
|
||||
|
||||
def register_device_dlt(self):
|
||||
chid = self.algorithms.get('legacy_dpp')
|
||||
phid = self.get_signature(self.get_phid())
|
||||
legacy_dpp = self.build.algorithms.get('ereuse22')
|
||||
chid = self.sign(legacy_dpp)
|
||||
phid = self.sign(json.dumps(self.build.get_doc()))
|
||||
register_device_dlt(chid, phid, self.uuid, self.user)
|
||||
register_passport_dlt(chid, phid, self.uuid, self.user)
|
||||
|
|
|
@ -1,407 +1,38 @@
|
|||
import re
|
||||
import json
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
from datetime import datetime
|
||||
from dmidecode import DMIParse
|
||||
|
||||
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
|
||||
from evidence import (
|
||||
legacy_parse_details,
|
||||
normal_parse_details,
|
||||
old_parse_details
|
||||
)
|
||||
|
||||
|
||||
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"]
|
||||
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.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)
|
||||
self.default = default
|
||||
self.device = self.build.snapshot_json.get("device")
|
||||
self.components = self.build.snapshot_json.get("components")
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
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)
|
|
@ -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_devices' %}">{% translate "Cancel" %}</a>
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% 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
1
example/snapshots/snapshot_workbench-script.json
Normal file
1
example/snapshots/snapshot_workbench-script.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
example/snapshots/snapshot_workbench-script_legacy.json
Normal file
1
example/snapshots/snapshot_workbench-script_legacy.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -17,17 +17,17 @@ class LoginView(auth_views.LoginView):
|
|||
template_name = 'login.html'
|
||||
extra_context = {
|
||||
'title': _('Login'),
|
||||
'success_url': reverse_lazy('dashboard:unassigned_devices'),
|
||||
'success_url': reverse_lazy('dashboard:unassigned'),
|
||||
'commit_id': settings.COMMIT,
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.extra_context['success_url'] = request.GET.get(
|
||||
'next',
|
||||
reverse_lazy('dashboard:unassigned_devices')
|
||||
reverse_lazy('dashboard:unassigned')
|
||||
)
|
||||
if not self.request.user.is_anonymous:
|
||||
return redirect(reverse_lazy('dashboard:unassigned_devices'))
|
||||
return redirect(reverse_lazy('dashboard:unassigned'))
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
@ -72,4 +72,3 @@ class PasswordResetView(auth_views.PasswordResetView):
|
|||
except Exception as err:
|
||||
logger.error(err)
|
||||
return HttpResponseRedirect(self.success_url)
|
||||
|
||||
|
|
19
lot/migrations/0005_alter_lotproperty_type.py
Normal file
19
lot/migrations/0005_alter_lotproperty_type.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# 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
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# 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'),
|
||||
),
|
||||
]
|
18
lot/migrations/0007_lottag_inbox.py
Normal file
18
lot/migrations/0007_lottag_inbox.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
18
lot/migrations/0008_rename_closed_lot_archived.py
Normal file
18
lot/migrations/0008_rename_closed_lot_archived.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# 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',
|
||||
),
|
||||
]
|
|
@ -15,6 +15,7 @@ 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
|
||||
|
@ -31,7 +32,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)
|
||||
closed = models.BooleanField(default=False)
|
||||
archived = 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)
|
||||
|
@ -45,12 +46,17 @@ class Lot(models.Model):
|
|||
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")
|
||||
]
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
{% endif %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-actions-no-box">
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Delete' %}" />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Lot {{ lot.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="tab-pane fade show active" id="details">
|
||||
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
|
||||
<a href="{% url 'lot:add_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 %}
|
|
@ -11,20 +11,26 @@
|
|||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% 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 %}
|
||||
<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>
|
||||
|
||||
<button class="btn btn-green-admin" type="submit">Save</button>
|
||||
</form>
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
<h3>{{ subtitle }}</h3>
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
{% if show_closed %}
|
||||
<a href="?show_closed=false" class="btn btn-green-admin">
|
||||
{% trans 'Hide closed lots' %}
|
||||
{% if show_archived %}
|
||||
<a href="?show_archived=false" class="btn btn-green-admin">
|
||||
{% trans 'Show active lots' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="?show_closed=true" class="btn btn-green-admin">
|
||||
{% trans 'Show closed lots' %}
|
||||
<a href="?show_archived=true" class="btn btn-green-admin">
|
||||
{% trans 'Show archived lots' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
{% endif %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-actions-no-box">
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
<div class="container">
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a>
|
||||
<a class="btn btn-grey" href="{% url 'lot:properties' lot_id %}">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="tab-pane fade show active" id="details">
|
||||
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
|
||||
<a href="{% url 'lot:add_property' lot.pk %}" class="btn btn-primary">
|
||||
<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">
|
||||
|
||||
<i class="bi bi-plus"></i>
|
||||
Add new lot Property
|
||||
|
@ -20,32 +20,33 @@
|
|||
</div>
|
||||
|
||||
<h5 class="card-title mt-2">Properties</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<table class="table table-hover table-bordered table-responsive align-middle">
|
||||
<thead class="table-light">
|
||||
<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>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in properties %}
|
||||
<tr>
|
||||
<td>{{ a.key }}</td>
|
||||
<td>{{ a.value }}</td>
|
||||
<td>{{ a.created }}</td>
|
||||
<td class="text-center">
|
||||
<a href="#" class="text-info" data-bs-toggle="modal" data-bs-target="#editPropertyModal{{ a.id }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="#" class="text-danger" data-bs-toggle="modal" data-bs-target="#deletePropertyModal{{ a.id }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
<div class="modal fade" id="editPropertyModal{{ a.id }}" tabindex="-1" aria-labelledby="editPropertyModalLabel{{ a.id }}" aria-hidden="true">
|
||||
|
|
|
@ -9,9 +9,7 @@ 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("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("group/<int:pk>/", views.LotsTagsView.as_view(), name="tags"),
|
||||
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"),
|
||||
|
|
146
lot/views.py
146
lot/views.py
|
@ -1,3 +1,4 @@
|
|||
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
|
||||
|
@ -17,16 +18,24 @@ class NewLotView(DashboardView, CreateView):
|
|||
template_name = "new_lot.html"
|
||||
title = _("New lot")
|
||||
breadcrumb = "lot / New lot"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
model = Lot
|
||||
fields = (
|
||||
"type",
|
||||
"name",
|
||||
"code",
|
||||
"description",
|
||||
"closed",
|
||||
"archived",
|
||||
)
|
||||
|
||||
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
|
||||
|
@ -38,14 +47,14 @@ class DeleteLotView(DashboardView, DeleteView):
|
|||
template_name = "delete_lot.html"
|
||||
title = _("Delete lot")
|
||||
breadcrumb = "lot / Delete lot"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
model = Lot
|
||||
fields = (
|
||||
"type",
|
||||
"name",
|
||||
"code",
|
||||
"description",
|
||||
"closed",
|
||||
"archived",
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -57,14 +66,14 @@ class EditLotView(DashboardView, UpdateView):
|
|||
template_name = "new_lot.html"
|
||||
title = _("Edit lot")
|
||||
breadcrumb = "Lot / Edit lot"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
model = Lot
|
||||
fields = (
|
||||
"type",
|
||||
"name",
|
||||
"code",
|
||||
"description",
|
||||
"closed",
|
||||
"archived",
|
||||
)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
|
@ -78,12 +87,20 @@ 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_devices')
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
form_class = LotsForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -98,7 +115,8 @@ 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):
|
||||
|
@ -123,7 +141,7 @@ class LotsTagsView(DashboardView, TemplateView):
|
|||
template_name = "lots.html"
|
||||
title = _("lots")
|
||||
breadcrumb = _("lots") + " /"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.pk = kwargs.get('pk')
|
||||
|
@ -131,60 +149,15 @@ 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_closed = self.request.GET.get('show_closed', 'false') == 'true'
|
||||
lots = Lot.objects.filter(owner=self.request.user.institution).filter(type=tag, closed=show_closed)
|
||||
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
|
||||
)
|
||||
context.update({
|
||||
'lots': lots,
|
||||
'title': self.title,
|
||||
'breadcrumb': self.breadcrumb,
|
||||
'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
|
||||
'show_archived': show_archived
|
||||
})
|
||||
return context
|
||||
|
||||
|
@ -225,8 +198,13 @@ class AddLotPropertyView(DashboardView, CreateView):
|
|||
form.instance.user = self.request.user
|
||||
form.instance.lot = self.lot
|
||||
form.instance.type = LotProperty.Type.USER
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
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)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
|
@ -235,6 +213,11 @@ 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"
|
||||
|
@ -245,31 +228,33 @@ 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):
|
||||
old_key= self.object.key
|
||||
old_value = self.object.value
|
||||
new_key = form.cleaned_data['key']
|
||||
new_value = form.cleaned_data['value']
|
||||
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)
|
||||
|
||||
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]))
|
||||
def form_invalid(self, form):
|
||||
super().form_invalid(form)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
class DeleteLotPropertyView(DashboardView, DeleteView):
|
||||
|
@ -277,18 +262,15 @@ 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
|
||||
)
|
||||
old_value = self.object.key
|
||||
lot_pk = self.object.lot.pk
|
||||
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(referer)
|
||||
return redirect(self.success_url)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TagConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "tag"
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
8
tests/end-to-end/.gitignore
vendored
Normal file
8
tests/end-to-end/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
tests-examples
|
||||
example.spec.ts
|
97
tests/end-to-end/package-lock.json
generated
Normal file
97
tests/end-to-end/package-lock.json
generated
Normal file
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
15
tests/end-to-end/package.json
Normal file
15
tests/end-to-end/package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
77
tests/end-to-end/playwright.config.ts
Normal file
77
tests/end-to-end/playwright.config.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
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,
|
||||
// },
|
||||
});
|
21
tests/end-to-end/run.sh
Executable file
21
tests/end-to-end/run.sh
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/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; -*-
|
237
tests/end-to-end/tests/tests.spec.ts
Normal file
237
tests/end-to-end/tests/tests.spec.ts
Normal file
|
@ -0,0 +1,237 @@
|
|||
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
|
||||
//});
|
|
@ -1,6 +1,7 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from user.models import Institution
|
||||
from lot.models import LotTag
|
||||
from lot.models import LotTag, Lot
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Create a new Institution"
|
||||
|
@ -11,8 +12,14 @@ 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",
|
||||
|
@ -23,3 +30,59 @@ 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
|
||||
)
|
||||
|
|
18
user/migrations/0002_institution_algorithm.py
Normal file
18
user/migrations/0002_institution_algorithm.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
|
@ -1,8 +1,10 @@
|
|||
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
|
||||
|
||||
# Create your models here.
|
||||
|
||||
ALGORITHMS = [(x, x) for x in ALGOS.keys()]
|
||||
|
||||
|
||||
class Institution(models.Model):
|
||||
|
@ -27,6 +29,7 @@ class Institution(models.Model):
|
|||
blank=True,
|
||||
null=True
|
||||
)
|
||||
algorithm = models.CharField(max_length=30, choices=ALGORITHMS, default='ereuse24')
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
|
|
|
@ -9,27 +9,28 @@ STR_EXTEND_SIZE = 256
|
|||
|
||||
|
||||
# Algorithms for build hids
|
||||
HID_ALGO1 = [
|
||||
EREUSE24 = [
|
||||
"manufacturer",
|
||||
"model",
|
||||
"chassis",
|
||||
"serialNumber",
|
||||
"sku"
|
||||
"serial_number",
|
||||
"mac"
|
||||
]
|
||||
|
||||
LEGACY_DPP = [
|
||||
# EREUSE22 is used for build the chid of DPP
|
||||
EREUSE22 = [
|
||||
"manufacturer",
|
||||
"model",
|
||||
"chassis",
|
||||
"serialNumber",
|
||||
"serial_number",
|
||||
"sku",
|
||||
"type",
|
||||
"version"
|
||||
]
|
||||
|
||||
ALGOS = {
|
||||
"hidalgo1": HID_ALGO1,
|
||||
"legacy_dpp": LEGACY_DPP
|
||||
"ereuse24": EREUSE24,
|
||||
"ereuse22": EREUSE22
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue