Merge branch 'main' into bugfix/170_180

This commit is contained in:
cayop 2025-02-20 16:31:40 +00:00
commit bef3fb6cf9
24 changed files with 528 additions and 84 deletions

View file

@ -0,0 +1,173 @@
{% extends "base.html" %}
{% load i18n django_bootstrap5 %}
{% block content %}
<div class="row">
<div class="col">
<h3>{{ subtitle }}</h3>
</div>
<div class="col text-end">
<button type="button" class="btn btn-green-admin" data-bs-toggle="modal" data-bs-target="#addLotTagModal">
{% trans "Add" %}
</button>
</div>
</div>
<div class="row mt-4">
<div class="col">
{% if lot_tags_edit %}
<table class="table table-hover table-bordered align-middle">
<thead class="table-light">
<tr>
<th scope="col">{% trans "Lot Group Name" %}
</th>
<th scope="col" width="15%" class="text-center">{% trans "Actions" %}
</th>
</tr>
</thead>
<tbody id="sortable_list">
{% for tag in lot_tags_edit %}
<tr>
<td class="font-monospace">
{{ tag.name }}
</td>
<!-- action buttons -->
<td>
<div class="btn-group float-end">
<button
type="button"
class="btn btn-sm btn-outline-info d-flex align-items-center"
data-bs-toggle="modal" data-bs-target="#editLotTagModal{{ tag.id }}">
<i class="bi bi-pencil me-1"></i>
{% trans 'Edit' %}
</button>
<button
type="button" class="btn btn-sm btn-outline-danger d-flex align-items-center"
data-bs-toggle="modal"
data-bs-target="#deleteLotTagModal{{ tag.id }}" >
<i class="bi bi-trash me-1"></i>
{% trans 'Delete' %}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-primary text-center mt-5" role="alert">
{% trans "No Lot Groups found on current organization" %}
</div>
{% endif %}
</div>
</div>
<!-- add lot tag Modal -->
<div class="modal fade" id="addLotTagModal" tabindex="-1" aria-labelledby="addLoTagModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addLotTagModalLabel">{% trans "Add Lot Group" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'admin:add_lot_tag' %}">
{% csrf_token %}
<div class="mb-3">
<label for="lotTagInput" class="form-label">{% trans "Tag" %}</label>
<input type="text" class="form-control" id="lotTagInput" name="name" maxlength="50" required>
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Add Lot tag" %}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit Lot Group Modals -->
{% for tag in lot_tags_edit %}
<div class="modal fade" id="editLotTagModal{{ tag.id }}" tabindex="-1" aria-labelledby="editLotTagModalLabel{{ tag.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{% url 'admin:edit_lot_tag' tag.id %}">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title" id="editLotTagModalLabel{{ tag.id }}">
{% trans "Edit Lot Group" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="editLotTagInput{{ tag.id }}" class="form-label">{% trans "Tag" %}</label>
<input type="text" class="form-control" id="editLotTagInput{{ tag.id }}" name="name" maxlength="50" value="{{ tag.name }}" required>
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-green-admin">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
<!-- delete lot tag definition Modal -->
{% for tag in lot_tags_edit %}
<div class="modal fade" id="deleteLotTagModal{{ tag.id }}" tabindex="-1" aria-labelledby="deleteLotTagModalLabel{{ tag.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="deleteLotTagModalLabel{{ tag.id }}">
{% trans "Delete Lot Group" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
</div>
<div class="modal-body">
{% if tag.lot_set.first %}
<div class="alert alert-warning text-center" role="alert">
{% trans "Failed to remove Lot Group, it is not empty" %}
</div>
{% endif %}
<div class="d-flex align-items-center border rounded p-3 mt-3">
<div>
<p class="mb-0 fw-bold">{{ tag.name }}</p>
</div>
</div>
</div>
<div class="modal-footer">
<form method="post" action="{% url 'admin:delete_lot_tag' tag.pk %}">
{% csrf_token %}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans "Cancel" %}
</button>
{% if tag.lot_set.first %}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans "Delete" %}
</button>
{% else %}
<button type="submit" class="btn btn-danger">
{% trans "Delete" %}
</button>
{% endif %}
</form>
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}

View file

@ -15,4 +15,8 @@ urlpatterns = [
path("states/delete/<int:pk>", views.DeleteStateDefinitionView.as_view(), name='delete_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/update_order/", views.UpdateStateOrderView.as_view(), name='update_state_order'),
path("states/edit/<int:pk>/", views.UpdateStateDefinitionView.as_view(), name='edit_state_definition'), path("states/edit/<int:pk>/", views.UpdateStateDefinitionView.as_view(), name='edit_state_definition'),
path("lot/", views.LotTagPanelView.as_view(), name="tag_panel"),
path("lot/add", views.AddLotTagView.as_view(), name="add_lot_tag"),
path("lot/delete/<int:pk>", views.DeleteLotTagView.as_view(), name='delete_lot_tag'),
path("lot/edit/<int:pk>/", views.UpdateLotTagView.as_view(), name='edit_lot_tag'),
] ]

View file

@ -18,6 +18,7 @@ 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 from action.models import StateDefinition
from lot.models import LotTag
class AdminView(DashboardView): class AdminView(DashboardView):
@ -112,6 +113,99 @@ class EditUserView(AdminView, UpdateView):
return kwargs return kwargs
class LotTagPanelView(AdminView, TemplateView):
template_name = "lot_tag_panel.html"
title = _("Lot Groups Panel")
breadcrumb = _("admin / Lot Groups Panel")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
lot_tags = LotTag.objects.filter(
owner=self.request.user.institution
)
context.update({"lot_tags_edit": lot_tags})
return context
class AddLotTagView(AdminView, CreateView):
template_name = "lot_tag_panel.html"
title = _("New lot group Definition")
breadcrumb = "Admin / New lot tag"
success_url = reverse_lazy('admin:tag_panel')
model = LotTag
fields = ('name',)
def form_valid(self, form):
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
name = form.instance.name
if LotTag.objects.filter(name=name).first():
msg = _(f"The name '{name}' exist.")
messages.error(self.request, msg)
return redirect(self.success_url)
response = super().form_valid(form)
messages.success(self.request, _("Lot Group successfully added."))
return response
class DeleteLotTagView(AdminView, DeleteView):
model = LotTag
success_url = reverse_lazy('admin:tag_panel')
def post(self, request, *args, **kwargs):
pk = kwargs.get('pk')
self.object = get_object_or_404(
self.model,
owner=self.request.user.institution,
pk=pk
)
if self.object.lot_set.first():
msg = _('This group have lots. Impossible to delete.')
messages.warning(self.request, msg)
return redirect(reverse_lazy('admin:tag_panel'))
if self.object.inbox:
msg = f"The lot group '{self.object.name}'"
msg += " is INBOX, so it cannot be deleted, only renamed."
messages.error(self.request, msg)
return redirect(self.success_url)
response = super().delete(request, *args, **kwargs)
msg = _('Lot Group has been deleted.')
messages.success(self.request, msg)
return response
class UpdateLotTagView(AdminView, UpdateView):
model = LotTag
template_name = 'lot_tag_panel.html'
fields = ['name']
success_url = reverse_lazy('admin:tag_panel')
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
self.object = get_object_or_404(
self.model,
owner=self.request.user.institution,
pk=pk
)
return super().get_form_kwargs()
def form_valid(self, form):
name = form.instance.name
if LotTag.objects.filter(name=name).first():
msg = _(f"The name '{name}' exist.")
messages.error(self.request, msg)
return redirect(self.success_url)
response = super().form_valid(form)
msg = _("Lot Group updated successfully.")
messages.success(self.request, msg)
return response
class InstitutionView(AdminView, UpdateView): class InstitutionView(AdminView, UpdateView):
template_name = "institution.html" template_name = "institution.html"
title = _("Edit institution") title = _("Edit institution")
@ -124,7 +218,8 @@ class InstitutionView(AdminView, UpdateView):
"logo", "logo",
"location", "location",
"responsable_person", "responsable_person",
"supervisor_person" "supervisor_person",
"algorithm"
) )
def get_form_kwargs(self): def get_form_kwargs(self):
@ -180,14 +275,11 @@ class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessM
def get_success_message(self, cleaned_data): def get_success_message(self, cleaned_data):
return f'State definition: {self.object.state}, has been deleted' return f'State definition: {self.object.state}, has been deleted'
def delete(self, request, *args, **kwargs): def form_valid(self, form):
self.object = self.get_object() if not self.object.institution == self.request.user.institution:
#only an admin of current institution can delete
if not object.institution == self.request.user.institution:
raise Http404 raise Http404
return super().delete(request, *args, **kwargs) return super().form_valid(form)
class UpdateStateOrderView(AdminView, TemplateView): class UpdateStateOrderView(AdminView, TemplateView):

View file

@ -8,6 +8,7 @@ from django.views.generic.base import TemplateView
from device.models import Device from device.models import Device
from evidence.models import SystemProperty from evidence.models import SystemProperty
from lot.models import LotTag from lot.models import LotTag
from action.models import StateDefinition
class Http403(PermissionDenied): class Http403(PermissionDenied):
@ -32,7 +33,12 @@ class DashboardView(LoginRequiredMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
lot_tags = LotTag.objects.filter(
owner=self.request.user.institution,
inbox=False
)
context.update({ context.update({
"inbox": LotTag.objects.get(inbox=True).name,
"commit_id": settings.COMMIT, "commit_id": settings.COMMIT,
'title': self.title, 'title': self.title,
'subtitle': self.subtitle, 'subtitle': self.subtitle,
@ -41,7 +47,7 @@ class DashboardView(LoginRequiredMixin):
'section': self.section, 'section': self.section,
'path': resolve(self.request.path).url_name, 'path': resolve(self.request.path).url_name,
'user': self.request.user, 'user': self.request.user,
'lot_tags': LotTag.objects.filter(owner=self.request.user.institution) 'lot_tags': lot_tags
}) })
return context return context

View file

@ -82,11 +82,11 @@
<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 states_panel edit_user delete_user new_user institution' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()"> <a class="admin {% if path in 'panel users states_panel tag_panel edit_user delete_user new_user institution' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()">
<i class="bi bi-person-fill-gear icon_sidebar"></i> <i class="bi bi-person-fill-gear icon_sidebar"></i>
{% trans 'Admin' %} {% trans 'Admin' %}
</a> </a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'panel institution users edit_user new_user delete_user states_panel' %}expanded{% else %}collapse{% endif %}" id="ul_admin" data-bs-parent="#sidebarMenu"> <ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'panel users tag_panel states_panel' %}expanded{% else %}collapse{% endif %}" id="ul_admin" data-bs-parent="#sidebarMenu">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if path in 'panel institution' %} active2{% endif %}" href="{% url 'admin:panel' %}"> <a class="nav-link{% if path in 'panel institution' %} active2{% endif %}" href="{% url 'admin:panel' %}">
{% trans 'Panel' %} {% trans 'Panel' %}
@ -100,33 +100,44 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if path == 'states_panel' %} active2{% endif %}" href="{% url 'admin:states_panel' %}"> <a class="nav-link{% if path == 'states_panel' %} active2{% endif %}" href="{% url 'admin:states_panel' %}">
{% trans 'States' %} {% trans 'States' %}
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if path == 'tag_panel' %} active2{% endif %}" href="{% url 'admin:tag_panel' %}">
{% trans 'Lot Groups' %}
</a> </a>
</li> </li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
<li class="nav-item"> <li class="nav-item">
<a class="admin {% if path == 'unassigned_devices' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_devices" aria-expanded="false" aria-controls="ul_devices" href="javascript:void()"> <a class="admin {% if path in 'all_device' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_device" aria-expanded="false" aria-controls="ul_lots" href="javascript:void()">
<i class="bi bi-laptop icon_sidebar"></i> <i class="bi bi-laptop icon_sidebar"></i>
{% trans 'Devices' %} {% trans 'Device' %}
</a> </a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path == 'unassigned_devices' %}expanded{% else %}collapse{% endif %}" id="ul_devices" data-bs-parent="#sidebarMenu"> <ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'all_device' %}expanded{% else %}collapse{% endif %}" id="ul_device" data-bs-parent="#sidebarMenu">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if path == 'unassigned_devices' %} active2{% endif %}" href="{% url 'dashboard:unassigned_devices' %}"> <a class="nav-link{% if path == 'all_device' %} active2{% endif %}" href="{% url 'dashboard:all_device' %}">
{% trans 'Unassigned devices' %} {% trans 'All' %}
</a> </a>
</li> </li>
</ul> </ul>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="admin {% if path == 'tag' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_lots" aria-expanded="false" aria-controls="ul_lots" href="javascript:void()"> <a class="admin {% if path == 'tags' or path == 'lot' or path in 'unassigned dashboard' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_lots" aria-expanded="false" aria-controls="ul_lots" href="javascript:void()">
<i class="bi bi-database icon_sidebar"></i> <i class="bi bi-database icon_sidebar"></i>
{% trans 'Lots' %} {% trans 'Lots' %}
</a> </a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path == 'tag' %}expanded{% else %}collapse{% endif %}" id="ul_lots" data-bs-parent="#sidebarMenu"> <ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path == 'tags' or path == 'lot' or path in 'unassigned dashboard' %}expanded{% else %}collapse{% endif %}" id="ul_lots" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path == 'unassigned' %} active2{% endif %}" href="{% url 'dashboard:unassigned' %}">
{{ inbox }}
</a>
</li>
{% for tag in lot_tags %} {% for tag in lot_tags %}
<li class="nav-items"> <li class="nav-items">
<a class="nav-link{% if path == 'tag' %} active2{% endif %}" href="{% url 'lot:tag' tag.id %}"> <a class="nav-link{% if path == 'tags' %} active2{% endif %}" href="{% url 'lot:tags' tag.id %}">
{{ tag.name }} {{ tag.name }}
</a> </a>
</li> </li>
@ -134,37 +145,29 @@
</ul> </ul>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="admin {% if path in 'upload list' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_evidences" aria-expanded="false" aria-controls="ul_evidences" href="javascript:void()"> <a class="admin {% if path in 'upload list import add' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_evidences" aria-expanded="false" aria-controls="ul_evidences" href="javascript:void()">
<i class="bi bi-usb-drive icon_sidebar"></i> <i class="bi bi-usb-drive icon_sidebar"></i>
{% trans 'Evidences' %} {% trans 'Evidences' %}
</a> </a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'upload list' %}expanded{% else %}collapse{% endif %}" id="ul_evidences" data-bs-parent="#sidebarMenu"> <ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'upload list import add' %}expanded{% else %}collapse{% endif %}" id="ul_evidences" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path == 'upload' %} active2{% endif %}" href="{% url 'evidence:upload' %}">
{% trans 'Upload one' %}
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if path == 'list' %} active2{% endif %}" href="{% url 'evidence:list' %}"> <a class="nav-link{% if path == 'list' %} active2{% endif %}" href="{% url 'evidence:list' %}">
{% trans 'Old evidences' %} {% trans 'List of evidences' %}
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if path == 'upload' %} active2{% endif %}" href="{% url 'evidence:upload' %}">
{% trans 'Upload with JSON file' %}
</a> </a>
</li> </li>
</ul>
</li>
<li class="nav-item">
<a class="admin {% if path in 'import add' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_placeholders" aria-expanded="false" aria-controls="ul_placeholders" href="javascript:void()">
<i class="bi-menu-button-wide icon_sidebar"></i>
{% trans 'Placeholders' %}
</a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'import add' %}expanded{% else %}collapse{% endif %}" id="ul_placeholders" data-bs-parent="#sidebarMenu">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if path == 'import' %} active2{% endif %}" href="{% url 'evidence:import' %}"> <a class="nav-link{% if path == 'import' %} active2{% endif %}" href="{% url 'evidence:import' %}">
{% trans 'Upload Spreadsheet' %} {% trans 'Upload with Spreadsheet' %}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if path == 'add' %} active2{% endif %}" href="{% url 'device:add' %}"> <a class="nav-link{% if path == 'add' %} active2{% endif %}" href="{% url 'device:add' %}">
{% trans 'Create one' %} {% trans 'Upload with Web Form' %}
</a> </a>
</li> </li>
</ul> </ul>

View file

@ -25,6 +25,9 @@
<div class="dataTable-container"> <div class="dataTable-container">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
Lot actions: <button class="btn btn-green-admin" type="submit" name="url" value="{% url 'lot:add_devices' %}">Add</button> <button class="btn btn-green-admin" type="submit" value="{% url 'lot:del_devices' %}" name="url">Remove</button>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -43,6 +46,9 @@
<th scope="col" data-sortable=""> <th scope="col" data-sortable="">
model model
</th> </th>
<th scope="col" data-sortable="">
updated
</th>
</tr> </tr>
</thead> </thead>
{% for dev in devices %} {% for dev in devices %}
@ -69,11 +75,13 @@
{{ dev.model }} {{ dev.model }}
{% endif %} {% endif %}
</td> </td>
<td>
{{ dev.updated }}
</td>
</tr> </tr>
</tbody> </tbody>
{% endfor %} {% endfor %}
</table> </table>
<button class="btn btn-green-admin" type="submit" value="{% url 'lot:del_devices' %}" name="url">Remove</button> <button class="btn btn-green-admin" type="submit" name="url" value="{% url 'lot:add_devices' %}">add</button>
</form> </form>
</div> </div>
<div class="row mt-3"> <div class="row mt-3">

View file

@ -1,10 +1,11 @@
from django.urls import path from django.urls import path
from dashboard import views from dashboard import views
app_name = 'dashboard' app_name = 'dashboard'
urlpatterns = [ urlpatterns = [
path("", views.UnassignedDevicesView.as_view(), name="unassigned_devices"), path("", views.UnassignedDevicesView.as_view(), name="unassigned"),
path("all", views.AllDevicesView.as_view(), name="all_device"),
path("<int:pk>/", views.LotDashboardView.as_view(), name="lot"), path("<int:pk>/", views.LotDashboardView.as_view(), name="lot"),
path("search", views.SearchView.as_view(), name="search"), path("search", views.SearchView.as_view(), name="search"),
] ]

View file

@ -22,6 +22,16 @@ class UnassignedDevicesView(InventaryMixin):
return Device.get_unassigned(self.request.user.institution, offset, limit) return Device.get_unassigned(self.request.user.institution, offset, limit)
class AllDevicesView(InventaryMixin):
template_name = "unassigned_devices.html"
section = "All"
title = _("All Devices")
breadcrumb = "Devices / All Devices"
def get_devices(self, user, offset, limit):
return Device.get_all(self.request.user.institution, offset, limit)
class LotDashboardView(InventaryMixin, DetailsMixin): class LotDashboardView(InventaryMixin, DetailsMixin):
template_name = "unassigned_devices.html" template_name = "unassigned_devices.html"
section = "dashboard_lot" section = "dashboard_lot"

View file

@ -136,6 +136,87 @@ class Device:
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)]
@classmethod
def get_all(cls, institution, offset=0, limit=None):
sql = """
WITH RankedProperties AS (
SELECT
t1.value,
t1.key,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = '{algorithm}' THEN 2
END,
t1.created DESC
) AS row_num
FROM evidence_systemproperty AS t1
WHERE t1.owner_id = {institution}
AND t1.key IN ('CUSTOM_ID', '{algorithm}')
)
SELECT DISTINCT
value
FROM
RankedProperties
WHERE
row_num = 1
""".format(
institution=institution.id,
algorithm=institution.algorithm,
)
if limit:
sql += " limit {} offset {}".format(int(limit), int(offset))
sql += ";"
annotations = []
with connection.cursor() as cursor:
cursor.execute(sql)
annotations = cursor.fetchall()
devices = [cls(id=x[0]) for x in annotations]
count = cls.get_all_count(institution)
return devices, count
@classmethod
def get_all_count(cls, institution):
sql = """
WITH RankedProperties AS (
SELECT
t1.value,
t1.key,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = '{algorithm}' THEN 2
END,
t1.created DESC
) AS row_num
FROM evidence_systemproperty AS t1
WHERE t1.owner_id = {institution}
AND t1.key IN ('CUSTOM_ID', '{algorithm}')
)
SELECT
COUNT(DISTINCT value)
FROM
RankedProperties
WHERE
row_num = 1
""".format(
institution=institution.id,
algorithm=institution.algorithm
)
with connection.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()[0][0]
@classmethod @classmethod
def get_unassigned(cls, institution, offset=0, limit=None): def get_unassigned(cls, institution, offset=0, limit=None):
@ -149,8 +230,7 @@ class Device:
ORDER BY ORDER BY
CASE CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1 WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'ereuse24' THEN 2 WHEN t1.key = '{algorithm}' THEN 2
ELSE 3
END, END,
t1.created DESC t1.created DESC
) AS row_num ) AS row_num
@ -158,6 +238,7 @@ class Device:
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.key IN ('CUSTOM_ID', '{algorithm}')
) )
SELECT DISTINCT SELECT DISTINCT
value value
@ -167,6 +248,7 @@ class Device:
row_num = 1 row_num = 1
""".format( """.format(
institution=institution.id, institution=institution.id,
algorithm=institution.algorithm
) )
if limit: if limit:
sql += " limit {} offset {}".format(int(limit), int(offset)) sql += " limit {} offset {}".format(int(limit), int(offset))
@ -195,8 +277,7 @@ class Device:
ORDER BY ORDER BY
CASE CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1 WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'ereuse24' THEN 2 WHEN t1.key = '{algorithm}' THEN 2
ELSE 3
END, END,
t1.created DESC t1.created DESC
) AS row_num ) AS row_num
@ -204,6 +285,7 @@ class Device:
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.key IN ('CUSTOM_ID', '{algorithm}')
) )
SELECT SELECT
COUNT(DISTINCT value) COUNT(DISTINCT value)
@ -213,6 +295,7 @@ class Device:
row_num = 1 row_num = 1
""".format( """.format(
institution=institution.id, institution=institution.id,
algorithm=institution.algorithm
) )
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(sql) cursor.execute(sql)
@ -230,16 +313,14 @@ class Device:
ORDER BY ORDER BY
CASE CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1 WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'ereuse24' THEN 2 WHEN t1.key = '{algorithm}' THEN 2
ELSE 3
END, END,
t1.created DESC t1.created DESC
) AS row_num ) AS row_num
FROM evidence_systemproperty AS t1 FROM evidence_systemproperty AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id WHERE t1.owner_id = {institution}
WHERE t2.device_id IS NULL
AND t1.owner_id = {institution}
AND t1.uuid = '{uuid}' AND t1.uuid = '{uuid}'
AND t1.key IN ('CUSTOM_ID', '{algorithm}')
) )
SELECT DISTINCT SELECT DISTINCT
value value
@ -250,6 +331,7 @@ class Device:
""".format( """.format(
uuid=uuid.replace("-", ""), uuid=uuid.replace("-", ""),
institution=institution.id, institution=institution.id,
algorithm=institution.algorithm,
) )
properties = [] properties = []
@ -274,6 +356,12 @@ class Device:
self.get_last_evidence() self.get_last_evidence()
return self.last_evidence.get_manufacturer() return self.last_evidence.get_manufacturer()
@property
def updated(self):
"""get timestamp from last evidence created"""
self.get_last_evidence()
return self.last_evidence.created
@property @property
def serial_number(self): def serial_number(self):
self.get_last_evidence() self.get_last_evidence()

View file

@ -78,7 +78,7 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="container"> <div class="container">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a> <a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" /> <input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div> </div>

View file

@ -40,7 +40,7 @@ class NewDeviceView(DashboardView, FormView):
template_name = "new_device.html" template_name = "new_device.html"
title = _("New Device") title = _("New Device")
breadcrumb = "Device / New Device" breadcrumb = "Device / New Device"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned')
form_class = DeviceFormSet form_class = DeviceFormSet
def form_valid(self, form): def form_valid(self, form):

View file

@ -27,7 +27,7 @@ class BuildMix:
hid = "" hid = ""
for f in algorithm: for f in algorithm:
if hasattr(self, f): if hasattr(self, f):
hid += getattr(self, f) hid += getattr(self, f) or ''
return hid return hid
def generate_chids(self): def generate_chids(self):

View file

@ -11,8 +11,8 @@
<!-- override invalid-feedback class --> <!-- override invalid-feedback class -->
<style> <style>
.invalid-feedback { .invalid-feedback {
color: #670000; color: #670000;
font-size: 1rem; font-size: 1rem;
} }
</style> </style>
@ -22,7 +22,7 @@
{% bootstrap_form form alert_error_type="none" error_css_class="alert alert-danger alert-icon alert-icon-border" %} {% bootstrap_form form alert_error_type="none" error_css_class="alert alert-danger alert-icon alert-icon-border" %}
<div class="form-actions-no-box"> <div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a> <a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" /> <input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div> </div>

View file

@ -17,18 +17,18 @@ class LoginView(auth_views.LoginView):
template_name = 'login.html' template_name = 'login.html'
extra_context = { extra_context = {
'title': _('Login'), 'title': _('Login'),
'success_url': reverse_lazy('dashboard:unassigned_devices'), 'success_url': reverse_lazy('dashboard:unassigned'),
'commit_id': settings.COMMIT, 'commit_id': settings.COMMIT,
} }
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.extra_context['success_url'] = request.GET.get( self.extra_context['success_url'] = request.GET.get(
'next', 'next',
reverse_lazy('dashboard:unassigned_devices') reverse_lazy('dashboard:unassigned')
) )
if not self.request.user.is_anonymous: if not self.request.user.is_anonymous:
return redirect(reverse_lazy('dashboard:unassigned_devices')) return redirect(reverse_lazy('dashboard:unassigned'))
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
@ -72,4 +72,3 @@ class PasswordResetView(auth_views.PasswordResetView):
except Exception as err: except Exception as err:
logger.error(err) logger.error(err)
return HttpResponseRedirect(self.success_url) return HttpResponseRedirect(self.success_url)

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-02-17 10:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lot', '0006_lotproperty_property_unique_type_key_lot'),
]
operations = [
migrations.AddField(
model_name='lottag',
name='inbox',
field=models.BooleanField(default=False),
),
]

View file

@ -15,6 +15,7 @@ class LotTag(models.Model):
name = models.CharField(max_length=STR_SIZE, blank=False, null=False) name = models.CharField(max_length=STR_SIZE, blank=False, null=False)
owner = models.ForeignKey(Institution, on_delete=models.CASCADE) owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
inbox = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return self.name return self.name
@ -45,7 +46,7 @@ class Lot(models.Model):
for d in DeviceLot.objects.filter(lot=self, device_id=v): for d in DeviceLot.objects.filter(lot=self, device_id=v):
d.delete() d.delete()
class LotProperty (Property): class LotProperty(Property):
lot = models.ForeignKey(Lot, on_delete=models.CASCADE) lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
class Type(models.IntegerChoices): class Type(models.IntegerChoices):

View file

@ -30,7 +30,7 @@
{% endif %} {% endif %}
{% bootstrap_form form %} {% bootstrap_form form %}
<div class="form-actions-no-box"> <div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a> <a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Delete' %}" /> <input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Delete' %}" />
</div> </div>

View file

@ -11,20 +11,26 @@
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% for tag in lot_tags %} <table class="table">
<div class="row"> <thead>
<div class="col-lg-3 col-md-4 label ">{{ tag }}</div> <tr>
</div> <th>Select</th>
<th>Lot Group / Lot</th>
{% for lot in lots %} </tr>
{% if lot.type == tag %} </thead>
<div class="row"> <tbody>
<div class="col-lg-3 col-md-4 label "><input type="checkbox" name="lots" value="{{ lot.id }}" /></div> {% for tag in lot_tags %}
<div class="col-lg-3 col-md-4 label ">{{ lot.name }}</div> {% for lot in lots %}
</div> {% if lot.type == tag %}
{% endif %} <tr>
{% endfor %} <td><input type="checkbox" name="lots" value="{{ lot.id }}" /></td>
{% endfor %} <td>{{ tag }}/{{ lot.name }}</td>
</tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody>
</table>
<button class="btn btn-green-admin" type="submit">Save</button> <button class="btn btn-green-admin" type="submit">Save</button>
</form> </form>

View file

@ -24,7 +24,7 @@
{% endif %} {% endif %}
{% bootstrap_form form %} {% bootstrap_form form %}
<div class="form-actions-no-box"> <div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a> <a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" /> <input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div> </div>

View file

@ -9,7 +9,7 @@ urlpatterns = [
path("edit/<int:pk>/", views.EditLotView.as_view(), name="edit"), path("edit/<int:pk>/", views.EditLotView.as_view(), name="edit"),
path("add/devices/", views.AddToLotView.as_view(), name="add_devices"), path("add/devices/", views.AddToLotView.as_view(), name="add_devices"),
path("del/devices/", views.DelToLotView.as_view(), name="del_devices"), path("del/devices/", views.DelToLotView.as_view(), name="del_devices"),
path("tag/<int:pk>/", views.LotsTagsView.as_view(), name="tag"), path("group/<int:pk>/", views.LotsTagsView.as_view(), name="tags"),
path("<int:pk>/property", views.LotPropertiesView.as_view(), name="properties"), path("<int:pk>/property", views.LotPropertiesView.as_view(), name="properties"),
path("<int:pk>/property/add", views.AddLotPropertyView.as_view(), name="add_property"), path("<int:pk>/property/add", views.AddLotPropertyView.as_view(), name="add_property"),
path("<int:pk>/property/update", views.UpdateLotPropertyView.as_view(), name="update_property"), path("<int:pk>/property/update", views.UpdateLotPropertyView.as_view(), name="update_property"),

View file

@ -18,7 +18,7 @@ class NewLotView(DashboardView, CreateView):
template_name = "new_lot.html" template_name = "new_lot.html"
title = _("New lot") title = _("New lot")
breadcrumb = "lot / New lot" breadcrumb = "lot / New lot"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned')
model = Lot model = Lot
fields = ( fields = (
"type", "type",
@ -28,6 +28,14 @@ class NewLotView(DashboardView, CreateView):
"closed", "closed",
) )
def get_form(self):
form = super().get_form()
form.fields["type"].queryset = LotTag.objects.filter(
owner=self.request.user.institution,
inbox=False
)
return form
def form_valid(self, form): 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
@ -39,7 +47,7 @@ class DeleteLotView(DashboardView, DeleteView):
template_name = "delete_lot.html" template_name = "delete_lot.html"
title = _("Delete lot") title = _("Delete lot")
breadcrumb = "lot / Delete lot" breadcrumb = "lot / Delete lot"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned')
model = Lot model = Lot
fields = ( fields = (
"type", "type",
@ -58,7 +66,7 @@ class EditLotView(DashboardView, UpdateView):
template_name = "new_lot.html" template_name = "new_lot.html"
title = _("Edit lot") title = _("Edit lot")
breadcrumb = "Lot / Edit lot" breadcrumb = "Lot / Edit lot"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned')
model = Lot model = Lot
fields = ( fields = (
"type", "type",
@ -84,7 +92,7 @@ class AddToLotView(DashboardView, FormView):
template_name = "list_lots.html" template_name = "list_lots.html"
title = _("Add to lots") title = _("Add to lots")
breadcrumb = "lot / add to lots" breadcrumb = "lot / add to lots"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned')
form_class = LotsForm form_class = LotsForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -125,7 +133,7 @@ class LotsTagsView(DashboardView, TemplateView):
template_name = "lots.html" template_name = "lots.html"
title = _("lots") title = _("lots")
breadcrumb = _("lots") + " /" breadcrumb = _("lots") + " /"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
self.pk = kwargs.get('pk') self.pk = kwargs.get('pk')

View file

@ -2,6 +2,7 @@ from django.core.management.base import BaseCommand
from user.models import Institution from user.models import Institution
from lot.models import LotTag from lot.models import LotTag
class Command(BaseCommand): class Command(BaseCommand):
help = "Create a new Institution" help = "Create a new Institution"
@ -13,6 +14,11 @@ class Command(BaseCommand):
self.create_lot_tags() self.create_lot_tags()
def create_lot_tags(self): def create_lot_tags(self):
LotTag.objects.create(
inbox=True,
name="Inbox",
owner=self.institution
)
tags = [ tags = [
"Entrada", "Entrada",
"Salida", "Salida",

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-02-18 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='institution',
name='algorithm',
field=models.CharField(choices=[('ereuse24', 'ereuse24'), ('ereuse22', 'ereuse22')], default='ereuse24', max_length=30),
),
]

View file

@ -1,8 +1,10 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
from utils.constants import ALGOS
# Create your models here.
ALGORITHMS = [(x, x) for x in ALGOS.keys()]
class Institution(models.Model): class Institution(models.Model):
@ -27,6 +29,7 @@ class Institution(models.Model):
blank=True, blank=True,
null=True null=True
) )
algorithm = models.CharField(max_length=30, choices=ALGORITHMS, default='ereuse24')
class UserManager(BaseUserManager): class UserManager(BaseUserManager):