Compare commits

..

69 Commits

Author SHA1 Message Date
pedro a67fda6b51 better printing of DOMAIN var
when in settings, any command prints again DOMAIN which is boring and
inefficient
2024-11-05 04:01:44 +01:00
pedro 79a34c9b55 logger: always do traceback when DEBUG var is True
related to #13
2024-11-05 03:43:18 +01:00
Cayo Puigdefabregas e4124fb20b fix duplicate logs 2024-10-31 15:25:38 +01:00
pedro 517c3eb0c0 DEBUG false -> true
we are not ready to deploy without DEBUG

- collect static is not configured
- current demo in debug helps to find problems easily
2024-10-31 15:04:28 +01:00
pedro d4f50961bc improve logging text for certain messages 2024-10-31 14:24:16 +01:00
Cayo Puigdefabregas 65bd88a2a2 table in evidence page details 2024-10-31 13:15:56 +01:00
Cayo Puigdefabregas 7926943947 remove dashboard.js in login template 2024-10-31 12:51:49 +01:00
pedro 9de7dc6647 docker: disable debug by default 2024-10-31 11:57:33 +01:00
Cayo Puigdefabregas 16ba03bd0a fix 2024-10-31 10:40:53 +01:00
Cayo Puigdefabregas 4b3471d24e fix bug 2024-10-31 10:24:15 +01:00
Cayo Puigdefabregas e74ddc47a7 extract logs with colors and depending of DEBUG var 2024-10-31 10:14:02 +01:00
cayop ac1786c115 Merge pull request 'feature/90-implement-public-website-for-device' (#17) from feature/90-implement-public-website-for-device into main
Reviewed-on: https://gitea.pangea.org/ereuse/devicehub-django/pulls/17
2024-10-30 15:02:18 +00:00
Cayo Puigdefabregas cdcb78c433 Merge branch 'main' into feature/90-implement-public-website-for-device 2024-10-30 16:01:06 +01:00
Cayo Puigdefabregas ca0292aad9 fix some bugs 2024-10-30 16:00:23 +01:00
Cayo Puigdefabregas 2636e80ece error in build but not lock the flow 2024-10-30 15:17:15 +01:00
Cayo Puigdefabregas a5de0a92d8 add ev eraseserver template 2024-10-30 12:34:14 +01:00
sergio_gimenez 220d718d04 Add unit tests 2024-10-29 08:30:33 +01:00
sergio_gimenez 3a6c047bd1 Properly close div 2024-10-29 08:30:17 +01:00
sergio_gimenez fcc23cc656 Add space in copyrigight 2024-10-29 08:30:06 +01:00
sergio_gimenez cd666fa1ff Add consistent margin and add translations 2024-10-29 08:29:13 +01:00
sergio_gimenez 04390b76da Use nav-item instead of nav-items 2024-10-29 08:25:01 +01:00
sergio_gimenez eecde7ffaa This retrieves evidences in details view 2024-10-29 08:23:43 +01:00
sergio_gimenez cbfee93120 Not using old tests.py 2024-10-29 08:23:26 +01:00
sergio_gimenez 49b3931ac9 Merge branch 'main' into feature/90-implement-public-website-for-device 2024-10-29 08:07:31 +01:00
cayop e65dffe4d1 Merge pull request 'erase_server' (#16) from erase_server into main
Reviewed-on: #16
2024-10-28 17:11:15 +00:00
Cayo Puigdefabregas 356dd590d2 fix duplicates in search view 2024-10-28 18:09:45 +01:00
Cayo Puigdefabregas b2cda37dd0 add erase server definition 2024-10-28 17:52:34 +01:00
sergio_gimenez 23c0b004a0 Merge branch 'main' into feature/90-implement-public-website-for-device 2024-10-28 07:53:19 +01:00
Cayo Puigdefabregas fcb07a7337 Add reindex from snapshots files 2024-10-25 17:36:13 +02:00
cayop 1a2f1249ff Merge pull request 'api_v1' (#15) from api_v1 into main
Reviewed-on: #15
2024-10-24 10:00:10 +00:00
Cayo Puigdefabregas 31ed050718 endpoint for insert annotations from api 2024-10-24 11:53:37 +02:00
sergio_gimenez 29bc090d3a Remove duplicate code due to wron merge 2024-10-24 08:55:58 +02:00
Cayo Puigdefabregas 8308a313e3 fix 2024-10-23 13:42:09 +02:00
Cayo Puigdefabregas a2b5415149 response details of device with annotations from api 2024-10-23 13:39:16 +02:00
Cayo Puigdefabregas 9b96955c30 change def for views in api views 2024-10-23 09:23:45 +02:00
sergio_gimenez b1e45bd94e Merge branch 'main' into feature/90-implement-public-website-for-device 2024-10-23 08:54:12 +02:00
sergio_gimenez 42cf720613 Add public data 2024-10-23 08:47:40 +02:00
sergio_gimenez fd2adfa3a5 Remove wrong snapshot rendering 2024-10-23 08:39:18 +02:00
sergio_gimenez 64cf6bad17 Remove wrong parsing 2024-10-23 08:38:54 +02:00
Cayo Puigdefabregas 0f8815eb43 more logs for errors 500 in server side 2024-10-22 10:57:06 +02:00
Cayo Puigdefabregas ba94a01062 remove quotas in string of token fixed more 2024-10-22 10:09:53 +02:00
Cayo Puigdefabregas ff92e28b97 remove quotas in string of token 2024-10-22 10:01:29 +02:00
Cayo Puigdefabregas 9c78ff6c1b add v1 in url of api 2024-10-22 10:00:34 +02:00
sergio_gimenez 380c312c69 Update urls according to new view name 2024-10-22 09:19:16 +02:00
sergio_gimenez d49cc44b4c Make class view more descriptive 2024-10-22 09:18:59 +02:00
sergio_gimenez 1ec7a69c06 Fix title 2024-10-22 09:18:46 +02:00
sergio_gimenez a295896b33 Initial json response when GETting with json header 2024-10-22 08:20:26 +02:00
sergio_gimenez a9137dbacf Remove unneccessary stuff from view 2024-10-22 08:15:17 +02:00
sergio_gimenez dc5671c97f Make public web work for both legacy and new snapshot 2024-10-22 08:10:37 +02:00
sergio_gimenez 4c00ac8263 Fix url in device details template 2024-10-22 08:10:09 +02:00
sergio_gimenez 5d1513b300 Update breadcrumb 2024-10-22 07:35:47 +02:00
sergio_gimenez b65056ad92 Add "Public" to url for public website 2024-10-22 07:34:44 +02:00
sergio_gimenez 54f3bcd7f3 Fix details template 2024-10-22 07:24:42 +02:00
cayop 78ba2387ec Merge pull request 'hours' (#14) from hours into main
Reviewed-on: #14
2024-10-21 16:51:21 +00:00
Cayo Puigdefabregas 33cc462a4f resolve conflict 2024-10-21 18:41:43 +02:00
Cayo Puigdefabregas efcb53e062 repair bad jsons 2024-10-21 18:39:31 +02:00
Cayo Puigdefabregas e8d8c96700 add anchor in tabs of details of devices 2024-10-21 16:48:36 +02:00
Cayo Puigdefabregas ff91bc9ad9 change TIME_ZONE for env variable 2024-10-21 13:50:01 +02:00
Cayo Puigdefabregas 8d509e31c1 add date in list of events 2024-10-21 13:28:20 +02:00
Cayo Puigdefabregas 4bb71adf00 add hours of use 2024-10-21 13:27:45 +02:00
Cayo Puigdefabregas a0548b38c7 change EreuseWorkbench for workbench-script 2024-10-21 11:24:09 +02:00
cayop a9e39dcd3d Merge pull request 'lot: add a button to show/hide closed lots' (#2) from tau_showclosed-lot-sbtn into main
Reviewed-on: #2
2024-10-18 13:42:45 +00:00
sergio_gimenez ac91c23872 Merge branch 'main' into feature/90-implement-public-website-for-device 2024-10-15 11:06:41 +02:00
sergio_gimenez 84b3579851 Merge branch 'main' into feature/90-implement-public-website-for-device 2024-10-15 10:08:12 +02:00
sergio_gimenez 9b24d5e42b Merge branch 'main' into feature/90-implement-public-website-for-device 2024-10-15 10:05:01 +02:00
pedro 6e4bb3729f lot: add a button to show/hide closed lots
by default:

- a lot is open
- hide closed lots
2024-10-14 13:56:37 +02:00
sergio_gimenez 99eccc82aa 90: Add device model in the title 2024-10-08 08:02:42 +02:00
sergio_gimenez b8946dfbde 90: Playing with CSS to make it look good 2024-10-08 08:00:35 +02:00
sergio_gimenez 12e31758b3 90-initial-ugly-view 2024-10-08 07:26:48 +02:00
37 changed files with 1382 additions and 364 deletions

View File

@ -1,11 +1,12 @@
DOMAIN=localhost DOMAIN=localhost
DEMO=false # note that with DEBUG=true, logs are more verbose (include tracebacks)
DEBUG=true
DEMO=true
STATIC_ROOT=/tmp/static/ STATIC_ROOT=/tmp/static/
MEDIA_ROOT=/tmp/media/ MEDIA_ROOT=/tmp/media/
ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1, ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1,
DOMAIN=localhost DOMAIN=localhost
DEBUG=True
EMAIL_HOST="mail.example.org" EMAIL_HOST="mail.example.org"
EMAIL_HOST_USER="fillme_noreply" EMAIL_HOST_USER="fillme_noreply"
EMAIL_HOST_PASSWORD="fillme_passwd" EMAIL_HOST_PASSWORD="fillme_passwd"

View File

@ -6,9 +6,11 @@ from django.urls import path
app_name = 'api' app_name = 'api'
urlpatterns = [ urlpatterns = [
path('snapshot/', views.NewSnapshot, name='new_snapshot'), path('v1/snapshot/', views.NewSnapshotView.as_view(), name='new_snapshot'),
path('tokens/', views.TokenView.as_view(), name='tokens'), path('v1/annotation/<str:pk>/', views.AddAnnotationView.as_view(), name='new_annotation'),
path('tokens/new', views.TokenNewView.as_view(), name='new_token'), path('v1/device/<str:pk>/', views.DetailsDeviceView.as_view(), name='device'),
path("tokens/<int:pk>/edit", views.EditTokenView.as_view(), name="edit_token"), path('v1/tokens/', views.TokenView.as_view(), name='tokens'),
path('tokens/<int:pk>/del', views.TokenDeleteView.as_view(), name='delete_token'), path('v1/tokens/new', views.TokenNewView.as_view(), name='new_token'),
path("v1/tokens/<int:pk>/edit", views.EditTokenView.as_view(), name="edit_token"),
path('v1/tokens/<int:pk>/del', views.TokenDeleteView.as_view(), name='delete_token'),
] ]

View File

@ -1,12 +1,16 @@
import json import json
import uuid
import logging
from uuid import uuid4 from uuid import uuid4
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.conf import settings
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from django.views.generic.edit import ( from django.views.generic.edit import (
CreateView, CreateView,
@ -15,81 +19,124 @@ 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 dashboard.mixins import DashboardView from dashboard.mixins import DashboardView
from evidence.models import Annotation from evidence.models import Annotation
from evidence.parse_details import ParseSnapshot
from evidence.parse import Build from evidence.parse import Build
from device.models import Device
from api.models import Token from api.models import Token
from api.tables import TokensTable from api.tables import TokensTable
@csrf_exempt logger = logging.getLogger('django')
def NewSnapshot(request):
# Accept only posts
if request.method != 'POST':
return JsonResponse({'error': 'Invalid request method'}, status=400)
# Authentication
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
token = auth_header.split(' ')[1]
tk = Token.objects.filter(token=token).first()
if not tk:
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
# Validation snapshot
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
# try:
# Build(data, None, check=True)
# except Exception:
# return JsonResponse({'error': 'Invalid Snapshot'}, status=400)
exist_annotation = Annotation.objects.filter(
uuid=data['uuid']
).first()
if exist_annotation:
txt = "error: the snapshot {} exist".format(data['uuid'])
return JsonResponse({'status': txt}, status=500)
# Process snapshot
path_name = save_in_disk(data, tk.owner.institution.name)
try:
Build(data, tk.owner)
except Exception as err:
return JsonResponse({'status': f"fail: {err}"}, status=500)
annotation = Annotation.objects.filter(
uuid=data['uuid'],
type=Annotation.Type.SYSTEM,
# TODO this is hardcoded, it should select the user preferred algorithm
key="hidalgo1",
owner=tk.owner.institution
).first()
if not annotation: class ApiMixing(View):
return JsonResponse({'status': 'fail'}, status=500)
url_args = reverse_lazy("device:details", args=(annotation.value,)) @method_decorator(csrf_exempt)
url = request.build_absolute_uri(url_args) def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
response = { def auth(self):
"status": "success", # Authentication
"dhid": annotation.value[:6].upper(), auth_header = self.request.headers.get('Authorization')
"url": url, if not auth_header or not auth_header.startswith('Bearer '):
# TODO replace with public_url when available logger.error("Invalid or missing token %s", auth_header)
"public_url": url return JsonResponse({'error': 'Invalid or missing token'}, status=401)
}
move_json(path_name, tk.owner.institution.name)
return JsonResponse(response, status=200) token = auth_header.split(' ')[1].strip("'").strip('"')
try:
uuid.UUID(token)
except Exception:
logger.error("Invalid or missing token %s", token)
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
self.tk = Token.objects.filter(token=token).first()
if not self.tk:
logger.error("Invalid or missing token %s", token)
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
class NewSnapshotView(ApiMixing):
def get(self, request, *args, **kwargs):
return JsonResponse({}, status=404)
def post(self, request, *args, **kwargs):
response = self.auth()
if response:
return response
# Validation snapshot
try:
data = json.loads(request.body)
except json.JSONDecodeError:
txt = "error: the snapshot is not a json"
logger.error("%s", txt)
return JsonResponse({'error': 'Invalid JSON'}, status=500)
# Process snapshot
path_name = save_in_disk(data, self.tk.owner.institution.name)
# try:
# Build(data, None, check=True)
# except Exception:
# return JsonResponse({'error': 'Invalid Snapshot'}, status=400)
if not data.get("uuid"):
txt = "error: the snapshot not have uuid"
logger.error("%s", txt)
return JsonResponse({'status': txt}, status=500)
exist_annotation = Annotation.objects.filter(
uuid=data['uuid']
).first()
if exist_annotation:
txt = "error: the snapshot {} exist".format(data['uuid'])
logger.warning("%s", txt)
return JsonResponse({'status': txt}, status=500)
try:
Build(data, self.tk.owner)
except Exception as err:
if settings.DEBUG:
logger.exception("%s", err)
snapshot_id = data.get("uuid", "")
txt = "It is not possible to parse snapshot: %s."
logger.error(txt, snapshot_id)
text = "fail: It is not possible to parse snapshot"
return JsonResponse({'status': text}, status=500)
annotation = Annotation.objects.filter(
uuid=data['uuid'],
type=Annotation.Type.SYSTEM,
# TODO this is hardcoded, it should select the user preferred algorithm
key="hidalgo1",
owner=self.tk.owner.institution
).first()
if not annotation:
logger.error("Error: No annotation for uuid: %s", data["uuid"])
return JsonResponse({'status': 'fail'}, status=500)
url_args = reverse_lazy("device:details", args=(annotation.value,))
url = request.build_absolute_uri(url_args)
response = {
"status": "success",
"dhid": annotation.value[:6].upper(),
"url": url,
# TODO replace with public_url when available
"public_url": url
}
move_json(path_name, self.tk.owner.institution.name)
return JsonResponse(response, status=200)
class TokenView(DashboardView, SingleTableView): class TokenView(DashboardView, SingleTableView):
@ -165,3 +212,99 @@ class EditTokenView(DashboardView, UpdateView):
) )
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
return kwargs return kwargs
class DetailsDeviceView(ApiMixing):
def get(self, request, *args, **kwargs):
response = self.auth()
if response:
return response
self.pk = kwargs['pk']
self.object = Device(id=self.pk)
if not self.object.last_evidence:
return JsonResponse({}, status=404)
if self.object.owner != self.tk.owner.institution:
return JsonResponse({}, status=403)
data = self.get_data()
return JsonResponse(data, status=200)
def post(self, request, *args, **kwargs):
return JsonResponse({}, status=404)
def get_data(self):
data = {}
self.object.initial()
self.object.get_last_evidence()
evidence = self.object.last_evidence
if evidence.is_legacy():
data.update({
"device": evidence.get("device"),
"components": evidence.get("components"),
})
else:
evidence.get_doc()
snapshot = ParseSnapshot(evidence.doc).snapshot_json
data.update({
"device": snapshot.get("device"),
"components": snapshot.get("components"),
})
uuids = Annotation.objects.filter(
owner=self.tk.owner.institution,
value=self.pk
).values("uuid")
annotations = Annotation.objects.filter(
uuid__in=uuids,
owner=self.tk.owner.institution,
type = Annotation.Type.USER
).values_list("key", "value")
data.update({"annotations": list(annotations)})
return data
class AddAnnotationView(ApiMixing):
def post(self, request, *args, **kwargs):
response = self.auth()
if response:
return response
self.pk = kwargs['pk']
institution = self.tk.owner.institution
self.annotation = Annotation.objects.filter(
owner=institution,
value=self.pk,
type=Annotation.Type.SYSTEM
).first()
if not self.annotation:
return JsonResponse({}, status=404)
try:
data = json.loads(request.body)
key = data["key"]
value = data["value"]
except Exception:
logger.error("Invalid Snapshot of user %s", self.tk.owner)
return JsonResponse({'error': 'Invalid JSON'}, status=500)
Annotation.objects.create(
uuid=self.annotation.uuid,
owner=self.tk.owner.institution,
type = Annotation.Type.USER,
key = key,
value = value
)
return JsonResponse({"status": "success"}, status=200)
def get(self, request, *args, **kwargs):
return JsonResponse({}, status=404)

View File

@ -66,14 +66,21 @@ class SearchView(InventaryMixin):
limit limit
) )
if not matches.size(): if not matches or not matches.size():
return self.search_hids(query, offset, limit) return self.search_hids(query, offset, limit)
devices = [] devices = []
dev_id = []
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)
if dev.id not in dev_id:
devices.append(dev)
dev_id.append(dev.id)
count = matches.size() count = matches.size()
# TODO fix of pagination, the count is not correct
return devices, count return devices, count
def get_annotations(self, xp): def get_annotations(self, xp):

View File

@ -1,8 +1,7 @@
from django.db import models, connection from django.db import models, connection
from utils.constants import STR_SM_SIZE, STR_SIZE, STR_EXTEND_SIZE, ALGOS from utils.constants import ALGOS
from evidence.models import Annotation, Evidence from evidence.models import Annotation, Evidence
from user.models import User
from lot.models import DeviceLot from lot.models import DeviceLot
@ -109,6 +108,22 @@ class Device:
return return
annotation = annotations.first() annotation = annotations.first()
self.last_evidence = Evidence(annotation.uuid) self.last_evidence = Evidence(annotation.uuid)
def is_eraseserver(self):
if not self.uuids:
self.get_uuids()
if not self.uuids:
return False
annotation = Annotation.objects.filter(
uuid__in=self.uuids,
owner=self.owner,
type=Annotation.Type.ERASE_SERVER
).first()
if annotation:
return True
return False
def last_uuid(self): def last_uuid(self):
return self.uuids[0] return self.uuids[0]

View File

@ -1,216 +1,254 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3>{{ object.shortid }}</h3> <h3>{{ object.shortid }}</h3>
</div>
</div>
<div class="row">
<div class="col">
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">General details</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">User annotations</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="#components">Components</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">Evidences</button>
</li>
<li class="nav-items">
<a class="nav-link" href="">Web</a>
</li>
</ul>
</div>
</div>
<div class="tab-content pt-2">
<div class="tab-pane fade show active" id="details">
<h5 class="card-title">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> </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>
{% if object.is_websnapshot %}
{% for k, v in object.last_user_evidence %}
<div class="row">
<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">
<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">Serial Number</div>
<div class="col-lg-9 col-md-8">{{ object.last_evidence.doc.device.serialNumber|default:"" }}</div>
</div>
{% endif %}
<div class="row">
<div class="col-lg-3 col-md-4 label">Identifiers</div>
</div>
{% for chid in object.hids %}
<div class="row">
<div class="col">{{ chid |default:"" }}</div>
</div>
{% endfor %}
</div> </div>
<div class="tab-pane fade profile-overview" id="annotations"> <div class="row">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown=""> <div class="col">
<a href="{% url 'device:add_annotation' object.pk %}" class="btn btn-primary"> <ul class="nav nav-tabs nav-tabs-bordered">
<i class="bi bi-plus"></i> <li class="nav-item">
Add new annotation <a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">{% trans 'General details' %}</a>
<span class="caret"></span> </li>
</a> <li class="nav-item">
<a href="#annotations" class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">{% trans 'User annotations' %}</a>
</li>
<li class="nav-item">
<a href="#documents" class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">{% trans 'Documents' %}</a>
</li>
<li class="nav-item">
<a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">{% trans 'Lots' %}</a>
</li>
<li class="nav-item">
<a href="#components" class="nav-link" data-bs-toggle="tab" data-bs-target="#components">{% trans 'Components' %}</a>
</li>
<li class="nav-item">
<a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
</li>
</ul>
</div> </div>
<h5 class="card-title mt-2">Annotations</h5>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Key</th>
<th scope="col">Value</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Created on</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in 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>
<div class="tab-content pt-2">
<div class="tab-pane fade profile-overview" id="lots"> <div class="tab-pane fade show active" id="details">
{% for tag in lot_tags %} <h5 class="card-title">{% trans 'Details' %}</h5>
<h5 class="card-title">{{ tag }}</h5> <div class="row mb-3">
<div class="col-lg-3 col-md-4 label">Phid</div>
{% for lot in object.lots %} <div class="col-lg-9 col-md-8">{{ object.id }}</div>
{% if lot.type == tag %}
<div class="row">
<div class="col">
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}</a>
</div> </div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
<div class="tab-pane fade profile-overview" id="documents"> {% if object.is_eraseserver %}
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown=""> <div class="row mb-3">
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary"> <div class="col-lg-3 col-md-4 label">
{% trans 'Is a erase server' %}
<i class="bi bi-plus"></i> </div>
Add new document <div class="col-lg-9 col-md-8"></div>
<span class="caret"></span>
</a>
</div>
<h5 class="card-title mt-2">Documents</h5>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Key</th>
<th scope="col">Value</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Created on</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in 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 profile-overview" id="components">
<h5 class="card-title">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"> {% endif %}
{% for k, v in c.items %}
{% if k not in "actions,type" %}
{{ k }}: {{ v }}<br />
{% endif %}
{% endfor %}
<br />
</p>
<small class="text-muted">
</small>
</div>
{% endfor %}
</div>
</div>
<div class="tab-pane fade profile-overview" id="evidences"> <div class="row mb-3">
<h5 class="card-title">List of evidences</h5> <div class="col-lg-3 col-md-4 label">Type</div>
<div class="list-group col-6"> <div class="col-lg-9 col-md-8">{{ object.type }}</div>
{% for snap in object.evidences %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1"></h5>
<small class="text-muted">{{ snap.created }}</small>
</div>
<p class="mb-1">
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
</p>
<small class="text-muted">
</small>
</div> </div>
{% if object.is_websnapshot %}
{% 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.last_evidence.doc.device.serialNumber|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 %} {% endfor %}
</div> </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>
{% endif %}
{% endfor %}
{% endfor %}
</div>
<div class="tab-pane fade" id="components">
<h5 class="card-title">{% trans 'Components last evidence' %}</h5>
<div class="list-group col-6">
{% for c in object.components %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ c.type }}</h5>
<small class="text-muted">{{ evidence.created }}</small>
</div>
<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>
<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> {% endblock %}
{% block extrascript %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Obtener el hash de la URL (ejemplo: #components)
const hash = window.location.hash
// Verificar si hay un hash en la URL
if (hash) {
// Buscar el botón o enlace que corresponde al hash y activarlo
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`)
if (tabTrigger) {
// Crear una instancia de tab de Bootstrap para activar el tab
const tab = new bootstrap.Tab(tabTrigger)
tab.show()
}
}
})
</script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,169 @@
<!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">
<h1 class="text-center mb-4" style="color: #545f71;">{{ object.manufacturer }} {{ object.type }} {{ object.model }}</h1>
<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>
<div class="info-row row">
<div class="col-md-4 info-label">Serial Number</div>
<div class="col-md-8 info-value">{{ object.last_evidence.doc.device.serialNumber|default:'' }}</div>
</div>
{% 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' %}
<strong>{{ component_key }}:</strong> {{ component_value }}<br />
{% endif %}
{% endfor %}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<footer>
<p>
&copy;{% now 'Y' %} eReuse. All rights reserved.
</p>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

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

0
device/tests/__init__.py Normal file
View File

View File

@ -0,0 +1,76 @@
from device.models import Device
from unittest.mock import MagicMock
class TestDevice(Device):
"""A test subclass of Device that overrides the database-dependent methods"""
# TODO Leaving commented bc not used, but might be useful at some point
# def get_annotations(self):
# """Return empty list instead of querying database"""
# return []
# def get_uuids(self):
# """Set uuids directly instead of querying"""
# self.uuids = ['uuid1', 'uuid2']
# def get_hids(self):
# """Set hids directly instead of querying"""
# self.hids = ['hid1', 'hid2']
# def get_evidences(self):
# """Set evidences directly instead of querying"""
# self.evidences = []
# def get_lots(self):
# """Set lots directly instead of querying"""
# self.lots = []
def get_last_evidence(self):
if not hasattr(self, '_evidence'):
self._evidence = MagicMock()
self._evidence.doc = {
'type': 'Computer',
'manufacturer': 'Test Manufacturer',
'model': 'Test Model',
'device': {
'serialNumber': 'SN123456',
'type': 'Computer'
}
}
self._evidence.get_manufacturer = lambda: 'Test Manufacturer'
self._evidence.get_model = lambda: 'Test Model'
self._evidence.get_chassis = lambda: 'Computer'
self._evidence.get_components = lambda: [
{
'type': 'CPU',
'model': 'Intel i7',
'manufacturer': 'Intel'
},
{
'type': 'RAM',
'size': '8GB',
'manufacturer': 'Kingston'
}
]
self.last_evidence = self._evidence
class TestWebSnapshotDevice(TestDevice):
"""A test subclass of Device that simulates a WebSnapshot device"""
def get_last_evidence(self):
if not hasattr(self, '_evidence'):
self._evidence = MagicMock()
self._evidence.doc = {
'type': 'WebSnapshot',
'kv': {
'URL': 'http://example.com',
'Title': 'Test Page',
'Timestamp': '2024-01-01'
},
'device': {
'type': 'Laptop'
}
}
self.last_evidence = self._evidence
return self._evidence

View File

@ -0,0 +1,67 @@
from django.test import TestCase, Client
from django.urls import reverse
from unittest.mock import patch
from device.views import PublicDeviceWebView
from device.tests.test_mock_device import TestDevice, TestWebSnapshotDevice
class PublicDeviceWebViewTests(TestCase):
def setUp(self):
self.client = Client()
self.test_id = "test123"
self.test_url = reverse('device:device_web',
kwargs={'pk': self.test_id})
def test_url_resolves_correctly(self):
"""Test that the URL is constructed correctly"""
url = reverse('device:device_web', kwargs={'pk': self.test_id})
self.assertEqual(url, f'/device/{self.test_id}/public/')
@patch('device.views.Device')
def test_html_response(self, MockDevice):
test_device = TestDevice(id=self.test_id)
MockDevice.return_value = test_device
response = self.client.get(self.test_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'device_web.html')
self.assertContains(response, 'Test Manufacturer')
self.assertContains(response, 'Test Model')
self.assertContains(response, 'Computer')
self.assertContains(response, self.test_id)
self.assertContains(response, 'CPU')
self.assertContains(response, 'Intel')
self.assertContains(response, 'RAM')
self.assertContains(response, 'Kingston')
@patch('device.views.Device')
def test_json_response(self, MockDevice):
test_device = TestDevice(id=self.test_id)
MockDevice.return_value = test_device
response = self.client.get(
self.test_url,
HTTP_ACCEPT='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
json_data = response.json()
self.assertEqual(json_data['id'], self.test_id)
self.assertEqual(json_data['shortid'], self.test_id[:6].upper())
self.assertEqual(json_data['components'], test_device.components)
@patch('device.views.Device')
def test_websnapshot_device(self, MockDevice):
test_device = TestWebSnapshotDevice(id=self.test_id)
MockDevice.return_value = test_device
response = self.client.get(self.test_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'device_web.html')
self.assertContains(response, 'http://example.com')
self.assertContains(response, 'Test Page')

View File

@ -9,4 +9,6 @@ urlpatterns = [
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>/annotation/add", views.AddAnnotationView.as_view(), name="add_annotation"),
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"), path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"),
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
] ]

View File

@ -1,4 +1,5 @@
import json import json
from django.http import JsonResponse
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -95,7 +96,7 @@ class DetailsView(DashboardView, TemplateView):
raise Http404 raise Http404
if self.object.owner != self.request.user.institution: if self.object.owner != self.request.user.institution:
raise Http403 raise Http403
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -110,6 +111,39 @@ class DetailsView(DashboardView, TemplateView):
return context return context
class PublicDeviceWebView(TemplateView):
template_name = "device_web.html"
def get(self, request, *args, **kwargs):
self.pk = kwargs['pk']
self.object = Device(id=self.pk)
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):
context = super().get_context_data(**kwargs)
self.object.initial()
context.update({
'object': self.object
})
return context
def get_json_response(self):
data = {
'id': self.object.id,
'shortid': self.object.shortid,
'uuids': self.object.uuids,
'hids': self.object.hids,
'components': self.object.components
}
return JsonResponse(data)
class AddAnnotationView(DashboardView, CreateView): class AddAnnotationView(DashboardView, CreateView):
template_name = "new_annotation.html" template_name = "new_annotation.html"
title = _("New annotation") title = _("New annotation")
@ -134,7 +168,7 @@ class AddAnnotationView(DashboardView, CreateView):
value=pk, value=pk,
type=Annotation.Type.SYSTEM type=Annotation.Type.SYSTEM
).first() ).first()
if not self.annotation: if not self.annotation:
raise Http404 raise Http404

View File

@ -17,6 +17,8 @@ from pathlib import Path
from django.contrib.messages import constants as messages from django.contrib.messages import constants as messages
from decouple import config, Csv from decouple import config, Csv
from utils.logger import CustomFormatter
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -32,8 +34,6 @@ DEBUG = config('DEBUG', default=False, cast=bool)
DOMAIN = config("DOMAIN") DOMAIN = config("DOMAIN")
assert DOMAIN not in [None, ''], "DOMAIN var is MANDATORY" assert DOMAIN not in [None, ''], "DOMAIN var is MANDATORY"
# this var is very important, we print it
print("DOMAIN: " + DOMAIN)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=DOMAIN, cast=Csv()) ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=DOMAIN, cast=Csv())
assert DOMAIN in ALLOWED_HOSTS, f"DOMAIN {DOMAIN} is not in ALLOWED_HOSTS {ALLOWED_HOSTS}" assert DOMAIN in ALLOWED_HOSTS, f"DOMAIN {DOMAIN} is not in ALLOWED_HOSTS {ALLOWED_HOSTS}"
@ -158,11 +158,14 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC" TIME_ZONE = config("TIME_ZONE", default="UTC")
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = False
if TIME_ZONE == "UTC":
USE_TZ = True
USE_L10N = True USE_L10N = True
LANGUAGES = [ LANGUAGES = [
@ -202,12 +205,34 @@ LOGOUT_REDIRECT_URL = '/'
LOGGING = { LOGGING = {
"version": 1, "version": 1,
"disable_existing_loggers": False, "disable_existing_loggers": False,
'formatters': {
'colored': {
'()': CustomFormatter,
'format': '%(levelname)s %(asctime)s %(message)s'
},
},
"handlers": { "handlers": {
"console": {"level": "DEBUG", "class": "logging.StreamHandler"}, "console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "colored"
},
}, },
"root": { "root": {
"handlers": ["console"], "handlers": ["console"],
"level": "DEBUG", "level": "DEBUG",
},
"loggers": {
"django": {
"handlers": ["console"],
"level": "INFO",
"propagate": False, # Asegura que no se reenvíen a los manejadores raíz
},
"django.request": {
"handlers": ["console"],
"level": "ERROR",
"propagate": False,
}
} }
} }

View File

@ -4,7 +4,7 @@ services:
build: build:
dockerfile: docker/devicehub-django.Dockerfile dockerfile: docker/devicehub-django.Dockerfile
environment: environment:
- DEBUG=true - DEBUG=${DEBUG:-false}
- DOMAIN=${DOMAIN:-localhost} - DOMAIN=${DOMAIN:-localhost}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN} - ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN}
- DEMO=${DEMO:-false} - DEMO=${DEMO:-false}

View File

@ -18,6 +18,8 @@ deploy() {
if [ "${DEBUG:-}" = 'true' ]; then if [ "${DEBUG:-}" = 'true' ]; then
./manage.py print_settings ./manage.py print_settings
else
echo "DOMAIN: ${DOMAIN}"
fi fi
# detect if existing deployment (TODO only works with sqlite) # detect if existing deployment (TODO only works with sqlite)

View File

@ -162,3 +162,56 @@ class ImportForm(forms.Form):
return table return table
return return
class EraseServerForm(forms.Form):
erase_server = forms.BooleanField(label=_("Is a Erase Server"), required=False)
def __init__(self, *args, **kwargs):
self.pk = None
self.uuid = kwargs.pop('uuid', None)
self.user = kwargs.pop('user')
instance = Annotation.objects.filter(
uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
owner=self.user.institution
).first()
if instance:
kwargs["initial"]["erase_server"] = instance.value
self.pk = instance.pk
super().__init__(*args, **kwargs)
def clean(self):
self.erase_server = self.cleaned_data.get('erase_server', False)
self.instance = Annotation.objects.filter(
uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
owner=self.user.institution
).first()
return True
def save(self, user, commit=True):
if not commit:
return
if not self.erase_server:
if self.instance:
self.instance.delete()
return
if self.instance:
return
Annotation.objects.create(
uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
value=self.erase_server,
owner=self.user.institution,
user=self.user
)

View File

@ -0,0 +1,83 @@
import os
import json
import logging
from django.core.management.base import BaseCommand
from django.conf import settings
from utils.device import create_annotation, create_doc, create_index
from user.models import Institution
from evidence.parse import Build
logger = logging.getLogger('django')
class Command(BaseCommand):
help = "Reindex snapshots"
snapshots = []
EVIDENCES = settings.EVIDENCES_DIR
def handle(self, *args, **kwargs):
if os.path.isdir(self.EVIDENCES):
self.read_files(self.EVIDENCES)
self.parsing()
def read_files(self, directory):
for filename in os.listdir(directory):
filepath = os.path.join(directory, filename)
if not os.path.isdir(filepath):
continue
institution = Institution.objects.filter(name=filename).first()
if not institution:
continue
user = institution.user_set.filter(is_admin=True).first()
if not user:
txt = "No there are Admins for the institution: %s"
logger.warning(txt, institution.name)
continue
snapshots_path = os.path.join(filepath, "snapshots")
placeholders_path = os.path.join(filepath, "placeholders")
for f in os.listdir(snapshots_path):
f_path = os.path.join(snapshots_path, f)
if f_path[-5:] == ".json" and os.path.isfile(f_path):
self.open(f_path, user)
for f in os.listdir(placeholders_path):
f_path = os.path.join(placeholders_path, f)
if f_path[-5:] == ".json" and os.path.isfile(f_path):
self.open(f_path, user)
def open(self, filepath, user):
with open(filepath, 'r') as file:
content = json.loads(file.read())
self.snapshots.append((content, user, filepath))
def parsing(self):
for s, user, f_path in self.snapshots:
if s.get("type") == "Websnapshot":
self.build_placeholder(s, user, f_path)
else:
self.build_snapshot(s, user, f_path)
def build_placeholder(self, s, user, f_path):
try:
create_index(s, user)
create_annotation(s, user, commit=True)
except Exception as err:
txt = "In placeholder %s \n%s"
logger.warning(txt, f_path, err)
def build_snapshot(self, s, user, f_path):
try:
Build(s, user)
except Exception:
txt = "Error: in Snapshot {}".format(f_path)
logger.error(txt)

View File

@ -1,12 +1,18 @@
import os import os
import json import json
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 evidence.parse import Build from evidence.parse import Build
logger = logging.getLogger('django')
User = get_user_model() User = get_user_model()
@ -33,7 +39,7 @@ class Command(BaseCommand):
self.read_directory(path) self.read_directory(path)
self.parsing() self.parsing()
def read_directory(self, directory): def read_directory(self, directory):
for filename in os.listdir(directory): for filename in os.listdir(directory):
filepath = os.path.join(directory, filename) filepath = os.path.join(directory, filename)
@ -42,9 +48,16 @@ class Command(BaseCommand):
def open(self, filepath): def open(self, filepath):
with open(filepath, 'r') as file: with open(filepath, 'r') as file:
content = json.loads(file.read()) content = json.loads(file.read())
self.snapshots.append(content) path_name = save_in_disk(content, self.user.institution.name)
self.snapshots.append((content, path_name))
def parsing(self): def parsing(self):
for s in self.snapshots: for s, p in self.snapshots:
self.devices.append(Build(s, self.user)) try:
self.devices.append(Build(s, self.user))
move_json(p, self.user.institution.name)
except Exception as err:
snapshot_id = s.get("uuid", "")
txt = "Could not parse snapshot: %s"
logger.error(txt, snapshot_id)

View File

@ -0,0 +1,25 @@
# Generated by Django 5.0.6 on 2024-10-28 12:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("evidence", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="annotation",
name="type",
field=models.SmallIntegerField(
choices=[
(0, "System"),
(1, "User"),
(2, "Document"),
(3, "EraseServer"),
]
),
),
]

View File

@ -3,7 +3,7 @@ import json
from dmidecode import DMIParse from dmidecode import DMIParse
from django.db import models from django.db import models
from utils.constants import STR_SM_SIZE, 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 user.models import User, Institution from user.models import User, Institution
@ -14,6 +14,7 @@ class Annotation(models.Model):
SYSTEM= 0, "System" SYSTEM= 0, "System"
USER = 1, "User" USER = 1, "User"
DOCUMENT = 2, "Document" DOCUMENT = 2, "Document"
ERASE_SERVER = 3, "EraseServer"
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField() uuid = models.UUIDField()
@ -61,13 +62,13 @@ class Evidence:
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.size() < 0: if matches and matches.size() < 0:
return return
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 self.doc.get("software") == "EreuseWorkbench": if not self.is_legacy():
dmidecode_raw = self.doc["data"]["dmidecode"] dmidecode_raw = self.doc["data"]["dmidecode"]
self.dmi = DMIParse(dmidecode_raw) self.dmi = DMIParse(dmidecode_raw)
@ -80,7 +81,7 @@ class Evidence:
self.created = self.annotations.last().created self.created = self.annotations.last().created
def get_components(self): def get_components(self):
if self.doc.get("software") != "EreuseWorkbench": if self.is_legacy():
return self.doc.get('components', []) return self.doc.get('components', [])
self.set_components() self.set_components()
return self.components return self.components
@ -92,7 +93,7 @@ class Evidence:
return "" return ""
return list(self.doc.get('kv').values())[0] return list(self.doc.get('kv').values())[0]
if self.doc.get("software") != "EreuseWorkbench": if self.is_legacy():
return self.doc['device']['manufacturer'] return self.doc['device']['manufacturer']
return self.dmi.manufacturer().strip() return self.dmi.manufacturer().strip()
@ -104,13 +105,13 @@ class Evidence:
return "" return ""
return list(self.doc.get('kv').values())[1] return list(self.doc.get('kv').values())[1]
if self.doc.get("software") != "EreuseWorkbench": if self.is_legacy():
return self.doc['device']['model'] return self.doc['device']['model']
return self.dmi.model().strip() return self.dmi.model().strip()
def get_chassis(self): def get_chassis(self):
if self.doc.get("software") != "EreuseWorkbench": if self.is_legacy():
return self.doc['device']['model'] return self.doc['device']['model']
chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual') chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual')
@ -127,8 +128,11 @@ class Evidence:
owner=user.institution, owner=user.institution,
type=Annotation.Type.SYSTEM, type=Annotation.Type.SYSTEM,
key="hidalgo1", key="hidalgo1",
).order_by("-created").values_list("uuid", flat=True).distinct() ).order_by("-created").values_list("uuid", "created").distinct()
def set_components(self): def set_components(self):
snapshot = ParseSnapshot(self.doc).snapshot_json snapshot = ParseSnapshot(self.doc).snapshot_json
self.components = snapshot['components'] self.components = snapshot['components']
def is_legacy(self):
return self.doc.get("software") != "workbench-script"

View File

@ -1,13 +1,16 @@
import os
import json import json
import shutil
import hashlib import hashlib
import logging
from datetime import datetime
from dmidecode import DMIParse from dmidecode import DMIParse
from json_repair import repair_json
from evidence.models import Annotation from evidence.models import Annotation
from evidence.xapian import index from evidence.xapian import index
from utils.constants import ALGOS, CHASSIS_DH from utils.constants import CHASSIS_DH
logger = logging.getLogger('django')
def get_network_cards(child, nets): def get_network_cards(child, nets):
@ -15,14 +18,22 @@ def get_network_cards(child, nets):
nets.append(child) nets.append(child)
if child.get('children'): if child.get('children'):
[get_network_cards(x, nets) for x in child['children']] [get_network_cards(x, nets) for x in child['children']]
def get_mac(lshw): def get_mac(lshw):
nets = [] nets = []
try: try:
get_network_cards(json.loads(lshw), nets) if type(lshw) is dict:
hw = lshw
else:
hw = json.loads(lshw)
except json.decoder.JSONDecodeError:
hw = json.loads(repair_json(lshw))
try:
get_network_cards(hw, nets)
except Exception as ss: except Exception as ss:
print("WARNING!! {}".format(ss)) logger.warning("%s", ss)
return return
nets_sorted = sorted(nets, key=lambda x: x['businfo']) nets_sorted = sorted(nets, key=lambda x: x['businfo'])
@ -57,7 +68,7 @@ class Build:
} }
def get_hid_14(self): def get_hid_14(self):
if self.json.get("software") == "EreuseWorkbench": if self.json.get("software") == "workbench-script":
hid = self.get_hid(self.json) hid = self.get_hid(self.json)
else: else:
device = self.json['device'] device = self.json['device']
@ -67,10 +78,21 @@ class Build:
serial_number = device.get("serialNumber", '') serial_number = device.get("serialNumber", '')
sku = device.get("sku", '') sku = device.get("sku", '')
hid = f"{manufacturer}{model}{chassis}{serial_number}{sku}" hid = f"{manufacturer}{model}{chassis}{serial_number}{sku}"
return hashlib.sha3_256(hid.encode()).hexdigest() return hashlib.sha3_256(hid.encode()).hexdigest()
def create_annotations(self): def create_annotations(self):
annotation = Annotation.objects.filter(
uuid=self.uuid,
owner=self.user.institution,
type=Annotation.Type.SYSTEM,
)
if annotation:
txt = "Warning: Snapshot %s already registered (annotation exists)"
logger.warning(txt, self.uuid)
return
for k, v in self.algorithms.items(): for k, v in self.algorithms.items():
Annotation.objects.create( Annotation.objects.create(
@ -92,7 +114,7 @@ class Build:
def get_sku(self): def get_sku(self):
return self.dmi.get("System")[0].get("SKU Number", "n/a").strip() return self.dmi.get("System")[0].get("SKU Number", "n/a").strip()
def get_chassis(self): def get_chassis(self):
return self.dmi.get("Chassis")[0].get("Type", '_virtual') return self.dmi.get("Chassis")[0].get("Type", '_virtual')
@ -108,14 +130,12 @@ class Build:
if not snapshot["data"].get('lshw'): if not snapshot["data"].get('lshw'):
return f"{manufacturer}{model}{chassis}{serial_number}{sku}" return f"{manufacturer}{model}{chassis}{serial_number}{sku}"
lshw = snapshot["data"]["lshw"] lshw = snapshot["data"]["lshw"]
# mac = get_mac2(hwinfo_raw) or "" # mac = get_mac2(hwinfo_raw) or ""
mac = get_mac(lshw) or "" mac = get_mac(lshw) or ""
if not mac: if not mac:
print(f"WARNING: Could not retrieve MAC address in snapshot {snapshot['uuid']}" ) txt = "Could not retrieve MAC address in snapshot %s"
# TODO generate system annotation for that snapshot logger.warning(txt, snapshot['uuid'])
else:
print(f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}")
return f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}" return f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}"

View File

@ -1,11 +1,17 @@
import json import json
import logging
import numpy as np import numpy as np
from datetime import datetime from datetime import datetime
from dmidecode import DMIParse from dmidecode import DMIParse
from json_repair import repair_json
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
logger = logging.getLogger('django')
def get_lshw_child(child, nets, component): def get_lshw_child(child, nets, component):
if child.get('id') == component: if child.get('id') == component:
nets.append(child) nets.append(child)
@ -160,6 +166,7 @@ class ParseSnapshot:
continue continue
model = sm.get('model_name') model = sm.get('model_name')
manufacturer = None manufacturer = None
hours = sm.get("power_on_time", {}).get("hours", 0)
if model and len(model.split(" ")) > 1: if model and len(model.split(" ")) > 1:
mm = model.split(" ") mm = model.split(" ")
model = mm[-1] model = mm[-1]
@ -175,6 +182,7 @@ class ParseSnapshot:
"size": self.get_data_storage_size(sm), "size": self.get_data_storage_size(sm),
"variant": sm.get("firmware_version"), "variant": sm.get("firmware_version"),
"interface": self.get_data_storage_interface(sm), "interface": self.get_data_storage_interface(sm),
"hours": hours,
} }
) )
@ -478,9 +486,13 @@ class ParseSnapshot:
def loads(self, x): def loads(self, x):
if isinstance(x, str): if isinstance(x, str):
try: try:
return json.loads(x) try:
hw = json.loads(x)
except json.decoder.JSONDecodeError:
hw = json.loads(repair_json(x))
return hw
except Exception as ss: except Exception as ss:
print("WARNING!! {}".format(ss)) logger.warning("%s", ss)
return {} return {}
return x return x
@ -489,5 +501,5 @@ class ParseSnapshot:
return self._errors return self._errors
logger.error(txt) logger.error(txt)
self._errors.append(txt) self._errors.append("%s", txt)

View File

@ -12,13 +12,16 @@
<div class="col"> <div class="col">
<ul class="nav nav-tabs nav-tabs-bordered"> <ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items"> <li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#device">Devices</button> <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#device">{% trans "Devices" %}</button>
</li> </li>
<li class="nav-items"> <li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tag">Tag</button> <a href="#tag" class="nav-link" data-bs-toggle="tab" data-bs-target="#tag">{% trans "Tag" %}</a>
<li class="nav-items">
<a href="{% url 'evidence:download' object.uuid %}" class="nav-link">Download File</a>
</li> </li>
<li class="nav-items">
<a href="{% url 'evidence:erase_server' object.uuid %}" class="nav-link">{% trans "Erase Server" %}</a>
</li>
<li class="nav-items">
<a href="{% url 'evidence:download' object.uuid %}" class="nav-link">{% trans "Download File" %}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -26,26 +29,44 @@
<div class="tab-content pt-2"> <div class="tab-content pt-2">
<div class="tab-pane fade show active" id="device"> <div class="tab-pane fade show active" id="device">
<h5 class="card-title">List of chids</h5> <h5 class="card-title"></h5>
<div class="list-group col-6"> <div class="list-group col-6">
{% for snap in object.annotations %} <table class="table">
{% if snap.type == 0 %} <thead>
<div class="list-group-item"> <tr>
<div class="d-flex w-100 justify-content-between"> <th scope="col" data-sortable="">
<h5 class="mb-1"></h5> {% trans "Type" %}
<small class="text-muted"> </th>
{{ snap.created }} <th scope="col" data-sortable="">
</small> {% trans "Identificator" %}
</div> </th>
<p class="mb-1"> <th scope="col" data-sortable="">
{{ snap.key }}<br /> {% trans "Data" %}
</p> </th>
<small class="text-muted"> </tr>
<a href="{% url 'device:details' snap.value %}">{{ snap.value }}</a> </thead>
</small> {% for snap in object.annotations %}
</div> <tbody>
{% endif %} {% if snap.type == 0 %}
{% endfor %} <tr>
<td>
{{ snap.key }}
</td>
<td>
<small class="text-muted">
<a href="{% url 'device:details' snap.value %}">{{ snap.value }}</a>
</small>
</td>
<td>
<small class="text-muted">
{{ snap.created }}
</small>
</td>
</tr>
{% endif %}
</tbody>
{% endfor %}
</table>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="tag"> <div class="tab-pane fade" id="tag">
@ -83,3 +104,24 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extrascript %}
<script>
document.addEventListener("DOMContentLoaded", function() {
// Obtener el hash de la URL (ejemplo: #components)
const hash = window.location.hash;
// Verificar si hay un hash en la URL
if (hash) {
// Buscar el botón o enlace que corresponde al hash y activarlo
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`);
if (tabTrigger) {
// Crear una instancia de tab de Bootstrap para activar el tab
const tab = new bootstrap.Tab(tabTrigger);
tab.show();
}
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col">
<h3>{{ object.id }}</h3>
</div>
</div>
<div class="row">
<div class="col">
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items">
<a href="{% url 'evidence:details' object.uuid %}" class="nav-link">{% trans "Devices" %}</a>
</li>
<li class="nav-items">
<a href="{% url 'evidence:details' object.uuid %}#tag" class="nav-link">{% trans "Tag" %}</a>
</li>
<li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#erase_server">{% trans "Erase Server" %}</button>
</li>
<li class="nav-items">
<a href="{% url 'evidence:download' object.uuid %}" class="nav-link">{% trans "Download File" %}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content pt-2">
<div class="tab-pane fade show active" id="erase_server">
{% load django_bootstrap5 %}
<div class="list-group col-6">
<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="container">
<div class="row">
<div class="col">
<a class="btn btn-grey" href="">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -14,10 +14,13 @@
{% for ev in evidences %} {% for ev in evidences %}
<tr> <tr>
<td> <td>
<a href="{% url 'evidence:details' ev %}">{{ ev }}</a> <a href="{% url 'evidence:details' ev.0 %}">{{ ev.0 }}</a>
</td> </td>
<td> <td>
<a href="{# url 'evidence:delete' ev #}"><i class="bi bi-trash text-danger"></i></a> <small class="text-muted">{{ ev.1 }}</small>
</td>
<td>
<a href="{# url 'evidence:delete' ev.0 #}"><i class="bi bi-trash text-danger"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -18,6 +18,7 @@ urlpatterns = [
path("upload", views.UploadView.as_view(), name="upload"), path("upload", views.UploadView.as_view(), name="upload"),
path("import", views.ImportView.as_view(), name="import"), path("import", views.ImportView.as_view(), name="import"),
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>/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('annotation/<int:pk>/del', views.AnnotationDeleteView.as_view(), name='delete_annotation'),
] ]

View File

@ -11,18 +11,14 @@ from django.views.generic.edit import (
FormView, FormView,
) )
from dashboard.mixins import DashboardView, Http403 from dashboard.mixins import DashboardView, Http403
from evidence.models import Evidence, Annotation from evidence.models import Evidence, Annotation
from evidence.forms import UploadForm, UserTagForm, ImportForm from evidence.forms import (
# from django.shortcuts import render UploadForm,
# from rest_framework import viewsets UserTagForm,
# from snapshot.serializers import SnapshotSerializer ImportForm,
EraseServerForm
)
# class SnapshotViewSet(viewsets.ModelViewSet):
# queryset = Snapshot.objects.all()
# serializer_class = SnapshotSerializer
class ListEvidencesView(DashboardView, TemplateView): class ListEvidencesView(DashboardView, TemplateView):
@ -167,3 +163,48 @@ class AnnotationDeleteView(DashboardView, DeleteView):
return redirect(url_name, **kwargs_view) return redirect(url_name, **kwargs_view)
class EraseServerView(DashboardView, FormView):
template_name = "ev_eraseserver.html"
section = "evidences"
title = _("Evidences")
breadcrumb = "Evidences / Details"
success_url = reverse_lazy('evidence:list')
form_class = EraseServerForm
def get(self, request, *args, **kwargs):
self.pk = kwargs['pk']
self.object = Evidence(self.pk)
if self.object.owner != self.request.user.institution:
raise Http403
self.object.get_annotations()
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'object': self.object,
})
return context
def get_form_kwargs(self):
self.pk = self.kwargs.get('pk')
kwargs = super().get_form_kwargs()
kwargs['uuid'] = self.pk
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
form.save(self.request.user)
response = super().form_valid(form)
return response
def form_invalid(self, form):
response = super().form_invalid(form)
return response
def get_success_url(self):
success_url = reverse_lazy('evidence:details', args=[self.pk])
return success_url

View File

@ -11,7 +11,10 @@ import xapian
def search(institution, qs, offset=0, limit=10): def search(institution, qs, offset=0, limit=10):
database = xapian.Database("db") try:
database = xapian.Database("db")
except (xapian.DatabaseNotFoundError, xapian.DatabaseOpeningError):
return
qp = xapian.QueryParser() qp = xapian.QueryParser()
qp.set_database(database) qp.set_database(database)
@ -31,12 +34,9 @@ def search(institution, qs, offset=0, limit=10):
def index(institution, uuid, snap): def index(institution, uuid, snap):
uuid = 'uuid:"{}"'.format(uuid) uuid = 'uuid:"{}"'.format(uuid)
try: matches = search(institution, uuid, limit=1)
matches = search(institution, uuid, limit=1) if matches and matches.size() > 0:
if matches.size() > 0: return
return
except (xapian.DatabaseNotFoundError, xapian.DatabaseOpeningError):
pass
database = xapian.WritableDatabase("db", xapian.DB_CREATE_OR_OPEN) database = xapian.WritableDatabase("db", xapian.DB_CREATE_OR_OPEN)
indexer = xapian.TermGenerator() indexer = xapian.TermGenerator()

View File

@ -110,7 +110,6 @@
<script src="/static/js/bootstrap.bundle.min.js"></script> <script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
<script src="/static/js/dashboard.js"></script>
<script> <script>
const togglePassword = document.querySelector('#togglePassword'); const togglePassword = document.querySelector('#togglePassword');
const password = document.querySelector('#id_password'); const password = document.querySelector('#id_password');

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-10-28 12:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("lot", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="lot",
name="closed",
field=models.BooleanField(default=False),
),
]

View File

@ -31,7 +31,7 @@ class Lot(models.Model):
name = models.CharField(max_length=STR_SIZE, blank=True, null=True) name = models.CharField(max_length=STR_SIZE, blank=True, null=True)
code = models.CharField(max_length=STR_SIZE, blank=True, null=True) code = models.CharField(max_length=STR_SIZE, blank=True, null=True)
description = models.CharField(max_length=STR_SIZE, blank=True, null=True) description = models.CharField(max_length=STR_SIZE, blank=True, null=True)
closed = models.BooleanField(default=True) closed = models.BooleanField(default=False)
owner = models.ForeignKey(Institution, on_delete=models.CASCADE) owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.ForeignKey(LotTag, on_delete=models.CASCADE) type = models.ForeignKey(LotTag, on_delete=models.CASCADE)

View File

@ -7,6 +7,16 @@
<h3>{{ subtitle }}</h3> <h3>{{ subtitle }}</h3>
</div> </div>
<div class="col text-center"> <div class="col text-center">
{% if show_closed %}
<a href="?show_closed=false" class="btn btn-green-admin">
{% trans 'Hide closed lots' %}
</a>
{% else %}
<a href="?show_closed=true" class="btn btn-green-admin">
{% trans 'Show closed lots' %}
</a>
{% endif %}
<a href="{% url 'lot:add' %}" type="button" class="btn btn-green-admin"> <a href="{% url 'lot:add' %}" type="button" class="btn btn-green-admin">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
{% trans 'Add new lot' %} {% trans 'Add new lot' %}

View File

@ -131,11 +131,13 @@ class LotsTagsView(DashboardView, TemplateView):
tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk) tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk)
self.title += " {}".format(tag.name) self.title += " {}".format(tag.name)
self.breadcrumb += " {}".format(tag.name) self.breadcrumb += " {}".format(tag.name)
lots = Lot.objects.filter(owner=self.request.user.institution).filter(type=tag) show_closed = self.request.GET.get('show_closed', 'false') == 'true'
lots = Lot.objects.filter(owner=self.request.user.institution).filter(type=tag, closed=show_closed)
context.update({ context.update({
'lots': lots, 'lots': lots,
'title': self.title, 'title': self.title,
'breadcrumb': self.breadcrumb 'breadcrumb': self.breadcrumb,
'show_closed': show_closed
}) })
return context return context

View File

@ -10,4 +10,4 @@ pandas==2.2.2
xlrd==2.0.1 xlrd==2.0.1
odfpy==1.4.1 odfpy==1.4.1
pytz==2024.2 pytz==2024.2
json-repair==0.30.0

View File

@ -2,12 +2,17 @@ import json
import uuid import uuid
import hashlib import hashlib
import datetime import datetime
import logging
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from evidence.xapian import index from evidence.xapian import index
from evidence.models import Annotation from evidence.models import Annotation
from device.models import Device from device.models import Device
logger = logging.getLogger('django')
def create_doc(data): def create_doc(data):
if not data: if not data:
return return
@ -76,6 +81,17 @@ def create_annotation(doc, user, commit=False):
'value': doc['CUSTOMER_ID'], 'value': doc['CUSTOMER_ID'],
} }
if commit: if commit:
annotation = Annotation.objects.filter(
uuid=doc["uuid"],
owner=user.institution,
type=Annotation.Type.SYSTEM,
)
if annotation:
txt = "Warning: Snapshot %s already registered (annotation exists)"
logger.warning(txt, doc["uuid"])
return annotation
return Annotation.objects.create(**data) return Annotation.objects.create(**data)
return Annotation(**data) return Annotation(**data)

37
utils/logger.py Normal file
View File

@ -0,0 +1,37 @@
import logging
from django.conf import settings
# Colors
RED = "\033[91m"
PURPLE = "\033[95m"
YELLOW = "\033[93m"
RESET = "\033[0m"
class CustomFormatter(logging.Formatter):
def format(self, record):
if record.levelname == "ERROR":
color = RED
elif record.levelname == "WARNING":
color = PURPLE
elif record.levelname in ["INFO", "DEBUG"]:
color = YELLOW
else:
color = RESET
record.levelname = f"{color}{record.levelname}{RESET}"
if record.args:
record.msg = self.highlight_args(record.msg, record.args, color)
record.args = ()
# provide trace when DEBUG config
if settings.DEBUG:
import traceback
print(traceback.format_exc())
return super().format(record)
def highlight_args(self, message, args, color):
highlighted_args = tuple(f"{color}{arg}{RESET}" for arg in args)
return message % highlighted_args