WIP: Changed annotation syntax and added States/StatesDefinitions #37
16
action/forms.py
Normal file
16
action/forms.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django import forms
|
||||||
|
from .models import State
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeStateForm(forms.Form):
|
||||||
|
previous_state = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
snapshot_uuid = forms.UUIDField(widget=forms.HiddenInput())
|
||||||
|
new_state = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
|
||||||
|
|
||||||
|
class AddNoteForm(forms.Form):
|
||||||
|
snapshot_uuid = forms.UUIDField(widget=forms.HiddenInput())
|
||||||
|
note = forms.CharField(
|
||||||
|
required=True,
|
||||||
|
widget=forms.Textarea(attrs={'rows': 4, 'maxlength': 200, 'placeholder': 'Max 200 characters'}),
|
||||||
|
)
|
43
action/management/commands/create_default_states.py
Normal file
43
action/management/commands/create_default_states.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import logging
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from action.models import StateDefinition, Institution
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Create default StateDefinitions for a given institution. "'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('institution_name', type=str, help='The name of the institution')
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
default_states = [
|
||||||
|
_("INBOX"),
|
||||||
|
_("VISUAL INSPECTION"),
|
||||||
|
_("REPAIR"),
|
||||||
|
_("INSTALL"),
|
||||||
|
_("TEST"),
|
||||||
|
_("PACKAGING"),
|
||||||
|
_("DONATION"),
|
||||||
|
_("DISMANTLE")
|
||||||
|
]
|
||||||
|
|
||||||
|
institution_name = kwargs['institution_name']
|
||||||
|
institution = Institution.objects.filter(name=institution_name).first()
|
||||||
|
|
||||||
|
if not institution:
|
||||||
|
txt = "No institution found for: %s. Please create an institution first"
|
||||||
|
logger.error(txt, institution.name)
|
||||||
|
return
|
||||||
|
|
||||||
|
for state in default_states:
|
||||||
|
state_def, created = StateDefinition.objects.get_or_create(
|
||||||
|
institution=institution,
|
||||||
|
state=state
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'Successfully created state: {state}'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(f'State already exists: {state}'))
|
83
action/migrations/0001_initial.py
Normal file
83
action/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-11 18:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="State",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("state", models.CharField(max_length=50)),
|
||||||
|
("snapshot_uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"institution",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="StateDefinition",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.PositiveIntegerField(default=0)),
|
||||||
|
("state", models.CharField(max_length=50)),
|
||||||
|
(
|
||||||
|
"institution",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["order"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="statedefinition",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("institution", "state"), name="unique_institution_state"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
89
action/migrations/0002_devicelog_note.py
Normal file
89
action/migrations/0002_devicelog_note.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-17 19:40
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("action", "0001_initial"),
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="DeviceLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("event", models.CharField(max_length=255)),
|
||||||
|
("snapshot_uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"institution",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-date"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Note",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("description", models.TextField()),
|
||||||
|
("snapshot_uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"institution",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-date"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,3 +1,82 @@
|
||||||
from django.db import models
|
from django.db import models, connection
|
||||||
|
from django.db.models import Max
|
||||||
|
from user.models import User, Institution
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
# Create your models here.
|
class State(models.Model):
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
institution = models.ForeignKey(Institution, on_delete=models.SET_NULL, null=True)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||||
|
|
||||||
|
state = models.CharField(max_length=50)
|
||||||
|
snapshot_uuid = models.UUIDField()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not StateDefinition.objects.filter(institution=self.institution, state=self.state).exists():
|
||||||
|
raise ValidationError(f"The state '{self.state}' is not valid for the institution '{self.institution.name}'.")
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.institution.name} - {self.state} - {self.snapshot_uuid}"
|
||||||
|
|
||||||
|
class StateDefinition(models.Model):
|
||||||
|
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
order = models.PositiveIntegerField(default=0)
|
||||||
|
state = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['order']
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['institution', 'state'], name='unique_institution_state')
|
||||||
|
]
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pk:
|
||||||
|
# set the order to be last
|
||||||
|
max_order = StateDefinition.objects.filter(institution=self.institution).aggregate(Max('order'))['order__max']
|
||||||
|
self.order = (max_order or 0) + 1
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
institution = self.institution
|
||||||
|
order = self.order
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
# Adjust the order of other instances
|
||||||
|
StateDefinition.objects.filter(institution=institution, order__gt=order).update(order=models.F('order') - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.institution.name} - {self.state}"
|
||||||
|
|
||||||
|
|
||||||
|
class Note(models.Model):
|
||||||
|
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
description = models.TextField()
|
||||||
|
snapshot_uuid = models.UUIDField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-date']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f" Note: {self.description}, by {self.user.username} @ {self.user.institution} - {self.date}, for {self.snapshot_uuid}"
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceLog(models.Model):
|
||||||
|
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
event = models.CharField(max_length=255)
|
||||||
|
snapshot_uuid = models.UUIDField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-date']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.event} by {self.user.username} @ {self.institution.name} - {self.date}, for {self.snapshot_uuid}"
|
||||||
|
|
|
@ -1 +1,12 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
from action import views
|
||||||
|
|
||||||
|
app_name = 'action'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
|
||||||
|
path("new/", views.ChangeStateView.as_view(), name="change_state"),
|
||||||
|
path('state/<int:pk>/undo/', views.UndoStateView.as_view(), name='undo_state'),
|
||||||
|
path('note/add/', views.AddNoteView.as_view(), name='add_note'),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
|
@ -1 +1,82 @@
|
||||||
# from django.shortcuts import render
|
from django.views import View
|
||||||
|
from django.shortcuts import redirect, get_object_or_404
|
||||||
|
from django.contrib import messages
|
||||||
|
from action.forms import ChangeStateForm, AddNoteForm
|
||||||
|
from django.views.generic.edit import DeleteView, CreateView, FormView
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from action.models import State, StateDefinition, Note, DeviceLog
|
||||||
|
from device.models import Device
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeStateView(View):
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
form = ChangeStateForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
previous_state = form.cleaned_data['previous_state']
|
||||||
|
new_state = form.cleaned_data['new_state']
|
||||||
|
snapshot_uuid = form.cleaned_data['snapshot_uuid']
|
||||||
|
|
||||||
|
State.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
state=new_state,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
|
||||||
|
message = _("<Created> State '{}'. Previous State: '{}' ".format(new_state, previous_state) )
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
messages.success(request,message)
|
||||||
|
else:
|
||||||
|
messages.error(request, "There was an error with your submission.")
|
||||||
|
|
||||||
|
return redirect(request.META.get('HTTP_REFERER') )
|
||||||
|
|
||||||
|
|
||||||
|
class UndoStateView(DeleteView):
|
||||||
|
model = State
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
return super().delete(request, *args, **kwarg)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
messages.info(self.request, f"Action to state: {self.object.state} has been deleted.")
|
||||||
|
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.snapshot_uuid]))
|
||||||
|
|
||||||
|
|
||||||
|
class AddNoteView(View):
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
form = AddNoteForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
note = form.cleaned_data['note']
|
||||||
|
snapshot_uuid = form.cleaned_data['snapshot_uuid']
|
||||||
|
Note.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
description=note,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
|
||||||
|
message = _("<Created> Note: '{}' ".format(note) )
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=snapshot_uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution,
|
||||||
|
)
|
||||||
|
messages.success(request, _("Note has been added"))
|
||||||
|
else:
|
||||||
|
messages.error(request, "There was an error with your submission.")
|
||||||
|
|
||||||
|
return redirect(request.META.get('HTTP_REFERER') )
|
||||||
|
|
5
admin/forms.py
Normal file
5
admin/forms.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class OrderingStateForm(forms.Form):
|
||||||
|
ordering = forms.CharField()
|
225
admin/templates/states_panel.html
Normal file
225
admin/templates/states_panel.html
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n django_bootstrap5 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3>{{ subtitle }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col text-end">
|
||||||
|
<button type="button" class="btn btn-green-admin" data-bs-toggle="modal" data-bs-target="#addStateModal">
|
||||||
|
{% trans "Add" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col">
|
||||||
|
{% if state_definitions %}
|
||||||
|
<table class="table table-hover table-bordered align-middle">
|
||||||
|
<caption class="text-muted small">
|
||||||
|
{% trans 'Move and drag state definitions to reorder' %}
|
||||||
|
</caption>
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" width="5%" class="text-start">
|
||||||
|
</th>
|
||||||
|
<th scope="col">{% trans "State Definition" %}
|
||||||
|
</th>
|
||||||
|
<th scope="col" width="15%" class="text-center">{% trans "Actions" %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="sortable_list">
|
||||||
|
{% for state_definition in state_definitions %}
|
||||||
|
<tr
|
||||||
|
data-lookup="{{ state_definition.id }}"
|
||||||
|
style="cursor: grab;"
|
||||||
|
class="align-items-center">
|
||||||
|
|
||||||
|
<td class="text-center">
|
||||||
|
<i class="fas fa-light fa-grip" aria-hidden="true">
|
||||||
|
</i>
|
||||||
|
<strong class="ps-2">{{ state_definition.order }} </strong>
|
||||||
|
</td>
|
||||||
|
<td class="font-monospace">
|
||||||
|
{{ state_definition.state }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- action buttons -->
|
||||||
|
<td>
|
||||||
|
<div class="btn-group float-end">
|
||||||
|
<button type="button" class="btn btn-sm btn-info text-white" data-bs-toggle="modal" data-bs-target="#editStateModal{{ state_definition.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="#deleteStateModal{{ state_definition.id }}">
|
||||||
|
<i class="bi bi-trash">
|
||||||
|
</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form id="orderingForm" method="post" action="{% url 'admin:update_state_order' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" id="orderingInput" name="ordering">
|
||||||
|
<button id="saveOrderBtn" class="btn btn-success mt-5 float-start collapse" >{% trans "Update Order" %}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-primary text-center mt-5" role="alert">
|
||||||
|
{% trans "No states found on current organization" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- add state definition Modal -->
|
||||||
|
<div class="modal fade" id="addStateModal" tabindex="-1" aria-labelledby="addStateModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addStateModalLabel">{% trans "Add State Definition" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<form method="post" action="{%url 'admin:add_state_definition'%}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stateInput" class="form-label">{% trans "State" %}</label>
|
||||||
|
<input type="text" class="form-control" id="stateInput" name="state" maxlength="50" required>
|
||||||
|
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans "Add state definition" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit State Definition Modals -->
|
||||||
|
{% for state_definition in state_definitions %}
|
||||||
|
<div class="modal fade" id="editStateModal{{ state_definition.id }}" tabindex="-1" aria-labelledby="editStateModalLabel{{ state_definition.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" action="{% url 'admin:edit_state_definition' state_definition.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editStateModalLabel{{ state_definition.id }}">
|
||||||
|
{% trans "Edit State Definition" %}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-warning text-center" role="alert">
|
||||||
|
{% trans "Existing devices with this state will not have their state names changed." %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editStateInput{{ state_definition.id }}" class="form-label">{% trans "State" %}</label>
|
||||||
|
<input type="text" class="form-control" id="editStateInput{{ state_definition.id }}" name="state" maxlength="50" value="{{ state_definition.state }}" required>
|
||||||
|
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted text-end">{% trans "Any changes in order will not be saved." %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-green-admin">{% trans "Save Changes" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- delete state definition Modal -->
|
||||||
|
{% for state_definition in state_definitions %}
|
||||||
|
<div class="modal fade" id="deleteStateModal{{ state_definition.id }}" tabindex="-1" aria-labelledby="deleteStateModalLabel{{ state_definition.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold" id="deleteStateModalLabel{{ state_definition.id }}">
|
||||||
|
{% trans "Delete State Definition" %}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-warning text-center" role="alert">
|
||||||
|
{% trans "Devices with a State of this description will not have their State altered" %}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center border rounded p-3 mt-3">
|
||||||
|
<span class="badge bg-secondary me-3 display-6">{{ state_definition.order }}</span>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 fw-bold">{{ state_definition.state }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted text-end mt-3">{% trans "Any changes in order will not be saved." %}</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<form method="post" action="{% url 'admin:delete_state_definition' state_definition.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
{% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//following https://dev.to/nemecek_f/django-how-to-let-user-re-order-sort-table-of-content-with-drag-and-drop-3nlp
|
||||||
|
const saveOrderingButton = document.getElementById('saveOrderBtn');
|
||||||
|
const orderingForm = document.getElementById('orderingForm');
|
||||||
|
const formInput = orderingForm.querySelector('#orderingInput');
|
||||||
|
const sortable_table = document.getElementById('sortable_list');
|
||||||
|
|
||||||
|
const sortable = new Sortable(sortable_table, {
|
||||||
|
animation: 150,
|
||||||
|
swapThreshold: 0.10,
|
||||||
|
onChange: () => {
|
||||||
|
//TODO: change hide/show animation to a nicer one
|
||||||
|
const collapse = new bootstrap.Collapse(saveOrderingButton, {
|
||||||
|
toggle: false
|
||||||
|
});
|
||||||
|
collapse.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveOrdering() {
|
||||||
|
const rows = sortable_table.querySelectorAll('tr');
|
||||||
|
let ids = [];
|
||||||
|
for (let row of rows) {
|
||||||
|
ids.push(row.dataset.lookup);
|
||||||
|
}
|
||||||
|
formInput.value = ids.join(',');
|
||||||
|
orderingForm.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveOrderingButton.addEventListener('click', saveOrdering);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -10,4 +10,9 @@ urlpatterns = [
|
||||||
path("users/edit/<int:pk>", views.EditUserView.as_view(), name="edit_user"),
|
path("users/edit/<int:pk>", views.EditUserView.as_view(), name="edit_user"),
|
||||||
path("users/delete/<int:pk>", views.DeleteUserView.as_view(), name="delete_user"),
|
path("users/delete/<int:pk>", views.DeleteUserView.as_view(), name="delete_user"),
|
||||||
path("institution/<int:pk>", views.InstitutionView.as_view(), name="institution"),
|
path("institution/<int:pk>", views.InstitutionView.as_view(), name="institution"),
|
||||||
|
path("states/", views.StatesPanelView.as_view(), name="states_panel"),
|
||||||
|
path("states/add", views.AddStateDefinitionView.as_view(), name="add_state_definition"),
|
||||||
|
path("states/delete/<int:pk>", views.DeleteStateDefinitionView.as_view(), name='delete_state_definition'),
|
||||||
|
path("states/update_order/", views.UpdateStateOrderView.as_view(), name='update_state_order'),
|
||||||
|
path("states/edit/<int:pk>/", views.UpdateStateDefinitionView.as_view(), name='edit_state_definition'),
|
||||||
]
|
]
|
||||||
|
|
109
admin/views.py
109
admin/views.py
|
@ -1,16 +1,23 @@
|
||||||
|
import logging
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
from django.contrib import messages
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic.base import TemplateView
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.views.generic.base import TemplateView, ContextMixin
|
||||||
from django.views.generic.edit import (
|
from django.views.generic.edit import (
|
||||||
CreateView,
|
CreateView,
|
||||||
UpdateView,
|
UpdateView,
|
||||||
DeleteView,
|
DeleteView,
|
||||||
)
|
)
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import IntegrityError, transaction
|
||||||
from dashboard.mixins import DashboardView, Http403
|
from dashboard.mixins import DashboardView, Http403
|
||||||
|
from admin.forms import OrderingStateForm
|
||||||
from user.models import User, Institution
|
from user.models import User, Institution
|
||||||
from admin.email import NotifyActivateUserByEmail
|
from admin.email import NotifyActivateUserByEmail
|
||||||
|
from action.models import StateDefinition
|
||||||
|
|
||||||
|
|
||||||
class AdminView(DashboardView):
|
class AdminView(DashboardView):
|
||||||
|
@ -124,3 +131,101 @@ class InstitutionView(AdminView, UpdateView):
|
||||||
self.object = self.request.user.institution
|
self.object = self.request.user.institution
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class StateDefinitionContextMixin(ContextMixin):
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context.update({
|
||||||
|
"state_definitions": StateDefinition.objects.filter(institution=self.request.user.institution).order_by('order'),
|
||||||
|
"help_text": _('State definitions are the custom finite states that a device can be in.'),
|
||||||
|
})
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class StatesPanelView(AdminView, StateDefinitionContextMixin, TemplateView):
|
||||||
|
template_name = "states_panel.html"
|
||||||
|
title = _("States Panel")
|
||||||
|
breadcrumb = _("admin / States Panel") + " /"
|
||||||
|
|
||||||
|
|
||||||
|
class AddStateDefinitionView(AdminView, StateDefinitionContextMixin, CreateView):
|
||||||
|
template_name = "states_panel.html"
|
||||||
|
title = _("New State Definition")
|
||||||
|
breadcrumb = "Admin / New state"
|
||||||
|
success_url = reverse_lazy('admin:states_panel')
|
||||||
|
model = StateDefinition
|
||||||
|
fields = ('state',)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.institution = self.request.user.institution
|
||||||
|
form.instance.user = self.request.user
|
||||||
|
try:
|
||||||
|
response = super().form_valid(form)
|
||||||
|
messages.success(self.request, _("State definition successfully added."))
|
||||||
|
return response
|
||||||
|
except IntegrityError:
|
||||||
|
messages.error(self.request, _("State is already defined."))
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessMessageMixin, DeleteView):
|
||||||
|
model = StateDefinition
|
||||||
|
success_url = reverse_lazy('admin:states_panel')
|
||||||
|
|
||||||
|
def get_success_message(self, cleaned_data):
|
||||||
|
return f'State definition: {self.object.state}, has been deleted'
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
|
||||||
|
#only an admin of current institution can delete
|
||||||
|
if not object.institution == self.request.user.institution:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateStateOrderView(AdminView, TemplateView):
|
||||||
|
success_url = reverse_lazy('admin:states_panel')
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
form = OrderingStateForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
ordered_ids = form.cleaned_data["ordering"].split(',')
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
current_order = 1
|
||||||
|
_log = []
|
||||||
|
for lookup_id in ordered_ids:
|
||||||
|
state_definition = StateDefinition.objects.get(id=lookup_id)
|
||||||
|
state_definition.order = current_order
|
||||||
|
state_definition.save()
|
||||||
|
_log.append(f"{state_definition.state} (ID: {lookup_id} -> Order: {current_order})")
|
||||||
|
current_order += 1
|
||||||
|
|
||||||
|
messages.success(self.request, _("Order changed succesfuly."))
|
||||||
|
return redirect(self.success_url)
|
||||||
|
else:
|
||||||
|
return Http404
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateStateDefinitionView(AdminView, UpdateView):
|
||||||
|
model = StateDefinition
|
||||||
|
template_name = 'states_panel.html'
|
||||||
|
fields = ['state']
|
||||||
|
pk_url_kwarg = 'pk'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return StateDefinition.objects.filter(institution=self.request.user.institution)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
messages.success(self.request, _("State definition updated successfully."))
|
||||||
|
return reverse_lazy('admin:states_panel')
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
36
api/views.py
36
api/views.py
|
@ -21,7 +21,7 @@ from django.views.generic.edit import (
|
||||||
from utils.save_snapshots import move_json, save_in_disk
|
from utils.save_snapshots import move_json, save_in_disk
|
||||||
from django.views.generic.edit import View
|
from django.views.generic.edit import View
|
||||||
from dashboard.mixins import DashboardView
|
from dashboard.mixins import DashboardView
|
||||||
from evidence.models import Annotation
|
from evidence.models import SystemProperty, UserProperty
|
||||||
from evidence.parse_details import ParseSnapshot
|
from evidence.parse_details import ParseSnapshot
|
||||||
from evidence.parse import Build
|
from evidence.parse import Build
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
|
@ -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,24 @@ 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,
|
|
||||||
# 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 +254,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 +277,12 @@ class AddAnnotationView(ApiMixing):
|
||||||
|
|
||||||
self.pk = kwargs['pk']
|
self.pk = kwargs['pk']
|
||||||
institution = self.tk.owner.institution
|
institution = self.tk.owner.institution
|
||||||
self.annotation = Annotation.objects.filter(
|
self.property = SystemProperty.objects.filter(
|
||||||
owner=institution,
|
owner=institution,
|
||||||
value=self.pk,
|
value=self.pk,
|
||||||
type=Annotation.Type.SYSTEM
|
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not self.annotation:
|
if not self.property:
|
||||||
return JsonResponse({}, status=404)
|
return JsonResponse({}, status=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -296,10 +293,9 @@ class AddAnnotationView(ApiMixing):
|
||||||
logger.error("Invalid Snapshot of user %s", self.tk.owner)
|
logger.error("Invalid Snapshot of user %s", self.tk.owner)
|
||||||
return JsonResponse({'error': 'Invalid JSON'}, status=500)
|
return JsonResponse({'error': 'Invalid JSON'}, status=500)
|
||||||
|
|
||||||
Annotation.objects.create(
|
UserProperty.objects.create(
|
||||||
uuid=self.annotation.uuid,
|
uuid=self.property.uuid,
|
||||||
owner=self.tk.owner.institution,
|
owner=self.tk.owner.institution,
|
||||||
type = Annotation.Type.USER,
|
|
||||||
key = key,
|
key = key,
|
||||||
value = value
|
value = value
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.core.exceptions import PermissionDenied
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from evidence.models import Annotation
|
from evidence.models import SystemProperty
|
||||||
from lot.models import LotTag
|
from lot.models import LotTag
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class DashboardView(LoginRequiredMixin):
|
||||||
dev_ids = self.request.session.pop("devices", [])
|
dev_ids = self.request.session.pop("devices", [])
|
||||||
|
|
||||||
self._devices = []
|
self._devices = []
|
||||||
for x in Annotation.objects.filter(value__in=dev_ids).filter(
|
for x in SystemProperty.objects.filter(value__in=dev_ids).filter(
|
||||||
owner=self.request.user.institution
|
owner=self.request.user.institution
|
||||||
).distinct():
|
).distinct():
|
||||||
self._devices.append(Device(id=x.value))
|
self._devices.append(Device(id=x.value))
|
||||||
|
|
2
dashboard/static/js/Sortable.min.js
vendored
Normal file
2
dashboard/static/js/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -18,6 +18,7 @@
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
|
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
|
||||||
|
<script src="{% static 'js/Sortable.min.js' %}"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.bd-placeholder-img {
|
.bd-placeholder-img {
|
||||||
|
@ -81,7 +82,7 @@
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="admin {% if path in 'panel users' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()">
|
<a class="admin {% if path in 'panel users states_panel' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()">
|
||||||
<i class="bi bi-person-fill-gear icon_sidebar"></i>
|
<i class="bi bi-person-fill-gear icon_sidebar"></i>
|
||||||
{% trans 'Admin' %}
|
{% trans 'Admin' %}
|
||||||
</a>
|
</a>
|
||||||
|
@ -96,6 +97,11 @@
|
||||||
{% trans 'Users' %}
|
{% trans 'Users' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link{% if path == 'states' %} active2{% endif %}" href="{% url 'admin:states_panel' %}">
|
||||||
|
{% trans 'States' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -178,7 +184,13 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock messages %}
|
{% endblock messages %}
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
|
||||||
<h1 class="h2">{{ title }}</h1>
|
<h1 class="h2">{{ title }}
|
||||||
|
{% if help_text %}
|
||||||
|
<span class="ms-1" data-bs-toggle="tooltip" data-bs-placement="right" title="{{ help_text }}">
|
||||||
|
<i class="fas fa-question-circle text-secondary h6 align-top"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<form method="post" action="{% url 'dashboard:search' %}">
|
<form method="post" action="{% url 'dashboard:search' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -221,4 +233,13 @@
|
||||||
{% block extrascript %}{% endblock %}
|
{% block extrascript %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//If help_text is passed to the view as context, a hover-able help icon is displayed
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.shortcuts import Http404
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dashboard.mixins import InventaryMixin, DetailsMixin
|
from dashboard.mixins import InventaryMixin, DetailsMixin
|
||||||
from evidence.models import Annotation
|
from evidence.models import SystemProperty
|
||||||
from evidence.xapian import search
|
from evidence.xapian import search
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from lot.models import Lot
|
from lot.models import Lot
|
||||||
|
@ -74,7 +74,7 @@ class SearchView(InventaryMixin):
|
||||||
|
|
||||||
for x in matches:
|
for x in matches:
|
||||||
# devices.append(self.get_annotations(x))
|
# devices.append(self.get_annotations(x))
|
||||||
dev = self.get_annotations(x)
|
dev = self.get_properties(x)
|
||||||
if dev.id not in dev_id:
|
if dev.id not in dev_id:
|
||||||
devices.append(dev)
|
devices.append(dev)
|
||||||
dev_id.append(dev.id)
|
dev_id.append(dev.id)
|
||||||
|
@ -83,10 +83,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,7 @@ class SearchView(InventaryMixin):
|
||||||
if i:
|
if i:
|
||||||
qry |= Q(value__startswith=i)
|
qry |= Q(value__startswith=i)
|
||||||
|
|
||||||
chids = Annotation.objects.filter(
|
chids = SystemProperty.objects.filter(
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
owner=self.request.user.institution
|
owner=self.request.user.institution
|
||||||
).filter(
|
).filter(
|
||||||
qry
|
qry
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from utils.device import create_annotation, create_doc, create_index
|
from utils.device import create_property, create_doc, create_index
|
||||||
from utils.save_snapshots import move_json, save_in_disk
|
from utils.save_snapshots import move_json, save_in_disk
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class BaseDeviceFormSet(forms.BaseFormSet):
|
||||||
|
|
||||||
path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
|
path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
|
||||||
create_index(doc, self.user)
|
create_index(doc, self.user)
|
||||||
create_annotation(doc, user, commit=commit)
|
create_property(doc, user, commit=commit)
|
||||||
move_json(path_name, self.user.institution.name, place="placeholder")
|
move_json(path_name, self.user.institution.name, place="placeholder")
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
103
device/models.py
103
device/models.py
|
@ -1,8 +1,9 @@
|
||||||
from django.db import models, connection
|
from django.db import models, connection
|
||||||
|
|
||||||
from utils.constants import ALGOS
|
from utils.constants import ALGOS
|
||||||
from evidence.models import Annotation, Evidence
|
from evidence.models import SystemProperty, UserProperty, Evidence
|
||||||
from lot.models import DeviceLot
|
from lot.models import DeviceLot
|
||||||
|
from action.models import State
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
|
@ -29,7 +30,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 +39,59 @@ class Device:
|
||||||
self.get_last_evidence()
|
self.get_last_evidence()
|
||||||
|
|
||||||
def initial(self):
|
def initial(self):
|
||||||
self.get_annotations()
|
self.get_properties()
|
||||||
self.get_uuids()
|
self.get_uuids()
|
||||||
self.get_hids()
|
self.get_hids()
|
||||||
self.get_evidences()
|
self.get_evidences()
|
||||||
self.get_lots()
|
self.get_lots()
|
||||||
|
|
||||||
def get_annotations(self):
|
def get_properties(self):
|
||||||
if self.annotations:
|
if self.properties:
|
||||||
return self.annotations
|
return self.properties
|
||||||
|
|
||||||
self.annotations = Annotation.objects.filter(
|
self.properties = SystemProperty.objects.filter(
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
value=self.id
|
value=self.id
|
||||||
).order_by("-created")
|
).order_by("-created")
|
||||||
|
|
||||||
if self.annotations.count():
|
if self.properties.count():
|
||||||
self.algorithm = self.annotations[0].key
|
self.algorithm = self.properties[0].key
|
||||||
self.owner = self.annotations[0].owner
|
self.owner = self.properties[0].owner
|
||||||
|
|
||||||
return self.annotations
|
return self.properties
|
||||||
|
|
||||||
def get_user_annotations(self):
|
def get_user_properties(self):
|
||||||
if not self.uuids:
|
if not self.uuids:
|
||||||
self.get_uuids()
|
self.get_uuids()
|
||||||
|
|
||||||
annotations = Annotation.objects.filter(
|
user_properties = UserProperty.objects.filter(
|
||||||
uuid__in=self.uuids,
|
uuid__in=self.uuids,
|
||||||
owner=self.owner,
|
owner=self.owner,
|
||||||
type=Annotation.Type.USER
|
type=UserProperty.Type.USER,
|
||||||
)
|
)
|
||||||
return annotations
|
return user_properties
|
||||||
|
|
||||||
def get_user_documents(self):
|
def get_user_documents(self):
|
||||||
if not self.uuids:
|
if not self.uuids:
|
||||||
self.get_uuids()
|
self.get_uuids()
|
||||||
|
|
||||||
annotations = Annotation.objects.filter(
|
properties = UserProperty.objects.filter(
|
||||||
uuid__in=self.uuids,
|
uuid__in=self.uuids,
|
||||||
owner=self.owner,
|
owner=self.owner,
|
||||||
type=Annotation.Type.DOCUMENT
|
type=UserProperty.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,
|
|
||||||
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,19 +115,24 @@ class Device:
|
||||||
if not self.uuids:
|
if not self.uuids:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
annotation = Annotation.objects.filter(
|
property = UserProperty.objects.filter(
|
||||||
uuid__in=self.uuids,
|
uuid__in=self.uuids,
|
||||||
owner=self.owner,
|
owner=self.owner,
|
||||||
type=Annotation.Type.ERASE_SERVER
|
type=UserProperty.Type.ERASE_SERVER
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if annotation:
|
if property:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def last_uuid(self):
|
def last_uuid(self):
|
||||||
return self.uuids[0]
|
return self.uuids[0]
|
||||||
|
|
||||||
|
def get_current_state(self):
|
||||||
|
uuid = self.last_uuid
|
||||||
|
|
||||||
|
return State.objects.filter(snapshot_uuid=uuid).order_by('-date').first()
|
||||||
|
|
||||||
def get_lots(self):
|
def get_lots(self):
|
||||||
self.lots = [
|
self.lots = [
|
||||||
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
|
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
|
||||||
|
@ -136,7 +141,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,
|
||||||
|
@ -150,33 +155,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}
|
|
||||||
)
|
)
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
value
|
value
|
||||||
FROM
|
FROM
|
||||||
RankedAnnotations
|
RankedProperties
|
||||||
WHERE
|
WHERE
|
||||||
row_num = 1
|
row_num = 1
|
||||||
""".format(
|
""".format(
|
||||||
institution=institution.id,
|
institution=institution.id,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
)
|
)
|
||||||
if limit:
|
if limit:
|
||||||
sql += " limit {} offset {}".format(int(limit), int(offset))
|
sql += " limit {} offset {}".format(int(limit), int(offset))
|
||||||
|
|
||||||
sql += ";"
|
sql += ";"
|
||||||
|
|
||||||
annotations = []
|
properties = []
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
annotations = cursor.fetchall()
|
properties = cursor.fetchall()
|
||||||
|
|
||||||
devices = [cls(id=x[0]) for x in annotations]
|
devices = [cls(id=x[0]) for x in properties]
|
||||||
count = cls.get_unassigned_count(institution)
|
count = cls.get_unassigned_count(institution)
|
||||||
return devices, count
|
return devices, count
|
||||||
|
|
||||||
|
@ -184,7 +187,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 +201,28 @@ 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}
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(DISTINCT value)
|
COUNT(DISTINCT value)
|
||||||
FROM
|
FROM
|
||||||
RankedAnnotations
|
RankedProperties
|
||||||
WHERE
|
WHERE
|
||||||
row_num = 1
|
row_num = 1
|
||||||
""".format(
|
""".format(
|
||||||
institution=institution.id,
|
institution=institution.id,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
)
|
)
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
return cursor.fetchall()[0][0]
|
return cursor.fetchall()[0][0]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_annotation_from_uuid(cls, uuid, institution):
|
def get_properties_from_uuid(cls, uuid, institution):
|
||||||
sql = """
|
sql = """
|
||||||
WITH RankedAnnotations AS (
|
WITH RankedProperties AS (
|
||||||
SELECT
|
SELECT
|
||||||
t1.value,
|
t1.value,
|
||||||
t1.key,
|
t1.key,
|
||||||
|
@ -235,31 +236,29 @@ 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.uuid = '{uuid}'
|
AND t1.uuid = '{uuid}'
|
||||||
)
|
)
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
value
|
value
|
||||||
FROM
|
FROM
|
||||||
RankedAnnotations
|
RankedProperties
|
||||||
WHERE
|
WHERE
|
||||||
row_num = 1;
|
row_num = 1;
|
||||||
""".format(
|
""".format(
|
||||||
uuid=uuid.replace("-", ""),
|
uuid=uuid.replace("-", ""),
|
||||||
institution=institution.id,
|
institution=institution.id,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
annotations = []
|
properties = []
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
annotations = cursor.fetchall()
|
properties = cursor.fetchall()
|
||||||
|
|
||||||
return cls(id=annotations[0][0])
|
return cls(id=properties[0][0])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_websnapshot(self):
|
def is_websnapshot(self):
|
||||||
|
|
|
@ -2,11 +2,58 @@
|
||||||
{% 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="col text-end">
|
||||||
|
<div class="btn-group" role="group" aria-label="Actions">
|
||||||
|
|
||||||
|
<!-- change state button -->
|
||||||
|
{% if state_definitions %}
|
||||||
|
<div class="dropdown ms-2">
|
||||||
|
<a class="btn btn-green-admin dropdown-toggle" id="addStateDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{% trans "Change state" %}
|
||||||
|
{% if device_states %}
|
||||||
|
({{ device_states.0.state }})
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="addStateDropdown" style="width: 100%;">
|
||||||
|
{% for state in state_definitions %}
|
||||||
|
<li style="width: 100%;">
|
||||||
|
<form id="changeStateForm{{ state.id }}" method="post" action="{% url 'action:change_state' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="previous_state" value="{{ device_states.0.state|default:"nil" }}">
|
||||||
|
<input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
|
||||||
|
<input type="hidden" name="new_state" value="{{ state.state }}">
|
||||||
|
|
||||||
|
<a class="dropdown-item d-flex justify-content-between align-items-center" href="#" onclick="document.getElementById('changeStateForm{{ state.id }}').submit(); return false;">
|
||||||
|
<span class="font-monospace">{{ state.state }}</span>
|
||||||
|
<span class="badge bg-secondary rounded-pill-sm">{{ forloop.counter }}</span>
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-green-admin" type="button" disabled>
|
||||||
|
<i class="bi bi-plus"></i> {% trans "Change state" %}
|
||||||
|
{% if device_states %}
|
||||||
|
({{ device_states.0.state }})
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<!-- Add note button -->
|
||||||
|
<button class="btn btn-warning ms-2" type="button" data-bs-toggle="modal" data-bs-target="#addNoteModal">
|
||||||
|
<i class="bi bi-sticky"></i> {% trans "Add a note" %}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
@ -15,7 +62,10 @@
|
||||||
<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="#log" class="nav-link" data-bs-toggle="tab" data-bs-target="#log">{% trans 'Log' %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#user_properties" class="nav-link" data-bs-toggle="tab" data-bs-target="#user_properties">{% trans 'User properties' %}</a>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
||||||
|
@ -35,201 +85,45 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-content pt-4">
|
||||||
|
|
||||||
<div class="tab-content pt-2">
|
{% include 'tabs/general_details.html' %}
|
||||||
<div class="tab-pane fade show active" id="details">
|
|
||||||
<h5 class="card-title">{% trans 'Details' %}</h5>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Phid</div>
|
|
||||||
<div class="col-lg-9 col-md-8">{{ object.id }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if object.is_eraseserver %}
|
{% include 'tabs/log.html' %}
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-lg-3 col-md-4 label">
|
|
||||||
{% trans 'Is a erase server' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-9 col-md-8"></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
{% include 'tabs/user_properties.html' %}
|
||||||
<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 and object.last_user_evidence %}
|
{% include 'tabs/documents.html' %}
|
||||||
{% 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">
|
{% include 'tabs/lots.html' %}
|
||||||
<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">
|
{% include 'tabs/evidences.html' %}
|
||||||
<div class="col-lg-3 col-md-4 label">
|
|
||||||
{% trans 'Serial Number' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-9 col-md-8">{{ object.serial_number|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="modal fade" id="addNoteModal" tabindex="-1" aria-labelledby="addNoteModalLabel" aria-hidden="true">
|
||||||
<div class="col-lg-3 col-md-4 label">
|
<div class="modal-dialog">
|
||||||
{% trans 'Identifiers' %}
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addNoteModalLabel">{% trans "Add a Note" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form method="post" action="{% url 'action:add_note' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
|
||||||
|
<label for="noteDescription" class="form-label">{% trans "Note" %}</label>
|
||||||
|
<textarea class="form-control" id="noteDescription" name="note" placeholder="Max 250 characters" name="note" rows="3" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-green-admin">{% trans "Save Note" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for chid in object.hids %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">{{ chid|default:'' }}</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="annotations">
|
|
||||||
<div class="btn-group mt-1 mb-3">
|
|
||||||
<a href="{% url 'device:add_annotation' object.pk %}" class="btn btn-primary">
|
|
||||||
<i class="bi bi-plus"></i>
|
|
||||||
{% trans 'Add new annotation' %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5 class="card-title">{% trans 'Annotations' %}</h5>
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">
|
|
||||||
{% trans 'Key' %}
|
|
||||||
</th>
|
|
||||||
<th scope="col">
|
|
||||||
{% trans 'Value' %}
|
|
||||||
</th>
|
|
||||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
|
|
||||||
{% trans 'Created on' %}
|
|
||||||
</th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for a in object.get_user_annotations %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ a.key }}</td>
|
|
||||||
<td>{{ a.value }}</td>
|
|
||||||
<td>{{ a.created }}</td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="documents">
|
|
||||||
<div class="btn-group mt-1 mb-3">
|
|
||||||
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">
|
|
||||||
<i class="bi bi-plus"></i>
|
|
||||||
{% trans 'Add new document' %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5 class="card-title">{% trans 'Documents' %}</h5>
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">
|
|
||||||
{% trans 'Key' %}
|
|
||||||
</th>
|
|
||||||
<th scope="col">
|
|
||||||
{% trans 'Value' %}
|
|
||||||
</th>
|
|
||||||
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
|
|
||||||
{% trans 'Created on' %}
|
|
||||||
</th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for a in object.get_user_documents %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ a.key }}</td>
|
|
||||||
<td>{{ a.value }}</td>
|
|
||||||
<td>{{ a.created }}</td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="lots">
|
|
||||||
{% for tag in lot_tags %}
|
|
||||||
<h5 class="card-title">{{ tag }}</h5>
|
|
||||||
{% for lot in object.lots %}
|
|
||||||
{% if lot.type == tag %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% 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>
|
</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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrascript %}
|
{% block extrascript %}
|
||||||
|
|
27
device/templates/tabs/components.html
Normal file
27
device/templates/tabs/components.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="components">
|
||||||
|
<h5 class="card-title">{% trans 'Components last evidence' %}
|
||||||
|
</h5>
|
||||||
|
<div class="list-group col-6">
|
||||||
|
{% for c in object.components %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h5 class="mb-1">{{ c.type }}
|
||||||
|
</h5>
|
||||||
|
<small class="text-muted">{{ evidence.created }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<p class="mb-1">
|
||||||
|
{% for k, v in c.items %}
|
||||||
|
{% if k not in 'actions,type' %}
|
||||||
|
{{ k }}: {{ v }}
|
||||||
|
<br />
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
49
device/templates/tabs/documents.html
Normal file
49
device/templates/tabs/documents.html
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
<div class="tab-pane fade" id="documents">
|
||||||
|
<div class="btn-group mt-1 mb-3">
|
||||||
|
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus">
|
||||||
|
</i>
|
||||||
|
{% trans 'Add new document' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="card-title">{% trans 'Documents' %}
|
||||||
|
</h5>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">
|
||||||
|
{% trans 'Key' %}
|
||||||
|
</th>
|
||||||
|
<th scope="col">
|
||||||
|
{% trans 'Value' %}
|
||||||
|
</th>
|
||||||
|
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
|
||||||
|
{% trans 'Created on' %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for a in object.get_user_documents %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ a.key }}
|
||||||
|
</td>
|
||||||
|
<td>{{ a.value }}
|
||||||
|
</td>
|
||||||
|
<td>{{ a.created }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
19
device/templates/tabs/evidences.html
Normal file
19
device/templates/tabs/evidences.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<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>
|
74
device/templates/tabs/general_details.html
Normal file
74
device/templates/tabs/general_details.html
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<!-- Device Details -->
|
||||||
|
<div class="tab-pane fade show active" id="details">
|
||||||
|
<h5 class="card-title">{% trans 'Details' %}
|
||||||
|
</h5>
|
||||||
|
<hr>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Phid' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object.is_eraseserver %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Is an erase server' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{% trans 'Yes' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Type' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object.is_websnapshot and object.last_user_evidence %}
|
||||||
|
{% for k, v in object.last_user_evidence.items %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{{ k }}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ v|default:'' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Manufacturer' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.manufacturer|default:'' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Model' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.model|default:'' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Serial Number' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">{{ object.serial_number|default:'' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4 text-muted fw-bold">{% trans 'Identifiers' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{% for chid in object.hids %}
|
||||||
|
<div>{{ chid|default:'' }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
28
device/templates/tabs/log.html
Normal file
28
device/templates/tabs/log.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="log">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover table-bordered bg-gradient">
|
||||||
|
<thead >
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans 'Date' %}</th>
|
||||||
|
<th scope="col">{% trans 'Event' %}</th>
|
||||||
|
<th scope="col">{% trans 'User' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for log in device_logs %}
|
||||||
|
<tr>
|
||||||
|
<td width="13%">{{ log.date|date:"M j, Y, H:i" }}</td>
|
||||||
|
<td class="fst-italic">{{ log.event }}</td>
|
||||||
|
<td>{{ log.user.get_full_name|default:log.user.username }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center">{% trans 'No logs recorded.' %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
19
device/templates/tabs/lots.html
Normal file
19
device/templates/tabs/lots.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="lots">
|
||||||
|
{% for tag in lot_tags %}
|
||||||
|
<h5 class="card-title">{{ tag }}
|
||||||
|
</h5>
|
||||||
|
{% for lot in object.lots %}
|
||||||
|
{% if lot.type == tag %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
132
device/templates/tabs/user_properties.html
Normal file
132
device/templates/tabs/user_properties.html
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="user_properties">
|
||||||
|
<div class="btn-group mt-1 mb-3">
|
||||||
|
<a href="{% url 'device:add_user_property' object.pk %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus">
|
||||||
|
</i>
|
||||||
|
{% trans 'New user property' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="card-title">{% trans 'User properties' %}
|
||||||
|
</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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for a in object.get_user_properties %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ a.key }}
|
||||||
|
</td>
|
||||||
|
<td>{{ a.value }}
|
||||||
|
</td>
|
||||||
|
<td>{{ a.created }}
|
||||||
|
</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>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- pop up modal for delete confirmation -->
|
||||||
|
{% for a in object.get_user_properties %}
|
||||||
|
<div class="modal fade" id="deleteModal{{ a.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ a.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deleteModalLabel{{ a.id }}">{% trans "Confirm Deletion" %}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Key:" %}
|
||||||
|
</strong> {{ a.key }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Value:" %}
|
||||||
|
</strong> {{ a.value }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Created on:" %}
|
||||||
|
</strong> {{ a.created }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-center">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}
|
||||||
|
</button>
|
||||||
|
<form method="post" action="{% url 'device:delete_user_property' 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 %}
|
|
@ -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"),
|
||||||
|
|
||||||
|
|
132
device/views.py
132
device/views.py
|
@ -1,18 +1,22 @@
|
||||||
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 action.models import StateDefinition, State, DeviceLog
|
||||||
from dashboard.mixins import DashboardView, Http403
|
from dashboard.mixins import DashboardView, Http403
|
||||||
from evidence.models import Annotation
|
from evidence.models import UserProperty, SystemProperty
|
||||||
from lot.models import LotTag
|
from lot.models import LotTag
|
||||||
from device.models import Device
|
from device.models import Device
|
||||||
from device.forms import DeviceFormSet
|
from device.forms import DeviceFormSet
|
||||||
|
@ -69,7 +73,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 +91,7 @@ class DetailsView(DashboardView, TemplateView):
|
||||||
template_name = "details.html"
|
template_name = "details.html"
|
||||||
title = _("Device")
|
title = _("Device")
|
||||||
breadcrumb = "Device / Details"
|
breadcrumb = "Device / Details"
|
||||||
model = Annotation
|
model = SystemProperty
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.pk = kwargs['pk']
|
self.pk = kwargs['pk']
|
||||||
|
@ -103,10 +107,15 @@ class DetailsView(DashboardView, TemplateView):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
self.object.initial()
|
self.object.initial()
|
||||||
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
|
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
|
||||||
|
last_evidence= self.object.get_last_evidence(),
|
||||||
|
uuid=self.object.last_uuid()
|
||||||
context.update({
|
context.update({
|
||||||
'object': self.object,
|
'object': self.object,
|
||||||
'snapshot': self.object.get_last_evidence(),
|
'snapshot': last_evidence,
|
||||||
'lot_tags': lot_tags,
|
'lot_tags': lot_tags,
|
||||||
|
"state_definitions": StateDefinition.objects.filter(institution=self.request.user.institution).order_by('order'),
|
||||||
|
"device_states": State.objects.filter(snapshot_uuid=uuid).order_by('-date'),
|
||||||
|
"device_logs": DeviceLog.objects.filter(snapshot_uuid=uuid).order_by('-date'),
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -167,65 +176,144 @@ 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 = UserProperty.Type.USER
|
||||||
|
|
||||||
|
message = _("<Created> UserProperty: {}: {}".format(form.instance.key, form.instance.value))
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=form.instance.uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
|
||||||
).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 = UserProperty.Type.USER
|
||||||
|
response = super().form_valid(form)
|
||||||
|
messages.success(self.request, _("User property updated successfully."))
|
||||||
|
|
||||||
|
message = _("<Updated> UserProperty: {}: {} to {}: {}".format(old_key, old_value, new_key, new_value ))
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=form.instance.uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
message = _("<Deleted> User Property: {}:{}".format(self.object.key, self.object.value ))
|
||||||
|
DeviceLog.objects.create(
|
||||||
|
snapshot_uuid=self.object.uuid,
|
||||||
|
event=message,
|
||||||
|
user=self.request.user,
|
||||||
|
institution=self.request.user.institution
|
||||||
|
)
|
||||||
|
|
||||||
|
self.object.delete()
|
||||||
|
messages.info(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 = 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.DOCUMENT
|
form.instance.type = UserProperty.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
|
|
||||||
).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])
|
||||||
|
|
|
@ -65,6 +65,8 @@ ENABLE_EMAIL = config("ENABLE_EMAIL", default=True, cast=bool)
|
||||||
|
|
||||||
EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
|
EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
|
||||||
|
|
||||||
|
DEVICE_LOG_PATH = config("DEVICE_LOG_PATH", default="/tmp")
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
@ -210,6 +212,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": {
|
||||||
|
@ -232,7 +238,7 @@ LOGGING = {
|
||||||
"handlers": ["console"],
|
"handlers": ["console"],
|
||||||
"level": "ERROR",
|
"level": "ERROR",
|
||||||
"propagate": False,
|
"propagate": False,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ urlpatterns = [
|
||||||
path("", include("login.urls")),
|
path("", include("login.urls")),
|
||||||
path("dashboard/", include("dashboard.urls")),
|
path("dashboard/", include("dashboard.urls")),
|
||||||
path("evidence/", include("evidence.urls")),
|
path("evidence/", include("evidence.urls")),
|
||||||
|
path('action/', include('action.urls')),
|
||||||
path("device/", include("device.urls")),
|
path("device/", include("device.urls")),
|
||||||
path("admin/", include("admin.urls")),
|
path("admin/", include("admin.urls")),
|
||||||
path("user/", include("user.urls")),
|
path("user/", include("user.urls")),
|
||||||
|
|
|
@ -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, UserProperty
|
||||||
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,8 @@ class UserTagForm(forms.Form):
|
||||||
self.pk = None
|
self.pk = None
|
||||||
self.uuid = kwargs.pop('uuid', None)
|
self.uuid = kwargs.pop('uuid', None)
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
instance = Annotation.objects.filter(
|
instance = SystemProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
key='CUSTOM_ID',
|
key='CUSTOM_ID',
|
||||||
owner=self.user.institution
|
owner=self.user.institution
|
||||||
).first()
|
).first()
|
||||||
|
@ -86,9 +85,8 @@ class UserTagForm(forms.Form):
|
||||||
if not data:
|
if not data:
|
||||||
return False
|
return False
|
||||||
self.tag = data
|
self.tag = data
|
||||||
self.instance = Annotation.objects.filter(
|
self.instance = SystemProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.SYSTEM,
|
|
||||||
key='CUSTOM_ID',
|
key='CUSTOM_ID',
|
||||||
owner=self.user.institution
|
owner=self.user.institution
|
||||||
).first()
|
).first()
|
||||||
|
@ -106,9 +104,8 @@ 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,
|
|
||||||
key='CUSTOM_ID',
|
key='CUSTOM_ID',
|
||||||
value=self.tag,
|
value=self.tag,
|
||||||
owner=self.user.institution,
|
owner=self.user.institution,
|
||||||
|
@ -164,8 +161,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 +183,9 @@ class EraseServerForm(forms.Form):
|
||||||
self.pk = None
|
self.pk = None
|
||||||
self.uuid = kwargs.pop('uuid', None)
|
self.uuid = kwargs.pop('uuid', None)
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
instance = Annotation.objects.filter(
|
instance = UserProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.ERASE_SERVER,
|
type=UserProperty.Type.ERASE_SERVER,
|
||||||
key='ERASE_SERVER',
|
key='ERASE_SERVER',
|
||||||
owner=self.user.institution
|
owner=self.user.institution
|
||||||
).first()
|
).first()
|
||||||
|
@ -201,9 +198,9 @@ class EraseServerForm(forms.Form):
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.erase_server = self.cleaned_data.get('erase_server', False)
|
self.erase_server = self.cleaned_data.get('erase_server', False)
|
||||||
self.instance = Annotation.objects.filter(
|
self.instance = UserProperty.objects.filter(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.ERASE_SERVER,
|
type=UserProperty.Type.ERASE_SERVER,
|
||||||
key='ERASE_SERVER',
|
key='ERASE_SERVER',
|
||||||
owner=self.user.institution
|
owner=self.user.institution
|
||||||
).first()
|
).first()
|
||||||
|
@ -222,9 +219,9 @@ class EraseServerForm(forms.Form):
|
||||||
if self.instance:
|
if self.instance:
|
||||||
return
|
return
|
||||||
|
|
||||||
Annotation.objects.create(
|
UserProperty.objects.create(
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
type=Annotation.Type.ERASE_SERVER,
|
type=UserProperty.Type.ERASE_SERVER,
|
||||||
key='ERASE_SERVER',
|
key='ERASE_SERVER',
|
||||||
value=self.erase_server,
|
value=self.erase_server,
|
||||||
owner=self.user.institution,
|
owner=self.user.institution,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-10 19:37
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("evidence", "0002_alter_annotation_type"),
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SystemProperty",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("key", models.CharField(max_length=256)),
|
||||||
|
("value", models.CharField(max_length=256)),
|
||||||
|
("uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserProperty",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("key", models.CharField(max_length=256)),
|
||||||
|
("value", models.CharField(max_length=256)),
|
||||||
|
("uuid", models.UUIDField()),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.SmallIntegerField(
|
||||||
|
choices=[(1, "User"), (2, "Document"), (3, "EraseServer")],
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="Annotation",
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="systemproperty",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("key", "uuid"), name="system_unique_type_key_uuid"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="userproperty",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("key", "uuid", "type"), name="user_unique_type_key_uuid"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-18 12:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("evidence", "0003_systemproperty_userproperty_delete_annotation_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="userproperty",
|
||||||
|
name="user_unique_type_key_uuid",
|
||||||
|
),
|
||||||
|
]
|
|
@ -3,35 +3,48 @@ 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
|
||||||
|
|
||||||
|
|
||||||
class Annotation(models.Model):
|
class Property(models.Model):
|
||||||
class Type(models.IntegerChoices):
|
|
||||||
SYSTEM = 0, "System"
|
|
||||||
USER = 1, "User"
|
|
||||||
DOCUMENT = 2, "Document"
|
|
||||||
ERASE_SERVER = 3, "EraseServer"
|
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
uuid = models.UUIDField()
|
|
||||||
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User, on_delete=models.SET_NULL, null=True, blank=True)
|
User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
type = models.SmallIntegerField(choices=Type)
|
|
||||||
key = models.CharField(max_length=STR_EXTEND_SIZE)
|
key = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||||
value = models.CharField(max_length=STR_EXTEND_SIZE)
|
value = models.CharField(max_length=STR_EXTEND_SIZE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
#Only for shared behaviour, it is not a table
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class SystemProperty(Property):
|
||||||
|
uuid = models.UUIDField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=["type", "key", "uuid"], name="unique_type_key_uuid")
|
fields=["key", "uuid"], name="system_unique_type_key_uuid")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UserProperty(Property):
|
||||||
|
uuid = models.UUIDField()
|
||||||
|
|
||||||
|
class Type(models.IntegerChoices):
|
||||||
|
USER = 1, "User"
|
||||||
|
DOCUMENT = 2, "Document"
|
||||||
|
ERASE_SERVER = 3, "EraseServer"
|
||||||
|
|
||||||
|
type = models.SmallIntegerField(choices=Type, default=Type.USER)
|
||||||
|
|
||||||
|
|
||||||
class Evidence:
|
class Evidence:
|
||||||
def __init__(self, uuid):
|
def __init__(self, uuid):
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
|
@ -39,22 +52,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 +93,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 +144,8 @@ 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,
|
|
||||||
key="hidalgo1",
|
key="hidalgo1",
|
||||||
).order_by("-created").values_list("uuid", "created").distinct()
|
).order_by("-created").values_list("uuid", "created").distinct()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
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,22 @@ 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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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,
|
|
||||||
key=k,
|
key=k,
|
||||||
value=v
|
value=v
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 'device:delete_user_property' form.pk %}">{% translate "Delete" %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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'),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -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 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,33 +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):
|
||||||
template_name = "ev_eraseserver.html"
|
template_name = "ev_eraseserver.html"
|
||||||
section = "evidences"
|
section = "evidences"
|
||||||
|
@ -182,7 +155,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):
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-10 19:37
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("lot", "0002_alter_lot_closed"),
|
||||||
|
("user", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="LotProperty",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("key", models.CharField(max_length=256)),
|
||||||
|
("value", models.CharField(max_length=256)),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.SmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, "System"),
|
||||||
|
(1, "User"),
|
||||||
|
(2, "Document"),
|
||||||
|
(3, "EraseServer"),
|
||||||
|
],
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lot",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="lot.lot"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="user.institution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="LotAnnotation",
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="lotproperty",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("key", "lot", "type"), name="lot_unique_type_key_lot"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-12-18 12:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("lot", "0003_lotproperty_delete_lotannotation_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="lotproperty",
|
||||||
|
name="lot_unique_type_key_lot",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="lotproperty",
|
||||||
|
name="type",
|
||||||
|
field=models.SmallIntegerField(
|
||||||
|
choices=[(0, "System"), (1, "User"), (2, "Document")], default=1
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -7,8 +7,8 @@ from utils.constants import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from user.models import User, Institution
|
from user.models import User, Institution
|
||||||
|
from evidence.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,12 @@ 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):
|
||||||
|
lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class LotAnnotation(models.Model):
|
|
||||||
class Type(models.IntegerChoices):
|
class Type(models.IntegerChoices):
|
||||||
SYSTEM= 0, "System"
|
SYSTEM = 0, "System"
|
||||||
USER = 1, "User"
|
USER = 1, "User"
|
||||||
DOCUMENT = 2, "Document"
|
DOCUMENT = 2, "Document"
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
type = models.SmallIntegerField(choices=Type.choices, default=Type.USER)
|
||||||
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)
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<h3>Lot {{ lot.name }}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="tab-pane fade show active" id="details">
|
|
||||||
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
|
|
||||||
<a href="{% url 'lot:add_annotation' lot.pk %}" class="btn btn-primary">
|
|
||||||
|
|
||||||
<i class="bi bi-plus"></i>
|
|
||||||
Add new annotation
|
|
||||||
<span class="caret"></span>
|
|
||||||
</a>
|
|
||||||
</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 annotations %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ a.key }}</td>
|
|
||||||
<td>{{ a.value }}</td>
|
|
||||||
<td>{{ a.created }}</td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
105
lot/templates/properties.html
Normal file
105
lot/templates/properties.html
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3>Lot {{ lot.name }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="tab-pane fade show active" id="details">
|
||||||
|
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
|
||||||
|
<a href="{% url 'lot:add_property' lot.pk %}" class="btn btn-primary">
|
||||||
|
|
||||||
|
<i class="bi bi-plus"></i>
|
||||||
|
Add new lot Property
|
||||||
|
<span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="card-title mt-2">Properties</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 properties %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ a.key }}</td>
|
||||||
|
<td>{{ a.value }}</td>
|
||||||
|
<td>{{ a.created }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="#" class="text-info" data-bs-toggle="modal" data-bs-target="#editPropertyModal{{ a.id }}">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="#" class="text-danger" data-bs-toggle="modal" data-bs-target="#deletePropertyModal{{ a.id }}">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<div class="modal fade" id="editPropertyModal{{ a.id }}" tabindex="-1" aria-labelledby="editPropertyModalLabel{{ a.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editPropertyModalLabel{{ a.id }}">{% trans "Edit Property" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form method="post" action="{% url 'lot:update_property' a.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="propertyKey{{ a.id }}" class="form-label">{% trans "Key" %}</label>
|
||||||
|
<input type="text" class="form-control" id="propertyKey{{ a.id }}" name="key" value="{{ a.key }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="propertyValue{{ a.id }}" class="form-label">{% trans "Value" %}</label>
|
||||||
|
<input type="text" class="form-control" id="propertyValue{{ a.id }}" name="value" value="{{ a.value }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans "Save changes" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="deletePropertyModal{{ a.id }}" tabindex="-1" aria-labelledby="deletePropertyModalLabel{{ a.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deletePropertyModalLabel{{ a.id }}">{% trans "Delete Property" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{% trans "Are you sure you want to delete this property?" %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<form method="post" action="{% url 'lot:delete_property' a.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -12,6 +12,8 @@ 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.AddLotPropertyView.as_view(), name="add_property"),
|
||||||
|
path("<int:pk>/property/update", views.UpdateLotPropertyView.as_view(), name="update_property"),
|
||||||
|
path("<int:pk>/property/delete", views.DeleteLotPropertyView.as_view(), name="delete_property"),
|
||||||
]
|
]
|
||||||
|
|
104
lot/views.py
104
lot/views.py
|
@ -1,5 +1,6 @@
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||||
|
from django.contrib import messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
from django.views.generic.edit import (
|
from django.views.generic.edit import (
|
||||||
|
@ -9,10 +10,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
|
||||||
|
|
||||||
|
|
||||||
class NewLotView(DashboardView, CreateView):
|
class NewLotView(DashboardView, CreateView):
|
||||||
template_name = "new_lot.html"
|
template_name = "new_lot.html"
|
||||||
title = _("New lot")
|
title = _("New lot")
|
||||||
|
@ -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 = LotProperty.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=LotProperty.Type.DOCUMENT,
|
||||||
)
|
)
|
||||||
context.update({
|
context.update({
|
||||||
'lot': lot,
|
'lot': lot,
|
||||||
|
@ -189,48 +189,106 @@ 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=LotProperty.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 AddLotPropertyView(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 = LotProperty.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
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateLotPropertyView(DashboardView, UpdateView):
|
||||||
|
template_name = "properties.html"
|
||||||
|
title = _("Update lot Property")
|
||||||
|
breadcrumb = "Lot / Update Property"
|
||||||
|
model = LotProperty
|
||||||
|
fields = ("key", "value")
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
pk = self.kwargs.get('pk')
|
||||||
|
lot_property = get_object_or_404(LotProperty, pk=pk, owner=self.request.user.institution)
|
||||||
|
|
||||||
|
if not lot_property:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['instance'] = lot_property
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
old_key= self.object.key
|
||||||
|
old_value = self.object.value
|
||||||
|
new_key = form.cleaned_data['key']
|
||||||
|
new_value = form.cleaned_data['value']
|
||||||
|
|
||||||
|
form.instance.owner = self.request.user.institution
|
||||||
|
form.instance.user = self.request.user
|
||||||
|
form.instance.type = LotProperty.Type.USER
|
||||||
|
response = super().form_valid(form)
|
||||||
|
|
||||||
|
messages.success(self.request, _("Lot property updated successfully."))
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteLotPropertyView(DashboardView, DeleteView):
|
||||||
|
model = LotProperty
|
||||||
|
|
||||||
|
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()
|
||||||
|
messages.success(self.request, _("Lot property deleted successfully."))
|
||||||
|
|
||||||
|
# Redirect back to the original URL
|
||||||
|
return redirect(referer)
|
||||||
|
|
|
@ -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
|
||||||
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,23 @@ 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,
|
|
||||||
'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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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):
|
||||||
|
|
Loading…
Reference in a new issue