Changed annotation syntax to properties and created mutable user_properties #31

Open
rskthomas wants to merge 20 commits from rework/properties into main
25 changed files with 386 additions and 241 deletions

View File

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

View File

@ -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, Property
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
@ -90,11 +90,11 @@ class NewSnapshotView(ApiMixing):
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=data['uuid']
).first() ).first()
if exist_annotation: if exist_property:
txt = "error: the snapshot {} exist".format(data['uuid']) txt = "error: the snapshot {} exist".format(data['uuid'])
logger.warning("%s", txt) logger.warning("%s", txt)
return JsonResponse({'status': txt}, status=500) return JsonResponse({'status': txt}, status=500)
@ -111,25 +111,25 @@ class NewSnapshotView(ApiMixing):
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( property = SystemProperty.objects.filter(
uuid=data['uuid'], uuid=data['uuid'],
type=Annotation.Type.SYSTEM, type=Property.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="hidalgo1",
owner=self.tk.owner.institution owner=self.tk.owner.institution
).first() ).first()
if not annotation: if not property:
logger.error("Error: No annotation for uuid: %s", data["uuid"]) logger.error("Error: No property for uuid: %s", data["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 +255,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 +278,13 @@ 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 type=Property.Type.SYSTEM
).first() ).first()
if not self.annotation: if not self.property:
return JsonResponse({}, status=404) return JsonResponse({}, status=404)
try: try:
@ -296,10 +295,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
) )

View File

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

View File

@ -20,9 +20,9 @@
{% 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>

View File

@ -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 Property, 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,10 @@ 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 = xp.document.get_data()
uuid = json.loads(snap).get('uuid') uuid = json.loads(snap).get('uuid')
return Device.get_annotation_from_uuid(uuid, self.request.user.institution) 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 +95,8 @@ 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, type=Property.Type.SYSTEM,
owner=self.request.user.institution owner=self.request.user.institution
).filter( ).filter(
qry qry

View File

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

View File

@ -1,7 +1,7 @@
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, Property, Evidence
from lot.models import DeviceLot from lot.models import DeviceLot
@ -29,7 +29,7 @@ class Device:
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 +38,60 @@ 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, type=Property.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
) )
return annotations return user_properties
def get_user_documents(self): def get_user_documents(self):
if not self.uuids: if not self.uuids:
self.get_uuids() self.get_uuids()
annotations = Annotation.objects.filter( properties = SystemProperty.objects.filter(
uuid__in=self.uuids, uuid__in=self.uuids,
owner=self.owner, owner=self.owner,
type=Annotation.Type.DOCUMENT type=Property.Type.DOCUMENT
) )
return annotations return properties
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, type=Property.Type.SYSTEM,
key__in=algos, key__in=algos,
).values_list("value", flat=True))) ).values_list("value", flat=True)))
@ -103,11 +102,12 @@ 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() properties = self.get_properties()
if not annotations.count(): if not properties.count():
return return
annotation = annotations.first() property = properties.first()
self.last_evidence = Evidence(annotation.uuid)
self.last_evidence = Evidence(property.uuid)
def is_eraseserver(self): def is_eraseserver(self):
if not self.uuids: if not self.uuids:
@ -115,13 +115,13 @@ class Device:
if not self.uuids: if not self.uuids:
return False return False
annotation = Annotation.objects.filter( property = SystemProperty.objects.filter(
uuid__in=self.uuids, uuid__in=self.uuids,
owner=self.owner, owner=self.owner,
type=Annotation.Type.ERASE_SERVER type=Property.Type.ERASE_SERVER
).first() ).first()
if annotation: if property:
return True return True
return False return False
@ -136,7 +136,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,
@ -149,8 +149,8 @@ class Device:
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}
@ -159,24 +159,24 @@ class Device:
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, type=Property.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 +184,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,
@ -198,30 +198,30 @@ class Device:
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.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, type=Property.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,
@ -235,31 +235,31 @@ class Device:
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.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, type=Property.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):

View File

@ -15,7 +15,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 'User properties' %}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="#documents" class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">{% trans 'Documents' %}</a> <a href="#documents" class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">{% trans 'Documents' %}</a>
@ -100,15 +100,15 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="tab-pane fade" id="annotations"> <div class="tab-pane fade" id="user_properties">
<div class="btn-group mt-1 mb-3"> <div class="btn-group mt-1 mb-3">
<a href="{% url 'device:add_annotation' object.pk %}" class="btn btn-primary"> <a href="{% url 'device:add_user_property' object.pk %}" class="btn btn-primary">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
{% trans 'Add new annotation' %} {% trans 'New user property' %}
</a> </a>
</div> </div>
<h5 class="card-title">{% trans 'Annotations' %}</h5> <h5 class="card-title">{% trans 'User properties' %}</h5>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -122,23 +122,87 @@
{% trans 'Created on' %} {% trans 'Created on' %}
</th> </th>
<th></th> <th></th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for a in object.get_user_annotations %} {% for a in object.get_user_properties %}
<tr> <tr>
<td>{{ a.key }}</td> <td>{{ a.key }}</td>
<td>{{ a.value }}</td> <td>{{ a.value }}</td>
<td>{{ a.created }}</td> <td>{{ a.created }}</td>
<td></td> <td>
<td></td> <div class="btn-group float-end">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#editModal{{ a.id }}">
<i class="bi bi-pencil"></i> {% trans 'Edit' %}
</button>
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ a.id }}">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- pop up modal for delete confirmation -->
{% for a in object.get_user_properties %}
<div class="modal fade" id="deleteModal{{ a.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ a.id }}" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel{{ a.id }}">{% trans "Confirm Deletion" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p><strong>{% trans "Key:" %}</strong> {{ a.key }}</p>
<p><strong>{% trans "Value:" %}</strong> {{ a.value }}</p>
<p><strong>{% trans "Created on:" %}</strong> {{ a.created }}</p>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<form method="post" action="{% url 'device:delete_user_property' a.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- popup modals for edit button -->
{% for a in object.get_user_properties %}
<div class="modal fade" id="editModal{{ a.id }}" tabindex="-1" aria-labelledby="editModalLabel{{ a.id }}" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel{{ a.id }}">{% trans "Edit User Property" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editForm{{ a.id }}" method="post" action="{% url 'device:update_user_property' a.id %}">
{% csrf_token %}
<div class="mb-3">
<label for="key" class="form-label">{% trans "Key" %}</label>
<input type="text" class="form-control" id="key" name="key" value="{{ a.key }}">
</div>
<div class="mb-3">
<label for="value" class="form-label">{% trans "Value" %}</label>
<input type="text" class="form-control" id="value" name="value" value="{{ a.value }}">
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Save changes" %}</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
<div class="tab-pane fade" id="documents"> <div class="tab-pane fade" id="documents">
<div class="btn-group mt-1 mb-3"> <div class="btn-group mt-1 mb-3">
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary"> <a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">

View File

@ -7,7 +7,9 @@ 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", views.AddUserPropertyView.as_view(), name="add_user_property"),
path("user_property/<int:pk>/delete", views.DeleteUserPropertyView.as_view(), name="delete_user_property"),
path("user_property/<int:pk>/update", views.UpdateUserPropertyView.as_view(), name="update_user_property"),
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"), path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"),
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"), path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),

View File

@ -1,23 +1,28 @@
import json import json
import logging
from django.http import JsonResponse 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
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 dashboard.mixins import DashboardView, Http403 from dashboard.mixins import DashboardView, Http403
from evidence.models import Annotation from evidence.models import UserProperty, SystemProperty, Property
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
device_logger = logging.getLogger('device_log')
class NewDeviceView(DashboardView, FormView): class NewDeviceView(DashboardView, FormView):
template_name = "new_device.html" template_name = "new_device.html"
title = _("New Device") title = _("New Device")
@ -69,7 +74,7 @@ class EditDeviceView(DashboardView, UpdateView):
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 +92,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']
@ -167,65 +172,128 @@ class PublicDeviceWebView(TemplateView):
return JsonResponse(device_data) return JsonResponse(device_data)
class AddAnnotationView(DashboardView, CreateView): class AddUserPropertyView(DashboardView, 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') success_url = reverse_lazy('dashboard:unassigned_devices')
model = Annotation model = UserProperty
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 = Property.Type.USER
messages.success(self.request, _("User property successfully added."))
response = super().form_valid(form) response = super().form_valid(form)
return response 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.property = SystemProperty.objects.filter(
owner=institution, owner=institution,
value=pk, value=pk,
type=Annotation.Type.SYSTEM type=Property.Type.SYSTEM
).first() ).first()
if not self.annotation: if not self.property:
raise Http404 raise Http404
self.success_url = reverse_lazy('device:details', args=[pk]) self.success_url = reverse_lazy('device:details', args=[pk])
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
return kwargs return kwargs
class UpdateUserPropertyView(DashboardView, UpdateView):
template_name = "new_user_property.html"
title = _("Update User Property")
breadcrumb = "Device / Update Property"
model = UserProperty
fields = ("key", "value")
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
user_property = get_object_or_404(UserProperty, pk=pk, owner=self.request.user.institution)
if not user_property:
raise Http404
kwargs = super().get_form_kwargs()
kwargs['instance'] = user_property
return kwargs
def form_valid(self, form):
old_key= self.object.key
old_value = self.object.value
new_key = form.cleaned_data['key']
new_value = form.cleaned_data['value']
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
form.instance.type = Property.Type.USER
response = super().form_valid(form)
messages.success(self.request, _("User property updated successfully."))
device_logger.info(
f"Updated property from (key='{old_key}', value='{old_value}') to (key='{new_key}', value='{new_value}') by user {self.request.user}."
)
return response
def get_success_url(self):
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
class DeleteUserPropertyView(DashboardView, DeleteView):
model = UserProperty
def post(self, request, *args, **kwargs):
self.pk = kwargs['pk']
referer = request.META.get('HTTP_REFERER')
if not referer:
raise Http404("No referer header found")
self.object = get_object_or_404(
self.model,
pk=self.pk,
owner=self.request.user.institution
)
old_value = self.object.key
self.object.delete()
device_logger.info(f"Deleted property with key '{old_value}' by user {self.request.user}.")
messages.success(self.request, _("User property deleted successfully."))
# Redirect back to the original URL
return redirect(referer)
class AddDocumentView(DashboardView, CreateView): class AddDocumentView(DashboardView, CreateView):
template_name = "new_annotation.html" template_name = "new_user_property.html"
title = _("New Document") title = _("New Document")
breadcrumb = "Device / New document" breadcrumb = "Device / New document"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned_devices')
model = Annotation model = SystemProperty
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.DOCUMENT form.instance.type = Property.Type.DOCUMENT
response = super().form_valid(form) response = super().form_valid(form)
return response 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.property = SystemProperty.objects.filter(
owner=institution, owner=institution,
value=pk, value=pk,
type=Annotation.Type.SYSTEM type=Property.Type.SYSTEM
).first() ).first()
if not self.annotation: if not self.property:
raise Http404 raise Http404
self.success_url = reverse_lazy('device:details', args=[pk]) self.success_url = reverse_lazy('device:details', args=[pk])

View File

@ -210,6 +210,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": {
@ -217,6 +221,12 @@ LOGGING = {
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "colored" "formatter": "colored"
}, },
'device_log_file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/var/log/device_changes.log',
'formatter': 'verbose',
},
}, },
"root": { "root": {
"handlers": ["console"], "handlers": ["console"],
@ -232,7 +242,12 @@ LOGGING = {
"handlers": ["console"], "handlers": ["console"],
"level": "ERROR", "level": "ERROR",
"propagate": False, "propagate": False,
} },
'device_log': {
'handlers': ['device_log_file'],
'level': 'INFO',
'propagate': False,
},
} }
} }

View File

@ -4,11 +4,11 @@ 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, Property
from utils.save_snapshots import move_json, save_in_disk from utils.save_snapshots import move_json, save_in_disk
@ -30,11 +30,11 @@ class UploadForm(forms.Form):
try: try:
file_json = json.loads(file_data) file_json = json.loads(file_data)
Build(file_json, None, check=True) Build(file_json, None, check=True)
exist_annotation = Annotation.objects.filter( exists_property = SystemProperty.objects.filter(
uuid=file_json['uuid'] uuid=file_json['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",
@ -68,9 +68,9 @@ 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, type=Property.Type.SYSTEM,
key='CUSTOM_ID', key='CUSTOM_ID',
owner=self.user.institution owner=self.user.institution
).first() ).first()
@ -86,9 +86,9 @@ 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, type=Property.Type.SYSTEM,
key='CUSTOM_ID', key='CUSTOM_ID',
owner=self.user.institution owner=self.user.institution
).first() ).first()
@ -106,9 +106,9 @@ class UserTagForm(forms.Form):
self.instance.save() self.instance.save()
return return
Annotation.objects.create( SystemProperty.objects.create(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.SYSTEM, type=Property.Type.SYSTEM,
key='CUSTOM_ID', key='CUSTOM_ID',
value=self.tag, value=self.tag,
owner=self.user.institution, owner=self.user.institution,
@ -164,8 +164,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 +186,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 = SystemProperty.objects.filter(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER, type=SystemProperty.Type.ERASE_SERVER,
key='ERASE_SERVER', key='ERASE_SERVER',
owner=self.user.institution owner=self.user.institution
).first() ).first()
@ -201,9 +201,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 = SystemProperty.objects.filter(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER, type=Property.Type.ERASE_SERVER,
key='ERASE_SERVER', key='ERASE_SERVER',
owner=self.user.institution owner=self.user.institution
).first() ).first()
@ -222,9 +222,9 @@ class EraseServerForm(forms.Form):
if self.instance: if self.instance:
return return
Annotation.objects.create( SystemProperty.objects.create(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER, type=Property.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,

View File

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

View File

@ -3,13 +3,15 @@ import json
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 user.models import User, Institution from user.models import User, Institution
#TODO: base class is abstract; revise if should be for query efficiency
class Annotation(models.Model): class Property(models.Model):
class Type(models.IntegerChoices): class Type(models.IntegerChoices):
SYSTEM = 0, "System" SYSTEM = 0, "System"
USER = 1, "User" USER = 1, "User"
@ -26,11 +28,42 @@ class Annotation(models.Model):
value = models.CharField(max_length=STR_EXTEND_SIZE) value = models.CharField(max_length=STR_EXTEND_SIZE)
class Meta: class Meta:
#Only for shared behaviour, it is not a table
abstract = True
class SystemProperty(Property):
class Meta:
constraints = [
models.CheckConstraint(
check=~Q(type=1), #Enforce that type is not User
name='property_cannot_be_user'
),
]
#Django orm wont inherit constraints to child
#TODO: check if this is needed
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=["type", "key", "uuid"], name="unique_type_key_uuid") fields=["type", "key", "uuid"], name="system_unique_type_key_uuid")
] ]
class UserProperty(Property):
type = models.SmallIntegerField(default=Property.Type.USER)
class Meta:
constraints = [
models.CheckConstraint(
check=Q(type=1), #Enforce that type is User
name='property_needs_to_be_user'
),
]
constraints = [
models.UniqueConstraint(
fields=["type", "key", "uuid"], name="user_unique_type_key_uuid")
]
class Evidence: class Evidence:
def __init__(self, uuid): def __init__(self, uuid):
@ -39,22 +72,22 @@ class Evidence:
self.doc = None self.doc = None
self.created = None self.created = None
self.dmi = None self.dmi = None
self.annotations = [] 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
@ -80,7 +113,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():
@ -131,9 +164,9 @@ class Evidence:
@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, type=Property.Type.SYSTEM,
key="hidalgo1", key="hidalgo1",
).order_by("-created").values_list("uuid", "created").distinct() ).order_by("-created").values_list("uuid", "created").distinct()

View File

@ -6,7 +6,7 @@ from dmidecode import DMIParse
from json_repair import repair_json from json_repair import repair_json
from evidence.parse_details import get_lshw_child from evidence.parse_details import get_lshw_child
from evidence.models import Annotation from evidence.models import SystemProperty, Property
from evidence.xapian import index from evidence.xapian import index
from utils.constants import CHASSIS_DH from utils.constants import CHASSIS_DH
@ -46,7 +46,7 @@ class Build:
return return
self.index() self.index()
self.create_annotations() self.create_properties()
def index(self): def index(self):
snap = json.dumps(self.json) snap = json.dumps(self.json)
@ -72,24 +72,24 @@ class Build:
return hashlib.sha3_256(hid.encode()).hexdigest() return hashlib.sha3_256(hid.encode()).hexdigest()
def create_annotations(self): def create_properties(self):
annotation = Annotation.objects.filter( property = SystemProperty.objects.filter(
uuid=self.uuid, uuid=self.uuid,
owner=self.user.institution, owner=self.user.institution,
type=Annotation.Type.SYSTEM, type=Property.Type.SYSTEM,
) )
if annotation: if property:
txt = "Warning: Snapshot %s already registered (annotation exists)" txt = "Warning: Snapshot %s already registered (property 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.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, type=Property.Type.SYSTEM,
key=k, key=k,
value=v value=v
) )

View File

@ -45,7 +45,7 @@
</th> </th>
</tr> </tr>
</thead> </thead>
{% for snap in object.annotations %} {% for snap in object.properties %}
<tbody> <tbody>
{% if snap.type == 0 %} {% if snap.type == 0 %}
<tr> <tr>
@ -94,7 +94,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_user_property' form.pk %}">{% translate "Delete" %}</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -20,5 +20,4 @@ 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'),
] ]

View File

@ -13,7 +13,7 @@ from django.views.generic.edit import (
) )
from dashboard.mixins import DashboardView, Http403 from dashboard.mixins import DashboardView, Http403
from evidence.models import Evidence, Annotation from evidence.models import Property, SystemProperty, UserProperty, Evidence
from evidence.forms import ( from evidence.forms import (
UploadForm, UploadForm,
UserTagForm, UserTagForm,
@ -95,7 +95,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):
@ -141,31 +141,6 @@ class DownloadEvidenceView(DashboardView, TemplateView):
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):
@ -182,7 +157,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):

View File

@ -7,8 +7,8 @@ from utils.constants import (
) )
from user.models import User, Institution from user.models import User, Institution
from device.models import Property
# from device.models import Device # from device.models import Device
# from evidence.models import Annotation
class LotTag(models.Model): class LotTag(models.Model):
@ -45,17 +45,8 @@ class Lot(models.Model):
for d in DeviceLot.objects.filter(lot=self, device_id=v): for d in DeviceLot.objects.filter(lot=self, device_id=v):
d.delete() d.delete()
class LotProperty (Property):
class LotAnnotation(models.Model): #uuid is not needed for id
class Type(models.IntegerChoices): uuid = None
SYSTEM= 0, "System" #lot foreign key is
USER = 1, "User"
DOCUMENT = 2, "Document"
created = models.DateTimeField(auto_now_add=True)
lot = models.ForeignKey(Lot, on_delete=models.CASCADE) lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.SmallIntegerField(choices=Type)
key = models.CharField(max_length=STR_EXTEND_SIZE)
value = models.CharField(max_length=STR_EXTEND_SIZE)

View File

@ -11,15 +11,15 @@
<div class="row"> <div class="row">
<div class="tab-pane fade show active" id="details"> <div class="tab-pane fade show active" id="details">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown=""> <div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="{% url 'lot:add_annotation' lot.pk %}" class="btn btn-primary"> <a href="{% url 'lot:add_property' lot.pk %}" class="btn btn-primary">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Add new annotation Add new lot Property
<span class="caret"></span> <span class="caret"></span>
</a> </a>
</div> </div>
<h5 class="card-title mt-2">Annotations</h5> <h5 class="card-title mt-2">Properties</h5>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -31,7 +31,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for a in annotations %} {% for a in properties %}
<tr> <tr>
<td>{{ a.key }}</td> <td>{{ a.key }}</td>
<td>{{ a.value }}</td> <td>{{ a.value }}</td>

View File

@ -12,6 +12,6 @@ urlpatterns = [
path("tag/<int:pk>/", views.LotsTagsView.as_view(), name="tag"), path("tag/<int:pk>/", views.LotsTagsView.as_view(), name="tag"),
path("<int:pk>/document/", views.LotDocumentsView.as_view(), name="documents"), path("<int:pk>/document/", views.LotDocumentsView.as_view(), name="documents"),
path("<int:pk>/document/add", views.LotAddDocumentView.as_view(), name="add_document"), path("<int:pk>/document/add", views.LotAddDocumentView.as_view(), name="add_document"),
path("<int:pk>/annotation", views.LotAnnotationsView.as_view(), name="annotations"), path("<int:pk>/property", views.LotPropertiesView.as_view(), name="properties"),
path("<int:pk>/annotation/add", views.LotAddAnnotationView.as_view(), name="add_annotation"), path("<int:pk>/property/add", views.LotAddPropertyView.as_view(), name="add_property"),
] ]

View File

@ -9,9 +9,9 @@ from django.views.generic.edit import (
FormView, FormView,
) )
from dashboard.mixins import DashboardView from dashboard.mixins import DashboardView
from lot.models import Lot, LotTag, LotAnnotation from lot.models import Lot, LotTag, LotProperty
from lot.forms import LotsForm from lot.forms import LotsForm
from device.models import Property
class NewLotView(DashboardView, CreateView): class NewLotView(DashboardView, CreateView):
template_name = "new_lot.html" template_name = "new_lot.html"
@ -143,18 +143,18 @@ class LotsTagsView(DashboardView, TemplateView):
class LotAddDocumentView(DashboardView, CreateView): class LotAddDocumentView(DashboardView, CreateView):
template_name = "new_annotation.html" template_name = "new_property.html"
title = _("New Document") title = _("New Document")
breadcrumb = "Device / New document" breadcrumb = "Device / New document"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned_devices')
model = LotAnnotation model = LotProperty
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.lot = self.lot form.instance.lot = self.lot
form.instance.type = LotAnnotation.Type.DOCUMENT form.instance.type = Property.Type.DOCUMENT
response = super().form_valid(form) response = super().form_valid(form)
return response return response
@ -169,16 +169,16 @@ class LotAddDocumentView(DashboardView, CreateView):
class LotDocumentsView(DashboardView, TemplateView): class LotDocumentsView(DashboardView, TemplateView):
template_name = "documents.html" template_name = "documents.html"
title = _("New Document") title = _("New Document")
breadcrumb = "Device / New document" breadcrumb = "Devicce / New document"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
self.pk = kwargs.get('pk') self.pk = kwargs.get('pk')
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
lot = get_object_or_404(Lot, owner=self.request.user.institution, id=self.pk) lot = get_object_or_404(Lot, owner=self.request.user.institution, id=self.pk)
documents = LotAnnotation.objects.filter( documents = LotProperty.objects.filter(
lot=lot, lot=lot,
owner=self.request.user.institution, owner=self.request.user.institution,
type=LotAnnotation.Type.DOCUMENT, type=Property.Type.DOCUMENT,
) )
context.update({ context.update({
'lot': lot, 'lot': lot,
@ -189,48 +189,48 @@ class LotDocumentsView(DashboardView, TemplateView):
return context return context
class LotAnnotationsView(DashboardView, TemplateView): class LotPropertiesView(DashboardView, TemplateView):
template_name = "annotations.html" template_name = "properties.html"
title = _("New Annotation") title = _("New Lot Property")
breadcrumb = "Device / New annotation" breadcrumb = "Lot / New property"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
self.pk = kwargs.get('pk') self.pk = kwargs.get('pk')
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
lot = get_object_or_404(Lot, owner=self.request.user.institution, id=self.pk) lot = get_object_or_404(Lot, owner=self.request.user.institution, id=self.pk)
annotations = LotAnnotation.objects.filter( properties = LotProperty.objects.filter(
lot=lot, lot=lot,
owner=self.request.user.institution, owner=self.request.user.institution,
type=LotAnnotation.Type.USER, type=Property.Type.USER,
) )
context.update({ context.update({
'lot': lot, 'lot': lot,
'annotations': annotations, 'properties': properties,
'title': self.title, 'title': self.title,
'breadcrumb': self.breadcrumb 'breadcrumb': self.breadcrumb
}) })
return context return context
class LotAddAnnotationView(DashboardView, CreateView): class LotAddPropertyView(DashboardView, CreateView):
template_name = "new_annotation.html" template_name = "new_property.html"
title = _("New Annotation") title = _("New Lot Property")
breadcrumb = "Device / New annotation" breadcrumb = "Device / New property"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned_devices')
model = LotAnnotation model = LotProperty
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.lot = self.lot form.instance.lot = self.lot
form.instance.type = LotAnnotation.Type.USER form.instance.type = Property.Type.USER
response = super().form_valid(form) response = super().form_valid(form)
return response return response
def get_form_kwargs(self): def get_form_kwargs(self):
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
self.lot = get_object_or_404(Lot, pk=pk, owner=self.request.user.institution) self.lot = get_object_or_404(Lot, pk=pk, owner=self.request.user.institution)
self.success_url = reverse_lazy('lot:annotations', args=[pk]) self.success_url = reverse_lazy('lot:properties', args=[pk])
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
return kwargs return kwargs

View File

@ -6,7 +6,7 @@ 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 SystemProperty, Property
from device.models import Device from device.models import Device
@ -68,7 +68,7 @@ def create_doc(data):
return doc return doc
def create_annotation(doc, user, commit=False): def create_property(doc, user, commit=False):
if not doc or not doc.get('uuid') or not doc.get("CUSTOMER_ID"): if not doc or not doc.get('uuid') or not doc.get("CUSTOMER_ID"):
return [] return []
@ -76,25 +76,25 @@ def create_annotation(doc, user, commit=False):
'uuid': doc['uuid'], 'uuid': doc['uuid'],
'owner': user.institution, 'owner': user.institution,
'user': user, 'user': user,
'type': Annotation.Type.SYSTEM, 'type': Property.Type.SYSTEM,
'key': 'CUSTOMER_ID', 'key': 'CUSTOMER_ID',
'value': doc['CUSTOMER_ID'], 'value': doc['CUSTOMER_ID'],
} }
if commit: if commit:
annotation = Annotation.objects.filter( property = SystemProperty.objects.filter(
uuid=doc["uuid"], uuid=doc["uuid"],
owner=user.institution, owner=user.institution,
type=Annotation.Type.SYSTEM, type=Property.Type.SYSTEM,
) )
if annotation: if property:
txt = "Warning: Snapshot %s already registered (annotation exists)" txt = "Warning: Snapshot %s already registered (system property exists)"
logger.warning(txt, doc["uuid"]) logger.warning(txt, doc["uuid"])
return annotation return property
return Annotation.objects.create(**data) return SystemProperty.objects.create(**data)
return Annotation(**data) return SystemProperty(**data)
def create_index(doc, user): def create_index(doc, user):