Compare commits
244 commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
78de86c43a | ||
|
498da24ee6 | ||
|
67213e46d2 | ||
|
5ec44d3378 | ||
|
1d195806aa | ||
|
d8202fea0f | ||
|
e6fea8c1c3 | ||
|
782f6dac51 | ||
|
46bbb940d7 | ||
|
abdefe885e | ||
|
1902647337 | ||
|
989120bd0a | ||
|
dbdcffe5d9 | ||
|
1ea400051c | ||
|
f5ea0f8d8d | ||
|
c5dd3a759d | ||
|
3bf23c5d3b | ||
|
e54d8ba49c | ||
|
7f44d88b61 | ||
|
b2e6406fb6 | ||
|
b477431fcd | ||
|
2c21977c7c | ||
|
563e394afc | ||
|
f566bfdf03 | ||
|
f04d425a31 | ||
|
8aefc90ece | ||
|
13249d564f | ||
|
ddb764b3ea | ||
|
351c62b45d | ||
|
16a3a870be | ||
|
76c4b10fc4 | ||
|
1910609f68 | ||
|
53524643c8 | ||
|
9b7bbd6bcf | ||
|
f7f6da5892 | ||
|
8643495a9d | ||
|
dd327e5231 | ||
|
cea5a279b7 | ||
|
594904905b | ||
|
74f0d5a507 | ||
|
6d53a2acda | ||
|
e64e9b3c06 | ||
|
f1c21af654 | ||
|
6efcf5ac18 | ||
|
5631da453a | ||
|
0c5adf87c6 | ||
|
dde4a114a0 | ||
|
e791d0c63c | ||
|
833c458840 | ||
|
e6424af251 | ||
|
d31c5c7921 | ||
|
5432d22f35 | ||
|
fdc431b43a | ||
|
ba6c955463 | ||
|
8eb89e602d | ||
|
80ab639759 | ||
|
162fc9a8ef | ||
|
eec14f4ffb | ||
|
3ab21b8c32 | ||
|
316184df47 | ||
|
7d6119c11a | ||
|
1248ab6894 | ||
|
41e4374612 | ||
|
6e7cb0bf84 | ||
|
09f6cd2e68 | ||
|
e25b8e5994 | ||
|
2793344684 | ||
|
f70c75a92f | ||
|
83719edc8e | ||
|
69bc95c0cd | ||
|
9863a59911 | ||
|
eb78d38c3a | ||
|
a49b31dd85 | ||
|
493c7636b2 | ||
|
88e036eb3c | ||
|
b759c53e75 | ||
|
7ab88ad290 | ||
|
f6d1cf719c | ||
|
fb836edfbb | ||
|
cc350775ed | ||
|
0d574cae63 | ||
|
9857891b63 |
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
|
57
.env.example
57
.env.example
|
@ -1,8 +1,19 @@
|
||||||
DOMAIN=localhost
|
####
|
||||||
|
# DEV OPTIONS
|
||||||
|
####
|
||||||
|
|
||||||
|
DEV_DOCKER_ALWAYS_BUILD=false
|
||||||
|
|
||||||
|
####
|
||||||
|
# DEVICEHUB
|
||||||
|
####
|
||||||
|
|
||||||
|
DEVICEHUB_DOMAIN=localhost
|
||||||
|
DEVICEHUB_PORT=8001
|
||||||
DEMO=true
|
DEMO=true
|
||||||
# note that with DEBUG=true, logs are more verbose (include tracebacks)
|
# note that with DEBUG=true, logs are more verbose (include tracebacks)
|
||||||
DEBUG=true
|
DEBUG=true
|
||||||
ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1,
|
DPP=false
|
||||||
|
|
||||||
STATIC_ROOT=/tmp/static/
|
STATIC_ROOT=/tmp/static/
|
||||||
MEDIA_ROOT=/tmp/media/
|
MEDIA_ROOT=/tmp/media/
|
||||||
|
@ -15,6 +26,48 @@ EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
|
||||||
EMAIL_FILE_PATH="/tmp/app-messages"
|
EMAIL_FILE_PATH="/tmp/app-messages"
|
||||||
ENABLE_EMAIL=false
|
ENABLE_EMAIL=false
|
||||||
PREDEFINED_TOKEN='5018dd65-9abd-4a62-8896-80f34ac66150'
|
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
|
# TODO review these vars
|
||||||
#SNAPSHOTS_DIR=/path/to/TODO
|
#SNAPSHOTS_DIR=/path/to/TODO
|
||||||
#EVIDENCES_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/
|
env/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# the following could be autogenerated by devicehub
|
||||||
|
db.sqlite3
|
||||||
|
example/snapshots/snapshot_workbench-script_verifiable-credential.json
|
||||||
|
|
16
action/forms.py
Normal file
16
action/forms.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django import forms
|
||||||
|
from .models import State
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeStateForm(forms.Form):
|
||||||
|
previous_state = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
snapshot_uuid = forms.UUIDField(widget=forms.HiddenInput())
|
||||||
|
new_state = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
|
||||||
|
|
||||||
|
class AddNoteForm(forms.Form):
|
||||||
|
snapshot_uuid = forms.UUIDField(widget=forms.HiddenInput())
|
||||||
|
note = forms.CharField(
|
||||||
|
required=True,
|
||||||
|
widget=forms.Textarea(attrs={'rows': 4, 'maxlength': 200, 'placeholder': 'Max 200 characters'}),
|
||||||
|
)
|
43
action/management/commands/create_default_states.py
Normal file
43
action/management/commands/create_default_states.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import logging
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from action.models import StateDefinition, Institution
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Create default StateDefinitions for a given institution. "'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('institution_name', type=str, help='The name of the institution')
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
default_states = [
|
||||||
|
_("INBOX"),
|
||||||
|
_("VISUAL INSPECTION"),
|
||||||
|
_("REPAIR"),
|
||||||
|
_("INSTALL"),
|
||||||
|
_("TEST"),
|
||||||
|
_("PACKAGING"),
|
||||||
|
_("DONATION"),
|
||||||
|
_("DISMANTLE")
|
||||||
|
]
|
||||||
|
|
||||||
|
institution_name = kwargs['institution_name']
|
||||||
|
institution = Institution.objects.filter(name=institution_name).first()
|
||||||
|
|
||||||
|
if not institution:
|
||||||
|
txt = "No institution found for: %s. Please create an institution first"
|
||||||
|
logger.error(txt, institution.name)
|
||||||
|
return
|
||||||
|
|
||||||
|
for state in default_states:
|
||||||
|
state_def, created = StateDefinition.objects.get_or_create(
|
||||||
|
institution=institution,
|
||||||
|
state=state
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'Successfully created state: {state}'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(f'State already exists: {state}'))
|
83
action/migrations/0001_initial.py
Normal file
83
action/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-11 18:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="State",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("state", models.CharField(max_length=50)),
|
||||||
|
("snapshot_uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"institution",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="StateDefinition",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.PositiveIntegerField(default=0)),
|
||||||
|
("state", models.CharField(max_length=50)),
|
||||||
|
(
|
||||||
|
"institution",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["order"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="statedefinition",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("institution", "state"), name="unique_institution_state"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
89
action/migrations/0002_devicelog_note.py
Normal file
89
action/migrations/0002_devicelog_note.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-17 19:40
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("action", "0001_initial"),
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="DeviceLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("event", models.CharField(max_length=255)),
|
||||||
|
("snapshot_uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"institution",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-date"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Note",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("description", models.TextField()),
|
||||||
|
("snapshot_uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"institution",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-date"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,3 +1,83 @@
|
||||||
from django.db import models
|
from django.db import models, connection
|
||||||
|
from django.db.models import Max
|
||||||
|
from user.models import User, Institution
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
# Create your models here.
|
class State(models.Model):
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
institution = models.ForeignKey(Institution, on_delete=models.SET_NULL, null=True)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||||
|
|
||||||
|
state = models.CharField(max_length=50)
|
||||||
|
snapshot_uuid = models.UUIDField()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not StateDefinition.objects.filter(institution=self.institution, state=self.state).exists():
|
||||||
|
raise ValidationError(f"The state '{self.state}' is not valid for the institution '{self.institution.name}'.")
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.institution.name} - {self.state} - {self.snapshot_uuid}"
|
||||||
|
|
||||||
|
|
||||||
|
class StateDefinition(models.Model):
|
||||||
|
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
order = models.PositiveIntegerField(default=0)
|
||||||
|
state = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['order']
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['institution', 'state'], name='unique_institution_state')
|
||||||
|
]
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pk:
|
||||||
|
# set the order to be last
|
||||||
|
max_order = StateDefinition.objects.filter(institution=self.institution).aggregate(Max('order'))['order__max']
|
||||||
|
self.order = (max_order or 0) + 1
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
institution = self.institution
|
||||||
|
order = self.order
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
# Adjust the order of other instances
|
||||||
|
StateDefinition.objects.filter(institution=institution, order__gt=order).update(order=models.F('order') - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.institution.name} - {self.state}"
|
||||||
|
|
||||||
|
|
||||||
|
class Note(models.Model):
|
||||||
|
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
description = models.TextField()
|
||||||
|
snapshot_uuid = models.UUIDField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-date']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f" Note: {self.description}, by {self.user.username} @ {self.user.institution} - {self.date}, for {self.snapshot_uuid}"
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceLog(models.Model):
|
||||||
|
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
event = models.CharField(max_length=255)
|
||||||
|
snapshot_uuid = models.UUIDField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-date']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.event} by {self.user.username} @ {self.institution.name} - {self.date}, for {self.snapshot_uuid}"
|
||||||
|
|
|
@ -1 +1,12 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
from action import views
|
||||||
|
|
||||||
|
app_name = 'action'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
|
||||||
|
path("new/", views.ChangeStateView.as_view(), name="change_state"),
|
||||||
|
path('note/add/', views.AddNoteView.as_view(), name='add_note'),
|
||||||
|
path('note/edit/<int:pk>', views.UpdateNoteView.as_view(), name='update_note'),
|
||||||
|
path('note/delete/<int:pk>', views.DeleteNoteView.as_view(), name='delete_note'),
|
||||||
|
]
|
||||||
|
|
142
action/views.py
142
action/views.py
|
@ -1 +1,141 @@
|
||||||
# from django.shortcuts import render
|
from django.views import View
|
||||||
|
from django.shortcuts import redirect, get_object_or_404
|
||||||
|
from django.contrib import messages
|
||||||
|
from action.forms import ChangeStateForm, AddNoteForm
|
||||||
|
from django.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 _
|
||||||
|
from action.models import State, StateDefinition, Note, DeviceLog
|
||||||
|
from device.models import Device
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeStateView(LoginRequiredMixin, FormView):
|
||||||
|
form_class = ChangeStateForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
previous_state = form.cleaned_data['previous_state']
|
||||||
|
new_state = form.cleaned_data['new_state']
|
||||||
|
snapshot_uuid = form.cleaned_data['snapshot_uuid']
|
||||||
|
|
||||||
|
State.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
state=new_state,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
|
||||||
|
message = _("<Created> State '{}'. Previous State: '{}'").format(new_state, previous_state)
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
messages.success(self.request, _("State successfully changed from '{}' to '{}'").format(previous_state, new_state))
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
messages.error(self.request, _("There was an error with your submission."))
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
|
||||||
|
|
||||||
|
|
||||||
|
class AddNoteView(LoginRequiredMixin, FormView):
|
||||||
|
form_class = AddNoteForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
note_text = form.cleaned_data['note']
|
||||||
|
snapshot_uuid = form.cleaned_data['snapshot_uuid']
|
||||||
|
Note.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
description=note_text,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
|
||||||
|
message = _("<Created> Note: '{}'").format(note_text)
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
messages.success(self.request, _("Note has been added"))
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
messages.error(self.request, _("There was an error with your submission."))
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateNoteView(LoginRequiredMixin, UpdateView):
|
||||||
|
model = Note
|
||||||
|
fields = ['description']
|
||||||
|
pk_url_kwarg = 'pk'
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
old_description = self.get_object().description
|
||||||
|
new_description = self.object.description
|
||||||
|
snapshot_uuid = self.object.snapshot_uuid
|
||||||
|
|
||||||
|
if old_description != new_description:
|
||||||
|
message = _("<Updated> Note. Old Description: '{}'. New Description: '{}'").format(old_description, new_description)
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
messages.success(self.request, "Note has been updated.")
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
new_description = form.cleaned_data.get('description', '').strip()
|
||||||
|
if not new_description:
|
||||||
|
messages.error(self.request, _("Note cannot be empty."))
|
||||||
|
super().form_invalid(form)
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details'))
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteNoteView(LoginRequiredMixin, View):
|
||||||
|
model = Note
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.pk = kwargs['pk']
|
||||||
|
referer = request.META.get('HTTP_REFERER')
|
||||||
|
if not referer:
|
||||||
|
raise Http404("No referer header found")
|
||||||
|
|
||||||
|
self.object = get_object_or_404(
|
||||||
|
self.model,
|
||||||
|
pk=self.pk,
|
||||||
|
institution=self.request.user.institution
|
||||||
|
)
|
||||||
|
description = self.object.description
|
||||||
|
snapshot_uuid= self.object.snapshot_uuid
|
||||||
|
|
||||||
|
if request.user != self.object.user and not request.user.is_admin:
|
||||||
|
messages.error(request, _("You do not have permission to delete this note."))
|
||||||
|
return redirect(referer)
|
||||||
|
|
||||||
|
message = _("<Deleted> Note. Description: '{}'. ").format(description)
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
event=message,
|
||||||
|
user=request.user,
|
||||||
|
institution=request.user.institution,
|
||||||
|
)
|
||||||
|
messages.warning(self.request, _("Note '{}' deleted successfully.").format(description))
|
||||||
|
|
||||||
|
self.object.delete()
|
||||||
|
|
||||||
|
return redirect(referer)
|
||||||
|
|
5
admin/forms.py
Normal file
5
admin/forms.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class OrderingStateForm(forms.Form):
|
||||||
|
ordering = forms.CharField()
|
233
admin/templates/states_panel.html
Normal file
233
admin/templates/states_panel.html
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n django_bootstrap5 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3>{{ subtitle }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col text-end">
|
||||||
|
<button type="button" class="btn btn-green-admin" data-bs-toggle="modal" data-bs-target="#addStateModal">
|
||||||
|
{% trans "Add" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col">
|
||||||
|
{% if state_definitions %}
|
||||||
|
<table class="table table-hover table-bordered align-middle">
|
||||||
|
<caption class="text-muted small">
|
||||||
|
{% trans 'Move and drag state definitions to reorder' %}
|
||||||
|
</caption>
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" width="1%" class="text-start">
|
||||||
|
</th>
|
||||||
|
<th scope="col" width="5%" class="text-center">
|
||||||
|
#</th>
|
||||||
|
<th scope="col">{% trans "State Definition" %}
|
||||||
|
</th>
|
||||||
|
<th scope="col" width="15%" class="text-center">{% trans "Actions" %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="sortable_list">
|
||||||
|
{% for state_definition in state_definitions %}
|
||||||
|
<tr
|
||||||
|
data-lookup="{{ state_definition.id }}"
|
||||||
|
style="cursor: grab;"
|
||||||
|
class="align-items-center">
|
||||||
|
|
||||||
|
<td class="">
|
||||||
|
<i class="bi bi-grip-vertical" aria-hidden="true">
|
||||||
|
</i>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<strong>{{ state_definition.order }} </strong>
|
||||||
|
</td>
|
||||||
|
<td class="font-monospace">
|
||||||
|
{{ state_definition.state }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- action buttons -->
|
||||||
|
<td>
|
||||||
|
<div class="btn-group float-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-info d-flex align-items-center"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#editStateModal{{ state_definition.id }}">
|
||||||
|
<i class="bi bi-pencil me-1"></i>
|
||||||
|
{% trans 'Edit' %}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button" class="btn btn-sm btn-outline-danger d-flex align-items-center"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteStateModal{{ state_definition.id }}" >
|
||||||
|
<i class="bi bi-trash me-1"></i>
|
||||||
|
{% trans 'Delete' %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form id="orderingForm" method="post" action="{% url 'admin:update_state_order' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" id="orderingInput" name="ordering">
|
||||||
|
<button id="saveOrderBtn" class="btn btn-success mt-5 float-start collapse" >{% trans "Update Order" %}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-primary text-center mt-5" role="alert">
|
||||||
|
{% trans "No states found on current organization" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- add state definition Modal -->
|
||||||
|
<div class="modal fade" id="addStateModal" tabindex="-1" aria-labelledby="addStateModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addStateModalLabel">{% trans "Add State Definition" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<form method="post" action="{%url 'admin:add_state_definition'%}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stateInput" class="form-label">{% trans "State" %}</label>
|
||||||
|
<input type="text" class="form-control" id="stateInput" name="state" maxlength="50" required>
|
||||||
|
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans "Add state definition" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit State Definition Modals -->
|
||||||
|
{% for state_definition in state_definitions %}
|
||||||
|
<div class="modal fade" id="editStateModal{{ state_definition.id }}" tabindex="-1" aria-labelledby="editStateModalLabel{{ state_definition.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" action="{% url 'admin:edit_state_definition' state_definition.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editStateModalLabel{{ state_definition.id }}">
|
||||||
|
{% trans "Edit State Definition" %}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-warning text-center" role="alert">
|
||||||
|
{% trans "Existing devices with this state will not have their state names changed." %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editStateInput{{ state_definition.id }}" class="form-label">{% trans "State" %}</label>
|
||||||
|
<input type="text" class="form-control" id="editStateInput{{ state_definition.id }}" name="state" maxlength="50" value="{{ state_definition.state }}" required>
|
||||||
|
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted text-end">{% trans "Any changes in order will not be saved." %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-green-admin">{% trans "Save Changes" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- delete state definition Modal -->
|
||||||
|
{% for state_definition in state_definitions %}
|
||||||
|
<div class="modal fade" id="deleteStateModal{{ state_definition.id }}" tabindex="-1" aria-labelledby="deleteStateModalLabel{{ state_definition.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold" id="deleteStateModalLabel{{ state_definition.id }}">
|
||||||
|
{% trans "Delete State Definition" %}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-warning text-center" role="alert">
|
||||||
|
{% trans "Devices with a State of this description will not have their State altered" %}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center border rounded p-3 mt-3">
|
||||||
|
<span class="badge bg-secondary me-3 display-6">{{ state_definition.order }}</span>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 fw-bold">{{ state_definition.state }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted text-end mt-3">{% trans "Any changes in order will not be saved." %}</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<form method="post" action="{% url 'admin:delete_state_definition' state_definition.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
{% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//following https://dev.to/nemecek_f/django-how-to-let-user-re-order-sort-table-of-content-with-drag-and-drop-3nlp
|
||||||
|
const saveOrderingButton = document.getElementById('saveOrderBtn');
|
||||||
|
const orderingForm = document.getElementById('orderingForm');
|
||||||
|
const formInput = orderingForm.querySelector('#orderingInput');
|
||||||
|
const sortable_table = document.getElementById('sortable_list');
|
||||||
|
|
||||||
|
const sortable = new Sortable(sortable_table, {
|
||||||
|
animation: 150,
|
||||||
|
swapThreshold: 0.10,
|
||||||
|
onChange: () => {
|
||||||
|
//TODO: change hide/show animation to a nicer one
|
||||||
|
const collapse = new bootstrap.Collapse(saveOrderingButton, {
|
||||||
|
toggle: false
|
||||||
|
});
|
||||||
|
collapse.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveOrdering() {
|
||||||
|
const rows = sortable_table.querySelectorAll('tr');
|
||||||
|
let ids = [];
|
||||||
|
for (let row of rows) {
|
||||||
|
ids.push(row.dataset.lookup);
|
||||||
|
}
|
||||||
|
formInput.value = ids.join(',');
|
||||||
|
orderingForm.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveOrderingButton.addEventListener('click', saveOrdering);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -10,4 +10,9 @@ urlpatterns = [
|
||||||
path("users/edit/<int:pk>", views.EditUserView.as_view(), name="edit_user"),
|
path("users/edit/<int:pk>", views.EditUserView.as_view(), name="edit_user"),
|
||||||
path("users/delete/<int:pk>", views.DeleteUserView.as_view(), name="delete_user"),
|
path("users/delete/<int:pk>", views.DeleteUserView.as_view(), name="delete_user"),
|
||||||
path("institution/<int:pk>", views.InstitutionView.as_view(), name="institution"),
|
path("institution/<int:pk>", views.InstitutionView.as_view(), name="institution"),
|
||||||
|
path("states/", views.StatesPanelView.as_view(), name="states_panel"),
|
||||||
|
path("states/add", views.AddStateDefinitionView.as_view(), name="add_state_definition"),
|
||||||
|
path("states/delete/<int:pk>", views.DeleteStateDefinitionView.as_view(), name='delete_state_definition'),
|
||||||
|
path("states/update_order/", views.UpdateStateOrderView.as_view(), name='update_state_order'),
|
||||||
|
path("states/edit/<int:pk>/", views.UpdateStateDefinitionView.as_view(), name='edit_state_definition'),
|
||||||
]
|
]
|
||||||
|
|
121
admin/views.py
121
admin/views.py
|
@ -1,16 +1,23 @@
|
||||||
|
import logging
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
from django.contrib import messages
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic.base import TemplateView
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.views.generic.base import TemplateView, ContextMixin
|
||||||
from django.views.generic.edit import (
|
from django.views.generic.edit import (
|
||||||
CreateView,
|
CreateView,
|
||||||
UpdateView,
|
UpdateView,
|
||||||
DeleteView,
|
DeleteView,
|
||||||
)
|
)
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import IntegrityError, transaction
|
||||||
from dashboard.mixins import DashboardView, Http403
|
from dashboard.mixins import DashboardView, Http403
|
||||||
|
from admin.forms import OrderingStateForm
|
||||||
from user.models import User, Institution
|
from user.models import User, Institution
|
||||||
from admin.email import NotifyActivateUserByEmail
|
from admin.email import NotifyActivateUserByEmail
|
||||||
|
from action.models import StateDefinition
|
||||||
|
|
||||||
|
|
||||||
class AdminView(DashboardView):
|
class AdminView(DashboardView):
|
||||||
|
@ -18,7 +25,7 @@ class AdminView(DashboardView):
|
||||||
response = super().get(*args, **kwargs)
|
response = super().get(*args, **kwargs)
|
||||||
if not self.request.user.is_admin:
|
if not self.request.user.is_admin:
|
||||||
raise Http403
|
raise Http403
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
class PanelView(AdminView, TemplateView):
|
class PanelView(AdminView, TemplateView):
|
||||||
|
@ -104,7 +111,7 @@ class EditUserView(AdminView, UpdateView):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class InstitutionView(AdminView, UpdateView):
|
class InstitutionView(AdminView, UpdateView):
|
||||||
template_name = "institution.html"
|
template_name = "institution.html"
|
||||||
title = _("Edit institution")
|
title = _("Edit institution")
|
||||||
|
@ -124,3 +131,109 @@ class InstitutionView(AdminView, UpdateView):
|
||||||
self.object = self.request.user.institution
|
self.object = self.request.user.institution
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class StateDefinitionContextMixin(ContextMixin):
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context.update({
|
||||||
|
"state_definitions": StateDefinition.objects.filter(institution=self.request.user.institution).order_by('order'),
|
||||||
|
"help_text": _('State definitions are the custom finite states that a device can be in.'),
|
||||||
|
})
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class StatesPanelView(AdminView, StateDefinitionContextMixin, TemplateView):
|
||||||
|
template_name = "states_panel.html"
|
||||||
|
title = _("States Panel")
|
||||||
|
breadcrumb = _("admin / States Panel") + " /"
|
||||||
|
|
||||||
|
|
||||||
|
class AddStateDefinitionView(AdminView, StateDefinitionContextMixin, CreateView):
|
||||||
|
template_name = "states_panel.html"
|
||||||
|
title = _("New State Definition")
|
||||||
|
breadcrumb = "Admin / New state"
|
||||||
|
success_url = reverse_lazy('admin:states_panel')
|
||||||
|
model = StateDefinition
|
||||||
|
fields = ('state',)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.institution = self.request.user.institution
|
||||||
|
form.instance.user = self.request.user
|
||||||
|
try:
|
||||||
|
response = super().form_valid(form)
|
||||||
|
messages.success(self.request, _("State definition successfully added."))
|
||||||
|
return response
|
||||||
|
except IntegrityError:
|
||||||
|
messages.error(self.request, _("State is already defined."))
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
super().form_invalid(form)
|
||||||
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessMessageMixin, DeleteView):
|
||||||
|
model = StateDefinition
|
||||||
|
success_url = reverse_lazy('admin:states_panel')
|
||||||
|
|
||||||
|
def get_success_message(self, cleaned_data):
|
||||||
|
return f'State definition: {self.object.state}, has been deleted'
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
|
||||||
|
#only an admin of current institution can delete
|
||||||
|
if not object.institution == self.request.user.institution:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateStateOrderView(AdminView, TemplateView):
|
||||||
|
success_url = reverse_lazy('admin:states_panel')
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
form = OrderingStateForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
ordered_ids = form.cleaned_data["ordering"].split(',')
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
current_order = 1
|
||||||
|
_log = []
|
||||||
|
for lookup_id in ordered_ids:
|
||||||
|
state_definition = StateDefinition.objects.get(id=lookup_id)
|
||||||
|
state_definition.order = current_order
|
||||||
|
state_definition.save()
|
||||||
|
_log.append(f"{state_definition.state} (ID: {lookup_id} -> Order: {current_order})")
|
||||||
|
current_order += 1
|
||||||
|
|
||||||
|
messages.success(self.request, _("Order changed succesfuly."))
|
||||||
|
return redirect(self.success_url)
|
||||||
|
else:
|
||||||
|
return Http404
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateStateDefinitionView(AdminView, UpdateView):
|
||||||
|
model = StateDefinition
|
||||||
|
template_name = 'states_panel.html'
|
||||||
|
fields = ['state']
|
||||||
|
success_url = reverse_lazy('admin:states_panel')
|
||||||
|
pk_url_kwarg = 'pk'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return StateDefinition.objects.filter(institution=self.request.user.institution)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
try:
|
||||||
|
response = super().form_valid(form)
|
||||||
|
messages.success(self.request, _("State definition updated successfully."))
|
||||||
|
return response
|
||||||
|
except IntegrityError:
|
||||||
|
messages.error(self.request, _("State is already defined."))
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
super().form_invalid(form)
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
|
@ -7,7 +7,7 @@ app_name = 'api'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('v1/snapshot/', views.NewSnapshotView.as_view(), name='new_snapshot'),
|
path('v1/snapshot/', views.NewSnapshotView.as_view(), name='new_snapshot'),
|
||||||
path('v1/annotation/<str:pk>/', views.AddAnnotationView.as_view(), name='new_annotation'),
|
path('v1/property/<str:pk>/', views.AddPropertyView.as_view(), name='new_property'),
|
||||||
path('v1/device/<str:pk>/', views.DetailsDeviceView.as_view(), name='device'),
|
path('v1/device/<str:pk>/', views.DetailsDeviceView.as_view(), name='device'),
|
||||||
path('v1/tokens/', views.TokenView.as_view(), name='tokens'),
|
path('v1/tokens/', views.TokenView.as_view(), name='tokens'),
|
||||||
path('v1/tokens/new', views.TokenNewView.as_view(), name='new_token'),
|
path('v1/tokens/new', views.TokenNewView.as_view(), name='new_token'),
|
||||||
|
|
54
api/views.py
54
api/views.py
|
@ -21,7 +21,7 @@ from django.views.generic.edit import (
|
||||||
from utils.save_snapshots import move_json, save_in_disk
|
from utils.save_snapshots import move_json, save_in_disk
|
||||||
from django.views.generic.edit import View
|
from django.views.generic.edit import View
|
||||||
from dashboard.mixins import DashboardView
|
from dashboard.mixins import DashboardView
|
||||||
from evidence.models import Annotation
|
from evidence.models import SystemProperty, UserProperty
|
||||||
from evidence.parse_details import ParseSnapshot
|
from evidence.parse_details import ParseSnapshot
|
||||||
from evidence.parse import Build
|
from evidence.parse import Build
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
|
@ -85,17 +85,21 @@ class NewSnapshotView(ApiMixing):
|
||||||
# except Exception:
|
# except Exception:
|
||||||
# return JsonResponse({'error': 'Invalid Snapshot'}, status=400)
|
# return JsonResponse({'error': 'Invalid Snapshot'}, status=400)
|
||||||
|
|
||||||
if not data.get("uuid"):
|
ev_uuid = data.get("uuid")
|
||||||
txt = "error: the snapshot not have uuid"
|
if data.get("credentialSubject"):
|
||||||
|
ev_uuid = data["credentialSubject"].get("uuid")
|
||||||
|
|
||||||
|
if not ev_uuid:
|
||||||
|
txt = "error: the snapshot does not have an uuid"
|
||||||
logger.error("%s", txt)
|
logger.error("%s", txt)
|
||||||
return JsonResponse({'status': txt}, status=500)
|
return JsonResponse({'status': txt}, status=500)
|
||||||
|
|
||||||
exist_annotation = Annotation.objects.filter(
|
exist_property = SystemProperty.objects.filter(
|
||||||
uuid=data['uuid']
|
uuid=ev_uuid
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if exist_annotation:
|
if exist_property:
|
||||||
txt = "error: the snapshot {} exist".format(data['uuid'])
|
txt = "error: the snapshot {} exist".format(ev_uuid)
|
||||||
logger.warning("%s", txt)
|
logger.warning("%s", txt)
|
||||||
return JsonResponse({'status': txt}, status=500)
|
return JsonResponse({'status': txt}, status=500)
|
||||||
|
|
||||||
|
@ -105,31 +109,30 @@ class NewSnapshotView(ApiMixing):
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
logger.exception("%s", err)
|
logger.exception("%s", err)
|
||||||
snapshot_id = data.get("uuid", "")
|
snapshot_id = ev_uuid
|
||||||
txt = "It is not possible to parse snapshot: %s."
|
txt = "It is not possible to parse snapshot: %s."
|
||||||
logger.error(txt, snapshot_id)
|
logger.error(txt, snapshot_id)
|
||||||
text = "fail: It is not possible to parse snapshot"
|
text = "fail: It is not possible to parse snapshot"
|
||||||
return JsonResponse({'status': text}, status=500)
|
return JsonResponse({'status': text}, status=500)
|
||||||
|
|
||||||
annotation = Annotation.objects.filter(
|
prop = SystemProperty.objects.filter(
|
||||||
uuid=data['uuid'],
|
uuid=ev_uuid,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
# TODO this is hardcoded, it should select the user preferred algorithm
|
# TODO this is hardcoded, it should select the user preferred algorithm
|
||||||
key="hidalgo1",
|
key="ereuse24",
|
||||||
owner=self.tk.owner.institution
|
owner=self.tk.owner.institution
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
|
||||||
if not annotation:
|
if not prop:
|
||||||
logger.error("Error: No annotation for uuid: %s", data["uuid"])
|
logger.error("Error: No property for uuid: %s", ev_uuid)
|
||||||
return JsonResponse({'status': 'fail'}, status=500)
|
return JsonResponse({'status': 'fail'}, status=500)
|
||||||
|
|
||||||
url_args = reverse_lazy("device:details", args=(annotation.value,))
|
url_args = reverse_lazy("device:details", args=(property.value,))
|
||||||
url = request.build_absolute_uri(url_args)
|
url = request.build_absolute_uri(url_args)
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"dhid": annotation.value[:6].upper(),
|
"dhid": property.value[:6].upper(),
|
||||||
"url": url,
|
"url": url,
|
||||||
# TODO replace with public_url when available
|
# TODO replace with public_url when available
|
||||||
"public_url": url
|
"public_url": url
|
||||||
|
@ -255,22 +258,21 @@ class DetailsDeviceView(ApiMixing):
|
||||||
"components": snapshot.get("components"),
|
"components": snapshot.get("components"),
|
||||||
})
|
})
|
||||||
|
|
||||||
uuids = Annotation.objects.filter(
|
uuids = SystemProperty.objects.filter(
|
||||||
owner=self.tk.owner.institution,
|
owner=self.tk.owner.institution,
|
||||||
value=self.pk
|
value=self.pk
|
||||||
).values("uuid")
|
).values("uuid")
|
||||||
|
|
||||||
annotations = Annotation.objects.filter(
|
properties = UserProperty.objects.filter(
|
||||||
uuid__in=uuids,
|
uuid__in=uuids,
|
||||||
owner=self.tk.owner.institution,
|
owner=self.tk.owner.institution,
|
||||||
type = Annotation.Type.USER
|
|
||||||
).values_list("key", "value")
|
).values_list("key", "value")
|
||||||
|
|
||||||
data.update({"annotations": list(annotations)})
|
data.update({"properties": list(properties)})
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class AddAnnotationView(ApiMixing):
|
class AddPropertyView(ApiMixing):
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
response = self.auth()
|
response = self.auth()
|
||||||
|
@ -279,13 +281,12 @@ class AddAnnotationView(ApiMixing):
|
||||||
|
|
||||||
self.pk = kwargs['pk']
|
self.pk = kwargs['pk']
|
||||||
institution = self.tk.owner.institution
|
institution = self.tk.owner.institution
|
||||||
self.annotation = Annotation.objects.filter(
|
self.property = SystemProperty.objects.filter(
|
||||||
owner=institution,
|
owner=institution,
|
||||||
value=self.pk,
|
value=self.pk,
|
||||||
type=Annotation.Type.SYSTEM
|
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not self.annotation:
|
if not self.property:
|
||||||
return JsonResponse({}, status=404)
|
return JsonResponse({}, status=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -296,10 +297,9 @@ class AddAnnotationView(ApiMixing):
|
||||||
logger.error("Invalid Snapshot of user %s", self.tk.owner)
|
logger.error("Invalid Snapshot of user %s", self.tk.owner)
|
||||||
return JsonResponse({'error': 'Invalid JSON'}, status=500)
|
return JsonResponse({'error': 'Invalid JSON'}, status=500)
|
||||||
|
|
||||||
Annotation.objects.create(
|
UserProperty.objects.create(
|
||||||
uuid=self.annotation.uuid,
|
uuid=self.property.uuid,
|
||||||
owner=self.tk.owner.institution,
|
owner=self.tk.owner.institution,
|
||||||
type = Annotation.Type.USER,
|
|
||||||
key = key,
|
key = key,
|
||||||
value = value
|
value = value
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.core.exceptions import PermissionDenied
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from evidence.models import Annotation
|
from evidence.models import SystemProperty
|
||||||
from lot.models import LotTag
|
from lot.models import LotTag
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class DashboardView(LoginRequiredMixin):
|
||||||
dev_ids = self.request.session.pop("devices", [])
|
dev_ids = self.request.session.pop("devices", [])
|
||||||
|
|
||||||
self._devices = []
|
self._devices = []
|
||||||
for x in Annotation.objects.filter(value__in=dev_ids).filter(
|
for x in SystemProperty.objects.filter(value__in=dev_ids).filter(
|
||||||
owner=self.request.user.institution
|
owner=self.request.user.institution
|
||||||
).distinct():
|
).distinct():
|
||||||
self._devices.append(Device(id=x.value))
|
self._devices.append(Device(id=x.value))
|
||||||
|
|
5
dashboard/static/css/bootstrap-icons.min.css
vendored
5
dashboard/static/css/bootstrap-icons.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
2
dashboard/static/js/Sortable.min.js
vendored
Normal file
2
dashboard/static/js/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -15,9 +15,10 @@
|
||||||
|
|
||||||
<!-- Bootstrap core CSS -->
|
<!-- Bootstrap core CSS -->
|
||||||
{% block style %}
|
{% block style %}
|
||||||
<script src="{% static 'fontawesomefree/js/all.min.js' %}"></script>
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
|
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
|
||||||
<link href="{% static "/css/bootstrap-icons.min.css" %}" rel="stylesheet">
|
<script src="{% static 'js/Sortable.min.js' %}"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.bd-placeholder-img {
|
.bd-placeholder-img {
|
||||||
|
@ -51,7 +52,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<!-- Custom styles for this template -->
|
<!-- Custom styles for this template -->
|
||||||
<link href="{% static "/css/dashboard.css" %}" rel="stylesheet">
|
<link href="{% static "/css/dashboard.css" %}" rel="stylesheet">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -81,21 +82,26 @@
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="admin {% if path in 'panel users' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()">
|
<a class="admin {% if path in 'panel users states_panel 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>
|
<i class="bi bi-person-fill-gear icon_sidebar"></i>
|
||||||
{% trans 'Admin' %}
|
{% trans 'Admin' %}
|
||||||
</a>
|
</a>
|
||||||
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'panel users' %}expanded{% else %}collapse{% endif %}" id="ul_admin" data-bs-parent="#sidebarMenu">
|
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'panel institution users edit_user new_user delete_user states_panel' %}expanded{% else %}collapse{% endif %}" id="ul_admin" data-bs-parent="#sidebarMenu">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link{% if path == 'panel' %} active2{% endif %}" href="{% url 'admin:panel' %}">
|
<a class="nav-link{% if path in 'panel institution' %} active2{% endif %}" href="{% url 'admin:panel' %}">
|
||||||
{% trans 'Panel' %}
|
{% trans 'Panel' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link{% if path == 'users' %} active2{% endif %}" href="{% url 'admin:users' %}">
|
<a class="nav-link{% if path in 'users edit_user new_user delete_user' %} active2{% endif %}" href="{% url 'admin:users' %}">
|
||||||
{% trans 'Users' %}
|
{% trans 'Users' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link{% if path == 'states_panel' %} active2{% endif %}" href="{% url 'admin:states_panel' %}">
|
||||||
|
{% trans 'States' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -178,9 +184,15 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock messages %}
|
{% endblock messages %}
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
|
||||||
<h1 class="h2">{{ title }}</h1>
|
<h1 class="h2">{{ title }}
|
||||||
|
{% if help_text %}
|
||||||
|
<span class="ms-1" data-bs-toggle="tooltip" data-bs-placement="right" title="{{ help_text }}">
|
||||||
|
<i class="fas fa-question-circle text-secondary h6 align-top"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<form method="post" action="{% url 'dashboard:search' %}">
|
<form method="post" action="{% url 'dashboard:search' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="input-group rounded">
|
<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" placeholder="Search your device..." aria-label="Search" aria-describedby="search-addon" />
|
||||||
|
@ -189,9 +201,9 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row border-bottom mb-3">
|
<div class="row border-bottom mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<small style="color:#899bbd"><i>{{ breadcrumb }}</i></small>
|
<small style="color:#899bbd"><i>{{ breadcrumb }}</i></small>
|
||||||
|
@ -221,4 +233,13 @@
|
||||||
{% block extrascript %}{% endblock %}
|
{% block extrascript %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//If help_text is passed to the view as context, a hover-able help icon is displayed
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -9,20 +9,14 @@
|
||||||
<h3>{{ subtitle }}</h3>
|
<h3>{{ subtitle }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-center">
|
<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">
|
<a href="{# url 'dashboard:exports' object.id #}" type="button" class="btn btn-green-admin">
|
||||||
<i class="bi bi-reply"></i>
|
<i class="bi bi-reply"></i>
|
||||||
{% trans 'Exports' %}
|
{% trans 'Exports' %}
|
||||||
</a>
|
</a>
|
||||||
{% if lot %}
|
{% if lot %}
|
||||||
<a href="{% url 'lot:annotations' object.id %}" type="button" class="btn btn-green-admin">
|
<a href="{% url 'lot:properties' object.id %}" type="button" class="btn btn-green-admin">
|
||||||
<i class="bi bi-tag"></i>
|
<i class="bi bi-tag"></i>
|
||||||
{% trans 'Annotations' %}
|
{% trans 'properties' %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,7 +63,11 @@
|
||||||
{{ dev.manufacturer }}
|
{{ dev.manufacturer }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if dev.version %}
|
||||||
|
{{dev.version}} {{ dev.model }}
|
||||||
|
{% else %}
|
||||||
{{ dev.model }}
|
{{ dev.model }}
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.shortcuts import Http404
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dashboard.mixins import InventaryMixin, DetailsMixin
|
from dashboard.mixins import InventaryMixin, DetailsMixin
|
||||||
from evidence.models import Annotation
|
from evidence.models import SystemProperty
|
||||||
from evidence.xapian import search
|
from evidence.xapian import search
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from lot.models import Lot
|
from lot.models import Lot
|
||||||
|
@ -74,7 +74,7 @@ class SearchView(InventaryMixin):
|
||||||
|
|
||||||
for x in matches:
|
for x in matches:
|
||||||
# devices.append(self.get_annotations(x))
|
# devices.append(self.get_annotations(x))
|
||||||
dev = self.get_annotations(x)
|
dev = self.get_properties(x)
|
||||||
if dev.id not in dev_id:
|
if dev.id not in dev_id:
|
||||||
devices.append(dev)
|
devices.append(dev)
|
||||||
dev_id.append(dev.id)
|
dev_id.append(dev.id)
|
||||||
|
@ -83,10 +83,14 @@ class SearchView(InventaryMixin):
|
||||||
# TODO fix of pagination, the count is not correct
|
# TODO fix of pagination, the count is not correct
|
||||||
return devices, count
|
return devices, count
|
||||||
|
|
||||||
def get_annotations(self, xp):
|
def get_properties(self, xp):
|
||||||
snap = xp.document.get_data()
|
snap = json.loads(xp.document.get_data())
|
||||||
uuid = json.loads(snap).get('uuid')
|
if snap.get("credentialSubject"):
|
||||||
return Device.get_annotation_from_uuid(uuid, self.request.user.institution)
|
uuid = snap["credentialSubject"]["uuid"]
|
||||||
|
else:
|
||||||
|
uuid = snap["uuid"]
|
||||||
|
|
||||||
|
return Device.get_properties_from_uuid(uuid, self.request.user.institution)
|
||||||
|
|
||||||
def search_hids(self, query, offset, limit):
|
def search_hids(self, query, offset, limit):
|
||||||
qry = Q()
|
qry = Q()
|
||||||
|
@ -95,8 +99,7 @@ class SearchView(InventaryMixin):
|
||||||
if i:
|
if i:
|
||||||
qry |= Q(value__startswith=i)
|
qry |= Q(value__startswith=i)
|
||||||
|
|
||||||
chids = Annotation.objects.filter(
|
chids = SystemProperty.objects.filter(
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
owner=self.request.user.institution
|
owner=self.request.user.institution
|
||||||
).filter(
|
).filter(
|
||||||
qry
|
qry
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from utils.device import create_annotation, create_doc, create_index
|
from utils.device import create_property, create_doc, create_index
|
||||||
from utils.save_snapshots import move_json, save_in_disk
|
from utils.save_snapshots import move_json, save_in_disk
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class BaseDeviceFormSet(forms.BaseFormSet):
|
||||||
|
|
||||||
path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
|
path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
|
||||||
create_index(doc, self.user)
|
create_index(doc, self.user)
|
||||||
create_annotation(doc, user, commit=commit)
|
create_property(doc, user, commit=commit)
|
||||||
move_json(path_name, self.user.institution.name, place="placeholder")
|
move_json(path_name, self.user.institution.name, place="placeholder")
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
155
device/models.py
155
device/models.py
|
@ -1,8 +1,9 @@
|
||||||
from django.db import models, connection
|
from django.db import models, connection
|
||||||
|
|
||||||
from utils.constants import ALGOS
|
from utils.constants import ALGOS
|
||||||
from evidence.models import Annotation, Evidence
|
from evidence.models import SystemProperty, UserProperty, Evidence
|
||||||
from lot.models import DeviceLot
|
from lot.models import DeviceLot
|
||||||
|
from action.models import State
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
|
@ -25,11 +26,12 @@ class Device:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# the id is the chid of the device
|
# the id is the chid of the device
|
||||||
self.id = kwargs["id"]
|
self.id = kwargs["id"]
|
||||||
|
self.uuid = kwargs.get("uuid")
|
||||||
self.pk = self.id
|
self.pk = self.id
|
||||||
self.shortid = self.pk[:6].upper()
|
self.shortid = self.pk[:6].upper()
|
||||||
self.algorithm = None
|
self.algorithm = None
|
||||||
self.owner = None
|
self.owner = None
|
||||||
self.annotations = []
|
self.properties = []
|
||||||
self.hids = []
|
self.hids = []
|
||||||
self.uuids = []
|
self.uuids = []
|
||||||
self.evidences = []
|
self.evidences = []
|
||||||
|
@ -38,61 +40,48 @@ class Device:
|
||||||
self.get_last_evidence()
|
self.get_last_evidence()
|
||||||
|
|
||||||
def initial(self):
|
def initial(self):
|
||||||
self.get_annotations()
|
self.get_properties()
|
||||||
self.get_uuids()
|
self.get_uuids()
|
||||||
self.get_hids()
|
self.get_hids()
|
||||||
self.get_evidences()
|
self.get_evidences()
|
||||||
self.get_lots()
|
self.get_lots()
|
||||||
|
|
||||||
def get_annotations(self):
|
def get_properties(self):
|
||||||
if self.annotations:
|
if self.properties:
|
||||||
return self.annotations
|
return self.properties
|
||||||
|
|
||||||
self.annotations = Annotation.objects.filter(
|
self.properties = SystemProperty.objects.filter(
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
value=self.id
|
value=self.id
|
||||||
).order_by("-created")
|
).order_by("-created")
|
||||||
|
|
||||||
if self.annotations.count():
|
if self.properties.count():
|
||||||
self.algorithm = self.annotations[0].key
|
self.algorithm = self.properties[0].key
|
||||||
self.owner = self.annotations[0].owner
|
self.owner = self.properties[0].owner
|
||||||
|
|
||||||
return self.annotations
|
return self.properties
|
||||||
|
|
||||||
def get_user_annotations(self):
|
def get_user_properties(self):
|
||||||
if not self.uuids:
|
if not self.uuids:
|
||||||
self.get_uuids()
|
self.get_uuids()
|
||||||
|
|
||||||
annotations = Annotation.objects.filter(
|
user_properties = UserProperty.objects.filter(
|
||||||
uuid__in=self.uuids,
|
uuid__in=self.uuids,
|
||||||
owner=self.owner,
|
owner=self.owner,
|
||||||
type=Annotation.Type.USER
|
type=UserProperty.Type.USER,
|
||||||
)
|
)
|
||||||
return annotations
|
return user_properties
|
||||||
|
|
||||||
def get_user_documents(self):
|
|
||||||
if not self.uuids:
|
|
||||||
self.get_uuids()
|
|
||||||
|
|
||||||
annotations = Annotation.objects.filter(
|
|
||||||
uuid__in=self.uuids,
|
|
||||||
owner=self.owner,
|
|
||||||
type=Annotation.Type.DOCUMENT
|
|
||||||
)
|
|
||||||
return annotations
|
|
||||||
|
|
||||||
def get_uuids(self):
|
def get_uuids(self):
|
||||||
for a in self.get_annotations():
|
for a in self.get_properties():
|
||||||
if a.uuid not in self.uuids:
|
if a.uuid not in self.uuids:
|
||||||
self.uuids.append(a.uuid)
|
self.uuids.append(a.uuid)
|
||||||
|
|
||||||
def get_hids(self):
|
def get_hids(self):
|
||||||
annotations = self.get_annotations()
|
properties = self.get_properties()
|
||||||
|
|
||||||
algos = list(ALGOS.keys())
|
algos = list(ALGOS.keys())
|
||||||
algos.append('CUSTOM_ID')
|
algos.append('CUSTOM_ID')
|
||||||
self.hids = list(set(annotations.filter(
|
self.hids = list(set(properties.filter(
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
key__in=algos,
|
key__in=algos,
|
||||||
).values_list("value", flat=True)))
|
).values_list("value", flat=True)))
|
||||||
|
|
||||||
|
@ -103,11 +92,19 @@ class Device:
|
||||||
self.evidences = [Evidence(u) for u in self.uuids]
|
self.evidences = [Evidence(u) for u in self.uuids]
|
||||||
|
|
||||||
def get_last_evidence(self):
|
def get_last_evidence(self):
|
||||||
annotations = self.get_annotations()
|
if self.last_evidence:
|
||||||
if not annotations.count():
|
|
||||||
return
|
return
|
||||||
annotation = annotations.first()
|
|
||||||
self.last_evidence = Evidence(annotation.uuid)
|
if self.uuid:
|
||||||
|
self.last_evidence = Evidence(self.uuid)
|
||||||
|
return
|
||||||
|
|
||||||
|
properties = self.get_properties()
|
||||||
|
if not properties.count():
|
||||||
|
return
|
||||||
|
prop = properties.first()
|
||||||
|
|
||||||
|
self.last_evidence = Evidence(prop.uuid)
|
||||||
|
|
||||||
def is_eraseserver(self):
|
def is_eraseserver(self):
|
||||||
if not self.uuids:
|
if not self.uuids:
|
||||||
|
@ -115,19 +112,26 @@ class Device:
|
||||||
if not self.uuids:
|
if not self.uuids:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
annotation = Annotation.objects.filter(
|
prop = UserProperty.objects.filter(
|
||||||
uuid__in=self.uuids,
|
uuid__in=self.uuids,
|
||||||
owner=self.owner,
|
owner=self.owner,
|
||||||
type=Annotation.Type.ERASE_SERVER
|
type=UserProperty.Type.ERASE_SERVER
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if annotation:
|
if prop:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def last_uuid(self):
|
def last_uuid(self):
|
||||||
|
if self.uuid:
|
||||||
|
return self.uuid
|
||||||
return self.uuids[0]
|
return self.uuids[0]
|
||||||
|
|
||||||
|
def get_current_state(self):
|
||||||
|
uuid = self.last_uuid
|
||||||
|
|
||||||
|
return State.objects.filter(snapshot_uuid=uuid).order_by('-date').first()
|
||||||
|
|
||||||
def get_lots(self):
|
def get_lots(self):
|
||||||
self.lots = [
|
self.lots = [
|
||||||
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
|
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
|
||||||
|
@ -136,7 +140,7 @@ class Device:
|
||||||
def get_unassigned(cls, institution, offset=0, limit=None):
|
def get_unassigned(cls, institution, offset=0, limit=None):
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
WITH RankedAnnotations AS (
|
WITH RankedProperties AS (
|
||||||
SELECT
|
SELECT
|
||||||
t1.value,
|
t1.value,
|
||||||
t1.key,
|
t1.key,
|
||||||
|
@ -145,38 +149,36 @@ class Device:
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN t1.key = 'CUSTOM_ID' THEN 1
|
WHEN t1.key = 'CUSTOM_ID' THEN 1
|
||||||
WHEN t1.key = 'hidalgo1' THEN 2
|
WHEN t1.key = 'ereuse24' THEN 2
|
||||||
ELSE 3
|
ELSE 3
|
||||||
END,
|
END,
|
||||||
t1.created DESC
|
t1.created DESC
|
||||||
) AS row_num
|
) AS row_num
|
||||||
FROM evidence_annotation AS t1
|
FROM evidence_systemproperty AS t1
|
||||||
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
||||||
WHERE t2.device_id IS NULL
|
WHERE t2.device_id IS NULL
|
||||||
AND t1.owner_id = {institution}
|
AND t1.owner_id = {institution}
|
||||||
AND t1.type = {type}
|
|
||||||
)
|
)
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
value
|
value
|
||||||
FROM
|
FROM
|
||||||
RankedAnnotations
|
RankedProperties
|
||||||
WHERE
|
WHERE
|
||||||
row_num = 1
|
row_num = 1
|
||||||
""".format(
|
""".format(
|
||||||
institution=institution.id,
|
institution=institution.id,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
)
|
)
|
||||||
if limit:
|
if limit:
|
||||||
sql += " limit {} offset {}".format(int(limit), int(offset))
|
sql += " limit {} offset {}".format(int(limit), int(offset))
|
||||||
|
|
||||||
sql += ";"
|
sql += ";"
|
||||||
|
|
||||||
annotations = []
|
properties = []
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
annotations = cursor.fetchall()
|
properties = cursor.fetchall()
|
||||||
|
|
||||||
devices = [cls(id=x[0]) for x in annotations]
|
devices = [cls(id=x[0]) for x in properties]
|
||||||
count = cls.get_unassigned_count(institution)
|
count = cls.get_unassigned_count(institution)
|
||||||
return devices, count
|
return devices, count
|
||||||
|
|
||||||
|
@ -184,7 +186,7 @@ class Device:
|
||||||
def get_unassigned_count(cls, institution):
|
def get_unassigned_count(cls, institution):
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
WITH RankedAnnotations AS (
|
WITH RankedProperties AS (
|
||||||
SELECT
|
SELECT
|
||||||
t1.value,
|
t1.value,
|
||||||
t1.key,
|
t1.key,
|
||||||
|
@ -193,35 +195,33 @@ class Device:
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN t1.key = 'CUSTOM_ID' THEN 1
|
WHEN t1.key = 'CUSTOM_ID' THEN 1
|
||||||
WHEN t1.key = 'hidalgo1' THEN 2
|
WHEN t1.key = 'ereuse24' THEN 2
|
||||||
ELSE 3
|
ELSE 3
|
||||||
END,
|
END,
|
||||||
t1.created DESC
|
t1.created DESC
|
||||||
) AS row_num
|
) AS row_num
|
||||||
FROM evidence_annotation AS t1
|
FROM evidence_systemproperty AS t1
|
||||||
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
||||||
WHERE t2.device_id IS NULL
|
WHERE t2.device_id IS NULL
|
||||||
AND t1.owner_id = {institution}
|
AND t1.owner_id = {institution}
|
||||||
AND t1.type = {type}
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(DISTINCT value)
|
COUNT(DISTINCT value)
|
||||||
FROM
|
FROM
|
||||||
RankedAnnotations
|
RankedProperties
|
||||||
WHERE
|
WHERE
|
||||||
row_num = 1
|
row_num = 1
|
||||||
""".format(
|
""".format(
|
||||||
institution=institution.id,
|
institution=institution.id,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
)
|
)
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
return cursor.fetchall()[0][0]
|
return cursor.fetchall()[0][0]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_annotation_from_uuid(cls, uuid, institution):
|
def get_properties_from_uuid(cls, uuid, institution):
|
||||||
sql = """
|
sql = """
|
||||||
WITH RankedAnnotations AS (
|
WITH RankedProperties AS (
|
||||||
SELECT
|
SELECT
|
||||||
t1.value,
|
t1.value,
|
||||||
t1.key,
|
t1.key,
|
||||||
|
@ -230,78 +230,79 @@ class Device:
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN t1.key = 'CUSTOM_ID' THEN 1
|
WHEN t1.key = 'CUSTOM_ID' THEN 1
|
||||||
WHEN t1.key = 'hidalgo1' THEN 2
|
WHEN t1.key = 'ereuse24' THEN 2
|
||||||
ELSE 3
|
ELSE 3
|
||||||
END,
|
END,
|
||||||
t1.created DESC
|
t1.created DESC
|
||||||
) AS row_num
|
) AS row_num
|
||||||
FROM evidence_annotation AS t1
|
FROM evidence_systemproperty AS t1
|
||||||
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
|
||||||
WHERE t2.device_id IS NULL
|
WHERE t2.device_id IS NULL
|
||||||
AND t1.owner_id = {institution}
|
AND t1.owner_id = {institution}
|
||||||
AND t1.type = {type}
|
|
||||||
AND t1.uuid = '{uuid}'
|
AND t1.uuid = '{uuid}'
|
||||||
)
|
)
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
value
|
value
|
||||||
FROM
|
FROM
|
||||||
RankedAnnotations
|
RankedProperties
|
||||||
WHERE
|
WHERE
|
||||||
row_num = 1;
|
row_num = 1;
|
||||||
""".format(
|
""".format(
|
||||||
uuid=uuid.replace("-", ""),
|
uuid=uuid.replace("-", ""),
|
||||||
institution=institution.id,
|
institution=institution.id,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
annotations = []
|
properties = []
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
annotations = cursor.fetchall()
|
properties = cursor.fetchall()
|
||||||
|
|
||||||
return cls(id=annotations[0][0])
|
return cls(id=properties[0][0])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_websnapshot(self):
|
def is_websnapshot(self):
|
||||||
if not self.last_evidence:
|
self.get_last_evidence()
|
||||||
self.get_last_evidence()
|
|
||||||
return self.last_evidence.doc['type'] == "WebSnapshot"
|
return self.last_evidence.doc['type'] == "WebSnapshot"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_user_evidence(self):
|
def last_user_evidence(self):
|
||||||
if not self.last_evidence:
|
self.get_last_evidence()
|
||||||
self.get_last_evidence()
|
|
||||||
return self.last_evidence.doc['kv'].items()
|
return self.last_evidence.doc['kv'].items()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def manufacturer(self):
|
def manufacturer(self):
|
||||||
if not self.last_evidence:
|
self.get_last_evidence()
|
||||||
self.get_last_evidence()
|
|
||||||
return self.last_evidence.get_manufacturer()
|
return self.last_evidence.get_manufacturer()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serial_number(self):
|
def serial_number(self):
|
||||||
if not self.last_evidence:
|
self.get_last_evidence()
|
||||||
self.get_last_evidence()
|
|
||||||
return self.last_evidence.get_serial_number()
|
return self.last_evidence.get_serial_number()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
|
self.get_last_evidence()
|
||||||
if self.last_evidence.doc['type'] == "WebSnapshot":
|
if self.last_evidence.doc['type'] == "WebSnapshot":
|
||||||
return self.last_evidence.doc.get("device", {}).get("type", "")
|
return self.last_evidence.doc.get("device", {}).get("type", "")
|
||||||
|
|
||||||
if not self.last_evidence:
|
|
||||||
self.get_last_evidence()
|
|
||||||
return self.last_evidence.get_chassis()
|
return self.last_evidence.get_chassis()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self):
|
||||||
if not self.last_evidence:
|
self.get_last_evidence()
|
||||||
self.get_last_evidence()
|
|
||||||
return self.last_evidence.get_model()
|
return self.last_evidence.get_model()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
self.get_last_evidence()
|
||||||
|
return self.last_evidence.get_version()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def components(self):
|
def components(self):
|
||||||
if not self.last_evidence:
|
self.get_last_evidence()
|
||||||
self.get_last_evidence()
|
|
||||||
return self.last_evidence.get_components()
|
return self.last_evidence.get_components()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def did_document(self):
|
||||||
|
self.get_last_evidence()
|
||||||
|
return self.last_evidence.get_did_document()
|
||||||
|
|
|
@ -2,11 +2,60 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<!-- Top bar buttons -->
|
||||||
<div class="col">
|
<div class="row">
|
||||||
<h3>{{ object.shortid }}</h3>
|
<div class="col">
|
||||||
|
<h3>{{ object.shortid }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col text-end">
|
||||||
|
<div class="btn-group" role="group" aria-label="Actions">
|
||||||
|
|
||||||
|
<!-- change state button -->
|
||||||
|
{% if state_definitions %}
|
||||||
|
<div class="dropdown ms-2">
|
||||||
|
<a class="btn btn-green-admin dropdown-toggle" id="addStateDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{% trans "Change state" %}
|
||||||
|
{% if device_states %}
|
||||||
|
({{ device_states.0.state }})
|
||||||
|
{% else %}
|
||||||
|
( {% trans "None" %} )
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="addStateDropdown" style="width: 100%;">
|
||||||
|
{% for state in state_definitions %}
|
||||||
|
<li style="width: 100%;">
|
||||||
|
<form id="changeStateForm{{ state.id }}" method="post" action="{% url 'action:change_state' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="previous_state" value="{{ device_states.0.state|default:"nil" }}">
|
||||||
|
<input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
|
||||||
|
<input type="hidden" name="new_state" value="{{ state.state }}">
|
||||||
|
|
||||||
|
<a class="dropdown-item d-flex justify-content-between align-items-center" href="#" onclick="document.getElementById('changeStateForm{{ state.id }}').submit(); return false;">
|
||||||
|
<span class="font-monospace">{{ state.state }}</span>
|
||||||
|
<span class="badge bg-secondary rounded-pill-sm">{{ forloop.counter }}</span>
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-green-admin" type="button" disabled>
|
||||||
|
<i class="bi bi-plus"></i> {% trans "Change state" %}
|
||||||
|
{% if device_states %}
|
||||||
|
({{ device_states.0.state }})
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<!-- Add note button -->
|
||||||
|
<button class="btn btn-yellow ms-2" type="button" data-bs-toggle="modal" data-bs-target="#addNoteModal">
|
||||||
|
<i class="bi bi-sticky"></i> {% trans "Add a note" %}
|
||||||
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
@ -15,10 +64,7 @@
|
||||||
<a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">{% trans 'General details' %}</a>
|
<a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">{% trans 'General details' %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#annotations" class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">{% trans 'User annotations' %}</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="#documents" class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">{% trans 'Documents' %}</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">{% trans 'Lots' %}</a>
|
<a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">{% trans 'Lots' %}</a>
|
||||||
|
@ -29,207 +75,62 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a>
|
<a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if dpps %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#dpps" class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">{% trans 'Dpps' %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
|
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#log" class="nav-link" data-bs-toggle="tab" data-bs-target="#log">{% trans 'Log' %}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-content pt-4">
|
||||||
|
|
||||||
<div class="tab-content pt-2">
|
{% include 'tabs/general_details.html' %}
|
||||||
<div class="tab-pane fade show active" id="details">
|
|
||||||
<h5 class="card-title">{% trans 'Details' %}</h5>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Phid</div>
|
|
||||||
<div class="col-lg-9 col-md-8">{{ object.id }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if object.is_eraseserver %}
|
{% include 'tabs/log.html' %}
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">
|
{% include 'tabs/user_properties.html' %}
|
||||||
{% trans 'Is a erase server' %}
|
|
||||||
</div>
|
{% include 'tabs/lots.html' %}
|
||||||
<div class="col-lg-9 col-md-8"></div>
|
|
||||||
|
{% 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">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addNoteModalLabel">{% trans "Add a Note" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="modal-body">
|
||||||
|
<form method="post" action="{% url 'action:add_note' %}">
|
||||||
<div class="row mb-3">
|
{% csrf_token %}
|
||||||
<div class="col-lg-3 col-md-4 label">Type</div>
|
<div class="mb-3">
|
||||||
<div class="col-lg-9 col-md-8">{{ object.type }}</div>
|
<input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
|
||||||
</div>
|
<label for="noteDescription" class="form-label">{% trans "Note" %}</label>
|
||||||
|
<textarea class="form-control" id="noteDescription" name="note" placeholder="Max 250 characters" name="note" rows="3" required></textarea>
|
||||||
{% if object.is_websnapshot and object.last_user_evidence %}
|
|
||||||
{% for k, v in object.last_user_evidence %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">{{ k }}</div>
|
|
||||||
<div class="col-lg-9 col-md-8">{{ v|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">
|
|
||||||
{% trans 'Manufacturer' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-9 col-md-8">{{ object.manufacturer|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">
|
|
||||||
{% trans 'Model' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-9 col-md-8">{{ object.model|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">
|
|
||||||
{% trans 'Serial Number' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-9 col-md-8">{{ object.serial_number|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">
|
|
||||||
{% trans 'Identifiers' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% for chid in object.hids %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">{{ chid|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="annotations">
|
|
||||||
<div class="btn-group mt-1 mb-3">
|
|
||||||
<a href="{% url 'device:add_annotation' object.pk %}" class="btn btn-primary">
|
|
||||||
<i class="bi bi-plus"></i>
|
|
||||||
{% trans 'Add new annotation' %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5 class="card-title">{% trans 'Annotations' %}</h5>
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">
|
|
||||||
{% trans 'Key' %}
|
|
||||||
</th>
|
|
||||||
<th scope="col">
|
|
||||||
{% trans 'Value' %}
|
|
||||||
</th>
|
|
||||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
|
|
||||||
{% trans 'Created on' %}
|
|
||||||
</th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for a in object.get_user_annotations %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ a.key }}</td>
|
|
||||||
<td>{{ a.value }}</td>
|
|
||||||
<td>{{ a.created }}</td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="documents">
|
|
||||||
<div class="btn-group mt-1 mb-3">
|
|
||||||
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">
|
|
||||||
<i class="bi bi-plus"></i>
|
|
||||||
{% trans 'Add new document' %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5 class="card-title">{% trans 'Documents' %}</h5>
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">
|
|
||||||
{% trans 'Key' %}
|
|
||||||
</th>
|
|
||||||
<th scope="col">
|
|
||||||
{% trans 'Value' %}
|
|
||||||
</th>
|
|
||||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
|
|
||||||
{% trans 'Created on' %}
|
|
||||||
</th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for a in object.get_user_documents %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ a.key }}</td>
|
|
||||||
<td>{{ a.value }}</td>
|
|
||||||
<td>{{ a.created }}</td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="lots">
|
|
||||||
{% for tag in lot_tags %}
|
|
||||||
<h5 class="card-title">{{ tag }}</h5>
|
|
||||||
{% for lot in object.lots %}
|
|
||||||
{% if lot.type == tag %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="modal-footer">
|
||||||
{% endfor %}
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
{% endfor %}
|
<button type="submit" class="btn btn-green-admin">{% trans "Save Note" %}</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="components">
|
|
||||||
<h5 class="card-title">{% trans 'Components last evidence' %}</h5>
|
|
||||||
<div class="list-group col-6">
|
|
||||||
{% for c in object.components %}
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="d-flex w-100 justify-content-between">
|
|
||||||
<h5 class="mb-1">{{ c.type }}</h5>
|
|
||||||
<small class="text-muted">{{ evidence.created }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-1">
|
</form>
|
||||||
{% for k, v in c.items %}
|
</div>
|
||||||
{% if k not in 'actions,type' %}
|
|
||||||
{{ k }}: {{ v }}<br />
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="evidences">
|
|
||||||
<h5 class="card-title">{% trans 'List of evidences' %}</h5>
|
|
||||||
<div class="list-group col-6">
|
|
||||||
{% for snap in object.evidences %}
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="d-flex w-100 justify-content-between">
|
|
||||||
<small class="text-muted">{{ snap.created }}</small>
|
|
||||||
</div>
|
|
||||||
<p class="mb-1">
|
|
||||||
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrascript %}
|
{% block extrascript %}
|
||||||
|
@ -237,12 +138,12 @@
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Obtener el hash de la URL (ejemplo: #components)
|
// Obtener el hash de la URL (ejemplo: #components)
|
||||||
const hash = window.location.hash
|
const hash = window.location.hash
|
||||||
|
|
||||||
// Verificar si hay un hash en la URL
|
// Verificar si hay un hash en la URL
|
||||||
if (hash) {
|
if (hash) {
|
||||||
// Buscar el botón o enlace que corresponde al hash y activarlo
|
// Buscar el botón o enlace que corresponde al hash y activarlo
|
||||||
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`)
|
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`)
|
||||||
|
|
||||||
if (tabTrigger) {
|
if (tabTrigger) {
|
||||||
// Crear una instancia de tab de Bootstrap para activar el tab
|
// Crear una instancia de tab de Bootstrap para activar el tab
|
||||||
const tab = new bootstrap.Tab(tabTrigger)
|
const tab = new bootstrap.Tab(tabTrigger)
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<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' %}" />
|
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||||
</div>
|
</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 %}
|
|
27
device/templates/tabs/components.html
Normal file
27
device/templates/tabs/components.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="components">
|
||||||
|
<h5 class="card-title">{% trans 'Components last evidence' %}
|
||||||
|
</h5>
|
||||||
|
<div class="list-group col-6">
|
||||||
|
{% for c in object.components %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h5 class="mb-1">{{ c.type }}
|
||||||
|
</h5>
|
||||||
|
<small class="text-muted">{{ evidence.created }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<p class="mb-1">
|
||||||
|
{% for k, v in c.items %}
|
||||||
|
{% if k not in 'actions,type' %}
|
||||||
|
{{ k }}: {{ v }}
|
||||||
|
<br />
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
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>
|
34
device/templates/tabs/evidences.html
Normal file
34
device/templates/tabs/evidences.html
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="evidences">
|
||||||
|
<h5 class="card-title">{% trans 'List of evidences' %}</h5>
|
||||||
|
<div class="list-group col">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">uuid</th>
|
||||||
|
<th scope="col">Did Document</th>
|
||||||
|
<th scope="col">{% trans "Date" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for snap in object.evidences %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if snap.did_document %}
|
||||||
|
<a href="{{ snap.did_document }}" target="_blank">DID</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">{{ snap.created }}</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
81
device/templates/tabs/general_details.html
Normal file
81
device/templates/tabs/general_details.html
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<!-- Device Details -->
|
||||||
|
<div class="tab-pane fade show active" id="details">
|
||||||
|
<h5 class="card-title">{% trans 'Details' %}
|
||||||
|
</h5>
|
||||||
|
<hr>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Phid' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object.is_eraseserver %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Is an erase server' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{% trans 'Yes' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Type' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object.is_websnapshot and object.last_user_evidence %}
|
||||||
|
{% for k, v in object.last_user_evidence.items %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{{ k }}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ v|default:'' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Manufacturer' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.manufacturer|default:'' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Model' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.model|default:'' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">
|
||||||
|
{% trans 'Version' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.version|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Serial Number' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.serial_number|default:'' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Identifiers' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{% for chid in object.hids %}
|
||||||
|
<div>{{ chid|default:'' }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
28
device/templates/tabs/log.html
Normal file
28
device/templates/tabs/log.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="log">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover table-bordered bg-gradient">
|
||||||
|
<thead >
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans 'Date' %}</th>
|
||||||
|
<th scope="col">{% trans 'Event' %}</th>
|
||||||
|
<th scope="col">{% trans 'User' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for log in device_logs %}
|
||||||
|
<tr>
|
||||||
|
<td width="13%">{{ log.date|date:"M j, Y, H:i" }}</td>
|
||||||
|
<td class="fst-italic">{{ log.event }}</td>
|
||||||
|
<td>{{ log.user.get_full_name|default:log.user.username }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center">{% trans 'No logs recorded.' %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
19
device/templates/tabs/lots.html
Normal file
19
device/templates/tabs/lots.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="lots">
|
||||||
|
{% for tag in lot_tags %}
|
||||||
|
<h5 class="card-title">{{ tag }}
|
||||||
|
</h5>
|
||||||
|
{% for lot in object.lots %}
|
||||||
|
{% if lot.type == tag %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
131
device/templates/tabs/user_properties.html
Normal file
131
device/templates/tabs/user_properties.html
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="user_properties">
|
||||||
|
<div class="d-flex justify-content-end mt-1 mb-3">
|
||||||
|
<a href="{% url 'device:add_user_property' object.pk %}"
|
||||||
|
class="btn btn-green-admin d-flex align-items-center">
|
||||||
|
<i class="bi bi-plus me-1"></i>
|
||||||
|
{% trans 'New user property' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="card-title">{% trans 'User properties' %}</h5>
|
||||||
|
|
||||||
|
<table class="table table-hover table-bordered table-responsive align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans 'Key' %}</th>
|
||||||
|
<th scope="col">{% trans 'Value' %}</th>
|
||||||
|
<th scope="col" data-type="date" class="text-end" data-format="YYYY-MM-DD HH:mm">{% trans 'Created on' %}</th>
|
||||||
|
<th scope="col" width="5%" class="text-end" title="{% trans 'Actions' %}"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for a in object.get_user_properties %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ a.key }}
|
||||||
|
</td>
|
||||||
|
<td>{{ a.value }}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">{{ a.created }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group ">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-info d-flex align-items-center" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editModal{{ a.id }}" >
|
||||||
|
<i class="bi bi-pencil me-1"></i>
|
||||||
|
{% trans 'Edit' %}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger d-flex align-items-center"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#deleteModal{{ a.id }}">
|
||||||
|
<i class="bi bi-trash me-1"></i>
|
||||||
|
{% trans 'Delete' %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- pop up modal for delete confirmation -->
|
||||||
|
{% for a in object.get_user_properties %}
|
||||||
|
<div class="modal fade" id="deleteModal{{ a.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ a.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deleteModalLabel{{ a.id }}">{% trans "Confirm Deletion" %}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Key:" %}
|
||||||
|
</strong> {{ a.key }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Value:" %}
|
||||||
|
</strong> {{ a.value }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Created on:" %}
|
||||||
|
</strong> {{ a.created }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-center">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}
|
||||||
|
</button>
|
||||||
|
<form method="post" action="{% url 'device:delete_user_property' object.id a.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger">{% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- popup modals for edit button -->
|
||||||
|
{% for a in object.get_user_properties %}
|
||||||
|
<div class="modal fade" id="editModal{{ a.id }}" tabindex="-1" aria-labelledby="editModalLabel{{ a.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editModalLabel{{ a.id }}">{% trans "Edit User Property" %}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editForm{{ a.id }}" method="post" action="{% url 'device:update_user_property' object.id a.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="key" class="form-label">{% trans "Key" %}
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="key" name="key" value="{{ a.key }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="value" class="form-label">{% trans "Value" %}
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="value" name="value" value="{{ a.value }}">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-center">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans "Save changes" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -7,8 +7,11 @@ urlpatterns = [
|
||||||
path("add/", views.NewDeviceView.as_view(), name="add"),
|
path("add/", views.NewDeviceView.as_view(), name="add"),
|
||||||
path("edit/<str:pk>/", views.EditDeviceView.as_view(), name="edit"),
|
path("edit/<str:pk>/", views.EditDeviceView.as_view(), name="edit"),
|
||||||
path("<str:pk>/", views.DetailsView.as_view(), name="details"),
|
path("<str:pk>/", views.DetailsView.as_view(), name="details"),
|
||||||
path("<str:pk>/annotation/add", views.AddAnnotationView.as_view(), name="add_annotation"),
|
path("<str:pk>/user_property/add",
|
||||||
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"),
|
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"),
|
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
226
device/views.py
226
device/views.py
|
@ -1,23 +1,41 @@
|
||||||
import json
|
import json
|
||||||
from django.http import JsonResponse
|
import logging
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import JsonResponse
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import IntegrityError
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.shortcuts import get_object_or_404, Http404
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic.edit import (
|
from django.views.generic.edit import (
|
||||||
CreateView,
|
CreateView,
|
||||||
UpdateView,
|
UpdateView,
|
||||||
FormView,
|
FormView,
|
||||||
|
DeleteView,
|
||||||
)
|
)
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
from action.models import StateDefinition, State, DeviceLog, Note
|
||||||
from dashboard.mixins import DashboardView, Http403
|
from dashboard.mixins import DashboardView, Http403
|
||||||
from evidence.models import Annotation
|
from evidence.models import UserProperty, SystemProperty
|
||||||
from lot.models import LotTag
|
from lot.models import LotTag
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from device.forms import DeviceFormSet
|
from device.forms import DeviceFormSet
|
||||||
|
if settings.DPP:
|
||||||
|
from dpp.models import Proof
|
||||||
|
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):
|
class NewDeviceView(DashboardView, FormView):
|
||||||
template_name = "new_device.html"
|
template_name = "new_device.html"
|
||||||
title = _("New Device")
|
title = _("New Device")
|
||||||
|
@ -35,41 +53,12 @@ class NewDeviceView(DashboardView, FormView):
|
||||||
return response
|
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):
|
class EditDeviceView(DashboardView, UpdateView):
|
||||||
template_name = "new_device.html"
|
template_name = "new_device.html"
|
||||||
title = _("Update Device")
|
title = _("Update Device")
|
||||||
breadcrumb = "Device / Update Device"
|
breadcrumb = "Device / Update Device"
|
||||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||||
model = Annotation
|
model = SystemProperty
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
pk = self.kwargs.get('pk')
|
pk = self.kwargs.get('pk')
|
||||||
|
@ -87,7 +76,7 @@ class DetailsView(DashboardView, TemplateView):
|
||||||
template_name = "details.html"
|
template_name = "details.html"
|
||||||
title = _("Device")
|
title = _("Device")
|
||||||
breadcrumb = "Device / Details"
|
breadcrumb = "Device / Details"
|
||||||
model = Annotation
|
model = SystemProperty
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.pk = kwargs['pk']
|
self.pk = kwargs['pk']
|
||||||
|
@ -103,10 +92,34 @@ class DetailsView(DashboardView, TemplateView):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
self.object.initial()
|
self.object.initial()
|
||||||
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
|
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
|
||||||
|
dpps = []
|
||||||
|
if settings.DPP:
|
||||||
|
_dpps = Proof.objects.filter(
|
||||||
|
uuid__in=self.object.uuids,
|
||||||
|
type=PROOF_TYPE["IssueDPP"]
|
||||||
|
)
|
||||||
|
for x in _dpps:
|
||||||
|
dpp = "{}:{}".format(self.pk, x.signature)
|
||||||
|
dpps.append((dpp, x.signature[:10], x))
|
||||||
|
|
||||||
|
last_evidence = self.object.get_last_evidence()
|
||||||
|
uuids = self.object.uuids
|
||||||
|
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({
|
context.update({
|
||||||
'object': self.object,
|
'object': self.object,
|
||||||
'snapshot': self.object.get_last_evidence(),
|
'snapshot': last_evidence,
|
||||||
'lot_tags': lot_tags,
|
'lot_tags': lot_tags,
|
||||||
|
'dpps': dpps,
|
||||||
|
"state_definitions": state_definitions,
|
||||||
|
"device_states": device_states,
|
||||||
|
"device_logs": device_logs,
|
||||||
|
"device_notes": device_notes,
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -167,67 +180,120 @@ class PublicDeviceWebView(TemplateView):
|
||||||
return JsonResponse(device_data)
|
return JsonResponse(device_data)
|
||||||
|
|
||||||
|
|
||||||
class AddAnnotationView(DashboardView, CreateView):
|
class AddUserPropertyView(DeviceLogMixin, CreateView):
|
||||||
template_name = "new_annotation.html"
|
template_name = "new_user_property.html"
|
||||||
title = _("New annotation")
|
title = _("New User Property")
|
||||||
breadcrumb = "Device / New annotation"
|
breadcrumb = "Device / New Property"
|
||||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
model = UserProperty
|
||||||
model = Annotation
|
|
||||||
fields = ("key", "value")
|
fields = ("key", "value")
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.owner = self.request.user.institution
|
form.instance.owner = self.request.user.institution
|
||||||
form.instance.user = self.request.user
|
form.instance.user = self.request.user
|
||||||
form.instance.uuid = self.annotation.uuid
|
form.instance.uuid = self.property.uuid
|
||||||
form.instance.type = Annotation.Type.USER
|
form.instance.type = UserProperty.Type.USER
|
||||||
response = super().form_valid(form)
|
|
||||||
return response
|
try:
|
||||||
|
response = super().form_valid(form)
|
||||||
|
messages.success(self.request, _("Property successfully added."))
|
||||||
|
log_message = _("<Created> UserProperty: {}: {}".format(
|
||||||
|
form.instance.key,
|
||||||
|
form.instance.value
|
||||||
|
))
|
||||||
|
|
||||||
|
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):
|
def get_form_kwargs(self):
|
||||||
pk = self.kwargs.get('pk')
|
pk = self.kwargs.get('pk')
|
||||||
institution = self.request.user.institution
|
institution = self.request.user.institution
|
||||||
self.annotation = Annotation.objects.filter(
|
self.property = SystemProperty.objects.filter(
|
||||||
owner=institution,
|
owner=institution, value=pk).first()
|
||||||
value=pk,
|
if not self.property:
|
||||||
type=Annotation.Type.SYSTEM
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not self.annotation:
|
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
self.success_url = reverse_lazy('device:details', args=[pk])
|
return super().get_form_kwargs()
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
return kwargs
|
def get_success_url(self):
|
||||||
|
pk = self.kwargs.get('pk')
|
||||||
|
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['pk'] = self.kwargs.get('pk')
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class AddDocumentView(DashboardView, CreateView):
|
class UpdateUserPropertyView(DeviceLogMixin, UpdateView):
|
||||||
template_name = "new_annotation.html"
|
template_name = "new_user_property.html"
|
||||||
title = _("New Document")
|
title = _("Update User Property")
|
||||||
breadcrumb = "Device / New document"
|
breadcrumb = "Device / Update Property"
|
||||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
model = UserProperty
|
||||||
model = Annotation
|
|
||||||
fields = ("key", "value")
|
fields = ("key", "value")
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
form.instance.owner = self.request.user.institution
|
|
||||||
form.instance.user = self.request.user
|
|
||||||
form.instance.uuid = self.annotation.uuid
|
|
||||||
form.instance.type = Annotation.Type.DOCUMENT
|
|
||||||
response = super().form_valid(form)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
pk = self.kwargs.get('pk')
|
pk = self.kwargs.get('pk')
|
||||||
institution = self.request.user.institution
|
institution = self.request.user.institution
|
||||||
self.annotation = Annotation.objects.filter(
|
self.object = get_object_or_404(UserProperty, owner=institution, pk=pk)
|
||||||
owner=institution,
|
self.old_key = self.object.key
|
||||||
value=pk,
|
self.old_value = self.object.value
|
||||||
type=Annotation.Type.SYSTEM
|
return super().get_form_kwargs()
|
||||||
).first()
|
|
||||||
|
|
||||||
if not self.annotation:
|
def form_valid(self, form):
|
||||||
raise Http404
|
new_key = form.cleaned_data['key']
|
||||||
|
new_value = form.cleaned_data['value']
|
||||||
|
|
||||||
self.success_url = reverse_lazy('device:details', args=[pk])
|
try:
|
||||||
kwargs = super().get_form_kwargs()
|
super().form_valid(form)
|
||||||
return kwargs
|
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)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
super().form_invalid(form)
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
pk = self.kwargs.get('device_id')
|
||||||
|
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
pk = self.kwargs.get('pk')
|
||||||
|
institution = self.request.user.institution
|
||||||
|
self.object = get_object_or_404(UserProperty, owner=institution, pk=pk)
|
||||||
|
self.object.delete()
|
||||||
|
|
||||||
|
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 redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
pk = self.kwargs.get('device_id')
|
||||||
|
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
|
||||||
|
|
|
@ -65,6 +65,7 @@ ENABLE_EMAIL = config("ENABLE_EMAIL", default=True, cast=bool)
|
||||||
|
|
||||||
EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
|
EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
@ -76,22 +77,23 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
'django_bootstrap5',
|
'django_bootstrap5',
|
||||||
'fontawesomefree',
|
|
||||||
'django_tables2',
|
'django_tables2',
|
||||||
"rest_framework",
|
|
||||||
"login",
|
"login",
|
||||||
"user",
|
"user",
|
||||||
"device",
|
"device",
|
||||||
"evidence",
|
"evidence",
|
||||||
"action",
|
|
||||||
"tag",
|
|
||||||
"lot",
|
"lot",
|
||||||
"documents",
|
|
||||||
"dashboard",
|
"dashboard",
|
||||||
|
"action",
|
||||||
"admin",
|
"admin",
|
||||||
"api",
|
"api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DPP = config("DPP", default=False, cast=bool)
|
||||||
|
|
||||||
|
if DPP:
|
||||||
|
INSTALLED_APPS.extend(["dpp", "did"])
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
@ -211,6 +213,10 @@ LOGGING = {
|
||||||
'()': CustomFormatter,
|
'()': CustomFormatter,
|
||||||
'format': '%(levelname)s %(asctime)s %(message)s'
|
'format': '%(levelname)s %(asctime)s %(message)s'
|
||||||
},
|
},
|
||||||
|
'verbose': {
|
||||||
|
'format': '{levelname} {asctime} {module} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"handlers": {
|
"handlers": {
|
||||||
"console": {
|
"console": {
|
||||||
|
@ -233,10 +239,16 @@ LOGGING = {
|
||||||
"handlers": ["console"],
|
"handlers": ["console"],
|
||||||
"level": "ERROR",
|
"level": "ERROR",
|
||||||
"propagate": False,
|
"propagate": False,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SNAPSHOT_PATH="/tmp/"
|
SNAPSHOT_PATH="/tmp/"
|
||||||
DATA_UPLOAD_MAX_NUMBER_FILES = 1000
|
DATA_UPLOAD_MAX_NUMBER_FILES = 1000
|
||||||
COMMIT = config('COMMIT', default='')
|
COMMIT = config('COMMIT', default='')
|
||||||
|
|
||||||
|
# DLT SETTINGS
|
||||||
|
TOKEN_DLT = config("API_DLT_TOKEN", default=None)
|
||||||
|
API_DLT = config("API_DLT", default=None)
|
||||||
|
API_RESOLVER = config("API_RESOLVER", default=None)
|
||||||
|
ID_FEDERATED = config("ID_FEDERATED", default=None)
|
||||||
|
|
|
@ -14,7 +14,7 @@ Including another URLconf
|
||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
from django.conf import settings
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -22,9 +22,16 @@ urlpatterns = [
|
||||||
path("", include("login.urls")),
|
path("", include("login.urls")),
|
||||||
path("dashboard/", include("dashboard.urls")),
|
path("dashboard/", include("dashboard.urls")),
|
||||||
path("evidence/", include("evidence.urls")),
|
path("evidence/", include("evidence.urls")),
|
||||||
|
path('action/', include('action.urls')),
|
||||||
path("device/", include("device.urls")),
|
path("device/", include("device.urls")),
|
||||||
path("admin/", include("admin.urls")),
|
path("admin/", include("admin.urls")),
|
||||||
path("user/", include("user.urls")),
|
path("user/", include("user.urls")),
|
||||||
path("lot/", include("lot.urls")),
|
path("lot/", include("lot.urls")),
|
||||||
path('api/', include('api.urls')),
|
path('api/', include('api.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.DPP:
|
||||||
|
urlpatterns.extend([
|
||||||
|
path('dpp/', include('dpp.urls')),
|
||||||
|
path('did/', include('did.urls')),
|
||||||
|
])
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class TagConfig(AppConfig):
|
class DidConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "tag"
|
name = "did"
|
41
did/template_credential.py
Normal file
41
did/template_credential.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
dpp_tmpl = {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/credentials/v2",
|
||||||
|
"https://test.uncefact.org/vocabulary/untp/dpp/0.5.0/"
|
||||||
|
],
|
||||||
|
"type": [
|
||||||
|
"DigitalProductPassport",
|
||||||
|
"VerifiableCredential"
|
||||||
|
],
|
||||||
|
"id": "https://example.ereuse.org/credentials/2a423366-a0d6-4855-ba65-2e0c926d09b0",
|
||||||
|
"issuer": {
|
||||||
|
"type": [
|
||||||
|
"CredentialIssuer"
|
||||||
|
],
|
||||||
|
"id": "did:web:r1.identifiers.ereuse.org:did-registry:z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ#z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ",
|
||||||
|
"name": "Refurbisher One"
|
||||||
|
},
|
||||||
|
"validFrom": "2024-11-15T12:00:00",
|
||||||
|
"validUntil": "2034-11-15T12:00:00",
|
||||||
|
"credentialSubject": {
|
||||||
|
"type": [
|
||||||
|
"Product"
|
||||||
|
],
|
||||||
|
"id": "https://id.ereuse.org/01/09520123456788/21/12345",
|
||||||
|
"name": "Refurbished XYZ Lenovo laptop item",
|
||||||
|
"registeredId": "09520123456788.21.12345",
|
||||||
|
"description": "XYZ Lenovo laptop refurbished by Refurbisher One",
|
||||||
|
"data": ""
|
||||||
|
},
|
||||||
|
"credentialSchema": {
|
||||||
|
"id": "https://idhub.pangea.org/vc_schemas/dpp.json",
|
||||||
|
"type": "FullJsonSchemaValidator2021",
|
||||||
|
"proof": {
|
||||||
|
"type": "Ed25519Signature2018",
|
||||||
|
"proofPurpose": "assertionMethod",
|
||||||
|
"verificationMethod": "did:web:r1.identifiers.ereuse.org:did-registry:z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ#z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ",
|
||||||
|
"created": "2024-12-03T15:33:42Z",
|
||||||
|
"jws": "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..rBPqbOcZCXB7GAnq1XIfV9Jvw4MKXlHff7qZkRfgwQ0Hnd9Ujt5s1xT4O0K6VESzWvdP2mOvMvu780fVNfraBQ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
497
did/templates/device_did.html
Normal file
497
did/templates/device_did.html
Normal file
|
@ -0,0 +1,497 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{ object.type }}</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.custom-container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 30px;
|
||||||
|
margin-top: 30px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
color: #7a9f4f;
|
||||||
|
border-bottom: 2px solid #9cc666;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #545f71;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.component-card {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 4px solid #9cc666;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.component-card:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.hash-value {
|
||||||
|
word-break: break-all;
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
color: #9cc666;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #9cc666;
|
||||||
|
border-color: #9cc666;
|
||||||
|
padding: 0.1em 2em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #8ab555;
|
||||||
|
border-color: #8ab555;
|
||||||
|
}
|
||||||
|
.btn-green-user {
|
||||||
|
background-color: #c7e3a3;
|
||||||
|
}
|
||||||
|
.btn-grey {
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
background-color: #545f71;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container custom-container">
|
||||||
|
<nav class="header-nav ms-auto">
|
||||||
|
<div class="d-flex align-items-right">
|
||||||
|
<span class="nav-item">
|
||||||
|
{% if not roles and user.is_anonymous %}
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#validateModal">Validate</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-primary" id="buttonRole" data-bs-toggle="modal" data-bs-target="#rolesModal">Select your role</button>
|
||||||
|
<a class="btn btn-primary" href="{% url 'login:logout' %}?next={{ path }}">Logout</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% if role %}
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<span class="nav-item">
|
||||||
|
Current Role: {{ role }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<h1 class="text-center mb-4" style="color: #545f71;">{{ object.manufacturer }} {{ object.type }} {{ object.model }}</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
{% if manuals.details.logo %}
|
||||||
|
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
{% if manuals.details.image %}
|
||||||
|
<img style="width: 100px;" src="{{ manuals.details.image }}" />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h2 class="section-title">Details</h2>
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Phid</div>
|
||||||
|
<div class="col-md-8 info-value">
|
||||||
|
<div class="hash-value">{{ object.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Type</div>
|
||||||
|
<div class="col-md-8 info-value">{{ object.type }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object.is_websnapshot %}
|
||||||
|
{% for snapshot_key, snapshot_value in object.last_user_evidence %}
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">{{ snapshot_key }}</div>
|
||||||
|
<div class="col-md-8 info-value">{{ snapshot_value|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Manufacturer</div>
|
||||||
|
<div class="col-md-8 info-value">{{ object.manufacturer|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Model</div>
|
||||||
|
<div class="col-md-8 info-value">{{ object.model|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="info-row row">
|
||||||
|
<div class="col-md-4 info-label">Serial Number</div>
|
||||||
|
<div class="col-md-8 info-value">{{ object.serial_number|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h2 class="section-title">Identifiers</h2>
|
||||||
|
{% for chid in object.hids %}
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="hash-value">{{ chid|default:'' }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="section-title mt-5">Components</h2>
|
||||||
|
<div class="row">
|
||||||
|
{% for component in object.components %}
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="card component-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ component.type }}</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
{% for component_key, component_value in component.items %}
|
||||||
|
{% if component_key not in 'actions,type' %}
|
||||||
|
{% if component_key != 'serialNumber' or user.is_authenticated %}
|
||||||
|
<strong>{{ component_key }}:</strong> {{ component_value }}<br />
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if manuals.icecat %}
|
||||||
|
<h5 class="card-title">Icecat data sheet</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 list-group-item d-flex align-items-center">
|
||||||
|
{% if manuals.details.logo %}
|
||||||
|
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% if manuals.details.image %}
|
||||||
|
<img style="max-width: 100px; margin-right: 15px;" src="{{ manuals.details.image }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% if manuals.details.pdf %}
|
||||||
|
<a href="{{ manuals.details.pdf }}" target="_blank">{{ manuals.details.title }}</a><br />
|
||||||
|
{% else %}
|
||||||
|
{{ manuals.details.title }}<br />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-12 accordion-item">
|
||||||
|
<h5 class="card-title accordion-header">
|
||||||
|
<button class="accordion-button collapsed" data-bs-target="#manuals-icecat" type="button"
|
||||||
|
data-bs-toggle="collapse" aria-expanded="false">
|
||||||
|
More examples
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
<div id="manuals-icecat" class="row accordion-collapse collapse">
|
||||||
|
<div class="accordion-body">
|
||||||
|
{% for m in manuals.icecat %}
|
||||||
|
<div class="list-group-item d-flex align-items-center">
|
||||||
|
{% if m.logo %}
|
||||||
|
<img style="max-width: 50px; margin-right: 15px;" src="{{ m.logo }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% if m.pdf %}
|
||||||
|
<a href="{{ m.pdf }}" target="_blank">{{ m.title }}</a><br />
|
||||||
|
{% else %}
|
||||||
|
{{ m.title }}<br />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if manuals.laer %}
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5 class="card-title">Recycled Content</h5>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-2">
|
||||||
|
Metal
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="progress">
|
||||||
|
|
||||||
|
<div class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ manuals.laer.0.metal }}%"
|
||||||
|
aria-valuenow="{{ manuals.laer.0.metal }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">{{ manuals.laer.0.metal }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-2">
|
||||||
|
Plastic post Consumer
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ manuals.laer.0.plastic_post_consumer }}%"
|
||||||
|
aria-valuenow="{{ manuals.laer.0.plastic_post_consumer }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">{{ manuals.laer.0.plastic_post_consumer }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-2">
|
||||||
|
Plastic post Industry
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ manuals.laer.0.plastic_post_industry }}%"
|
||||||
|
aria-valuenow="{{ manuals.laer.0.plastic_post_industry }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">{{ manuals.laer.0.plastic_post_industry }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar %}
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5 class="card-title">Energy spent</h5>
|
||||||
|
|
||||||
|
{% if manuals.energystar.long_idle_watts %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
Consumption when inactivity power function is activated (watts)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.long_idle_watts }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar.short_idle_watts %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
Consumption when inactivity power function is not activated (watts)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.short_idle_watts }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar.sleep_mode_watts %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
sleep_mode_watts
|
||||||
|
Consumption when computer goes into sleep mode (watts)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.sleep_mode_watts }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar.off_mode_watts %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
Consumption when the computer is off (watts)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.off_mode_watts }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar.tec_allowance_kwh %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
Power allocation for normal operation (kwh)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.tec_allowance_kwh }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar.tec_of_model_kwh %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
Consumption of the model configuration (kwh)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.tec_of_model_kwh }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar.tec_requirement_kwh %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
Energy allowance provided (kwh)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.tec_requirement_kwh }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar.work_off_mode_watts %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
The lowest power mode which cannot be switched off (watts)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.work_off_mode_watts }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if manuals.energystar.work_weighted_power_of_model_watts %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
Weighted energy consumption from all its states (watts)
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{{ manuals.energystar.work_weighted_power_of_model_watts }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if manuals.ifixit %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 accordion-item">
|
||||||
|
<h5 class="card-title accordion-header">
|
||||||
|
<button class="accordion-button collapsed" data-bs-target="#manuals-repair" type="button"
|
||||||
|
data-bs-toggle="collapse" aria-expanded="false">
|
||||||
|
Repair manuals
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
<div id="manuals-repair" class="row accordion-collapse collapse">
|
||||||
|
<div class="list-group col">
|
||||||
|
{% for m in manuals.ifixit %}
|
||||||
|
<div class="list-group-item d-flex align-items-center">
|
||||||
|
{% if m.image %}
|
||||||
|
<img style="max-width: 100px; margin-right: 15px;" src="{{ m.image }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% if m.url %}
|
||||||
|
<a href="{{ m.url }}" target="_blank">{{ m.title }}</a><br />
|
||||||
|
{% else %}
|
||||||
|
{{ m.title }}<br />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
©{% now 'Y' %}eReuse. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
{% if user.is_anonymous and not roles %}
|
||||||
|
<div class="modal fade" id="validateModal" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Validate as <span id="title-action"></span></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<a class="btn btn-primary" type="button"
|
||||||
|
href="{% url 'login:login' %}?next={{ path }}">
|
||||||
|
User of system
|
||||||
|
</a>
|
||||||
|
{% if oidc %}
|
||||||
|
<br />
|
||||||
|
<a class="btn btn-primary mt-3" type="button" href="{# url 'oidc:login_other_inventory' #}?next={{ path }}">
|
||||||
|
User of other inventory
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="modal fade" id="rolesModal" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<form action="{{ path }}" method="get">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Select your Role <span id="title-action"></span></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<select name="role">
|
||||||
|
{% for k, v in roles %}
|
||||||
|
<option value="{{ k }}" {% if v == role %}selected=selected{% endif %}>{{ v }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<input type="submit" class="btn btn-primary" value="Send" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
did/urls.py
Normal file
8
did/urls.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.urls import path
|
||||||
|
from did import views
|
||||||
|
|
||||||
|
app_name = 'did'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("<str:pk>", views.PublicDeviceWebView.as_view(), name="device_web"),
|
||||||
|
]
|
263
did/views.py
Normal file
263
did/views.py
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.http import JsonResponse, Http404
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
from device.models import Device
|
||||||
|
from evidence.parse import Build
|
||||||
|
from dpp.api_dlt import ALGORITHM
|
||||||
|
from dpp.models import Proof
|
||||||
|
from dpp.api_dlt import PROOF_TYPE
|
||||||
|
from did.template_credential import dpp_tmpl
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
|
||||||
|
class PublicDeviceWebView(TemplateView):
|
||||||
|
template_name = "device_did.html"
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.pk = kwargs['pk']
|
||||||
|
chid = self.pk.split(":")[0]
|
||||||
|
proof = Proof.objects.filter(signature=self.pk).first()
|
||||||
|
if proof:
|
||||||
|
self.object = Device(id=chid, uuid=proof.uuid)
|
||||||
|
else:
|
||||||
|
self.object = Device(id=chid)
|
||||||
|
|
||||||
|
if not self.object.last_evidence:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
if self.request.headers.get('Accept') == 'application/json':
|
||||||
|
return self.get_json_response()
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
self.context = super().get_context_data(**kwargs)
|
||||||
|
self.object.initial()
|
||||||
|
roles = [("Operator", "Operator")]
|
||||||
|
role = "Operator"
|
||||||
|
if self.request.user.is_anonymous:
|
||||||
|
roles = []
|
||||||
|
role = None
|
||||||
|
self.context.update({
|
||||||
|
'object': self.object,
|
||||||
|
'role': role,
|
||||||
|
'roles': roles,
|
||||||
|
'path': self.request.path,
|
||||||
|
'last_dpp': "",
|
||||||
|
'before_dpp': "",
|
||||||
|
})
|
||||||
|
if not self.request.user.is_anonymous:
|
||||||
|
self.get_manuals()
|
||||||
|
return self.context
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_fields(self):
|
||||||
|
return {
|
||||||
|
'id': self.object.id,
|
||||||
|
'shortid': self.object.shortid,
|
||||||
|
'uuids': self.object.uuids,
|
||||||
|
'hids': self.object.hids,
|
||||||
|
'components': self.remove_serial_number_from(self.object.components),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def authenticated_fields(self):
|
||||||
|
return {
|
||||||
|
'serial_number': self.object.serial_number,
|
||||||
|
'components': self.object.components,
|
||||||
|
}
|
||||||
|
|
||||||
|
def remove_serial_number_from(self, components):
|
||||||
|
for component in components:
|
||||||
|
if 'serial_number' in component:
|
||||||
|
del component['SerialNumber']
|
||||||
|
return components
|
||||||
|
|
||||||
|
def get_device_data(self):
|
||||||
|
data = self.public_fields
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
data.update(self.authenticated_fields)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_json_response(self):
|
||||||
|
device_data = self.get_result()
|
||||||
|
# device_data = self.get_device_data()
|
||||||
|
response = JsonResponse(device_data)
|
||||||
|
response["Access-Control-Allow-Origin"] = "*"
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_result(self):
|
||||||
|
|
||||||
|
if len(self.pk.split(":")) > 1:
|
||||||
|
return self.build_from_dpp()
|
||||||
|
else:
|
||||||
|
return self.build_from_chid()
|
||||||
|
|
||||||
|
def build_from_dpp(self):
|
||||||
|
data = {
|
||||||
|
'document': {},
|
||||||
|
'dpp': self.pk,
|
||||||
|
'algorithm': ALGORITHM,
|
||||||
|
'components': [],
|
||||||
|
'manufacturer DPP': '',
|
||||||
|
'device': {},
|
||||||
|
}
|
||||||
|
dev = Build(self.object.last_evidence.doc, None, check=True)
|
||||||
|
doc = dev.build.get_doc()
|
||||||
|
data['document'] = json.dumps(doc)
|
||||||
|
data['device'] = dev.build.device
|
||||||
|
data['components'] = dev.build.components
|
||||||
|
|
||||||
|
self.object.get_evidences()
|
||||||
|
last_dpp = Proof.objects.filter(
|
||||||
|
uuid__in=self.object.uuids, type=PROOF_TYPE['IssueDPP']
|
||||||
|
).order_by("-timestamp").first()
|
||||||
|
|
||||||
|
key = self.pk
|
||||||
|
if last_dpp:
|
||||||
|
key += ":"+last_dpp.signature
|
||||||
|
|
||||||
|
url = "https://{}/did/{}".format(
|
||||||
|
self.request.get_host(),
|
||||||
|
key
|
||||||
|
)
|
||||||
|
data['url_last'] = url
|
||||||
|
tmpl = dpp_tmpl.copy()
|
||||||
|
tmpl["credentialSubject"]["data"] = data
|
||||||
|
return tmpl
|
||||||
|
|
||||||
|
def build_from_chid(self):
|
||||||
|
dpps = []
|
||||||
|
self.object.initial()
|
||||||
|
for d in self.object.evidences:
|
||||||
|
d.get_doc()
|
||||||
|
dev = Build(d.doc, None, check=True)
|
||||||
|
doc = dev.build.get_doc()
|
||||||
|
ev = json.dumps(doc)
|
||||||
|
phid = dev.sign(ev)
|
||||||
|
dpp = "{}:{}".format(self.pk, phid)
|
||||||
|
rr = {
|
||||||
|
'dpp': dpp,
|
||||||
|
'document': ev,
|
||||||
|
'algorithm': ALGORITHM,
|
||||||
|
'manufacturer DPP': '',
|
||||||
|
'device': dev.build.device,
|
||||||
|
'components': dev.build.components
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl = dpp_tmpl.copy()
|
||||||
|
tmpl["credentialSubject"]["data"] = rr
|
||||||
|
|
||||||
|
dpps.append(tmpl)
|
||||||
|
return {
|
||||||
|
'@context': ['https://ereuse.org/dpp0.json'],
|
||||||
|
'data': dpps,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_manuals(self):
|
||||||
|
manuals = {
|
||||||
|
'ifixit': [],
|
||||||
|
'icecat': [],
|
||||||
|
'details': {},
|
||||||
|
'laer': [],
|
||||||
|
'energystar': {},
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
"manufacturer": self.object.manufacturer,
|
||||||
|
"model": self.object.model,
|
||||||
|
}
|
||||||
|
self.params = json.dumps(params)
|
||||||
|
manuals['ifixit'] = self.request_manuals('ifixit')
|
||||||
|
manuals['icecat'] = self.request_manuals('icecat')
|
||||||
|
manuals['laer'] = self.request_manuals('laer')
|
||||||
|
manuals['energystar'] = self.request_manuals('energystar') or {}
|
||||||
|
if manuals['icecat']:
|
||||||
|
manuals['details'] = manuals['icecat'][0]
|
||||||
|
except Exception as err:
|
||||||
|
logger.error("Error: {}".format(err))
|
||||||
|
|
||||||
|
self.context['manuals'] = manuals
|
||||||
|
self.parse_energystar()
|
||||||
|
|
||||||
|
def parse_energystar(self):
|
||||||
|
if not self.context.get('manuals', {}).get('energystar'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Defined in:
|
||||||
|
# https://dev.socrata.com/foundry/data.energystar.gov/j7nq-iepp
|
||||||
|
|
||||||
|
energy_types = [
|
||||||
|
'functional_adder_allowances_kwh',
|
||||||
|
'tec_allowance_kwh',
|
||||||
|
'long_idle_watts',
|
||||||
|
'short_idle_watts',
|
||||||
|
'off_mode_watts',
|
||||||
|
'sleep_mode_watts',
|
||||||
|
'tec_of_model_kwh',
|
||||||
|
'tec_requirement_kwh',
|
||||||
|
'work_off_mode_watts',
|
||||||
|
'work_weighted_power_of_model_watts',
|
||||||
|
]
|
||||||
|
energy = {}
|
||||||
|
for field in energy_types:
|
||||||
|
energy[field] = []
|
||||||
|
|
||||||
|
for e in self.context['manuals']['energystar']:
|
||||||
|
for field in energy_types:
|
||||||
|
for k, v in e.items():
|
||||||
|
if not v:
|
||||||
|
continue
|
||||||
|
if field in k:
|
||||||
|
energy[field].append(v)
|
||||||
|
|
||||||
|
for k, v in energy.items():
|
||||||
|
if not v:
|
||||||
|
energy[k] = 0
|
||||||
|
continue
|
||||||
|
tt = sum([float(i) for i in v])
|
||||||
|
energy[k] = round(tt / len(v), 2)
|
||||||
|
|
||||||
|
self.context['manuals']['energystar'] = energy
|
||||||
|
|
||||||
|
def request_manuals(self, prefix):
|
||||||
|
#TODO reimplement manuals service
|
||||||
|
response = {
|
||||||
|
"laer": [{"metal": 40, "plastic_post_consumer": 27, "plastic_post_industry": 34}],
|
||||||
|
"energystar": [{
|
||||||
|
'functional_adder_allowances_kwh': 180,
|
||||||
|
"long_idle_watts": 240,
|
||||||
|
"short_idle_watts": 120,
|
||||||
|
"sleep_mode_watts": 30,
|
||||||
|
"off_mode_watts": 3,
|
||||||
|
"tec_allowance_kwh": 180,
|
||||||
|
"tec_of_model_kwh": 150,
|
||||||
|
"tec_requirement_kwh": 220,
|
||||||
|
"work_off_mode_watts": 70,
|
||||||
|
"work_weighted_power_of_model_watts": 240
|
||||||
|
}],
|
||||||
|
"ifixit": [
|
||||||
|
{
|
||||||
|
"image": "https://guide-images.cdn.ifixit.com/igi/156EpI4YdQeVfVPa.medium",
|
||||||
|
"url": "https://es.ifixit.com/Gu%C3%ADa/HP+ProBook+450+G4+Back+Panel+Replacement/171196?lang=en",
|
||||||
|
"title": "HP ProBook 450 G4 Back Panel Replacement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image": "https://guide-images.cdn.ifixit.com/igi/usTIqCKpuxVWC3Ix.140x105",
|
||||||
|
"url": "https://es.ifixit.com/Gu%C3%ADa/HP+ProBook+450+G4+Display+Assembly+Replacement/171101?lang=en",
|
||||||
|
"title": "Display Assembly Replacement"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icecat": [
|
||||||
|
{
|
||||||
|
"logo": "https://images.icecat.biz/img/brand/thumb/1_cf8603f6de7b4c4d8ac4f5f0ef439a05.jpg",
|
||||||
|
"image": "https://guide-images.cdn.ifixit.com/igi/Q2nYjTIQfG6GaI5B.standard",
|
||||||
|
"pdf": "https://icecat.biz/rest/product-pdf?productId=32951710&lang=en",
|
||||||
|
"title": "HP ProBook 450 G3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return response.get(prefix, {})
|
|
@ -1,16 +1,59 @@
|
||||||
services:
|
services:
|
||||||
devicehub-django:
|
devicehub-django:
|
||||||
init: true
|
init: true
|
||||||
|
image: farga.pangea.org/ereuse/devicehub-django:latest
|
||||||
build:
|
build:
|
||||||
|
context: .
|
||||||
dockerfile: docker/devicehub-django.Dockerfile
|
dockerfile: docker/devicehub-django.Dockerfile
|
||||||
environment:
|
environment:
|
||||||
- DEBUG=${DEBUG:-false}
|
- DEBUG=${DEBUG:-false}
|
||||||
- DOMAIN=${DOMAIN:-localhost}
|
- DOMAIN=${DEVICEHUB_DOMAIN:-localhost}
|
||||||
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN}
|
- PORT=${DEVICEHUB_PORT:-8000}
|
||||||
|
- ALLOWED_HOSTS=${DEVICEHUB_ALLOWED_HOSTS:-$DEVICEHUB_DOMAIN}
|
||||||
- DEMO=${DEMO:-false}
|
- DEMO=${DEMO:-false}
|
||||||
|
- DEMO_IDHUB_DOMAIN=${DEMO_IDHUB_DOMAIN:-}
|
||||||
|
- DEMO_IDHUB_PREDEFINED_TOKEN=${IDHUB_PREDEFINED_TOKEN:-}
|
||||||
- PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-}
|
- PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-}
|
||||||
|
- DPP=${DPP:-false}
|
||||||
|
# TODO manage volumes dev vs prod
|
||||||
volumes:
|
volumes:
|
||||||
- .:/opt/devicehub-django
|
- .:/opt/devicehub-django
|
||||||
ports:
|
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,10 +19,24 @@ main() {
|
||||||
cp -v .env.example .env
|
cp -v .env.example .env
|
||||||
echo "WARNING: .env was not there, .env.example was copied, this only happens once"
|
echo "WARNING: .env was not there, .env.example was copied, this only happens once"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# load vars
|
||||||
|
. ./.env
|
||||||
|
|
||||||
|
if [ "${IDHUB_ENABLED:-}" = 'true' ]; then
|
||||||
|
export COMPOSE_PROFILES='idhub'
|
||||||
|
fi
|
||||||
# remove old database
|
# remove old database
|
||||||
sudo rm -vfr ./db/*
|
rm -vfr ./db/*
|
||||||
|
# deactivate configured flag
|
||||||
|
rm -vfr ./already_configured
|
||||||
docker compose down -v
|
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:-}
|
docker compose up ${detach_arg:-}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,16 @@ RUN apt update && \
|
||||||
python3-xapian \
|
python3-xapian \
|
||||||
git \
|
git \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
|
curl \
|
||||||
jq \
|
jq \
|
||||||
|
time \
|
||||||
|
vim \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# TODO I don't like this, but the whole ereuse-dpp works with user 1000 because of the volume mapping
|
||||||
|
# thanks https://stackoverflow.com/questions/70520205/docker-non-root-user-best-practices-for-python-images
|
||||||
|
RUN adduser --home /opt/devicehub-django -u 1000 app
|
||||||
|
|
||||||
WORKDIR /opt/devicehub-django
|
WORKDIR /opt/devicehub-django
|
||||||
|
|
||||||
# reduce size (python specifics) -> src https://stackoverflow.com/questions/74616667/removing-pip-cache-after-installing-dependencies-in-docker-image
|
# reduce size (python specifics) -> src https://stackoverflow.com/questions/74616667/removing-pip-cache-after-installing-dependencies-in-docker-image
|
||||||
|
@ -22,15 +29,19 @@ compile = no
|
||||||
no-cache-dir = True
|
no-cache-dir = True
|
||||||
END
|
END
|
||||||
|
|
||||||
# upgrade pip, which might fail on lxc, then remove the "corrupted file"
|
|
||||||
RUN python -m pip install --upgrade pip || (rm -rf /usr/local/lib/python3.11/site-packages/pip-*.dist-info && python -m pip install --upgrade pip)
|
|
||||||
|
|
||||||
COPY ./requirements.txt /opt/devicehub-django
|
COPY ./requirements.txt /opt/devicehub-django
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
# TODO hardcoded, is ignored in requirements.txt
|
||||||
|
RUN pip install -i https://test.pypi.org/simple/ ereuseapitest==0.0.14
|
||||||
|
|
||||||
# TODO Is there a better way?
|
# TODO Is there a better way?
|
||||||
# Set PYTHONPATH to include the directory with the xapian module
|
# Set PYTHONPATH to include the directory with the xapian module
|
||||||
ENV PYTHONPATH="${PYTHONPATH}:/usr/lib/python3/dist-packages"
|
ENV PYTHONPATH="${PYTHONPATH}:/usr/lib/python3/dist-packages"
|
||||||
|
|
||||||
|
COPY . .
|
||||||
COPY docker/devicehub-django.entrypoint.sh /
|
COPY docker/devicehub-django.entrypoint.sh /
|
||||||
|
|
||||||
|
RUN chown -R app:app /opt/devicehub-django
|
||||||
|
|
||||||
|
USER app
|
||||||
ENTRYPOINT sh /devicehub-django.entrypoint.sh
|
ENTRYPOINT sh /devicehub-django.entrypoint.sh
|
||||||
|
|
|
@ -5,6 +5,181 @@ set -u
|
||||||
# DEBUG
|
# DEBUG
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
|
# TODO there is a conflict between two shared vars
|
||||||
|
# 1. from the original docker compose devicehub-teal
|
||||||
|
# 2. from the new docker compose that integrates all dpp services
|
||||||
|
wait_for_dpp_shared() {
|
||||||
|
while true; do
|
||||||
|
# specially ensure VERAMO_API_CRED_FILE is not empty,
|
||||||
|
# it takes some time to get data in
|
||||||
|
OPERATOR_TOKEN_FILE='operator-token.txt'
|
||||||
|
if [ -f "/shared/${OPERATOR_TOKEN_FILE}" ] && \
|
||||||
|
[ -f "/shared/create_user_operator_finished" ]; then
|
||||||
|
sleep 5
|
||||||
|
echo "Files ready to process."
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Waiting for file in shared: ${OPERATOR_TOKEN_FILE}"
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Generate an environment .env file.
|
||||||
|
# TODO cargar via shared
|
||||||
|
gen_env_vars() {
|
||||||
|
INIT_ORG="${INIT_ORG:-example-org}"
|
||||||
|
INIT_USER="${INIT_USER:-user@example.org}"
|
||||||
|
INIT_PASSWD="${INIT_PASSWD:-1234}"
|
||||||
|
ADMIN='True'
|
||||||
|
PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}"
|
||||||
|
# specific dpp env vars
|
||||||
|
if [ "${DPP:-}" = 'true' ]; then
|
||||||
|
# fill env vars in this docker entrypoint
|
||||||
|
wait_for_dpp_shared
|
||||||
|
export API_DLT='http://api_connector:3010'
|
||||||
|
export API_DLT_TOKEN="$(cat "/shared/${OPERATOR_TOKEN_FILE}")"
|
||||||
|
export API_RESOLVER='http://id_index_api:3012'
|
||||||
|
# TODO hardcoded
|
||||||
|
export ID_FEDERATED='DH1'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_federated_id() {
|
||||||
|
|
||||||
|
# devicehub host and id federated checker
|
||||||
|
|
||||||
|
# //getAll queries are not accepted by this service, so we remove them
|
||||||
|
EXPECTED_ID_FEDERATED="$(curl -s "${API_RESOLVER%/}/getAll" \
|
||||||
|
| jq -r '.url | to_entries | .[] | select(.value == "'"${DEVICEHUB_HOST}"'") | .key' \
|
||||||
|
| head -n 1)"
|
||||||
|
|
||||||
|
# if is a new DEVICEHUB_HOST, then register it
|
||||||
|
if [ -z "${EXPECTED_ID_FEDERATED}" ]; then
|
||||||
|
# TODO better docker compose run command
|
||||||
|
cmd="docker compose run --entrypoint= devicehub flask dlt_insert_members ${DEVICEHUB_HOST}"
|
||||||
|
big_error "No FEDERATED ID maybe you should run \`${cmd}\`"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if not new DEVICEHUB_HOST, then check consistency
|
||||||
|
|
||||||
|
# if there is already an ID in the DLT, it should match with my internal ID
|
||||||
|
if [ ! "${EXPECTED_ID_FEDERATED}" = "${ID_FEDERATED}" ]; then
|
||||||
|
|
||||||
|
big_error "ID_FEDERATED should be ${EXPECTED_ID_FEDERATED} instead of ${ID_FEDERATED}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# not needed, but reserved
|
||||||
|
# EXPECTED_DEVICEHUB_HOST="$(curl -s "${API_RESOLVER%/}/getAll" \
|
||||||
|
# | jq -r '.url | to_entries | .[] | select(.key == "'"${ID_FEDERATED}"'") | .value' \
|
||||||
|
# | head -n 1)"
|
||||||
|
# if [ ! "${EXPECTED_DEVICEHUB_HOST}" = "${DEVICEHUB_HOST}" ]; then
|
||||||
|
# big_error "ERROR: DEVICEHUB_HOST should be ${EXPECTED_DEVICEHUB_HOST} instead of ${DEVICEHUB_HOST}"
|
||||||
|
# fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
config_dpp_part1() {
|
||||||
|
# 12. Add a new server to the 'api resolver'
|
||||||
|
if [ "${ID_SERVICE:-}" ]; then
|
||||||
|
handle_federated_id
|
||||||
|
else
|
||||||
|
# TODO when this runs more than one time per service, this is a problem, but for the docker-reset.sh workflow, that's fine
|
||||||
|
# TODO put this in already_configured
|
||||||
|
# TODO hardcoded http proto and port
|
||||||
|
./manage.py dlt_insert_members "http://${DOMAIN}:8000"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 13. Do a rsync api resolve
|
||||||
|
./manage.py dlt_rsync_members
|
||||||
|
|
||||||
|
# 14. Register a new user to the DLT
|
||||||
|
DATASET_FILE='/tmp/dataset.json'
|
||||||
|
cat > "${DATASET_FILE}" <<END
|
||||||
|
{
|
||||||
|
"email": "${INIT_USER}",
|
||||||
|
"password": "${INIT_PASSWD}",
|
||||||
|
"api_token": "${API_DLT_TOKEN}"
|
||||||
|
}
|
||||||
|
END
|
||||||
|
./manage.py dlt_register_user "${DATASET_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
/usr/bin/time ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
|
||||||
|
}
|
||||||
|
|
||||||
|
config_phase() {
|
||||||
|
# TODO review this flag file
|
||||||
|
init_flagfile="${program_dir}/already_configured"
|
||||||
|
if [ ! -f "${init_flagfile}" ]; then
|
||||||
|
|
||||||
|
# non DL user (only for the inventory)
|
||||||
|
./manage.py add_institution "${INIT_ORG}"
|
||||||
|
# TODO: one error on add_user, and you don't add user anymore
|
||||||
|
./manage.py add_user "${INIT_ORG}" "${INIT_USER}" "${INIT_PASSWD}" "${ADMIN}" "${PREDEFINED_TOKEN}"
|
||||||
|
|
||||||
|
if [ "${DPP:-}" = 'true' ]; then
|
||||||
|
# 12, 13, 14
|
||||||
|
config_dpp_part1
|
||||||
|
|
||||||
|
# cleanup other snapshots and copy dlt/dpp snapshots
|
||||||
|
# TODO make this better
|
||||||
|
rm example/snapshots/*
|
||||||
|
cp example/dpp-snapshots/*.json example/snapshots/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# # 15. Add inventory snapshots for user "${INIT_USER}".
|
||||||
|
if [ "${DEMO:-}" = 'true' ]; then
|
||||||
|
run_demo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# remain next command as the last operation for this if conditional
|
||||||
|
touch "${init_flagfile}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
check_app_is_there() {
|
check_app_is_there() {
|
||||||
if [ ! -f "./manage.py" ]; then
|
if [ ! -f "./manage.py" ]; then
|
||||||
usage
|
usage
|
||||||
|
@ -12,9 +187,11 @@ check_app_is_there() {
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy() {
|
deploy() {
|
||||||
# TODO this is weird, find better workaround
|
if [ -d /opt/devicehub-django/.git ]; then
|
||||||
git config --global --add safe.directory /opt/devicehub-django
|
# TODO this is weird, find better workaround
|
||||||
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
|
git config --global --add safe.directory "${program_dir}"
|
||||||
|
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "${DEBUG:-}" = 'true' ]; then
|
if [ "${DEBUG:-}" = 'true' ]; then
|
||||||
./manage.py print_settings
|
./manage.py print_settings
|
||||||
|
@ -30,19 +207,11 @@ deploy() {
|
||||||
# move the migrate thing in docker entrypoint
|
# move the migrate thing in docker entrypoint
|
||||||
# inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc
|
# inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc
|
||||||
echo "INFO detected NEW deployment"
|
echo "INFO detected NEW deployment"
|
||||||
./manage.py migrate
|
if [ ! -d "${program_dir}/db/" ]; then
|
||||||
INIT_ORG="${INIT_ORG:-example-org}"
|
mkdir -p "${program_dir}/db/"
|
||||||
INIT_USER="${INIT_USER:-user@example.org}"
|
|
||||||
INIT_PASSWD="${INIT_PASSWD:-1234}"
|
|
||||||
ADMIN='True'
|
|
||||||
PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}"
|
|
||||||
./manage.py add_institution "${INIT_ORG}"
|
|
||||||
# TODO: one error on add_user, and you don't add user anymore
|
|
||||||
./manage.py add_user "${INIT_ORG}" "${INIT_USER}" "${INIT_PASSWD}" "${ADMIN}" "${PREDEFINED_TOKEN}"
|
|
||||||
|
|
||||||
if [ "${DEMO:-}" = 'true' ]; then
|
|
||||||
./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
|
|
||||||
fi
|
fi
|
||||||
|
./manage.py migrate
|
||||||
|
config_phase
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +239,7 @@ runserver() {
|
||||||
main() {
|
main() {
|
||||||
program_dir='/opt/devicehub-django'
|
program_dir='/opt/devicehub-django'
|
||||||
cd "${program_dir}"
|
cd "${program_dir}"
|
||||||
|
gen_env_vars
|
||||||
deploy
|
deploy
|
||||||
runserver
|
runserver
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
166
dpp/api_dlt.py
Normal file
166
dpp/api_dlt.py
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from ereuseapi.methods import API
|
||||||
|
|
||||||
|
from dpp.models import Proof, UserDpp
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
|
||||||
|
# """The code of the status response of api dlt."""
|
||||||
|
STATUS_CODE = {
|
||||||
|
"Success": 201,
|
||||||
|
"Notwork": 400
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ALGORITHM = "sha3-256"
|
||||||
|
|
||||||
|
|
||||||
|
PROOF_TYPE = {
|
||||||
|
'Register': 'Register',
|
||||||
|
'IssueDPP': 'IssueDPP',
|
||||||
|
'proof_of_recycling': 'proof_of_recycling',
|
||||||
|
'Erase': 'Erase',
|
||||||
|
'EWaste': 'EWaste',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def connect_api(user):
|
||||||
|
|
||||||
|
dp = UserDpp.objects.filter(user=user).first()
|
||||||
|
if not dp:
|
||||||
|
return
|
||||||
|
|
||||||
|
api_dlt = settings.API_DLT
|
||||||
|
token_dlt = dp.api_keys_dlt
|
||||||
|
|
||||||
|
if not api_dlt or not token_dlt:
|
||||||
|
logger.error("NOT POSSIBLE CONNECT WITH API DLT!!!")
|
||||||
|
return
|
||||||
|
|
||||||
|
return API(api_dlt, token_dlt, "ethereum")
|
||||||
|
|
||||||
|
|
||||||
|
def register_dlt(api, chid, phid, proof_type=None):
|
||||||
|
if proof_type:
|
||||||
|
return api.generate_proof(
|
||||||
|
chid,
|
||||||
|
ALGORITHM,
|
||||||
|
phid,
|
||||||
|
proof_type,
|
||||||
|
settings.ID_FEDERATED
|
||||||
|
)
|
||||||
|
|
||||||
|
return api.register_device(
|
||||||
|
chid,
|
||||||
|
ALGORITHM,
|
||||||
|
phid,
|
||||||
|
settings.ID_FEDERATED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def issuer_dpp_dlt(api, dpp):
|
||||||
|
phid = dpp.split(":")[1]
|
||||||
|
|
||||||
|
return api.issue_passport(
|
||||||
|
dpp,
|
||||||
|
ALGORITHM,
|
||||||
|
phid,
|
||||||
|
settings.ID_FEDERATED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def save_proof(signature, ev_uuid, result, proof_type, user):
|
||||||
|
if result['Status'] == STATUS_CODE.get("Success"):
|
||||||
|
timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
|
||||||
|
|
||||||
|
if not timestamp:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("timestamp: %s", timestamp)
|
||||||
|
d = {
|
||||||
|
"type": proof_type,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"issuer": user.institution,
|
||||||
|
"user": user,
|
||||||
|
"uuid": ev_uuid,
|
||||||
|
"signature": signature,
|
||||||
|
}
|
||||||
|
Proof.objects.create(**d)
|
||||||
|
|
||||||
|
|
||||||
|
def register_device_dlt(chid, phid, ev_uuid, user):
|
||||||
|
cny_a = 1
|
||||||
|
while cny_a:
|
||||||
|
api = connect_api(user)
|
||||||
|
if not api:
|
||||||
|
cny_a = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
result = register_dlt(api, chid, phid)
|
||||||
|
try:
|
||||||
|
assert result['Status'] == STATUS_CODE.get("Success")
|
||||||
|
assert result['Data']['data']['timestamp']
|
||||||
|
cny_a = 0
|
||||||
|
except Exception:
|
||||||
|
if result.get("Data") != "Device already exists":
|
||||||
|
logger.error("API return: %s", result)
|
||||||
|
time.sleep(10)
|
||||||
|
else:
|
||||||
|
cny_a = 0
|
||||||
|
|
||||||
|
save_proof(phid, ev_uuid, result, PROOF_TYPE['Register'], user)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO is neccesary?
|
||||||
|
if settings.ID_FEDERATED:
|
||||||
|
cny = 1
|
||||||
|
while cny:
|
||||||
|
try:
|
||||||
|
api.add_service(
|
||||||
|
chid,
|
||||||
|
'DeviceHub',
|
||||||
|
settings.ID_FEDERATED,
|
||||||
|
'Inventory service',
|
||||||
|
'Inv',
|
||||||
|
)
|
||||||
|
cny = 0
|
||||||
|
except Exception:
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
|
def register_passport_dlt(chid, phid, ev_uuid, user):
|
||||||
|
token_dlt = settings.TOKEN_DLT
|
||||||
|
api_dlt = settings.API_DLT
|
||||||
|
if not token_dlt or not api_dlt:
|
||||||
|
return
|
||||||
|
|
||||||
|
dpp = "{chid}:{phid}".format(chid=chid, phid=phid)
|
||||||
|
if Proof.objects.filter(signature=dpp, type=PROOF_TYPE['IssueDPP']).exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
cny_a = 1
|
||||||
|
while cny_a:
|
||||||
|
try:
|
||||||
|
api = connect_api(user)
|
||||||
|
if not api:
|
||||||
|
cny_a = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
result = issuer_dpp_dlt(api, dpp)
|
||||||
|
cny_a = 0
|
||||||
|
except Exception as err:
|
||||||
|
logger.error("ERROR API issue passport return: %s", err)
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
if result['Status'] is not STATUS_CODE.get("Success"):
|
||||||
|
logger.error("ERROR API issue passport return: %s", result)
|
||||||
|
return
|
||||||
|
|
||||||
|
save_proof(phid, ev_uuid, result, PROOF_TYPE['IssueDPP'], user)
|
|
@ -1,6 +1,6 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class DocumentsConfig(AppConfig):
|
class DppConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "documents"
|
name = "dpp"
|
35
dpp/management/commands/dlt_insert_members.py
Normal file
35
dpp/management/commands/dlt_insert_members.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
from user.models import Institution
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Insert a new Institution in DLT"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('domain', type=str, help='institution')
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
domain = kwargs.get("domain")
|
||||||
|
api = settings.API_RESOLVER
|
||||||
|
if not api:
|
||||||
|
logger.error("you need set the var API_RESOLVER")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "http" not in domain:
|
||||||
|
logger.error("you need put https:// in %s", domain)
|
||||||
|
return
|
||||||
|
|
||||||
|
api = api.strip("/")
|
||||||
|
domain = domain.strip("/")
|
||||||
|
|
||||||
|
data = {"url": domain}
|
||||||
|
url = api + '/registerURL'
|
||||||
|
res = requests.post(url, json=data)
|
||||||
|
print(res.json())
|
72
dpp/management/commands/dlt_register_user.py
Normal file
72
dpp/management/commands/dlt_register_user.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ereuseapi.methods import API
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from user.models import User, Institution
|
||||||
|
from dpp.models import UserDpp
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Insert users than are in Dlt with params: path of data set file"
|
||||||
|
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('dataset_file', type=str, help='institution')
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
dataset_file = kwargs.get("dataset_file")
|
||||||
|
self.api_dlt = settings.API_DLT
|
||||||
|
self.institution = Institution.objects.filter().first()
|
||||||
|
if not self.api_dlt:
|
||||||
|
logger.error("you need set the var API_DLT")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.api_dlt = self.api_dlt.strip("/")
|
||||||
|
|
||||||
|
with open(dataset_file) as f:
|
||||||
|
dataset = json.loads(f.read())
|
||||||
|
|
||||||
|
self.add_user(dataset)
|
||||||
|
|
||||||
|
def add_user(self, data):
|
||||||
|
email = data.get("email")
|
||||||
|
password = data.get("password")
|
||||||
|
api_token = data.get("api_token")
|
||||||
|
# ethereum = {"data": {"api_token": api_token}}
|
||||||
|
# data_eth = json.dumps(ethereum)
|
||||||
|
data_eth = json.dumps(api_token)
|
||||||
|
# TODO encrypt in the future
|
||||||
|
# api_keys_dlt = encrypt(password, data_eth)
|
||||||
|
api_keys_dlt = data_eth.strip('"').strip("'")
|
||||||
|
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
user = User.objects.create(
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
institution = self.institution
|
||||||
|
)
|
||||||
|
|
||||||
|
roles = []
|
||||||
|
token_dlt = api_token
|
||||||
|
api = API(self.api_dlt, token_dlt, "ethereum")
|
||||||
|
result = api.check_user_roles()
|
||||||
|
|
||||||
|
if result.get('Status') == 200:
|
||||||
|
if 'Success' in result.get('Data', {}).get('status'):
|
||||||
|
rols = result.get('Data', {}).get('data', {})
|
||||||
|
roles = [(k, k) for k, v in rols.items() if v]
|
||||||
|
|
||||||
|
roles_dlt = json.dumps(roles)
|
||||||
|
|
||||||
|
UserDpp.objects.create(
|
||||||
|
roles_dlt=roles_dlt,
|
||||||
|
api_keys_dlt=api_keys_dlt,
|
||||||
|
user=user
|
||||||
|
)
|
47
dpp/management/commands/dlt_rsync_members.py
Normal file
47
dpp/management/commands/dlt_rsync_members.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
from dpp.models import MemberFederated
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Synchronize members of DLT"
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
api = settings.API_RESOLVER
|
||||||
|
if not api:
|
||||||
|
logger.error("you need set the var API_RESOLVER")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
api = api.strip("/")
|
||||||
|
|
||||||
|
url = api + '/getAll'
|
||||||
|
res = requests.get(url)
|
||||||
|
if res.status_code != 200:
|
||||||
|
return "Error, {}".format(res.text)
|
||||||
|
response = res.json()
|
||||||
|
members = response['url']
|
||||||
|
counter = members.pop('counter')
|
||||||
|
if counter <= MemberFederated.objects.count():
|
||||||
|
logger.info("Synchronize members of DLT -> All Ok")
|
||||||
|
return "All ok"
|
||||||
|
|
||||||
|
for k, v in members.items():
|
||||||
|
id = self.clean_id(k)
|
||||||
|
member = MemberFederated.objects.filter(dlt_id_provider=id).first()
|
||||||
|
if member:
|
||||||
|
if member.domain != v:
|
||||||
|
member.domain = v
|
||||||
|
member.save()
|
||||||
|
continue
|
||||||
|
MemberFederated.objects.create(dlt_id_provider=id, domain=v)
|
||||||
|
return res.text
|
||||||
|
|
||||||
|
def clean_id(self, id):
|
||||||
|
return int(id.split('DH')[-1])
|
52
dpp/migrations/0001_initial.py
Normal file
52
dpp/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-11-18 14:29
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Proof",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("timestamp", models.IntegerField()),
|
||||||
|
("uuid", models.UUIDField()),
|
||||||
|
("signature", models.CharField(max_length=256)),
|
||||||
|
("type", models.CharField(max_length=256)),
|
||||||
|
(
|
||||||
|
"issuer",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
25
dpp/migrations/0002_memberfederated.py
Normal file
25
dpp/migrations/0002_memberfederated.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-11-19 19:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("dpp", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="MemberFederated",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"dlt_id_provider",
|
||||||
|
models.IntegerField(primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
("domain", models.CharField(max_length=256)),
|
||||||
|
("client_id", models.CharField(max_length=256)),
|
||||||
|
("client_secret", models.CharField(max_length=256)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
60
dpp/migrations/0003_memberfederated_institution_and_more.py
Normal file
60
dpp/migrations/0003_memberfederated_institution_and_more.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-11-20 10:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("dpp", "0002_memberfederated"),
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="memberfederated",
|
||||||
|
name="institution",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="memberfederated",
|
||||||
|
name="client_id",
|
||||||
|
field=models.CharField(max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="memberfederated",
|
||||||
|
name="client_secret",
|
||||||
|
field=models.CharField(max_length=256, null=True),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserDpp",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("roles_dlt", models.TextField()),
|
||||||
|
("api_keys_dlt", models.TextField()),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
32
dpp/models.py
Normal file
32
dpp/models.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from django.db import models
|
||||||
|
from user.models import User, Institution
|
||||||
|
from utils.constants import STR_EXTEND_SIZE
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
|
||||||
|
class Proof(models.Model):
|
||||||
|
## The signature can be a phid or dpp depending of type of Proof
|
||||||
|
timestamp = models.IntegerField()
|
||||||
|
uuid = models.UUIDField()
|
||||||
|
signature = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||||
|
type = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||||
|
issuer = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MemberFederated(models.Model):
|
||||||
|
dlt_id_provider = models.IntegerField(primary_key=True)
|
||||||
|
domain = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||||
|
# This client_id and client_secret is used for connected to this domain as
|
||||||
|
# a client and this domain then is the server of auth
|
||||||
|
client_id = models.CharField(max_length=STR_EXTEND_SIZE, null=True)
|
||||||
|
client_secret = models.CharField(max_length=STR_EXTEND_SIZE, null=True)
|
||||||
|
institution = models.ForeignKey(
|
||||||
|
Institution, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserDpp(models.Model):
|
||||||
|
roles_dlt = models.TextField()
|
||||||
|
api_keys_dlt = models.TextField()
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
8
dpp/urls.py
Normal file
8
dpp/urls.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.urls import path
|
||||||
|
from dpp import views
|
||||||
|
|
||||||
|
app_name = 'dpp'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("<int:proof_id>/", views.ProofView.as_view(), name="proof"),
|
||||||
|
]
|
40
dpp/views.py
Normal file
40
dpp/views.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from django.views.generic.edit import View
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
from dpp.api_dlt import ALGORITHM
|
||||||
|
from evidence.models import Evidence
|
||||||
|
from evidence.parse import Build
|
||||||
|
from dpp.models import Proof
|
||||||
|
|
||||||
|
|
||||||
|
class ProofView(View):
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
timestamp = kwargs.get("proof_id")
|
||||||
|
proof = Proof.objects.filter(timestamp=timestamp).first()
|
||||||
|
if not proof:
|
||||||
|
return JsonResponse({}, status=404)
|
||||||
|
|
||||||
|
ev = Evidence(proof.uuid)
|
||||||
|
if not ev.doc:
|
||||||
|
return JsonResponse({}, status=404)
|
||||||
|
|
||||||
|
dev = Build(ev.doc, None, check=True)
|
||||||
|
doc = dev.build.get_doc()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"algorithm": ALGORITHM,
|
||||||
|
"document": json.dumps(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = {
|
||||||
|
'@context': ['https://ereuse.org/proof0.json'],
|
||||||
|
'data': data,
|
||||||
|
}
|
||||||
|
response = JsonResponse(d, status=200)
|
||||||
|
response["Access-Control-Allow-Origin"] = "*"
|
||||||
|
return response
|
|
@ -4,12 +4,13 @@ import pandas as pd
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from utils.device import create_annotation, create_doc, create_index
|
from utils.device import create_property, create_doc, create_index
|
||||||
from utils.forms import MultipleFileField
|
from utils.forms import MultipleFileField
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from evidence.parse import Build
|
from evidence.parse import Build
|
||||||
from evidence.models import Annotation
|
from evidence.models import SystemProperty, UserProperty
|
||||||
from utils.save_snapshots import move_json, save_in_disk
|
from utils.save_snapshots import move_json, save_in_disk
|
||||||
|
from action.models import DeviceLog
|
||||||
|
|
||||||
|
|
||||||
class UploadForm(forms.Form):
|
class UploadForm(forms.Form):
|
||||||
|
@ -29,17 +30,17 @@ class UploadForm(forms.Form):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_json = json.loads(file_data)
|
file_json = json.loads(file_data)
|
||||||
Build(file_json, None, check=True)
|
snap = Build(file_json, None, check=True)
|
||||||
exist_annotation = Annotation.objects.filter(
|
exists_property = SystemProperty.objects.filter(
|
||||||
uuid=file_json['uuid']
|
uuid=snap.uuid
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if exist_annotation:
|
if exists_property:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("The snapshot already exists"),
|
_("The snapshot already exists"),
|
||||||
code="duplicate_snapshot",
|
code="duplicate_snapshot",
|
||||||
)
|
)
|
||||||
|
|
||||||
#Catch any error and display it as Validation Error so the Form handles it
|
#Catch any error and display it as Validation Error so the Form handles it
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
@ -57,7 +58,9 @@ class UploadForm(forms.Form):
|
||||||
|
|
||||||
for ev in self.evidences:
|
for ev in self.evidences:
|
||||||
path_name = save_in_disk(ev[1], user.institution.name)
|
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)
|
move_json(path_name, user.institution.name)
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,9 +71,8 @@ class UserTagForm(forms.Form):
|
||||||
self.pk = None
|
self.pk = None
|
||||||
self.uuid = kwargs.pop('uuid', None)
|
self.uuid = kwargs.pop('uuid', None)
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
instance = Annotation.objects.filter(
|
instance = SystemProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
key='CUSTOM_ID',
|
key='CUSTOM_ID',
|
||||||
owner=self.user.institution
|
owner=self.user.institution
|
||||||
).first()
|
).first()
|
||||||
|
@ -86,9 +88,8 @@ class UserTagForm(forms.Form):
|
||||||
if not data:
|
if not data:
|
||||||
return False
|
return False
|
||||||
self.tag = data
|
self.tag = data
|
||||||
self.instance = Annotation.objects.filter(
|
self.instance = SystemProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
key='CUSTOM_ID',
|
key='CUSTOM_ID',
|
||||||
owner=self.user.institution
|
owner=self.user.institution
|
||||||
).first()
|
).first()
|
||||||
|
@ -100,20 +101,31 @@ class UserTagForm(forms.Form):
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.instance:
|
if self.instance:
|
||||||
|
old_value = self.instance.value
|
||||||
if not self.tag:
|
if not self.tag:
|
||||||
|
message =_("<Deleted> Evidence Tag. Old Value: '{}'").format(old_value)
|
||||||
self.instance.delete()
|
self.instance.delete()
|
||||||
self.instance.value = self.tag
|
else:
|
||||||
self.instance.save()
|
self.instance.value = self.tag
|
||||||
return
|
self.instance.save()
|
||||||
|
if old_value != self.tag:
|
||||||
Annotation.objects.create(
|
message=_("<Updated> Evidence Tag. Old Value: '{}'. New Value: '{}'").format(old_value, self.tag)
|
||||||
uuid=self.uuid,
|
else:
|
||||||
type=Annotation.Type.SYSTEM,
|
message =_("<Created> Evidence Tag. Value: '{}'").format(self.tag)
|
||||||
key='CUSTOM_ID',
|
SystemProperty.objects.create(
|
||||||
value=self.tag,
|
uuid=self.uuid,
|
||||||
owner=self.user.institution,
|
key='CUSTOM_ID',
|
||||||
user=self.user
|
value=self.tag,
|
||||||
)
|
owner=self.user.institution,
|
||||||
|
user=self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=self.uuid,
|
||||||
|
event= message,
|
||||||
|
user=self.user,
|
||||||
|
institution=self.user.institution
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ImportForm(forms.Form):
|
class ImportForm(forms.Form):
|
||||||
|
@ -164,8 +176,8 @@ class ImportForm(forms.Form):
|
||||||
table = []
|
table = []
|
||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
doc = create_doc(row)
|
doc = create_doc(row)
|
||||||
annotation = create_annotation(doc, self.user)
|
property = create_property(doc, self.user)
|
||||||
table.append((doc, annotation))
|
table.append((doc, property))
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
for doc, cred in table:
|
for doc, cred in table:
|
||||||
|
@ -186,9 +198,9 @@ class EraseServerForm(forms.Form):
|
||||||
self.pk = None
|
self.pk = None
|
||||||
self.uuid = kwargs.pop('uuid', None)
|
self.uuid = kwargs.pop('uuid', None)
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
instance = Annotation.objects.filter(
|
instance = UserProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.ERASE_SERVER,
|
type=UserProperty.Type.ERASE_SERVER,
|
||||||
key='ERASE_SERVER',
|
key='ERASE_SERVER',
|
||||||
owner=self.user.institution
|
owner=self.user.institution
|
||||||
).first()
|
).first()
|
||||||
|
@ -201,9 +213,9 @@ class EraseServerForm(forms.Form):
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.erase_server = self.cleaned_data.get('erase_server', False)
|
self.erase_server = self.cleaned_data.get('erase_server', False)
|
||||||
self.instance = Annotation.objects.filter(
|
self.instance = UserProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.ERASE_SERVER,
|
type=UserProperty.Type.ERASE_SERVER,
|
||||||
key='ERASE_SERVER',
|
key='ERASE_SERVER',
|
||||||
owner=self.user.institution
|
owner=self.user.institution
|
||||||
).first()
|
).first()
|
||||||
|
@ -221,10 +233,10 @@ class EraseServerForm(forms.Form):
|
||||||
|
|
||||||
if self.instance:
|
if self.instance:
|
||||||
return
|
return
|
||||||
|
|
||||||
Annotation.objects.create(
|
UserProperty.objects.create(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.ERASE_SERVER,
|
type=UserProperty.Type.ERASE_SERVER,
|
||||||
key='ERASE_SERVER',
|
key='ERASE_SERVER',
|
||||||
value=self.erase_server,
|
value=self.erase_server,
|
||||||
owner=self.user.institution,
|
owner=self.user.institution,
|
||||||
|
|
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)
|
|
@ -5,7 +5,7 @@ import logging
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from utils.device import create_annotation, create_doc, create_index
|
from utils.device import create_property, create_doc, create_index
|
||||||
from user.models import Institution
|
from user.models import Institution
|
||||||
from evidence.parse import Build
|
from evidence.parse import Build
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class Command(BaseCommand):
|
||||||
def build_placeholder(self, s, user, f_path):
|
def build_placeholder(self, s, user, f_path):
|
||||||
try:
|
try:
|
||||||
create_index(s, user)
|
create_index(s, user)
|
||||||
create_annotation(s, user, commit=True)
|
create_property(s, user, commit=True)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
txt = "In placeholder %s \n%s"
|
txt = "In placeholder %s \n%s"
|
||||||
logger.warning(txt, f_path, err)
|
logger.warning(txt, f_path, err)
|
||||||
|
|
|
@ -4,7 +4,6 @@ import logging
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from utils.save_snapshots import move_json, save_in_disk
|
from utils.save_snapshots import move_json, save_in_disk
|
||||||
from evidence.parse import Build
|
from evidence.parse import Build
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-10 19:37
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("evidence", "0002_alter_annotation_type"),
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SystemProperty",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("key", models.CharField(max_length=256)),
|
||||||
|
("value", models.CharField(max_length=256)),
|
||||||
|
("uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserProperty",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("key", models.CharField(max_length=256)),
|
||||||
|
("value", models.CharField(max_length=256)),
|
||||||
|
("uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.SmallIntegerField(
|
||||||
|
choices=[(1, "User"), (2, "Document"), (3, "EraseServer")],
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="Annotation",
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="systemproperty",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("key", "uuid"), name="system_unique_type_key_uuid"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="userproperty",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("key", "uuid", "type"), name="user_unique_type_key_uuid"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-18 12:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("evidence", "0003_systemproperty_userproperty_delete_annotation_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="userproperty",
|
||||||
|
name="user_unique_type_key_uuid",
|
||||||
|
),
|
||||||
|
]
|
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'),
|
||||||
|
),
|
||||||
|
]
|
60
evidence/mixin_parse.py
Normal file
60
evidence/mixin_parse.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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.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)
|
||||||
|
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
|
|
@ -1,34 +1,54 @@
|
||||||
import json
|
import json
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from dmidecode import DMIParse
|
from dmidecode import DMIParse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
from utils.constants import STR_EXTEND_SIZE, CHASSIS_DH
|
from utils.constants import STR_EXTEND_SIZE, CHASSIS_DH
|
||||||
from evidence.xapian import search
|
from evidence.xapian import search
|
||||||
from evidence.parse_details import ParseSnapshot
|
from evidence.parse_details import ParseSnapshot
|
||||||
|
from evidence.normal_parse_details import get_inxi, get_inxi_key
|
||||||
from user.models import User, Institution
|
from user.models import User, Institution
|
||||||
|
|
||||||
|
|
||||||
class Annotation(models.Model):
|
class Property(models.Model):
|
||||||
class Type(models.IntegerChoices):
|
|
||||||
SYSTEM = 0, "System"
|
|
||||||
USER = 1, "User"
|
|
||||||
DOCUMENT = 2, "Document"
|
|
||||||
ERASE_SERVER = 3, "EraseServer"
|
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
uuid = models.UUIDField()
|
|
||||||
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User, on_delete=models.SET_NULL, null=True, blank=True)
|
User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
type = models.SmallIntegerField(choices=Type)
|
|
||||||
key = models.CharField(max_length=STR_EXTEND_SIZE)
|
key = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||||
value = models.CharField(max_length=STR_EXTEND_SIZE)
|
value = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
#Only for shared behaviour, it is not a table
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class SystemProperty(Property):
|
||||||
|
uuid = models.UUIDField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=["type", "key", "uuid"], name="unique_type_key_uuid")
|
fields=["key", "uuid"], name="system_unique_type_key_uuid")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UserProperty(Property):
|
||||||
|
|
||||||
|
class Type(models.IntegerChoices):
|
||||||
|
USER = 1, "User"
|
||||||
|
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")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,29 +59,39 @@ class Evidence:
|
||||||
self.doc = None
|
self.doc = None
|
||||||
self.created = None
|
self.created = None
|
||||||
self.dmi = None
|
self.dmi = None
|
||||||
self.annotations = []
|
self.inxi = None
|
||||||
|
self.properties = []
|
||||||
self.components = []
|
self.components = []
|
||||||
self.default = "n/a"
|
self.default = "n/a"
|
||||||
|
|
||||||
self.get_owner()
|
self.get_owner()
|
||||||
self.get_time()
|
self.get_time()
|
||||||
|
|
||||||
def get_annotations(self):
|
def get_properties(self):
|
||||||
self.annotations = Annotation.objects.filter(
|
self.properties = SystemProperty.objects.filter(
|
||||||
uuid=self.uuid
|
uuid=self.uuid
|
||||||
).order_by("created")
|
).order_by("created")
|
||||||
|
|
||||||
def get_owner(self):
|
def get_owner(self):
|
||||||
if not self.annotations:
|
if not self.properties:
|
||||||
self.get_annotations()
|
self.get_properties()
|
||||||
a = self.annotations.first()
|
a = self.properties.first()
|
||||||
if a:
|
if a:
|
||||||
self.owner = a.owner
|
self.owner = a.owner
|
||||||
|
|
||||||
|
def get_phid(self):
|
||||||
|
if not self.doc:
|
||||||
|
self.get_doc()
|
||||||
|
|
||||||
|
return hashlib.sha3_256(json.dumps(self.doc)).hexdigest()
|
||||||
|
|
||||||
def get_doc(self):
|
def get_doc(self):
|
||||||
self.doc = {}
|
self.doc = {}
|
||||||
|
self.inxi = None
|
||||||
|
|
||||||
if not self.owner:
|
if not self.owner:
|
||||||
self.get_owner()
|
self.get_owner()
|
||||||
|
|
||||||
qry = 'uuid:"{}"'.format(self.uuid)
|
qry = 'uuid:"{}"'.format(self.uuid)
|
||||||
matches = search(self.owner, qry, limit=1)
|
matches = search(self.owner, qry, limit=1)
|
||||||
if matches and matches.size() < 0:
|
if matches and matches.size() < 0:
|
||||||
|
@ -70,9 +100,36 @@ class Evidence:
|
||||||
for xa in matches:
|
for xa in matches:
|
||||||
self.doc = json.loads(xa.document.get_data())
|
self.doc = json.loads(xa.document.get_data())
|
||||||
|
|
||||||
if not self.is_legacy():
|
if self.is_legacy():
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.doc.get("credentialSubject"):
|
||||||
|
for ev in self.doc["evidence"]:
|
||||||
|
if "dmidecode" == ev.get("operation"):
|
||||||
|
dmidecode_raw = ev["output"]
|
||||||
|
if "inxi" == ev.get("operation"):
|
||||||
|
self.inxi = ev["output"]
|
||||||
|
else:
|
||||||
dmidecode_raw = self.doc["data"]["dmidecode"]
|
dmidecode_raw = self.doc["data"]["dmidecode"]
|
||||||
|
inxi_raw = self.doc.get("data", {}).get("inxi")
|
||||||
self.dmi = DMIParse(dmidecode_raw)
|
self.dmi = DMIParse(dmidecode_raw)
|
||||||
|
try:
|
||||||
|
self.inxi = json.loads(inxi_raw)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if self.inxi:
|
||||||
|
try:
|
||||||
|
machine = get_inxi_key(self.inxi, 'Machine')
|
||||||
|
for m in machine:
|
||||||
|
system = get_inxi(m, "System")
|
||||||
|
if system:
|
||||||
|
self.device_manufacturer = system
|
||||||
|
self.device_model = get_inxi(m, "product")
|
||||||
|
self.device_serial_number = get_inxi(m, "serial")
|
||||||
|
self.device_chassis = get_inxi(m, "Type")
|
||||||
|
self.device_version = get_inxi(m, "v")
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
def get_time(self):
|
def get_time(self):
|
||||||
if not self.doc:
|
if not self.doc:
|
||||||
|
@ -80,7 +137,7 @@ class Evidence:
|
||||||
self.created = self.doc.get("endTime")
|
self.created = self.doc.get("endTime")
|
||||||
|
|
||||||
if not self.created:
|
if not self.created:
|
||||||
self.created = self.annotations.last().created
|
self.created = self.properties.last().created
|
||||||
|
|
||||||
def get_components(self):
|
def get_components(self):
|
||||||
if self.is_legacy():
|
if self.is_legacy():
|
||||||
|
@ -96,7 +153,10 @@ class Evidence:
|
||||||
return list(self.doc.get('kv').values())[0]
|
return list(self.doc.get('kv').values())[0]
|
||||||
|
|
||||||
if self.is_legacy():
|
if self.is_legacy():
|
||||||
return self.doc['device']['manufacturer']
|
return self.doc.get('device', {}).get('manufacturer', '')
|
||||||
|
|
||||||
|
if self.inxi:
|
||||||
|
return self.device_manufacturer
|
||||||
|
|
||||||
return self.dmi.manufacturer().strip()
|
return self.dmi.manufacturer().strip()
|
||||||
|
|
||||||
|
@ -108,13 +168,19 @@ class Evidence:
|
||||||
return list(self.doc.get('kv').values())[1]
|
return list(self.doc.get('kv').values())[1]
|
||||||
|
|
||||||
if self.is_legacy():
|
if self.is_legacy():
|
||||||
return self.doc['device']['model']
|
return self.doc.get('device', {}).get('model', '')
|
||||||
|
|
||||||
|
if self.inxi:
|
||||||
|
return self.device_model
|
||||||
|
|
||||||
return self.dmi.model().strip()
|
return self.dmi.model().strip()
|
||||||
|
|
||||||
def get_chassis(self):
|
def get_chassis(self):
|
||||||
if self.is_legacy():
|
if self.is_legacy():
|
||||||
return self.doc['device']['model']
|
return self.doc.get('device', {}).get('model', '')
|
||||||
|
|
||||||
|
if self.inxi:
|
||||||
|
return self.device_chassis
|
||||||
|
|
||||||
chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual')
|
chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual')
|
||||||
lower_type = chassis.lower()
|
lower_type = chassis.lower()
|
||||||
|
@ -126,23 +192,45 @@ class Evidence:
|
||||||
|
|
||||||
def get_serial_number(self):
|
def get_serial_number(self):
|
||||||
if self.is_legacy():
|
if self.is_legacy():
|
||||||
return self.doc['device']['serialNumber']
|
return self.doc.get('device', {}).get('serialNumber', '')
|
||||||
|
|
||||||
|
if self.inxi:
|
||||||
|
return self.device_serial_number
|
||||||
|
|
||||||
return self.dmi.serial_number().strip()
|
return self.dmi.serial_number().strip()
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
if self.inxi:
|
||||||
|
return self.device_version
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls, user):
|
def get_all(cls, user):
|
||||||
return Annotation.objects.filter(
|
return SystemProperty.objects.filter(
|
||||||
owner=user.institution,
|
owner=user.institution,
|
||||||
type=Annotation.Type.SYSTEM,
|
key="ereuse24",
|
||||||
key="hidalgo1",
|
|
||||||
).order_by("-created").values_list("uuid", "created").distinct()
|
).order_by("-created").values_list("uuid", "created").distinct()
|
||||||
|
|
||||||
def set_components(self):
|
def set_components(self):
|
||||||
snapshot = ParseSnapshot(self.doc).snapshot_json
|
self.components = ParseSnapshot(self.doc).components
|
||||||
self.components = snapshot['components']
|
|
||||||
|
|
||||||
def is_legacy(self):
|
def is_legacy(self):
|
||||||
|
if self.doc.get("credentialSubject"):
|
||||||
|
return False
|
||||||
|
|
||||||
return self.doc.get("software") != "workbench-script"
|
return self.doc.get("software") != "workbench-script"
|
||||||
|
|
||||||
def is_web_snapshot(self):
|
def is_web_snapshot(self):
|
||||||
return self.doc.get("type") == "WebSnapshot"
|
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(":", "/")
|
||||||
|
)
|
||||||
|
|
71
evidence/normal_parse.py
Normal file
71
evidence/normal_parse.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
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")
|
||||||
|
networks = [(nets[i], nets[i + 1]) for i in range(0, len(nets) - 1, 2)]
|
||||||
|
|
||||||
|
for n, iface in networks:
|
||||||
|
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)
|
402
evidence/normal_parse_details.py
Normal file
402
evidence/normal_parse_details.py
Normal file
|
@ -0,0 +1,402 @@
|
||||||
|
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 []
|
||||||
|
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)
|
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,130 +2,94 @@ import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dmidecode import DMIParse
|
from evidence import legacy_parse
|
||||||
from json_repair import repair_json
|
from evidence import old_parse
|
||||||
from evidence.parse_details import get_lshw_child
|
from evidence import normal_parse
|
||||||
|
from evidence.parse_details import ParseSnapshot
|
||||||
|
|
||||||
from evidence.models import Annotation
|
from evidence.models import SystemProperty
|
||||||
from evidence.xapian import index
|
from evidence.xapian import index
|
||||||
from utils.constants import CHASSIS_DH
|
from evidence.normal_parse_details import get_inxi_key, get_inxi
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
if settings.DPP:
|
||||||
|
from dpp.api_dlt import register_device_dlt, register_passport_dlt
|
||||||
|
|
||||||
logger = logging.getLogger('django')
|
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
|
|
||||||
|
|
||||||
|
def get_mac(inxi):
|
||||||
|
nets = get_inxi_key(inxi, "Network")
|
||||||
|
networks = [(nets[i], nets[i + 1]) for i in range(0, len(nets) - 1, 2)]
|
||||||
|
|
||||||
|
for n, iface in networks:
|
||||||
|
if get_inxi(n, "port"):
|
||||||
|
return get_inxi(iface, 'mac')
|
||||||
|
|
||||||
class Build:
|
class Build:
|
||||||
def __init__(self, evidence_json, user, check=False):
|
def __init__(self, evidence_json, user, check=False):
|
||||||
self.json = evidence_json
|
"""
|
||||||
self.uuid = self.json['uuid']
|
This Build do the save in xapian as document, in Annotations and do
|
||||||
|
register in dlt if is configured for that.
|
||||||
|
|
||||||
|
We have 4 cases for parser diferents snapshots than come from workbench.
|
||||||
|
1) worbench 11 is old_parse.
|
||||||
|
2) legacy is the worbench-script when create a snapshot for devicehub-teal
|
||||||
|
3) some snapshots come as a credential. In this case is parsed as normal_parse
|
||||||
|
4) normal snapshot from worbench-script is the most basic and is parsed as normal_parse
|
||||||
|
"""
|
||||||
|
self.evidence = evidence_json.copy()
|
||||||
|
self.uuid = self.evidence.get('uuid')
|
||||||
self.user = user
|
self.user = user
|
||||||
self.hid = None
|
|
||||||
self.generate_chids()
|
if evidence_json.get("credentialSubject"):
|
||||||
|
self.build = normal_parse.Build(evidence_json)
|
||||||
|
self.uuid = evidence_json.get("credentialSubject", {}).get("uuid")
|
||||||
|
elif evidence_json.get("software") != "workbench-script":
|
||||||
|
self.build = old_parse.Build(evidence_json)
|
||||||
|
elif evidence_json.get("data",{}).get("lshw"):
|
||||||
|
self.build = legacy_parse.Build(evidence_json)
|
||||||
|
else:
|
||||||
|
self.build = normal_parse.Build(evidence_json)
|
||||||
|
|
||||||
if check:
|
if check:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.index()
|
self.index()
|
||||||
self.create_annotations()
|
self.create_annotations()
|
||||||
|
if settings.DPP:
|
||||||
|
self.register_device_dlt()
|
||||||
|
|
||||||
def index(self):
|
def index(self):
|
||||||
snap = json.dumps(self.json)
|
snap = json.dumps(self.evidence)
|
||||||
index(self.user.institution, self.uuid, snap)
|
index(self.user.institution, self.uuid, snap)
|
||||||
|
|
||||||
def generate_chids(self):
|
|
||||||
self.algorithms = {
|
|
||||||
'hidalgo1': self.get_hid_14(),
|
|
||||||
}
|
|
||||||
|
|
||||||
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}"
|
|
||||||
|
|
||||||
|
|
||||||
return hashlib.sha3_256(hid.encode()).hexdigest()
|
|
||||||
|
|
||||||
def create_annotations(self):
|
def create_annotations(self):
|
||||||
annotation = Annotation.objects.filter(
|
prop = SystemProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
owner=self.user.institution,
|
owner=self.user.institution,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if annotation:
|
if prop:
|
||||||
txt = "Warning: Snapshot %s already registered (annotation exists)"
|
txt = "Warning: Snapshot %s already registered (annotation exists)"
|
||||||
logger.warning(txt, self.uuid)
|
logger.warning(txt, self.uuid)
|
||||||
return
|
return
|
||||||
|
|
||||||
for k, v in self.algorithms.items():
|
for k, v in self.build.algorithms.items():
|
||||||
Annotation.objects.create(
|
SystemProperty.objects.create(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
owner=self.user.institution,
|
owner=self.user.institution,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
key=k,
|
key=k,
|
||||||
value=v
|
value=self.sign(v)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_chassis_dh(self):
|
def sign(self, doc):
|
||||||
chassis = self.get_chassis()
|
return hashlib.sha3_256(doc.encode()).hexdigest()
|
||||||
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):
|
def register_device_dlt(self):
|
||||||
return self.dmi.get("System")[0].get("SKU Number", "n/a").strip()
|
legacy_dpp = self.build.algorithms.get('ereuse22')
|
||||||
|
chid = self.sign(legacy_dpp)
|
||||||
def get_chassis(self):
|
phid = self.sign(json.dumps(self.build.get_doc()))
|
||||||
return self.dmi.get("Chassis")[0].get("Type", '_virtual')
|
register_device_dlt(chid, phid, self.uuid, self.user)
|
||||||
|
register_passport_dlt(chid, phid, self.uuid, self.user)
|
||||||
def get_hid(self, snapshot):
|
|
||||||
dmidecode_raw = snapshot["data"]["dmidecode"]
|
|
||||||
self.dmi = DMIParse(dmidecode_raw)
|
|
||||||
|
|
||||||
manufacturer = self.dmi.manufacturer().strip()
|
|
||||||
model = self.dmi.model().strip()
|
|
||||||
chassis = self.get_chassis_dh()
|
|
||||||
serial_number = self.dmi.serial_number()
|
|
||||||
sku = self.get_sku()
|
|
||||||
|
|
||||||
if not snapshot["data"].get('lshw'):
|
|
||||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}"
|
|
||||||
|
|
||||||
lshw = snapshot["data"]["lshw"]
|
|
||||||
# mac = get_mac2(hwinfo_raw) or ""
|
|
||||||
mac = get_mac(lshw) or ""
|
|
||||||
if not mac:
|
|
||||||
txt = "Could not retrieve MAC address in snapshot %s"
|
|
||||||
logger.warning(txt, snapshot['uuid'])
|
|
||||||
|
|
||||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}"
|
|
||||||
|
|
|
@ -1,505 +1,38 @@
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from datetime import datetime
|
from evidence import (
|
||||||
from dmidecode import DMIParse
|
legacy_parse_details,
|
||||||
from json_repair import repair_json
|
normal_parse_details,
|
||||||
|
old_parse_details
|
||||||
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
|
)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('django')
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
|
||||||
def get_lshw_child(child, nets, component):
|
|
||||||
if child.get('id') == component:
|
|
||||||
nets.append(child)
|
|
||||||
if child.get('children'):
|
|
||||||
[get_lshw_child(x, nets, component) for x in child['children']]
|
|
||||||
|
|
||||||
|
|
||||||
class ParseSnapshot:
|
class ParseSnapshot:
|
||||||
def __init__(self, snapshot, default="n/a"):
|
def __init__(self, snapshot, default="n/a"):
|
||||||
self.default = default
|
if snapshot.get("credentialSubject"):
|
||||||
self.dmidecode_raw = snapshot["data"].get("dmidecode", "{}")
|
self.build = normal_parse_details.ParseSnapshot(
|
||||||
self.smart_raw = snapshot["data"].get("disks", [])
|
snapshot,
|
||||||
self.hwinfo_raw = snapshot["data"].get("hwinfo", "")
|
default=default
|
||||||
self.lshw_raw = snapshot["data"].get("lshw", {}) or {}
|
)
|
||||||
self.lscpi_raw = snapshot["data"].get("lspci", "")
|
elif snapshot.get("software") != "workbench-script":
|
||||||
self.device = {"actions": []}
|
self.build = old_parse_details.ParseSnapshot(
|
||||||
self.components = []
|
snapshot,
|
||||||
self.monitors = []
|
default=default
|
||||||
|
)
|
||||||
self.dmi = DMIParse(self.dmidecode_raw)
|
elif snapshot.get("data",{}).get("lshw"):
|
||||||
self.smart = self.loads(self.smart_raw)
|
self.build = legacy_parse_details.ParseSnapshot(
|
||||||
self.lshw = self.loads(self.lshw_raw)
|
snapshot,
|
||||||
self.hwinfo = self.parse_hwinfo()
|
default=default
|
||||||
|
)
|
||||||
self.set_computer()
|
else:
|
||||||
self.get_hwinfo_monitors()
|
self.build = normal_parse_details.ParseSnapshot(
|
||||||
self.set_components()
|
snapshot,
|
||||||
self.snapshot_json = {
|
default=default
|
||||||
"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": self.get_memory_video(c),
|
|
||||||
"manufacturer": c.get("vendor", self.default),
|
|
||||||
"model": c.get("product", self.default),
|
|
||||||
"serialNumber": c.get("serial", self.default),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_memory_video(self, c):
|
|
||||||
# get info of lspci
|
|
||||||
# pci_id = c['businfo'].split('@')[1]
|
|
||||||
# lspci.get(pci_id) | grep size
|
|
||||||
# lspci -v -s 00:02.0
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_data_storage(self):
|
|
||||||
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:
|
|
||||||
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
|
|
||||||
for ch in self.lshw.get('children', []):
|
|
||||||
for c in ch.get('children', []):
|
|
||||||
if c['class'] == 'processor':
|
|
||||||
return c.get('width', default)
|
|
||||||
return default
|
|
||||||
|
|
||||||
def get_usb_num(self):
|
|
||||||
return len(
|
|
||||||
[
|
|
||||||
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(err))
|
|
||||||
|
|
||||||
def get_data_storage_size(self, x):
|
|
||||||
return x.get('user_capacity', {}).get('bytes')
|
|
||||||
|
|
||||||
def parse_hwinfo(self):
|
|
||||||
hw_blocks = self.hwinfo_raw.split("\n\n")
|
|
||||||
return [x.split("\n") for x in hw_blocks]
|
|
||||||
|
|
||||||
def loads(self, x):
|
|
||||||
if isinstance(x, str):
|
|
||||||
try:
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
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)
|
|
|
@ -45,9 +45,8 @@
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% for snap in object.annotations %}
|
{% for snap in object.properties %}
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if snap.type == 0 %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{{ snap.key }}
|
{{ snap.key }}
|
||||||
|
@ -63,7 +62,6 @@
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -94,7 +92,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% if form.tag.value %}
|
{% if form.tag.value %}
|
||||||
<div class="col-1">
|
<div class="col-1">
|
||||||
<a class="btn btn-yellow" href="{% url 'evidence:delete_annotation' form.pk %}">{% translate "Delete" %}</a>
|
<a class="btn btn-yellow" href="{% url 'evidence:delete_tag' form.pk %}">{% translate "Delete" %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,5 +20,5 @@ urlpatterns = [
|
||||||
path("<uuid:pk>", views.EvidenceView.as_view(), name="details"),
|
path("<uuid:pk>", views.EvidenceView.as_view(), name="details"),
|
||||||
path("<uuid:pk>/eraseserver", views.EraseServerView.as_view(), name="erase_server"),
|
path("<uuid:pk>/eraseserver", views.EraseServerView.as_view(), name="erase_server"),
|
||||||
path("<uuid:pk>/download", views.DownloadEvidenceView.as_view(), name="download"),
|
path("<uuid:pk>/download", views.DownloadEvidenceView.as_view(), name="download"),
|
||||||
path('annotation/<int:pk>/del', views.AnnotationDeleteView.as_view(), name='delete_annotation'),
|
path("tag/<str:pk>/delete", views.DeleteEvidenceTagView.as_view(), name="delete_tag"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,8 +12,9 @@ from django.views.generic.edit import (
|
||||||
FormView,
|
FormView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from action.models import DeviceLog
|
||||||
from dashboard.mixins import DashboardView, Http403
|
from dashboard.mixins import DashboardView, Http403
|
||||||
from evidence.models import Evidence, Annotation
|
from evidence.models import SystemProperty, UserProperty, Evidence
|
||||||
from evidence.forms import (
|
from evidence.forms import (
|
||||||
UploadForm,
|
UploadForm,
|
||||||
UserTagForm,
|
UserTagForm,
|
||||||
|
@ -95,7 +96,7 @@ class EvidenceView(DashboardView, FormView):
|
||||||
if self.object.owner != self.request.user.institution:
|
if self.object.owner != self.request.user.institution:
|
||||||
raise Http403
|
raise Http403
|
||||||
|
|
||||||
self.object.get_annotations()
|
self.object.get_properties()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -137,37 +138,10 @@ class DownloadEvidenceView(DashboardView, TemplateView):
|
||||||
evidence.get_doc()
|
evidence.get_doc()
|
||||||
data = json.dumps(evidence.doc)
|
data = json.dumps(evidence.doc)
|
||||||
response = HttpResponse(data, content_type="application/json")
|
response = HttpResponse(data, content_type="application/json")
|
||||||
response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json")
|
response['Content-Disposition'] = 'attachment; filename={}'.format("evidence.json")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class AnnotationDeleteView(DashboardView, DeleteView):
|
|
||||||
model = Annotation
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
self.pk = kwargs['pk']
|
|
||||||
|
|
||||||
try:
|
|
||||||
referer = self.request.META["HTTP_REFERER"]
|
|
||||||
path_referer = urlparse(referer).path
|
|
||||||
resolver_match = resolve(path_referer)
|
|
||||||
url_name = resolver_match.view_name
|
|
||||||
kwargs_view = resolver_match.kwargs
|
|
||||||
except:
|
|
||||||
# if is not possible resolve the reference path return 404
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
self.object = get_object_or_404(
|
|
||||||
self.model,
|
|
||||||
pk=self.pk,
|
|
||||||
owner=self.request.user.institution
|
|
||||||
)
|
|
||||||
self.object.delete()
|
|
||||||
|
|
||||||
|
|
||||||
return redirect(url_name, **kwargs_view)
|
|
||||||
|
|
||||||
|
|
||||||
class EraseServerView(DashboardView, FormView):
|
class EraseServerView(DashboardView, FormView):
|
||||||
template_name = "ev_eraseserver.html"
|
template_name = "ev_eraseserver.html"
|
||||||
section = "evidences"
|
section = "evidences"
|
||||||
|
@ -182,7 +156,7 @@ class EraseServerView(DashboardView, FormView):
|
||||||
if self.object.owner != self.request.user.institution:
|
if self.object.owner != self.request.user.institution:
|
||||||
raise Http403
|
raise Http403
|
||||||
|
|
||||||
self.object.get_annotations()
|
self.object.get_properties()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -211,3 +185,35 @@ class EraseServerView(DashboardView, FormView):
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
success_url = reverse_lazy('evidence:details', args=[self.pk])
|
success_url = reverse_lazy('evidence:details', args=[self.pk])
|
||||||
return success_url
|
return success_url
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteEvidenceTagView(DashboardView, DeleteView):
|
||||||
|
model = SystemProperty
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# only those with 'CUSTOM_ID'
|
||||||
|
return SystemProperty.objects.filter(owner=self.request.user.institution, key='CUSTOM_ID')
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
|
||||||
|
message = _("<Deleted> Evidence Tag: {}").format(self.object.value)
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=self.object.uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution
|
||||||
|
)
|
||||||
|
self.object.delete()
|
||||||
|
|
||||||
|
messages.info(self.request, _("Evicende Tag deleted successfully."))
|
||||||
|
return self.handle_success()
|
||||||
|
|
||||||
|
def handle_success(self):
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.META.get(
|
||||||
|
'HTTP_REFERER',
|
||||||
|
reverse_lazy('evidence:details', args=[self.object.uuid])
|
||||||
|
)
|
||||||
|
|
|
@ -22,10 +22,14 @@ def search(institution, qs, offset=0, limit=10):
|
||||||
qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
|
qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
|
||||||
qp.add_prefix("uuid", "uuid")
|
qp.add_prefix("uuid", "uuid")
|
||||||
query = qp.parse_query(qs)
|
query = qp.parse_query(qs)
|
||||||
institution_term = "U{}".format(institution.id)
|
if institution:
|
||||||
final_query = xapian.Query(
|
institution_term = "U{}".format(institution.id)
|
||||||
xapian.Query.OP_AND, query, xapian.Query(institution_term)
|
final_query = xapian.Query(
|
||||||
)
|
xapian.Query.OP_AND, query, xapian.Query(institution_term)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
final_query = xapian.Query(query)
|
||||||
|
|
||||||
enquire = xapian.Enquire(database)
|
enquire = xapian.Enquire(database)
|
||||||
enquire.set_query(final_query)
|
enquire.set_query(final_query)
|
||||||
matches = enquire.get_mset(offset, limit)
|
matches = enquire.get_mset(offset, limit)
|
||||||
|
|
File diff suppressed because one or more lines are too long
1
example/dpp-snapshots/hp_probook_450.json
Normal file
1
example/dpp-snapshots/hp_probook_450.json
Normal file
File diff suppressed because one or more lines are too long
1
example/dpp-snapshots/hp_probook_450_2.json
Normal file
1
example/dpp-snapshots/hp_probook_450_2.json
Normal file
File diff suppressed because one or more lines are too long
1
example/dpp-snapshots/hp_probook_g2.json
Normal file
1
example/dpp-snapshots/hp_probook_g2.json
Normal file
File diff suppressed because one or more lines are too long
1
example/dpp-snapshots/hp_probook_g8.json
Normal file
1
example/dpp-snapshots/hp_probook_g8.json
Normal file
File diff suppressed because one or more lines are too long
1
example/dpp-snapshots/snapshot01.json
Normal file
1
example/dpp-snapshots/snapshot01.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"closed": true, "components": [{"actions": [], "manufacturer": "Intel Corporation", "model": "82579LM Gigabit Network Connection", "serialNumber": "00:11:11:11:11:00", "speed": 1000.0, "type": "NetworkAdapter", "variant": "04", "wireless": false}, {"actions": [], "manufacturer": "Intel Corporation", "model": "7 Series/C216 Chipset Family High Definition Audio Controller", "serialNumber": null, "type": "SoundCard"}, {"actions": [], "format": "DIMM", "interface": "DDR3", "manufacturer": "Micron", "model": "16KTF51264AZ", "serialNumber": "AAAAAAAA", "size": 4096.0, "speed": 1600.0, "type": "RamModule"}, {"actions": [{"endTime": "2022-10-11T13:45:31.239555+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.623967+00:00", "steps": [{"endTime": "2021-10-11T11:05:28.090897+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.624163+00:00", "type": "StepZero"}, {"endTime": "2021-10-11T13:45:31.239402+00:00", "severity": "Info", "startTime": "2021-10-11T11:05:28.091255+00:00", "type": "StepRandom"}], "type": "EraseSectors"}, {"assessment": true, "commandTimeout": 30, "currentPendingSectorCount": 0, "elapsed": 60, "length": "Short", "lifetime": 18720, "offlineUncorrectable": 0, "powerCycleCount": 2147, "reallocatedSectorCount": 0, "reportedUncorrectableErrors": 0, "severity": "Info", "status": "Completed without error", "type": "TestDataStorage"}, {"elapsed": 11, "readSpeed": 119.0, "type": "BenchmarkDataStorage", "writeSpeed": 32.7}], "interface": "ATA", "manufacturer": "Seagate", "model": "ST3500418AS", "serialNumber": "AAAAAAAA", "size": 500000.0, "type": "HardDrive", "variant": "CC46"}, {"actions": [{"elapsed": 0, "rate": 25540.36, "type": "BenchmarkProcessor"}, {"elapsed": 8, "rate": 7.6939, "type": "BenchmarkProcessorSysbench"}], "address": 64, "brand": "Core i5", "cores": 4, "generation": 3, "manufacturer": "Intel Corp.", "model": "Intel Core i5-3470 CPU @ 3.20GHz", "serialNumber": null, "speed": 1.6242180000000002, "threads": 4, "type": "Processor"}, {"actions": [], "manufacturer": "Intel Corporation", "memory": null, "model": "Xeon E3-1200 v2/3rd Gen Core processor Graphics Controller", "serialNumber": null, "type": "GraphicCard"}, {"actions": [], "biosDate": "2012-08-07T00:00:00", "firewire": 0, "manufacturer": "LENOVO", "model": "MAHOBAY", "pcmcia": 0, "ramMaxSize": 32, "ramSlots": 4, "serial": 1, "serialNumber": null, "slots": 4, "type": "Motherboard", "usb": 3, "version": "9SKT39AUS"}], "device": {"actions": [{"elapsed": 1, "rate": 0.6507, "type": "BenchmarkRamSysbench"}], "chassis": "Tower", "manufacturer": "LENOVO", "model": "3227A2G", "serialNumber": "AAAAAAAA", "sku": "LENOVO_MT_3227", "type": "Desktop", "version": "ThinkCentre M92P"}, "elapsed": 187302510, "endTime": "2016-11-03T17:17:01.116554+00:00", "software": "Workbench", "type": "Snapshot", "uuid": "ae913de1-e639-476a-ad9b-78eabbe4628b", "version": "11.0b11"}
|
1
example/dpp-snapshots/snapshot02.json
Normal file
1
example/dpp-snapshots/snapshot02.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"closed": true, "components": [{"actions": [], "manufacturer": "Intel Corporation", "model": "82579LM Gigabit Network Connection", "serialNumber": "00:11:11:11:11:00", "speed": 1000.0, "type": "NetworkAdapter", "variant": "04", "wireless": false}, {"actions": [], "manufacturer": "Intel Corporation", "model": "7 Series/C216 Chipset Family High Definition Audio Controller", "serialNumber": null, "type": "SoundCard"}, {"actions": [], "format": "DIMM", "interface": "DDR3", "manufacturer": "Micron", "model": "16KTF51264AZ", "serialNumber": "AAAAAAAA", "size": 4096.0, "speed": 1600.0, "type": "RamModule"}, {"actions": [{"endTime": "2022-10-11T13:45:31.239555+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.623967+00:00", "steps": [{"endTime": "2021-10-11T11:05:28.090897+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.624163+00:00", "type": "StepZero"}, {"endTime": "2021-10-11T13:45:31.239402+00:00", "severity": "Info", "startTime": "2021-10-11T11:05:28.091255+00:00", "type": "StepRandom"}], "type": "EraseSectors"}, {"assessment": true, "commandTimeout": 30, "currentPendingSectorCount": 0, "elapsed": 60, "length": "Short", "lifetime": 18720, "offlineUncorrectable": 0, "powerCycleCount": 2147, "reallocatedSectorCount": 0, "reportedUncorrectableErrors": 0, "severity": "Info", "status": "Completed without error", "type": "TestDataStorage"}, {"elapsed": 11, "readSpeed": 119.0, "type": "BenchmarkDataStorage", "writeSpeed": 32.7}], "interface": "ATA", "manufacturer": "Seagate", "model": "ST3500418AS", "serialNumber": "AAAAAAAA", "size": 500000.0, "type": "HardDrive", "variant": "CC46"}, {"actions": [{"elapsed": 0, "rate": 25540.36, "type": "BenchmarkProcessor"}, {"elapsed": 8, "rate": 7.6939, "type": "BenchmarkProcessorSysbench"}], "address": 64, "brand": "Core i5", "cores": 4, "generation": 3, "manufacturer": "Intel Corp.", "model": "Intel Core i5-3470 CPU @ 3.20GHz", "serialNumber": null, "speed": 1.6242180000000002, "threads": 4, "type": "Processor"}, {"actions": [], "manufacturer": "Intel Corporation", "memory": null, "model": "Xeon E3-1200 v2/3rd Gen Core processor Graphics Controller", "serialNumber": null, "type": "GraphicCard"}, {"actions": [], "biosDate": "2012-08-07T00:00:00", "firewire": 0, "manufacturer": "LENOVO", "model": "MAHOBAY", "pcmcia": 0, "ramMaxSize": 32, "ramSlots": 4, "serial": 1, "serialNumber": null, "slots": 4, "type": "Motherboard", "usb": 3, "version": "9SKT39AUS"}], "device": {"actions": [{"elapsed": 1, "rate": 0.6507, "type": "BenchmarkRamSysbench"}], "chassis": "Tower", "manufacturer": "LENOVO", "model": "3227A2G", "serialNumber": "AAAAAAAAD", "sku": "LENOVO_MT_3227", "type": "Desktop", "version": "ThinkCentre M92P"}, "elapsed": 187302510, "endTime": "2016-11-03T17:17:01.116554+00:00", "software": "Workbench", "type": "Snapshot", "uuid": "ae913de1-e639-476a-ad9b-78eabbe4625b", "version": "11.0b11"}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue