Compare commits
176 commits
main
...
feature/f3
Author | SHA1 | Date | |
---|---|---|---|
|
d4f971bfa3 | ||
|
fcd6e13bf9 | ||
|
e692417c73 | ||
|
9600a6064f | ||
|
b83afa6f12 | ||
|
c6f63d4d44 | ||
|
b2bf894338 | ||
|
fdc375f804 | ||
|
f26935b617 | ||
|
01bb822aa5 | ||
|
5c7fc8fd47 | ||
|
a7c19ac93e | ||
|
de1f090694 | ||
|
a876acc814 | ||
|
a8884ea012 | ||
|
4b1fb26c67 | ||
|
03bdd4818b | ||
|
1f93c88bc8 | ||
|
573603a6ea | ||
|
3d49db9436 | ||
|
d4d0a35e4a | ||
|
d8dc37ba94 | ||
|
8e29ef4bf5 | ||
|
62006eb4e3 | ||
|
e42f2c3ea3 | ||
|
911388718d | ||
|
3d744e7945 | ||
|
277a7606e2 | ||
|
fe1d020618 | ||
|
c8ddec6942 | ||
|
886cf20565 | ||
|
871ed179fb | ||
|
eb796de4d3 | ||
|
4b9bcb054e | ||
|
55e018ad51 | ||
|
782fc4a541 | ||
|
85011076e8 | ||
|
0fc50d7187 | ||
|
746a692118 | ||
|
a3613732a9 | ||
|
711fb9e171 | ||
|
d44310bcfd | ||
|
60c618ce09 | ||
|
4e69062452 | ||
|
490144fd86 | ||
|
e6995e74e0 | ||
|
1fe20e11b8 | ||
|
1267f388c1 | ||
|
369dc83154 | ||
|
3d0527edf1 | ||
|
0bbc3475c2 | ||
|
13a74b133d | ||
|
4f38cdf5b1 | ||
|
6bb31c40ff | ||
|
6fd9792d78 | ||
|
c27b2c2263 | ||
|
97c74ca9bb | ||
|
51cbd2bf62 | ||
|
93a70ed031 | ||
|
83885ceb84 | ||
|
79df0100b1 | ||
|
ccdd292a97 | ||
|
518866bbc9 | ||
|
4533395e3b | ||
|
1a30fa75dc | ||
|
9a3a5fe638 | ||
|
d085d448a9 | ||
|
0974e074d2 | ||
|
b707d78595 | ||
|
6c35210d4e | ||
|
d614fd4756 | ||
|
cd93e6dafa | ||
|
a81172ad8e | ||
|
73a72a7d15 | ||
|
bdf029abbd | ||
|
893bf0c14d | ||
|
96b0a0ad80 | ||
|
eeea0b3879 | ||
|
c30416d038 | ||
|
1757f70963 | ||
|
d7bd47554a | ||
|
ba2ddecc11 | ||
|
59bdab7776 | ||
|
5b90d7a648 | ||
|
9dc7a66ffd | ||
|
01dbe005e4 | ||
|
448287248e | ||
|
e6223420d2 | ||
|
6ee0184f66 | ||
|
8f206340f3 | ||
|
324eaa215c | ||
|
0da3e15a03 | ||
|
bd4f6b7d56 | ||
|
f9c9c9dd7c | ||
|
60ccbec369 | ||
|
3fb0961815 | ||
|
447946a576 | ||
|
5d190d07a3 | ||
|
d1abb206e8 | ||
|
85bae67189 | ||
|
d429485651 | ||
|
07c25f4a92 | ||
|
14277c17cb | ||
|
f7051c3130 | ||
|
09be1a2f74 | ||
|
a3dd5d9639 | ||
|
3f5460b81f | ||
|
bf7975bc24 | ||
|
8e128557c0 | ||
|
25e7e85548 | ||
|
ba126491be | ||
|
81e7ba267d | ||
|
1e08f0fc0c | ||
|
ebabb6b228 | ||
|
4954199610 | ||
|
e84b72c70b | ||
|
99435fff85 | ||
|
6c0e77891f | ||
|
a2d859494b | ||
|
ea6d990e56 | ||
|
612737d46c | ||
|
30be57ee25 | ||
|
88bdabb64f | ||
|
96268c8caf | ||
|
7ed05f0932 | ||
|
b652d7d452 | ||
|
04ecb4f2f1 | ||
|
1613eaaa44 | ||
|
06264558df | ||
|
80b4c3b4ca | ||
|
e2078c7bde | ||
|
cfae9d4ec9 | ||
|
578fa73fe5 | ||
|
f1d57ff618 | ||
|
3cf8ceb5d3 | ||
|
b56dc0dfda | ||
|
1c58bff515 | ||
|
e6c1ede93c | ||
|
371845971c | ||
|
b4efcfb171 | ||
|
ac0d36ea6f | ||
|
6a3a2b3a2b | ||
|
850678fbe4 | ||
|
f43aaf6ac6 | ||
|
355ed08561 | ||
|
d0e46aa0b0 | ||
|
771b216a31 | ||
|
263eacda99 | ||
|
8fcd20f609 | ||
|
15fb5d3739 | ||
|
d7ff3c2798 | ||
|
0e0ad400c2 | ||
|
367d3a7f87 | ||
|
c90ed58ea0 | ||
|
45629db102 | ||
|
1e29f9562d | ||
|
d0cac9d1d9 | ||
|
8b4d1f51f6 | ||
|
34ea4bedfc | ||
|
fe429e7db6 | ||
|
caf2606fd9 | ||
|
73d478f517 | ||
|
0f03171076 | ||
|
bfdcb33538 | ||
|
271ac83d71 | ||
|
f7b2687ca2 | ||
|
1dad22c3d3 | ||
|
7de6d69a6c | ||
|
fa5b9eec67 | ||
|
7fd42db3e4 | ||
|
bed40d3ee0 | ||
|
9553ed6a4c | ||
|
f3c9297ffd | ||
|
cb6c7f6fda | ||
|
a0276f439e | ||
|
a4d361ff9b |
.env.example
admin
api
dashboard
device
dhub
did
docker-compose.ymldocker-reset.shdocker
dpp
environmental_impact
evidence
locale
login
lot
requirements.txttests/end-to-end
utils
|
@ -13,6 +13,7 @@ DEVICEHUB_PORT=8001
|
|||
DEMO=true
|
||||
# note that with DEBUG=true, logs are more verbose (include tracebacks)
|
||||
DEBUG=true
|
||||
ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1,
|
||||
DPP=false
|
||||
|
||||
STATIC_ROOT=/tmp/static/
|
||||
|
|
|
@ -16,15 +16,8 @@
|
|||
<div class="col">
|
||||
{% if lot_tags_edit %}
|
||||
<table class="table table-hover table-bordered align-middle">
|
||||
<caption class="text-muted small">
|
||||
{% trans 'Inbox order CANNOT be changed' %}
|
||||
</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" width="1%" class="text-start">
|
||||
</th>
|
||||
<th scope="col" width="5%" class="text-center">
|
||||
#</th>
|
||||
<th scope="col">{% trans "Lot Group Name" %}
|
||||
</th>
|
||||
<th scope="col" width="15%" class="text-center">{% trans "Actions" %}
|
||||
|
@ -33,16 +26,7 @@
|
|||
</thead>
|
||||
<tbody id="sortable_list">
|
||||
{% for tag in lot_tags_edit %}
|
||||
<tr {% if tag.id == 1 %} class="bg-light no-sort"{% endif %}
|
||||
data-lookup="{{ tag.id }}"
|
||||
style="cursor: grab;" >
|
||||
|
||||
<td>
|
||||
<i class="bi bi-grip-vertical" aria-hidden="true" ></i>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<strong>{{ tag.order }} </strong>
|
||||
</td>
|
||||
<tr>
|
||||
<td class="font-monospace">
|
||||
{{ tag.name }}
|
||||
</td>
|
||||
|
@ -60,8 +44,7 @@
|
|||
<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 }}"
|
||||
{% if tag.id == 1 %} disabled {% endif %}>
|
||||
data-bs-target="#deleteLotTagModal{{ tag.id }}" >
|
||||
<i class="bi bi-trash me-1"></i>
|
||||
{% trans 'Delete' %}
|
||||
</button>
|
||||
|
@ -72,11 +55,6 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<form id="orderingForm" method="post" action="{% url 'admin:update_lot_tag_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">
|
||||
|
@ -132,9 +110,6 @@
|
|||
<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>
|
||||
{% if tag.id == 1 %}
|
||||
<p class="text-muted text-end mt-3">{% trans "INBOX can only be edited, not deleted." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -161,28 +136,16 @@
|
|||
<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 %}
|
||||
{% if tag.lot_set.first %}
|
||||
<div class="alert alert-warning text-center" role="alert">
|
||||
<strong class="text-bold mb-0"> {% trans "This lot group has" %} {{tag.lot_set.count}} {% trans "lot/s." %}</strong>
|
||||
|
||||
{% trans "Failed to remove Lot Group, it is not empty" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="mb-0 text-muted mt-2">{% trans "Are you sure you want to delete this lot group?" %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% 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>
|
||||
{% if tag.lot_set.first %}
|
||||
<p class="mb-0 text-muted text-end mt-3">
|
||||
{% trans "This lot group is not empty and therefore cannot be deleted." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
|
@ -192,7 +155,7 @@
|
|||
{% trans "Cancel" %}
|
||||
</button>
|
||||
{% if tag.lot_set.first %}
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" disabled>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
{% else %}
|
||||
|
@ -207,40 +170,4 @@
|
|||
</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 inbox_row = document.getElementById('inbox');
|
||||
|
||||
const sortable = new Sortable(sortable_table, {
|
||||
animation: 150,
|
||||
swapThreshold: 0.10,
|
||||
filter: '.no-sort',
|
||||
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 %}
|
||||
|
|
|
@ -19,5 +19,4 @@ urlpatterns = [
|
|||
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'),
|
||||
path("lot/update_order/", views.UpdateLotTagOrderView.as_view(), name='update_lot_tag_order'),
|
||||
]
|
||||
|
|
|
@ -122,7 +122,7 @@ class LotTagPanelView(AdminView, TemplateView):
|
|||
context = super().get_context_data(**kwargs)
|
||||
lot_tags = LotTag.objects.filter(
|
||||
owner=self.request.user.institution
|
||||
).order_by('order')
|
||||
)
|
||||
context.update({"lot_tags_edit": lot_tags})
|
||||
return context
|
||||
|
||||
|
@ -206,35 +206,6 @@ class UpdateLotTagView(AdminView, UpdateView):
|
|||
return response
|
||||
|
||||
|
||||
class UpdateLotTagOrderView(AdminView, TemplateView):
|
||||
success_url = reverse_lazy('admin:tag_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 = 2
|
||||
for lookup_id in ordered_ids:
|
||||
lot_tag = LotTag.objects.get(id=lookup_id)
|
||||
|
||||
if lookup_id != '1': # skip the inbox lot
|
||||
lot_tag.order = current_order
|
||||
current_order += 1
|
||||
else:
|
||||
#just make sure order is one
|
||||
lot_tag.order = 1
|
||||
|
||||
lot_tag.save()
|
||||
|
||||
messages.success(self.request, _("Order changed successfully."))
|
||||
return redirect(self.success_url)
|
||||
else:
|
||||
return Http404
|
||||
|
||||
|
||||
class InstitutionView(AdminView, UpdateView):
|
||||
template_name = "institution.html"
|
||||
title = _("Edit institution")
|
||||
|
|
13
api/views.py
13
api/views.py
|
@ -90,15 +90,15 @@ class NewSnapshotView(ApiMixing):
|
|||
ev_uuid = data["credentialSubject"].get("uuid")
|
||||
|
||||
if not ev_uuid:
|
||||
txt = "error: the snapshot does not have an uuid"
|
||||
txt = "error: the snapshot not have uuid"
|
||||
logger.error("%s", txt)
|
||||
return JsonResponse({'status': txt}, status=500)
|
||||
|
||||
exist_property = SystemProperty.objects.filter(
|
||||
exist_annotation = Annotation.objects.filter(
|
||||
uuid=ev_uuid
|
||||
).first()
|
||||
|
||||
if exist_property:
|
||||
if exist_annotation:
|
||||
txt = "error: the snapshot {} exist".format(ev_uuid)
|
||||
logger.warning("%s", txt)
|
||||
return JsonResponse({'status': txt}, status=500)
|
||||
|
@ -115,16 +115,17 @@ class NewSnapshotView(ApiMixing):
|
|||
text = "fail: It is not possible to parse snapshot"
|
||||
return JsonResponse({'status': text}, status=500)
|
||||
|
||||
prop = SystemProperty.objects.filter(
|
||||
annotation = Annotation.objects.filter(
|
||||
uuid=ev_uuid,
|
||||
type=Annotation.Type.SYSTEM,
|
||||
# TODO this is hardcoded, it should select the user preferred algorithm
|
||||
key="ereuse24",
|
||||
owner=self.tk.owner.institution
|
||||
).first()
|
||||
|
||||
|
||||
if not prop:
|
||||
logger.error("Error: No property for uuid: %s", ev_uuid)
|
||||
if not annotation:
|
||||
logger.error("Error: No annotation for uuid: %s", ev_uuid)
|
||||
return JsonResponse({'status': 'fail'}, status=500)
|
||||
|
||||
url_args = reverse_lazy("device:details", args=(prop.value,))
|
||||
|
|
|
@ -35,7 +35,7 @@ class DashboardView(LoginRequiredMixin):
|
|||
context = super().get_context_data(**kwargs)
|
||||
lot_tags = LotTag.objects.filter(
|
||||
owner=self.request.user.institution,
|
||||
).order_by('order')
|
||||
)
|
||||
context.update({
|
||||
"commit_id": settings.COMMIT,
|
||||
'title': self.title,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load i18n static language_code %}
|
||||
{% load i18n static %}
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
@ -113,7 +113,7 @@
|
|||
<li class="nav-item">
|
||||
<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>
|
||||
{% trans 'Devices' %}
|
||||
{% trans 'Device' %}
|
||||
</a>
|
||||
<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">
|
||||
|
@ -124,7 +124,7 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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()">
|
||||
<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>
|
||||
{% trans 'Lots' %}
|
||||
</a>
|
||||
|
@ -133,14 +133,11 @@
|
|||
{% for tag in lot_tags %}
|
||||
<li class="nav-items">
|
||||
{% if tag.inbox %}
|
||||
<a class="nav-link{% if path == 'inbox' %} active2{% endif %}" href="{% url 'dashboard:unassigned' %}">
|
||||
<i>
|
||||
{{ tag.name }}
|
||||
</i>
|
||||
<a class="nav-link{% if path == 'unassigned' %} active2{% endif %}" href="{% url 'dashboard:unassigned' %}">
|
||||
{% else %}
|
||||
<a class="nav-link{% if path == 'tags' %} active2{% endif %}" href="{% url 'lot:tags' tag.id %}">
|
||||
{{ tag.name }}
|
||||
{% endif %}
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -159,17 +156,17 @@
|
|||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if path == 'upload' %} active2{% endif %}" href="{% url 'evidence:upload' %}">
|
||||
{% trans 'Upload JSON file' %}
|
||||
{% trans 'Upload with JSON file' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if path == 'import' %} active2{% endif %}" href="{% url 'evidence:import' %}">
|
||||
{% trans 'Upload Spreadsheet' %}
|
||||
{% trans 'Upload with Spreadsheet' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if path == 'add' %} active2{% endif %}" href="{% url 'device:add' %}">
|
||||
{% trans 'Upload Web Form' %}
|
||||
{% trans 'Upload with Web Form' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -200,7 +197,7 @@
|
|||
<form method="get" action="{% url 'dashboard:search' %}">
|
||||
{% csrf_token %}
|
||||
<div class="input-group rounded">
|
||||
<input type="search" name="search" class="form-control rounded" {% if search %}value="{{ search }}" {% endif %}placeholder="{% trans "Search your device" %}" aria-label="Search" aria-describedby="search-addon" />
|
||||
<input type="search" name="search" class="form-control rounded" {% if search %}value="{{ search }}" {% endif %}placeholder="Search your device..." aria-label="Search" aria-describedby="search-addon" />
|
||||
<span class="input-group-text border-0" id="search-addon">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
|
@ -225,14 +222,11 @@
|
|||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer mt-auto py-3" style="width: 100%;">
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted mx-auto">{{ commit_id }}</span>
|
||||
{% include "language_picker.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<footer class="footer text-center mt-auto py-3">
|
||||
<div class="container">
|
||||
<span class="text-muted">{{ commit_id }}</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static "js/jquery-3.3.1.slim.min.js" %}"></script>
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
{% load i18n language_code %}
|
||||
|
||||
<div class="dropdown">
|
||||
<form action="{% url 'set_language' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-tertiary dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{{ LANGUAGE_CODE|get_language_code:languages }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="languageDropdown">
|
||||
{% for lang in languages %}
|
||||
<li>
|
||||
<button class="dropdown-item" type="submit" name="language" value="{{ lang.code }}">{{ lang.name }}</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
|
@ -26,29 +26,28 @@
|
|||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
{% trans "Lot Actions" %}: <button class="btn btn-green-admin" type="submit" name="url" value="{% url 'lot:add_devices' %}">{% trans " Add" %}</button> <button class="btn btn-green-admin" type="submit" value="{% url 'lot:del_devices' %}" name="url">{% trans "Remove" %}</button>
|
||||
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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "select" %}
|
||||
select
|
||||
</th>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "shortid" %}
|
||||
shortid
|
||||
</th>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "type" %}
|
||||
type
|
||||
</th>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "manufacturer" %}
|
||||
manufacturer
|
||||
</th>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "model" %}
|
||||
model
|
||||
</th>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "updated" %}
|
||||
updated
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -76,9 +75,6 @@
|
|||
{{ dev.model }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ dev.updated }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
from django import template
|
||||
from django.utils.translation import get_language_info
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def get_language_code(language_code, languages):
|
||||
for lang in languages:
|
||||
if lang['code'] == language_code:
|
||||
return lang['name_local'].lower()
|
||||
return language_code.lower()
|
|
@ -14,13 +14,14 @@ from lot.models import Lot
|
|||
|
||||
class UnassignedDevicesView(InventaryMixin):
|
||||
template_name = "unassigned_devices.html"
|
||||
section = "Inbox"
|
||||
title = _("Inbox")
|
||||
breadcrumb = "Lot / Inbox"
|
||||
section = "Unassigned"
|
||||
title = _("Unassigned Devices")
|
||||
breadcrumb = "Devices / Unassigned Devices"
|
||||
|
||||
def get_devices(self, user, offset, limit):
|
||||
return Device.get_unassigned(self.request.user.institution, offset, limit)
|
||||
|
||||
|
||||
class AllDevicesView(InventaryMixin):
|
||||
template_name = "unassigned_devices.html"
|
||||
section = "All"
|
||||
|
@ -118,14 +119,13 @@ class SearchView(InventaryMixin):
|
|||
# TODO fix of pagination, the count is not correct
|
||||
return devices, count
|
||||
|
||||
def get_properties(self, xp):
|
||||
def get_annotations(self, xp):
|
||||
snap = json.loads(xp.document.get_data())
|
||||
if snap.get("credentialSubject"):
|
||||
uuid = snap["credentialSubject"]["uuid"]
|
||||
else:
|
||||
uuid = snap["uuid"]
|
||||
|
||||
return Device.get_properties_from_uuid(uuid, self.request.user.institution)
|
||||
return Device.get_annotation_from_uuid(uuid, self.request.user.institution)
|
||||
|
||||
def search_hids(self, query, offset, limit):
|
||||
qry = Q()
|
||||
|
|
|
@ -99,12 +99,12 @@ class Device:
|
|||
self.last_evidence = Evidence(self.uuid)
|
||||
return
|
||||
|
||||
properties = self.get_properties()
|
||||
if not properties.count():
|
||||
annotations = self.get_annotations()
|
||||
if not annotations.count():
|
||||
return
|
||||
prop = properties.first()
|
||||
|
||||
self.last_evidence = Evidence(prop.uuid)
|
||||
annotation = annotations.first()
|
||||
self.last_evidence = Evidence(annotation.uuid)
|
||||
self.uuid = annotation.uuid
|
||||
|
||||
def is_eraseserver(self):
|
||||
if not self.uuids:
|
||||
|
@ -392,7 +392,8 @@ class Device:
|
|||
|
||||
@property
|
||||
def version(self):
|
||||
self.get_last_evidence()
|
||||
if not self.last_evidence:
|
||||
self.get_last_evidence()
|
||||
return self.last_evidence.get_version()
|
||||
|
||||
@property
|
||||
|
|
|
@ -75,19 +75,22 @@
|
|||
<li class="nav-item">
|
||||
<a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a>
|
||||
</li>
|
||||
{% if dpps %}
|
||||
<li class="nav-item">
|
||||
<a href="#dpps" class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">{% trans 'Dpps' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if dpps %}
|
||||
<li class="nav-item">
|
||||
<a href="#dpps" class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">{% trans 'Dpps' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#log" class="nav-link" data-bs-toggle="tab" data-bs-target="#log">{% trans 'Log' %}</a>
|
||||
<a href="#environmental_impact" class="nav-link" data-bs-toggle="tab" data-bs-target="#environmental_impact">{% trans 'Environmental impact' %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#environmental_impact" class="nav-link" data-bs-toggle="tab" data-bs-target="#environmental_impact">{% trans 'Environmental impact' %}</a>
|
||||
<a href="#environmental_impact" class="nav-link" data-bs-toggle="tab" data-bs-target="#environmental_impact">{% trans 'Environmental Impact' %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#environmental_impact" class="nav-link" data-bs-toggle="tab" data-bs-target="#environmental_impact">{% trans 'Environmental Impact' %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -118,22 +121,156 @@
|
|||
<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>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-1">
|
||||
<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 %}
|
||||
{% for k, v in object.last_user_evidence %}
|
||||
<div class="row mb-1">
|
||||
<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-1">
|
||||
<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-1">
|
||||
<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-1">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Version' %}
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8">{{ object.version|default:'' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Serial Number' %}
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8">{{ object.serial_number|default:'' }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-lg-3 col-md-4 label">
|
||||
{% trans 'Identifiers' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="environmental_impact">
|
||||
<div class="container-fluid py-3">
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a class="btn btn-success">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
{% trans 'Export to PDF' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-success">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-arrow-down-circle text-success" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
<h5 class="card-title text-success">Carbon Reduction</h5>
|
||||
<h2 class="mb-2">{{ impact.carbon_saved }}</h2>
|
||||
<p class="card-text text-muted">kg CO₂e saved</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-danger">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-cloud-fill text-danger" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
<h5 class="card-title text-danger">Carbon Consumed</h5>
|
||||
<h2 class="mb-2">{{ impact.co2_emissions }}</h2>
|
||||
<p class="card-text text-muted">kg CO₂e consumed</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-success">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-recycle text-success" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
<h5 class="card-title text-success">Additional Impact Metric</h5>
|
||||
<h2 class="mb-2">85%</h2>
|
||||
<p class="card-text text-muted">whatever</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Impact Details</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="bg-light" style="width: 30%;">Manufacturing Impact Avoided</th>
|
||||
<td>
|
||||
<span class="text-success">{{ impact.carbon_saved }}</span> kg CO₂e
|
||||
<br />
|
||||
<small class="text-muted">Based on average laptop manufacturing emissions</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h6>Calculation Method</h6>
|
||||
<small class="text-muted">Based on industry standards X Y and Z</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if dpps %}
|
||||
<div class="tab-pane fade" id="dpps">
|
||||
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
|
||||
<div class="list-group col">
|
||||
{% for d in dpps %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<small class="text-muted">{{ d.timestamp }}</small>
|
||||
<span>{{ d.type }}</span>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
<a href="{% url 'did:device_web' d.signature %}">{{ d.signature }}</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -18,19 +18,17 @@
|
|||
<div class="border p-2 rounded d-flex align-items-center">
|
||||
<label for="algorithmSelect" class="text-muted fw-bold me-2">{% trans 'Algorithm Selector' %}</label>
|
||||
<select class="form-select form-select-sm w-auto border-0 shadow-none" id="algorithmSelect" onchange="changeAlgorithm()">
|
||||
<option value="dummy" selected>
|
||||
{% trans 'Dummy Algorithm' %}
|
||||
</option>
|
||||
<option value="advanced">
|
||||
{% trans 'Advanced Algorithm' %}
|
||||
</option>
|
||||
<option value="dummy" selected>{% trans 'Dummy Algorithm' %}</option>
|
||||
<option value="advanced">{% trans 'Advanced Algorithm' %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#docsCollapse" aria-expanded="false" aria-controls="docsCollapse">{% trans 'Read about the algorithm insights' %}</button>
|
||||
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#docsCollapse" aria-expanded="false" aria-controls="docsCollapse">
|
||||
{% trans 'Read about the algorithm insights' %}
|
||||
</button>
|
||||
|
||||
<div class="collapse mt-3" id="docsCollapse">
|
||||
<div class="card card-body">
|
||||
|
@ -42,6 +40,6 @@
|
|||
|
||||
<script>
|
||||
function changeAlgorithm() {
|
||||
var selectedAlgorithm = document.getElementById('algorithmSelect').value
|
||||
var selectedAlgorithm = document.getElementById('algorithmSelect').value;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -7,11 +7,7 @@ urlpatterns = [
|
|||
path("add/", views.NewDeviceView.as_view(), name="add"),
|
||||
path("edit/<str:pk>/", views.EditDeviceView.as_view(), name="edit"),
|
||||
path("<str:pk>/", views.DetailsView.as_view(), name="details"),
|
||||
path("<str:pk>/user_property/add",
|
||||
views.AddUserPropertyView.as_view(), name="add_user_property"),
|
||||
path("<str:device_id>/user_property/<int:pk>/delete",
|
||||
views.DeleteUserPropertyView.as_view(), name="delete_user_property"),
|
||||
path("<str:device_id>/user_property/<int:pk>/update",
|
||||
views.UpdateUserPropertyView.as_view(), name="update_user_property"),
|
||||
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
|
||||
path("<str:pk>/annotation/add", views.AddAnnotationView.as_view(), name="add_annotation"),
|
||||
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"),
|
||||
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web")
|
||||
]
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||
|
@ -17,11 +13,11 @@ from django.views.generic.edit import (
|
|||
from django.views.generic.base import TemplateView
|
||||
from action.models import StateDefinition, State, DeviceLog, Note
|
||||
from dashboard.mixins import DashboardView, Http403
|
||||
from environmental_impact.algorithms.algorithm_factory import FactoryEnvironmentImpactAlgorithm
|
||||
from evidence.models import UserProperty, SystemProperty
|
||||
from lot.models import LotTag
|
||||
from device.models import Device
|
||||
from device.forms import DeviceFormSet
|
||||
from environmental_impact.algorithms.algorithm_factory import FactoryEnvironmentImpactAlgorithm
|
||||
if settings.DPP:
|
||||
from dpp.models import Proof
|
||||
from dpp.api_dlt import PROOF_TYPE
|
||||
|
@ -37,6 +33,7 @@ class DeviceLogMixin(DashboardView):
|
|||
institution=self.request.user.institution
|
||||
)
|
||||
|
||||
|
||||
class NewDeviceView(DashboardView, FormView):
|
||||
template_name = "new_device.html"
|
||||
title = _("New Device")
|
||||
|
@ -95,7 +92,7 @@ class DetailsView(DashboardView, TemplateView):
|
|||
lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
|
||||
dpps = []
|
||||
if settings.DPP:
|
||||
_dpps = Proof.objects.filter(
|
||||
dpps = Proof.objects.filter(
|
||||
uuid__in=self.object.uuids,
|
||||
type=PROOF_TYPE["IssueDPP"]
|
||||
)
|
||||
|
@ -113,20 +110,18 @@ class DetailsView(DashboardView, TemplateView):
|
|||
state_definitions = StateDefinition.objects.filter(
|
||||
institution=self.request.user.institution
|
||||
).order_by('order')
|
||||
device_states = State.objects.filter(snapshot_uuid__in=uuids).order_by('-date')
|
||||
device_states = State.objects.filter(
|
||||
snapshot_uuid__in=uuids).order_by('-date')
|
||||
device_logs = DeviceLog.objects.filter(
|
||||
snapshot_uuid__in=uuids).order_by('-date')
|
||||
device_notes = Note.objects.filter(snapshot_uuid__in=uuids).order_by('-date')
|
||||
device_notes = Note.objects.filter(
|
||||
snapshot_uuid__in=uuids).order_by('-date')
|
||||
context.update({
|
||||
'object': self.object,
|
||||
'snapshot': last_evidence,
|
||||
'lot_tags': lot_tags,
|
||||
'dpps': dpps,
|
||||
'impact': enviromental_impact,
|
||||
"state_definitions": state_definitions,
|
||||
"device_states": device_states,
|
||||
"device_logs": device_logs,
|
||||
"device_notes": device_notes,
|
||||
'dpps': dpps,
|
||||
})
|
||||
return context
|
||||
|
||||
|
@ -187,11 +182,12 @@ class PublicDeviceWebView(TemplateView):
|
|||
return JsonResponse(device_data)
|
||||
|
||||
|
||||
class AddUserPropertyView(DeviceLogMixin, CreateView):
|
||||
template_name = "new_user_property.html"
|
||||
title = _("New User Property")
|
||||
breadcrumb = "Device / New Property"
|
||||
model = UserProperty
|
||||
class AddAnnotationView(DashboardView, CreateView):
|
||||
template_name = "new_annotation.html"
|
||||
title = _("New annotation")
|
||||
breadcrumb = "Device / New annotation"
|
||||
success_url = reverse_lazy('dashboard:unassigned_devices')
|
||||
model = Annotation
|
||||
fields = ("key", "value")
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -284,7 +280,7 @@ class DeleteUserPropertyView(DeviceLogMixin, DeleteView):
|
|||
def get_queryset(self):
|
||||
return UserProperty.objects.filter(owner=self.request.user.institution)
|
||||
|
||||
#using post() method because delete() method from DeleteView has some issues
|
||||
# using post() method because delete() method from DeleteView has some issues
|
||||
# with messages framework
|
||||
def post(self, request, *args, **kwargs):
|
||||
pk = self.kwargs.get('pk')
|
||||
|
|
|
@ -68,11 +68,6 @@ EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
|
|||
|
||||
# Application definition
|
||||
|
||||
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# "django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
|
@ -92,6 +87,7 @@ INSTALLED_APPS = [
|
|||
"action",
|
||||
"admin",
|
||||
"api",
|
||||
"environmental_impact"
|
||||
]
|
||||
|
||||
DPP = config("DPP", default=False, cast=bool)
|
||||
|
@ -99,7 +95,6 @@ DPP = config("DPP", default=False, cast=bool)
|
|||
if DPP:
|
||||
INSTALLED_APPS.extend(["dpp", "did"])
|
||||
|
||||
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5.html"
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
|
@ -125,13 +120,7 @@ TEMPLATES = [
|
|||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"django.template.context_processors.i18n",
|
||||
],
|
||||
|
||||
'libraries':{
|
||||
'get_language_code': 'dashboard.templatetags.language_code',
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
]
|
||||
|
@ -184,9 +173,8 @@ if TIME_ZONE == "UTC":
|
|||
|
||||
USE_L10N = True
|
||||
LANGUAGES = [
|
||||
('es', 'español'),
|
||||
('en', 'english'),
|
||||
('ca', 'català'),
|
||||
('es', 'Spanish'),
|
||||
('en', 'English'),
|
||||
]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
|
|
|
@ -16,8 +16,6 @@ Including another URLconf
|
|||
"""
|
||||
from django.conf import settings
|
||||
from django.urls import path, include
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.views.i18n import set_language
|
||||
|
||||
urlpatterns = [
|
||||
# path('api/', include('snapshot.urls')),
|
||||
|
@ -32,10 +30,6 @@ urlpatterns = [
|
|||
path('api/', include('api.urls')),
|
||||
]
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
path("language/", set_language, name='set_language'),
|
||||
)
|
||||
|
||||
if settings.DPP:
|
||||
urlpatterns.extend([
|
||||
path('dpp/', include('dpp.urls')),
|
||||
|
|
16
did/views.py
16
did/views.py
|
@ -106,10 +106,10 @@ class PublicDeviceWebView(TemplateView):
|
|||
'device': {},
|
||||
}
|
||||
dev = Build(self.object.last_evidence.doc, None, check=True)
|
||||
doc = dev.build.get_doc()
|
||||
doc = dev.get_phid()
|
||||
data['document'] = json.dumps(doc)
|
||||
data['device'] = dev.build.device
|
||||
data['components'] = dev.build.components
|
||||
data['device'] = dev.device
|
||||
data['components'] = dev.components
|
||||
|
||||
self.object.get_evidences()
|
||||
last_dpp = Proof.objects.filter(
|
||||
|
@ -118,7 +118,7 @@ class PublicDeviceWebView(TemplateView):
|
|||
|
||||
key = self.pk
|
||||
if last_dpp:
|
||||
key += ":"+last_dpp.signature
|
||||
key = last_dpp.signature
|
||||
|
||||
url = "https://{}/did/{}".format(
|
||||
self.request.get_host(),
|
||||
|
@ -135,17 +135,17 @@ class PublicDeviceWebView(TemplateView):
|
|||
for d in self.object.evidences:
|
||||
d.get_doc()
|
||||
dev = Build(d.doc, None, check=True)
|
||||
doc = dev.build.get_doc()
|
||||
doc = dev.get_phid()
|
||||
ev = json.dumps(doc)
|
||||
phid = dev.sign(ev)
|
||||
phid = dev.get_signature(doc)
|
||||
dpp = "{}:{}".format(self.pk, phid)
|
||||
rr = {
|
||||
'dpp': dpp,
|
||||
'document': ev,
|
||||
'algorithm': ALGORITHM,
|
||||
'manufacturer DPP': '',
|
||||
'device': dev.build.device,
|
||||
'components': dev.build.components
|
||||
'device': dev.device,
|
||||
'components': dev.components
|
||||
}
|
||||
|
||||
tmpl = dpp_tmpl.copy()
|
||||
|
|
|
@ -15,7 +15,6 @@ services:
|
|||
- DEMO_IDHUB_PREDEFINED_TOKEN=${IDHUB_PREDEFINED_TOKEN:-}
|
||||
- PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-}
|
||||
- DPP=${DPP:-false}
|
||||
# TODO manage volumes dev vs prod
|
||||
volumes:
|
||||
- .:/opt/devicehub-django
|
||||
ports:
|
||||
|
|
|
@ -28,8 +28,6 @@ main() {
|
|||
fi
|
||||
# remove old database
|
||||
rm -vfr ./db/*
|
||||
# deactivate configured flag
|
||||
rm -vfr ./already_configured
|
||||
docker compose down -v
|
||||
if [ "${DEV_DOCKER_ALWAYS_BUILD:-}" = 'true' ]; then
|
||||
docker compose pull --ignore-buildable
|
||||
|
|
|
@ -42,7 +42,21 @@ gen_env_vars() {
|
|||
export API_RESOLVER='http://id_index_api:3012'
|
||||
# TODO hardcoded
|
||||
export ID_FEDERATED='DH1'
|
||||
# propagate to .env
|
||||
dpp_env_vars="$(cat <<END
|
||||
API_DLT=${API_DLT}
|
||||
API_DLT_TOKEN=${API_DLT_TOKEN}
|
||||
API_RESOLVER=${API_RESOLVER}
|
||||
ID_FEDERATED=${ID_FEDERATED}
|
||||
END
|
||||
)"
|
||||
fi
|
||||
|
||||
# generate config using env vars from docker
|
||||
# TODO rethink if this is needed because now this is django, not flask
|
||||
cat > .env <<END
|
||||
${dpp_env_vars:-}
|
||||
END
|
||||
}
|
||||
|
||||
handle_federated_id() {
|
||||
|
@ -105,54 +119,8 @@ END
|
|||
./manage.py dlt_register_user "${DATASET_FILE}"
|
||||
}
|
||||
|
||||
# wait until idhub api is prepared to received requests
|
||||
wait_idhub() {
|
||||
echo "Start waiting idhub API"
|
||||
while true; do
|
||||
result="$(curl -s "${url}" \
|
||||
| jq -r .error \
|
||||
|| echo "Reported errors, idhub API is still not ready")"
|
||||
|
||||
if [ "${result}" = "Invalid request method" ]; then
|
||||
break
|
||||
sleep 2
|
||||
else
|
||||
echo "Waiting idhub API"
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
demo__send_to_sign_credential() {
|
||||
filepath="${1}"
|
||||
# hashlib.sha3_256 of PREDEFINED_TOKEN for idhub
|
||||
DEMO_IDHUB_PREDEFINED_TOKEN="${DEMO_IDHUB_PREDEFINED_TOKEN:-}"
|
||||
auth_header="Authorization: Bearer ${DEMO_IDHUB_PREDEFINED_TOKEN}"
|
||||
json_header='Content-Type: application/json'
|
||||
curl -s -X POST \
|
||||
-H "${json_header}" \
|
||||
-H "${auth_header}" \
|
||||
-d @"${filepath}" \
|
||||
"${url}" \
|
||||
| jq -r .data
|
||||
}
|
||||
|
||||
run_demo() {
|
||||
if [ "${DEMO_IDHUB_DOMAIN:-}" ]; then
|
||||
DEMO_IDHUB_DOMAIN="${DEMO_IDHUB_DOMAIN:-}"
|
||||
# this demo only works with FQDN domain (with no ports)
|
||||
url="https://${DEMO_IDHUB_DOMAIN}/webhook/sign/"
|
||||
wait_idhub
|
||||
demo__send_to_sign_credential \
|
||||
'example/demo-snapshots-vc/snapshot_pre-verifiable-credential.json' \
|
||||
> 'example/snapshots/snapshot_workbench-script_verifiable-credential.json'
|
||||
fi
|
||||
./manage.py create_default_states "${INIT_ORG}"
|
||||
/usr/bin/time ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
|
||||
}
|
||||
|
||||
config_phase() {
|
||||
# TODO review this flag file
|
||||
# TODO review this flag file
|
||||
init_flagfile="${program_dir}/already_configured"
|
||||
if [ ! -f "${init_flagfile}" ]; then
|
||||
|
||||
|
@ -165,7 +133,7 @@ config_phase() {
|
|||
# 12, 13, 14
|
||||
config_dpp_part1
|
||||
|
||||
# cleanup other snapshots and copy dlt/dpp snapshots
|
||||
# cleanup other spnapshots and copy dlt/dpp snapshots
|
||||
# TODO make this better
|
||||
rm example/snapshots/*
|
||||
cp example/dpp-snapshots/*.json example/snapshots/
|
||||
|
@ -173,7 +141,7 @@ config_phase() {
|
|||
|
||||
# # 15. Add inventory snapshots for user "${INIT_USER}".
|
||||
if [ "${DEMO:-}" = 'true' ]; then
|
||||
run_demo
|
||||
/usr/bin/time ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
|
||||
fi
|
||||
|
||||
# remain next command as the last operation for this if conditional
|
||||
|
@ -188,11 +156,9 @@ check_app_is_there() {
|
|||
}
|
||||
|
||||
deploy() {
|
||||
if [ -d /opt/devicehub-django/.git ]; then
|
||||
# TODO this is weird, find better workaround
|
||||
git config --global --add safe.directory "${program_dir}"
|
||||
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
|
||||
fi
|
||||
# TODO this is weird, find better workaround
|
||||
git config --global --add safe.directory "${program_dir}"
|
||||
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
|
||||
|
||||
if [ "${DEBUG:-}" = 'true' ]; then
|
||||
./manage.py print_settings
|
||||
|
@ -208,9 +174,6 @@ deploy() {
|
|||
# move the migrate thing in docker entrypoint
|
||||
# inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc
|
||||
echo "INFO detected NEW deployment"
|
||||
if [ ! -d "${program_dir}/db/" ]; then
|
||||
mkdir -p "${program_dir}/db/"
|
||||
fi
|
||||
./manage.py migrate
|
||||
config_phase
|
||||
fi
|
||||
|
|
|
@ -12,7 +12,7 @@ from dpp.models import Proof
|
|||
|
||||
|
||||
class ProofView(View):
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
timestamp = kwargs.get("proof_id")
|
||||
proof = Proof.objects.filter(timestamp=timestamp).first()
|
||||
|
@ -22,9 +22,9 @@ class ProofView(View):
|
|||
ev = Evidence(proof.uuid)
|
||||
if not ev.doc:
|
||||
return JsonResponse({}, status=404)
|
||||
|
||||
|
||||
dev = Build(ev.doc, None, check=True)
|
||||
doc = dev.build.get_doc()
|
||||
doc = dev.get_phid()
|
||||
|
||||
data = {
|
||||
"algorithm": ALGORITHM,
|
||||
|
|
|
@ -12,7 +12,7 @@ class DummyEnvironmentalImpactAlgorithm(EnvironmentImpactAlgorithm):
|
|||
avg_watts = 40 # Arbitrary laptop average consumption
|
||||
co2_per_kwh = 0.475
|
||||
power_on_hours = self.get_power_on_hours_from(device)
|
||||
|
||||
|
||||
energy_kwh = (power_on_hours * avg_watts) / 1000
|
||||
co2_emissions = energy_kwh * co2_per_kwh
|
||||
current_dir = os.path.dirname(__file__)
|
||||
|
|
0
environmental_impact/migrations/__init__.py
Normal file
0
environmental_impact/migrations/__init__.py
Normal file
|
@ -1,149 +1,44 @@
|
|||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
from environmental_impact.algorithms.dummy_algo.dummy_calculator import (
|
||||
DummyEnvironmentalImpactAlgorithm,
|
||||
)
|
||||
from unittest.mock import patch
|
||||
import uuid
|
||||
from django.test import TestCase
|
||||
from device.models import Device
|
||||
from environmental_impact.models import EnvironmentalImpact
|
||||
from environmental_impact.algorithms.dummy_algo.dummy_calculator import DummyEnvironmentalImpactAlgorithm
|
||||
from evidence.models import Evidence
|
||||
|
||||
|
||||
class DummyEnvironmentalImpactAlgorithmTests(unittest.TestCase):
|
||||
class DummyEnvironmentalImpactAlgorithmTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@patch('evidence.models.Evidence.get_doc', return_value={'credentialSubject': {}})
|
||||
@patch('evidence.models.Evidence.get_time', return_value=None)
|
||||
def setUp(self, mock_get_time, mock_get_doc):
|
||||
self.device = Device(id='1')
|
||||
evidence = self.device.last_evidence = Evidence(uuid=uuid.uuid4())
|
||||
evidence.inxi = True
|
||||
evidence.doc = {'credentialSubject': {}}
|
||||
self.algorithm = DummyEnvironmentalImpactAlgorithm()
|
||||
self.device = Mock(spec=Device)
|
||||
self.device.last_evidence = Mock()
|
||||
self.device.last_evidence.inxi = True
|
||||
self.device.components = [
|
||||
{
|
||||
"type": "Motherboard",
|
||||
"manufacturer": "TOSHIBA",
|
||||
"model": "Portable PC",
|
||||
"serialNumber": "C0BZ6MN2",
|
||||
"version": "Version A0",
|
||||
"biosDate": "11/09/2011",
|
||||
"biosVersion": "1.40",
|
||||
"slots": 2,
|
||||
"ramSlots": "",
|
||||
"ramMaxSize": "10 GiB",
|
||||
},
|
||||
{
|
||||
"type": "Processor",
|
||||
"model": "0x2A (42)",
|
||||
"arch": "Sandy Bridge",
|
||||
"bits": 64,
|
||||
"gen": "core 2",
|
||||
"family": "6",
|
||||
"date": "2010-12",
|
||||
"L1": "128 KiB",
|
||||
"L2": "512 KiB",
|
||||
"L3": "3 MiB",
|
||||
"cpus": "1",
|
||||
"cores": 2,
|
||||
"threads": 4,
|
||||
"bogomips": 12769,
|
||||
"base/boost": "1600/3600",
|
||||
"min/max": "800/2300",
|
||||
"ext-clock": "100 MHz",
|
||||
"volts": "1.3 V",
|
||||
},
|
||||
{
|
||||
"type": "RamModule",
|
||||
"manufacturer": "802C",
|
||||
"model": "8KTF51264HZ-1G9P1",
|
||||
"serialNumber": "12A1582B",
|
||||
"speed": "1333 MT/s",
|
||||
"bits": "64",
|
||||
"interface": "DDR3",
|
||||
},
|
||||
{
|
||||
"type": "GraphicCard",
|
||||
"memory": "n/a",
|
||||
"manufacturer": "Toshiba",
|
||||
"model": "Intel 2nd Generation Core Processor Family Integrated Graphics",
|
||||
"arch": "Gen-6",
|
||||
"serialNumber": "",
|
||||
"integrated": False,
|
||||
},
|
||||
{
|
||||
"type": "Display",
|
||||
"model": "",
|
||||
"manufacturer": "",
|
||||
"serialNumber": "",
|
||||
"size": "N/A in console",
|
||||
"diagonal": "",
|
||||
"resolution": "",
|
||||
"date": "",
|
||||
"ratio": "",
|
||||
},
|
||||
{
|
||||
"type": "NetworkAdapter",
|
||||
"model": "Intel 82579V Gigabit Network",
|
||||
"manufacturer": "",
|
||||
"serialNumber": "e8:e0:b7:c8:66:51",
|
||||
"speed": "100 Mbps",
|
||||
"interface": "Integrated",
|
||||
},
|
||||
{
|
||||
"type": "SoundCard",
|
||||
"model": "Intel 6 Series/C200 Series Family High Definition Audio",
|
||||
"manufacturer": "Toshiba 6",
|
||||
"serialNumber": "",
|
||||
},
|
||||
{
|
||||
"type": "Storage",
|
||||
"manufacturer": "Toshiba",
|
||||
"model": "THNSNB128GMCJ",
|
||||
"serialNumber": "Y1LS11Z6TTEZ",
|
||||
"size": "119.24 GiB",
|
||||
"speed": "3.0 Gb/s",
|
||||
"interface": "",
|
||||
"firmware": "",
|
||||
"sata": "2.6",
|
||||
"cycles": "7291",
|
||||
"health": "PASSED",
|
||||
"time of used": "245d 7h",
|
||||
"read used": "",
|
||||
"written used": "",
|
||||
},
|
||||
{
|
||||
"type": "Battery",
|
||||
"model": "G71C000CH310",
|
||||
"serialNumber": "0000000942",
|
||||
"condition": "0.3/46.5 Wh (0.6%)",
|
||||
"cycles": "",
|
||||
"volts": "15.1",
|
||||
},
|
||||
]
|
||||
|
||||
def test_get_power_on_hours_from_inxi_device(self):
|
||||
def test_get_power_on_hours_from_legacy_device(self):
|
||||
# TODO is there a way to check that?
|
||||
pass
|
||||
|
||||
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
|
||||
def test_get_power_on_hours_from_inxi_device(self, mock_get_components):
|
||||
hours = self.algorithm.get_power_on_hours_from(self.device)
|
||||
self.assertEqual(hours, 5887) # 245 days + 7 hours in hours
|
||||
|
||||
def test_convert_str_time_to_hours(self):
|
||||
result = self.algorithm.convert_str_time_to_hours("1y 2d 3h", False)
|
||||
self.assertEqual(
|
||||
result,
|
||||
8760 + 48 + 3,
|
||||
"String to hours conversion should match expected output",
|
||||
)
|
||||
hours, 8811, "Inxi-parsed devices should correctly compute power-on hours")
|
||||
|
||||
@patch(
|
||||
"environmental_impact.algorithms.dummy_algo.dummy_calculator.render_docs",
|
||||
return_value="Dummy Docs",
|
||||
)
|
||||
def test_environmental_impact_calculation(self, mock_render_docs):
|
||||
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
|
||||
def test_convert_str_time_to_hours(self, mock_get_components):
|
||||
result = self.algorithm.convert_str_time_to_hours('1y 2d 3h', False)
|
||||
self.assertEqual(
|
||||
result, 8811, "String to hours conversion should match expected output")
|
||||
|
||||
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
|
||||
def test_environmental_impact_calculation(self, mock_get_components):
|
||||
impact = self.algorithm.get_device_environmental_impact(self.device)
|
||||
self.assertIsInstance(
|
||||
impact,
|
||||
EnvironmentalImpact,
|
||||
"Output should be an EnvironmentalImpact instance",
|
||||
)
|
||||
expected_co2 = 5887 * 40 * 0.475 / 1000
|
||||
self.assertAlmostEqual(
|
||||
impact.co2_emissions,
|
||||
expected_co2,
|
||||
2,
|
||||
"CO2 emissions calculation should be accurate",
|
||||
)
|
||||
self.assertEqual(impact.docs, "Dummy Docs", "Docs should be rendered correctly")
|
||||
self.assertIsInstance(impact, EnvironmentalImpact,
|
||||
"Output should be an EnvironmentalImpact instance")
|
||||
expected_co2 = 8811 * 40 * 0.475 / 1000
|
||||
self.assertAlmostEqual(impact.co2_emissions, expected_co2,
|
||||
2, "CO2 emissions calculation should be accurate")
|
||||
|
|
|
@ -31,11 +31,11 @@ class UploadForm(forms.Form):
|
|||
try:
|
||||
file_json = json.loads(file_data)
|
||||
snap = Build(file_json, None, check=True)
|
||||
exists_property = SystemProperty.objects.filter(
|
||||
exist_annotation = Annotation.objects.filter(
|
||||
uuid=snap.uuid
|
||||
).first()
|
||||
|
||||
if exists_property:
|
||||
if exist_annotation:
|
||||
raise ValidationError(
|
||||
_("The snapshot already exists"),
|
||||
code="duplicate_snapshot",
|
||||
|
@ -234,7 +234,7 @@ class EraseServerForm(forms.Form):
|
|||
if self.instance:
|
||||
return
|
||||
|
||||
UserProperty.objects.create(
|
||||
Annotation.objects.create(
|
||||
uuid=self.uuid,
|
||||
type=UserProperty.Type.ERASE_SERVER,
|
||||
key='ERASE_SERVER',
|
||||
|
|
|
@ -8,8 +8,7 @@ from django.db import models
|
|||
from django.db.models import Q
|
||||
from utils.constants import STR_EXTEND_SIZE, CHASSIS_DH
|
||||
from evidence.xapian import search
|
||||
from evidence.parse_details import ParseSnapshot
|
||||
from evidence.normal_parse_details import get_inxi, get_inxi_key
|
||||
from evidence.parse_details import ParseSnapshot, get_inxi, get_inxi_key
|
||||
from user.models import User, Institution
|
||||
|
||||
|
||||
|
@ -60,7 +59,7 @@ class Evidence:
|
|||
self.created = None
|
||||
self.dmi = None
|
||||
self.inxi = None
|
||||
self.properties = []
|
||||
self.annotations = []
|
||||
self.components = []
|
||||
self.default = "n/a"
|
||||
|
||||
|
@ -111,7 +110,7 @@ class Evidence:
|
|||
self.inxi = ev["output"]
|
||||
else:
|
||||
dmidecode_raw = self.doc["data"]["dmidecode"]
|
||||
inxi_raw = self.doc.get("data", {}).get("inxi")
|
||||
inxi_raw = self.doc["data"]["inxi"]
|
||||
self.dmi = DMIParse(dmidecode_raw)
|
||||
try:
|
||||
self.inxi = json.loads(inxi_raw)
|
||||
|
@ -160,6 +159,9 @@ class Evidence:
|
|||
if self.inxi:
|
||||
return self.device_manufacturer
|
||||
|
||||
if self.inxi:
|
||||
return self.device_manufacturer
|
||||
|
||||
return self.dmi.manufacturer().strip()
|
||||
|
||||
def get_model(self):
|
||||
|
@ -175,11 +177,14 @@ class Evidence:
|
|||
if self.inxi:
|
||||
return self.device_model
|
||||
|
||||
if self.inxi:
|
||||
return self.device_model
|
||||
|
||||
return self.dmi.model().strip()
|
||||
|
||||
def get_chassis(self):
|
||||
if self.is_legacy():
|
||||
return self.doc.get('device', {}).get('model', '')
|
||||
return self.doc['device']['model']
|
||||
|
||||
if self.inxi:
|
||||
return self.device_chassis
|
||||
|
@ -194,7 +199,7 @@ class Evidence:
|
|||
|
||||
def get_serial_number(self):
|
||||
if self.is_legacy():
|
||||
return self.doc.get('device', {}).get('serialNumber', '')
|
||||
return self.doc['device']['serialNumber']
|
||||
|
||||
if self.inxi:
|
||||
return self.device_serial_number
|
||||
|
|
|
@ -2,14 +2,12 @@ import json
|
|||
import hashlib
|
||||
import logging
|
||||
|
||||
from evidence import legacy_parse
|
||||
from evidence import old_parse
|
||||
from evidence import normal_parse
|
||||
from dmidecode import DMIParse
|
||||
from evidence.parse_details import ParseSnapshot
|
||||
|
||||
from evidence.models import SystemProperty
|
||||
from evidence.models import Annotation
|
||||
from evidence.xapian import index
|
||||
from evidence.normal_parse_details import get_inxi_key, get_inxi
|
||||
from evidence.parse_details import get_inxi_key, get_inxi
|
||||
from django.conf import settings
|
||||
|
||||
if settings.DPP:
|
||||
|
@ -26,31 +24,31 @@ def get_mac(inxi):
|
|||
if get_inxi(n, "port"):
|
||||
return get_inxi(iface, 'mac')
|
||||
|
||||
for n, iface in networks:
|
||||
if get_inxi(n, "port"):
|
||||
return get_inxi(iface, 'mac')
|
||||
|
||||
class Build:
|
||||
def __init__(self, evidence_json, user, check=False):
|
||||
"""
|
||||
This Build do the save in xapian as document, in Annotations and do
|
||||
register in dlt if is configured for that.
|
||||
|
||||
We have 4 cases for parser diferents snapshots than come from workbench.
|
||||
1) worbench 11 is old_parse.
|
||||
2) legacy is the worbench-script when create a snapshot for devicehub-teal
|
||||
3) some snapshots come as a credential. In this case is parsed as normal_parse
|
||||
4) normal snapshot from worbench-script is the most basic and is parsed as normal_parse
|
||||
"""
|
||||
self.evidence = evidence_json.copy()
|
||||
self.uuid = self.evidence.get('uuid')
|
||||
self.user = user
|
||||
self.json = evidence_json.copy()
|
||||
|
||||
if evidence_json.get("credentialSubject"):
|
||||
self.build = normal_parse.Build(evidence_json)
|
||||
self.uuid = evidence_json.get("credentialSubject", {}).get("uuid")
|
||||
elif evidence_json.get("software") != "workbench-script":
|
||||
self.build = old_parse.Build(evidence_json)
|
||||
elif evidence_json.get("data",{}).get("lshw"):
|
||||
self.build = legacy_parse.Build(evidence_json)
|
||||
else:
|
||||
self.build = normal_parse.Build(evidence_json)
|
||||
self.json.update(evidence_json["credentialSubject"])
|
||||
if evidence_json.get("evidence"):
|
||||
self.json["data"] = {}
|
||||
for ev in evidence_json["evidence"]:
|
||||
k = ev.get("operation")
|
||||
if not k:
|
||||
continue
|
||||
self.json["data"][k] = ev.get("output")
|
||||
|
||||
self.uuid = self.json['uuid']
|
||||
self.user = user
|
||||
self.hid = None
|
||||
self.chid = None
|
||||
self.phid = self.get_signature(self.json)
|
||||
self.generate_chids()
|
||||
|
||||
if check:
|
||||
return
|
||||
|
@ -67,6 +65,70 @@ class Build:
|
|||
snap = json.dumps(self.evidence)
|
||||
index(self.user.institution, self.uuid, snap)
|
||||
|
||||
def generate_chids(self):
|
||||
self.algorithms = {
|
||||
'hidalgo1': self.get_hid_14(),
|
||||
'legacy_dpp': self.get_chid_dpp(),
|
||||
}
|
||||
|
||||
def get_hid_14(self):
|
||||
if self.json.get("software") == "workbench-script":
|
||||
hid = self.get_hid(self.json)
|
||||
else:
|
||||
device = self.json['device']
|
||||
manufacturer = device.get("manufacturer", '')
|
||||
model = device.get("model", '')
|
||||
chassis = device.get("chassis", '')
|
||||
serial_number = device.get("serialNumber", '')
|
||||
sku = device.get("sku", '')
|
||||
hid = f"{manufacturer}{model}{chassis}{serial_number}{sku}"
|
||||
|
||||
self.chid = hashlib.sha3_256(hid.encode()).hexdigest()
|
||||
return self.chid
|
||||
|
||||
def get_chid_dpp(self):
|
||||
if self.json.get("software") == "workbench-script":
|
||||
device = ParseSnapshot(self.json).device
|
||||
else:
|
||||
device = self.json['device']
|
||||
|
||||
hid = self.get_id_hw_dpp(device)
|
||||
self.chid = hashlib.sha3_256(hid.encode("utf-8")).hexdigest()
|
||||
return self.chid
|
||||
|
||||
def get_id_hw_dpp(self, d):
|
||||
manufacturer = d.get("manufacturer", '')
|
||||
model = d.get("model", '')
|
||||
chassis = d.get("chassis", '')
|
||||
serial_number = d.get("serialNumber", '')
|
||||
sku = d.get("sku", '')
|
||||
typ = d.get("type", '')
|
||||
version = d.get("version", '')
|
||||
|
||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}{typ}{version}"
|
||||
|
||||
def get_phid(self):
|
||||
if self.json.get("software") == "workbench-script":
|
||||
data = ParseSnapshot(self.json)
|
||||
self.device = data.device
|
||||
self.components = data.components
|
||||
else:
|
||||
self.device = self.json.get("device")
|
||||
self.components = self.json.get("components", [])
|
||||
|
||||
self.device.pop("actions", None)
|
||||
for c in self.components:
|
||||
c.pop("actions", None)
|
||||
|
||||
device = self.get_id_hw_dpp(self.device)
|
||||
components = sorted(self.components, key=lambda x: x.get("type"))
|
||||
doc = [("computer", device)]
|
||||
|
||||
for c in components:
|
||||
doc.append((c.get("type"), self.get_id_hw_dpp(c)))
|
||||
|
||||
return doc
|
||||
|
||||
def create_annotations(self):
|
||||
prop = SystemProperty.objects.filter(
|
||||
uuid=self.uuid,
|
||||
|
@ -87,12 +149,39 @@ class Build:
|
|||
value=self.sign(v)
|
||||
)
|
||||
|
||||
def sign(self, doc):
|
||||
return hashlib.sha3_256(doc.encode()).hexdigest()
|
||||
def get_hid(self, snapshot):
|
||||
try:
|
||||
self.inxi = self.json["data"]["inxi"]
|
||||
if isinstance(self.inxi, str):
|
||||
self.inxi = json.loads(self.inxi)
|
||||
except Exception:
|
||||
logger.error("No inxi in snapshot %s", self.uuid)
|
||||
return ""
|
||||
|
||||
machine = get_inxi_key(self.inxi, 'Machine')
|
||||
for m in machine:
|
||||
system = get_inxi(m, "System")
|
||||
if system:
|
||||
manufacturer = system
|
||||
model = get_inxi(m, "product")
|
||||
serial_number = get_inxi(m, "serial")
|
||||
chassis = get_inxi(m, "Type")
|
||||
else:
|
||||
sku = get_inxi(m, "part-nu")
|
||||
|
||||
mac = get_mac(self.inxi) or ""
|
||||
if not mac:
|
||||
txt = "Could not retrieve MAC address in snapshot %s"
|
||||
logger.warning(txt, snapshot['uuid'])
|
||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}"
|
||||
|
||||
return f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}"
|
||||
|
||||
def get_signature(self, doc):
|
||||
return hashlib.sha3_256(json.dumps(doc).encode()).hexdigest()
|
||||
|
||||
def register_device_dlt(self):
|
||||
legacy_dpp = self.build.algorithms.get('ereuse22')
|
||||
chid = self.sign(legacy_dpp)
|
||||
phid = self.sign(json.dumps(self.build.get_doc()))
|
||||
chid = self.algorithms.get('legacy_dpp')
|
||||
phid = self.get_signature(self.get_phid())
|
||||
register_device_dlt(chid, phid, self.uuid, self.user)
|
||||
register_passport_dlt(chid, phid, self.uuid, self.user)
|
||||
|
|
|
@ -1,38 +1,406 @@
|
|||
import re
|
||||
import json
|
||||
import logging
|
||||
|
||||
from evidence import (
|
||||
legacy_parse_details,
|
||||
normal_parse_details,
|
||||
old_parse_details
|
||||
)
|
||||
from datetime import datetime
|
||||
from dmidecode import DMIParse
|
||||
|
||||
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
|
||||
|
||||
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
def get_inxi_key(inxi, component):
|
||||
for n in inxi:
|
||||
for k, v in n.items():
|
||||
if component in k:
|
||||
return v
|
||||
|
||||
|
||||
def get_inxi(n, name):
|
||||
for k, v in n.items():
|
||||
if f"#{name}" in k:
|
||||
return v
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
class ParseSnapshot:
|
||||
def __init__(self, snapshot, default="n/a"):
|
||||
if snapshot.get("credentialSubject"):
|
||||
self.build = normal_parse_details.ParseSnapshot(
|
||||
snapshot,
|
||||
default=default
|
||||
)
|
||||
elif snapshot.get("software") != "workbench-script":
|
||||
self.build = old_parse_details.ParseSnapshot(
|
||||
snapshot,
|
||||
default=default
|
||||
)
|
||||
elif snapshot.get("data",{}).get("lshw"):
|
||||
self.build = legacy_parse_details.ParseSnapshot(
|
||||
snapshot,
|
||||
default=default
|
||||
)
|
||||
else:
|
||||
self.build = normal_parse_details.ParseSnapshot(
|
||||
snapshot,
|
||||
default=default
|
||||
)
|
||||
self.default = default
|
||||
self.dmidecode_raw = snapshot.get("data", {}).get("dmidecode", "{}")
|
||||
self.smart_raw = snapshot.get("data", {}).get("smartctl", [])
|
||||
self.inxi_raw = snapshot.get("data", {}).get("inxi", "") or ""
|
||||
for ev in snapshot.get("evidence", []):
|
||||
if "dmidecode" == ev.get("operation"):
|
||||
self.dmidecode_raw = ev["output"]
|
||||
if "inxi" == ev.get("operation"):
|
||||
self.inxi_raw = ev["output"]
|
||||
if "smartctl" == ev.get("operation"):
|
||||
self.smart_raw = ev["output"]
|
||||
data = snapshot
|
||||
if snapshot.get("credentialSubject"):
|
||||
data = snapshot["credentialSubject"]
|
||||
|
||||
self.default = default
|
||||
self.device = self.build.snapshot_json.get("device")
|
||||
self.components = self.build.snapshot_json.get("components")
|
||||
self.device = {"actions": []}
|
||||
self.components = []
|
||||
|
||||
self.dmi = DMIParse(self.dmidecode_raw)
|
||||
self.smart = self.loads(self.smart_raw)
|
||||
self.inxi = self.loads(self.inxi_raw)
|
||||
|
||||
self.set_computer()
|
||||
self.set_components()
|
||||
self.snapshot_json = {
|
||||
"type": "Snapshot",
|
||||
"device": self.device,
|
||||
"software": data["software"],
|
||||
"components": self.components,
|
||||
"uuid": data['uuid'],
|
||||
"endTime": data["timestamp"],
|
||||
"elapsed": 1,
|
||||
}
|
||||
|
||||
def set_computer(self):
|
||||
machine = get_inxi_key(self.inxi, 'Machine') or []
|
||||
for m in machine:
|
||||
system = get_inxi(m, "System")
|
||||
if system:
|
||||
self.device['manufacturer'] = system
|
||||
self.device['model'] = get_inxi(m, "product")
|
||||
self.device['serialNumber'] = get_inxi(m, "serial")
|
||||
self.device['type'] = get_inxi(m, "Type")
|
||||
self.device['chassis'] = self.device['type']
|
||||
self.device['version'] = get_inxi(m, "v")
|
||||
else:
|
||||
self.device['system_uuid'] = get_inxi(m, "uuid")
|
||||
self.device['sku'] = get_inxi(m, "part-nu")
|
||||
|
||||
def set_components(self):
|
||||
self.get_mother_board()
|
||||
self.get_cpu()
|
||||
self.get_ram()
|
||||
self.get_graphic()
|
||||
self.get_display()
|
||||
self.get_networks()
|
||||
self.get_sound_card()
|
||||
self.get_data_storage()
|
||||
self.get_battery()
|
||||
|
||||
def get_mother_board(self):
|
||||
machine = get_inxi_key(self.inxi, 'Machine') or []
|
||||
mb = {"type": "Motherboard",}
|
||||
for m in machine:
|
||||
bios_date = get_inxi(m, "date")
|
||||
if not bios_date:
|
||||
continue
|
||||
mb["manufacturer"] = get_inxi(m, "Mobo")
|
||||
mb["model"] = get_inxi(m, "model")
|
||||
mb["serialNumber"] = get_inxi(m, "serial")
|
||||
mb["version"] = get_inxi(m, "v")
|
||||
mb["biosDate"] = bios_date
|
||||
mb["biosVersion"] = self.get_bios_version()
|
||||
mb["firewire"]: self.get_firmware_num()
|
||||
mb["pcmcia"]: self.get_pcmcia_num()
|
||||
mb["serial"]: self.get_serial_num()
|
||||
mb["usb"]: self.get_usb_num()
|
||||
|
||||
self.get_ram_slots(mb)
|
||||
|
||||
self.components.append(mb)
|
||||
|
||||
def get_ram_slots(self, mb):
|
||||
memory = get_inxi_key(self.inxi, 'Memory') or []
|
||||
for m in memory:
|
||||
slots = get_inxi(m, "slots")
|
||||
if not slots:
|
||||
continue
|
||||
mb["slots"] = slots
|
||||
mb["ramSlots"] = get_inxi(m, "modules")
|
||||
mb["ramMaxSize"] = get_inxi(m, "capacity")
|
||||
|
||||
|
||||
def get_cpu(self):
|
||||
cpu = get_inxi_key(self.inxi, 'CPU') or []
|
||||
cp = {"type": "Processor"}
|
||||
vulnerabilities = []
|
||||
for c in cpu:
|
||||
base = get_inxi(c, "model")
|
||||
if base:
|
||||
cp["model"] = get_inxi(c, "model")
|
||||
cp["arch"] = get_inxi(c, "arch")
|
||||
cp["bits"] = get_inxi(c, "bits")
|
||||
cp["gen"] = get_inxi(c, "gen")
|
||||
cp["family"] = get_inxi(c, "family")
|
||||
cp["date"] = get_inxi(c, "built")
|
||||
continue
|
||||
des = get_inxi(c, "L1")
|
||||
if des:
|
||||
cp["L1"] = des
|
||||
cp["L2"] = get_inxi(c, "L2")
|
||||
cp["L3"] = get_inxi(c, "L3")
|
||||
cp["cpus"] = get_inxi(c, "cpus")
|
||||
cp["cores"] = get_inxi(c, "cores")
|
||||
cp["threads"] = get_inxi(c, "threads")
|
||||
continue
|
||||
bogo = get_inxi(c, "bogomips")
|
||||
if bogo:
|
||||
cp["bogomips"] = bogo
|
||||
cp["base/boost"] = get_inxi(c, "base/boost")
|
||||
cp["min/max"] = get_inxi(c, "min/max")
|
||||
cp["ext-clock"] = get_inxi(c, "ext-clock")
|
||||
cp["volts"] = get_inxi(c, "volts")
|
||||
continue
|
||||
ctype = get_inxi(c, "Type")
|
||||
if ctype:
|
||||
v = {"Type": ctype}
|
||||
status = get_inxi(c, "status")
|
||||
if status:
|
||||
v["status"] = status
|
||||
mitigation = get_inxi(c, "mitigation")
|
||||
if mitigation:
|
||||
v["mitigation"] = mitigation
|
||||
vulnerabilities.append(v)
|
||||
|
||||
self.components.append(cp)
|
||||
|
||||
|
||||
def get_ram(self):
|
||||
memory = get_inxi_key(self.inxi, 'Memory') or []
|
||||
mem = {"type": "RamModule"}
|
||||
|
||||
for m in memory:
|
||||
base = get_inxi(m, "System RAM")
|
||||
if base:
|
||||
mem["size"] = get_inxi(m, "total")
|
||||
slot = get_inxi(m, "manufacturer")
|
||||
if slot:
|
||||
mem["manufacturer"] = slot
|
||||
mem["model"] = get_inxi(m, "part-no")
|
||||
mem["serialNumber"] = get_inxi(m, "serial")
|
||||
mem["speed"] = get_inxi(m, "speed")
|
||||
mem["bits"] = get_inxi(m, "data")
|
||||
mem["interface"] = get_inxi(m, "type")
|
||||
module = get_inxi(m, "modules")
|
||||
if module:
|
||||
mem["modules"] = module
|
||||
|
||||
self.components.append(mem)
|
||||
|
||||
def get_graphic(self):
|
||||
graphics = get_inxi_key(self.inxi, 'Graphics') or []
|
||||
|
||||
for c in graphics:
|
||||
if not get_inxi(c, "Device") or not get_inxi(c, "vendor"):
|
||||
continue
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"type": "GraphicCard",
|
||||
"memory": self.get_memory_video(c),
|
||||
"manufacturer": get_inxi(c, "vendor"),
|
||||
"model": get_inxi(c, "Device"),
|
||||
"arch": get_inxi(c, "arch"),
|
||||
"serialNumber": get_inxi(c, "serial"),
|
||||
"integrated": True if get_inxi(c, "port") else False
|
||||
}
|
||||
)
|
||||
|
||||
def get_battery(self):
|
||||
bats = get_inxi_key(self.inxi, 'Battery') or []
|
||||
for b in bats:
|
||||
self.components.append(
|
||||
{
|
||||
"type": "Battery",
|
||||
"model": get_inxi(b, "model"),
|
||||
"serialNumber": get_inxi(b, "serial"),
|
||||
"condition": get_inxi(b, "condition"),
|
||||
"cycles": get_inxi(b, "cycles"),
|
||||
"volts": get_inxi(b, "volts")
|
||||
}
|
||||
)
|
||||
|
||||
def get_memory_video(self, c):
|
||||
memory = get_inxi_key(self.inxi, 'Memory') or []
|
||||
|
||||
for m in memory:
|
||||
igpu = get_inxi(m, "igpu")
|
||||
agpu = get_inxi(m, "agpu")
|
||||
ngpu = get_inxi(m, "ngpu")
|
||||
gpu = get_inxi(m, "gpu")
|
||||
if igpu or agpu or gpu or ngpu:
|
||||
return igpu or agpu or gpu or ngpu
|
||||
|
||||
return self.default
|
||||
|
||||
def get_data_storage(self):
|
||||
hdds= get_inxi_key(self.inxi, 'Drives') or []
|
||||
for d in hdds:
|
||||
usb = get_inxi(d, "type")
|
||||
if usb == "USB":
|
||||
continue
|
||||
|
||||
serial = get_inxi(d, "serial")
|
||||
if serial:
|
||||
hd = {
|
||||
"type": "Storage",
|
||||
"manufacturer": get_inxi(d, "vendor"),
|
||||
"model": get_inxi(d, "model"),
|
||||
"serialNumber": get_inxi(d, "serial"),
|
||||
"size": get_inxi(d, "size"),
|
||||
"speed": get_inxi(d, "speed"),
|
||||
"interface": get_inxi(d, "tech"),
|
||||
"firmware": get_inxi(d, "fw-rev")
|
||||
}
|
||||
rpm = get_inxi(d, "rpm")
|
||||
if rpm:
|
||||
hd["rpm"] = rpm
|
||||
|
||||
family = get_inxi(d, "family")
|
||||
if family:
|
||||
hd["family"] = family
|
||||
|
||||
sata = get_inxi(d, "sata")
|
||||
if sata:
|
||||
hd["sata"] = sata
|
||||
|
||||
continue
|
||||
|
||||
|
||||
cycles = get_inxi(d, "cycles")
|
||||
if cycles:
|
||||
hd['cycles'] = cycles
|
||||
hd["health"] = get_inxi(d, "health")
|
||||
hd["time of used"] = get_inxi(d, "on")
|
||||
hd["read used"] = get_inxi(d, "read-units")
|
||||
hd["written used"] = get_inxi(d, "written-units")
|
||||
|
||||
self.components.append(hd)
|
||||
continue
|
||||
|
||||
hd = {}
|
||||
|
||||
def sanitize(self, action):
|
||||
return []
|
||||
|
||||
def get_networks(self):
|
||||
nets = get_inxi_key(self.inxi, "Network") or []
|
||||
networks = [(nets[i], nets[i + 1]) for i in range(0, len(nets) - 1, 2)]
|
||||
|
||||
for n, iface in networks:
|
||||
model = get_inxi(n, "Device")
|
||||
if not model:
|
||||
continue
|
||||
|
||||
interface = ''
|
||||
for k in n.keys():
|
||||
if "port" in k:
|
||||
interface = "Integrated"
|
||||
if "pcie" in k:
|
||||
interface = "PciExpress"
|
||||
if get_inxi(n, "type") == "USB":
|
||||
interface = "USB"
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"type": "NetworkAdapter",
|
||||
"model": model,
|
||||
"manufacturer": get_inxi(n, 'vendor'),
|
||||
"serialNumber": get_inxi(iface, 'mac'),
|
||||
"speed": get_inxi(n, "speed"),
|
||||
"interface": interface,
|
||||
}
|
||||
)
|
||||
|
||||
def get_sound_card(self):
|
||||
audio = get_inxi_key(self.inxi, "Audio") or []
|
||||
|
||||
for c in audio:
|
||||
model = get_inxi(c, "Device")
|
||||
if not model:
|
||||
continue
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"type": "SoundCard",
|
||||
"model": model,
|
||||
"manufacturer": get_inxi(c, 'vendor'),
|
||||
"serialNumber": get_inxi(c, 'serial'),
|
||||
}
|
||||
)
|
||||
|
||||
def get_display(self):
|
||||
graphics = get_inxi_key(self.inxi, "Graphics") or []
|
||||
for c in graphics:
|
||||
if not get_inxi(c, "Monitor"):
|
||||
continue
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"type": "Display",
|
||||
"model": get_inxi(c, "model"),
|
||||
"manufacturer": get_inxi(c, "vendor"),
|
||||
"serialNumber": get_inxi(c, "serial"),
|
||||
'size': get_inxi(c, "size"),
|
||||
'diagonal': get_inxi(c, "diag"),
|
||||
'resolution': get_inxi(c, "res"),
|
||||
"date": get_inxi(c, "built"),
|
||||
'ratio': get_inxi(c, "ratio"),
|
||||
}
|
||||
)
|
||||
|
||||
def get_usb_num(self):
|
||||
return len(
|
||||
[
|
||||
u
|
||||
for u in self.dmi.get("Port Connector")
|
||||
if "USB" in u.get("Port Type", "").upper()
|
||||
]
|
||||
)
|
||||
|
||||
def get_serial_num(self):
|
||||
return len(
|
||||
[
|
||||
u
|
||||
for u in self.dmi.get("Port Connector")
|
||||
if "SERIAL" in u.get("Port Type", "").upper()
|
||||
]
|
||||
)
|
||||
|
||||
def get_firmware_num(self):
|
||||
return len(
|
||||
[
|
||||
u
|
||||
for u in self.dmi.get("Port Connector")
|
||||
if "FIRMWARE" in u.get("Port Type", "").upper()
|
||||
]
|
||||
)
|
||||
|
||||
def get_pcmcia_num(self):
|
||||
return len(
|
||||
[
|
||||
u
|
||||
for u in self.dmi.get("Port Connector")
|
||||
if "PCMCIA" in u.get("Port Type", "").upper()
|
||||
]
|
||||
)
|
||||
|
||||
def get_bios_version(self):
|
||||
return self.dmi.get("BIOS")[0].get("BIOS Revision", '1')
|
||||
|
||||
def loads(self, x):
|
||||
if isinstance(x, str):
|
||||
try:
|
||||
return json.loads(x)
|
||||
except Exception as ss:
|
||||
logger.warning("%s", ss)
|
||||
return {}
|
||||
return x
|
||||
|
||||
def errors(self, txt=None):
|
||||
if not txt:
|
||||
return self._errors
|
||||
|
||||
logger.error(txt)
|
||||
self._errors.append("%s", txt)
|
||||
|
|
|
@ -2,144 +2,124 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ object.id }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="d-flex justify-content-end mb-4">
|
||||
<a href="{% url 'evidence:download' object.uuid %}" class="btn btn-green-user d-flex">
|
||||
{% trans "Download File" %}
|
||||
</a>
|
||||
</span>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ul class="nav nav-tabs nav-tabs-bordered">
|
||||
<li class="nav-items">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#device">{% trans "Devices" %}</button>
|
||||
</li>
|
||||
<li class="nav-items">
|
||||
<a href="#tag" class="nav-link" data-bs-toggle="tab" data-bs-target="#tag">{% trans "Tag" %}</a>
|
||||
</li>
|
||||
<li class="nav-items">
|
||||
<a href="{% url 'evidence:erase_server' object.uuid %}" class="nav-link">{% trans "Erase Server" %}</a>
|
||||
</li>
|
||||
<li class="nav-items">
|
||||
<a href="{% url 'evidence:download' object.uuid %}" class="nav-link">{% trans "Download File" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content pt-2">
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
||||
<form id="eraseServerForm" action="{% url "evidence:erase_server" object.uuid %}" method="post" class="d-flex align-items-center gap-2">
|
||||
<div class="tab-pane fade show active" id="device">
|
||||
<h5 class="card-title"></h5>
|
||||
<div class="list-group col-6">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "Type" %}
|
||||
</th>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "Identificator" %}
|
||||
</th>
|
||||
<th scope="col" data-sortable="">
|
||||
{% trans "Data" %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for snap in object.properties %}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{ snap.key }}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<a href="{% url 'device:details' snap.value %}">{{ snap.value }}</a>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{{ snap.created }}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tag">
|
||||
{% load django_bootstrap5 %}
|
||||
<div class="list-group col-6">
|
||||
<form role="form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="{{ form2.erase_server.id_for_label }}"
|
||||
name="{{ form2.erase_server.name }}"
|
||||
{% if form2.erase_server.value %}checked{% endif %}>
|
||||
</div>
|
||||
<h6 class="card-title mb-0">{% trans "Erase Server" %}</h6>
|
||||
{% if form2.erase_server.value %}
|
||||
<i class="bi bi-eraser-fill"></i>
|
||||
{% endif %}
|
||||
</form>
|
||||
<p class="text-muted mb-0" id="uuid">{{ object.uuid }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="card-body">
|
||||
<p class="mb-0">
|
||||
{% if form2.erase_server.value %}
|
||||
{% translate "It is an erase server" %}
|
||||
{% else %}
|
||||
{% translate "It is not an erase server" %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ul class="nav nav-tabs nav-tabs-bordered">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#device">{% trans "Device" %}</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tag">{% trans "Tag" %}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content pt-2">
|
||||
<div class="tab-pane fade show active" id="device">
|
||||
<h5 class="card-title"></h5>
|
||||
<div class="list-group col-6">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" data-sortable="">{% trans "Algorithm" %}</th>
|
||||
<th scope="col" data-sortable="">{% trans "Device ID" %}</th>
|
||||
<th scope="col" data-sortable="">{% trans "Date" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for snap in object.properties %}
|
||||
<tr>
|
||||
<td>{{ snap.key }}</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<a href="{% url 'device:details' snap.value %}">{{ snap.value|truncatechars:7|upper }}</a>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">{{ snap.created }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||
<div class="message">
|
||||
{% for field, error in form.errors.items %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tag">
|
||||
{% load django_bootstrap5 %}
|
||||
<div class="list-group col-6">
|
||||
<form role="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||
<div class="message">
|
||||
{% for field, error in form.errors.items %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a class="btn btn-grey" href="">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit_form1" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
{% if form.tag.value %}
|
||||
<div class="col-1">
|
||||
<a class="btn btn-yellow" href="{% url 'evidence:delete_tag' form.pk %}">{% translate "Delete" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a class="btn btn-grey" href="">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
{% if form.tag.value %}
|
||||
<div class="col-1">
|
||||
<a class="btn btn-yellow" href="{% url 'evidence:delete_tag' form.pk %}">{% translate "Delete" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascript %}
|
||||
<script>
|
||||
// Automatically submit the form when the checkbox is toggled
|
||||
document.getElementById("{{ form2.erase_server.id_for_label }}").addEventListener("change", function() {
|
||||
document.getElementById("eraseServerForm").submit();
|
||||
});
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Obtener el hash de la URL (ejemplo: #components)
|
||||
const hash = window.location.hash;
|
||||
|
||||
// Handle tab navigation based on URL hash
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`);
|
||||
if (tabTrigger) {
|
||||
const tab = new bootstrap.Tab(tabTrigger);
|
||||
tab.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
// Verificar si hay un hash en la URL
|
||||
if (hash) {
|
||||
// Buscar el botón o enlace que corresponde al hash y activarlo
|
||||
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`);
|
||||
|
||||
if (tabTrigger) {
|
||||
// Crear una instancia de tab de Bootstrap para activar el tab
|
||||
const tab = new bootstrap.Tab(tabTrigger);
|
||||
tab.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
61
evidence/templates/ev_eraseserver.html
Normal file
61
evidence/templates/ev_eraseserver.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ object.id }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ul class="nav nav-tabs nav-tabs-bordered">
|
||||
<li class="nav-items">
|
||||
<a href="{% url 'evidence:details' object.uuid %}" class="nav-link">{% trans "Devices" %}</a>
|
||||
</li>
|
||||
<li class="nav-items">
|
||||
<a href="{% url 'evidence:details' object.uuid %}#tag" class="nav-link">{% trans "Tag" %}</a>
|
||||
</li>
|
||||
<li class="nav-items">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#erase_server">{% trans "Erase Server" %}</button>
|
||||
</li>
|
||||
<li class="nav-items">
|
||||
<a href="{% url 'evidence:download' object.uuid %}" class="nav-link">{% trans "Download File" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content pt-2">
|
||||
|
||||
<div class="tab-pane fade show active" id="erase_server">
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
<div class="list-group col-6">
|
||||
<form role="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||
<div class="message">
|
||||
{% for field, error in form.errors.items %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a class="btn btn-grey" href="">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -103,7 +103,6 @@ class EvidenceView(DashboardView, FormView):
|
|||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'object': self.object,
|
||||
'form2': EraseServerForm(**self.get_form_kwargs(), data=self.request.POST or None),
|
||||
})
|
||||
return context
|
||||
|
||||
|
@ -144,7 +143,7 @@ class DownloadEvidenceView(DashboardView, TemplateView):
|
|||
|
||||
|
||||
class EraseServerView(DashboardView, FormView):
|
||||
template_name = "ev_details.html"
|
||||
template_name = "ev_eraseserver.html"
|
||||
section = "evidences"
|
||||
title = _("Evidences")
|
||||
breadcrumb = "Evidences / Details"
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -1,49 +1,46 @@
|
|||
{% extends "login_base.html" %}
|
||||
{% load i18n static language_code %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block login_content %}
|
||||
|
||||
<div class="pt-2 pb-3">
|
||||
<h5 class="card-title text-center pb-0 fs-4 help"> {% trans "Sign in" %}</h5>
|
||||
</div>
|
||||
|
||||
<form action="{% url 'login:login' %}" method="post" class="row g-3 needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="col-12 mb-">
|
||||
<div class="col-12">
|
||||
<input type="email" name="username" maxlength="100" autocapitalize="off"
|
||||
autocorrect="off" class="form-control textinput textInput {% if form.username.errors %}is-invalid{% endif %}" id="yourEmail" required
|
||||
autocorrect="off" class="form-control textinput textInput" id="yourEmail" required
|
||||
autofocus placeholder="{{ form.username.label }}"
|
||||
{% if form.username.value %}value="{{ form.username.value }}" {% endif %}>
|
||||
<div class="invalid-feedback">Please enter your email.</div>
|
||||
{% if form.username.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.username.errors|striptags }}
|
||||
</div>
|
||||
<p class="text-error">
|
||||
{{ form.username.errors|striptags }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-3">
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<input type="password" name="password" maxlength="100" autocapitalize="off"
|
||||
autocorrect="off" class="form-control textinput textInput {% if form.password.errors %}is-invalid{% endif %}" id="id_password"
|
||||
placeholder="{{ form.password.label }}" required>
|
||||
<i class="input-group-text bi bi-eye" id="togglePassword" style="cursor: pointer"></i>
|
||||
</div>
|
||||
<input type="password" name="password" maxlength="100" autocapitalize="off"
|
||||
autocorrect="off" class="form-control textinput textInput" id="id_password"
|
||||
placeholder="{{ form.password.label }}" required>
|
||||
{% if form.password.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.password.errors|striptags }}
|
||||
</div>
|
||||
<p class="text-error">
|
||||
{{ form.password.errors|striptags }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<i class="input-group-text bi bi-eye" id="togglePassword" style="cursor: pointer">
|
||||
</i>
|
||||
</div>
|
||||
<div class="invalid-feedback">Please enter your password!</div>
|
||||
</div>
|
||||
|
||||
<input name="next" type="hidden" value="{{ success_url }}">
|
||||
|
||||
<div class="col-12 mb-3">
|
||||
<button class="btn btn-green-user w-100" type="submit">{% trans "Login" %}</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-primary w-100" type="submit">Next</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="login-footer" class="d-flex justify-content-between align-items-center mt-4">
|
||||
<a href="{% url 'login:password_reset' %}" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password?" %}</a>
|
||||
{% include "language_picker.html" %}
|
||||
<div id="login-footer" class="mt-3">
|
||||
<a href="{% url 'login:password_reset' %}" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
<div class="container">
|
||||
|
||||
<section class="section register min-vh-100 d-flex flex-column align-items-center justify-content-center">
|
||||
<section class="section register min-vh-100 d-flex flex-column align-items-center justify-content-center py-4">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-4 col-md-6 d-flex flex-column align-items-center justify-content-center">
|
||||
|
@ -57,37 +57,51 @@
|
|||
</a>
|
||||
</div><!-- End Logo -->
|
||||
|
||||
<div class="card shadow bg-body rounded p-3">
|
||||
<div class="card mb-3 shadow p-3 mb-5 bg-body rounded">
|
||||
|
||||
<div class="card-body">
|
||||
{% block login_content %}
|
||||
|
||||
<div class="pt-2 pb-3">
|
||||
<h5 class="card-title text-center pb-0 fs-4 help">Sign in</h5>
|
||||
|
||||
</div>
|
||||
|
||||
{% block login_content %}
|
||||
{% endblock login_content %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% if messages %}
|
||||
<div class="col-12 mt-3">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-danger show text-center" role="alert">
|
||||
{{message}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="credits">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<main class="col-md-12 bt-5">
|
||||
{% block messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock messages %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer text-center fixed-bottom bg-light py-3">
|
||||
<footer class="footer text-center">
|
||||
<div class="container">
|
||||
<span class="text-muted">{{ commit_id }}</span>
|
||||
</div>
|
||||
|
|
|
@ -3,20 +3,25 @@
|
|||
|
||||
{% block login_content %}
|
||||
|
||||
<h4 class="card-title text-center help mb-4"> {% trans "Password Reset" %}</h5>
|
||||
<p class="text-muted fs-6">{% trans "Enter your email address below, and we'll email instructions for setting a new one." %}</p>
|
||||
|
||||
<form action="{% url 'login:password_reset' %}" method="post" class="mt-4">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='floating' %}
|
||||
{% bootstrap_form_errors form type='non_fields' %}
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Reset Password' %}</button>
|
||||
<div class="well">
|
||||
<div class="row-fluid">
|
||||
<h2>{% trans 'Password reset' %}</h2>
|
||||
<span>{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-end mt-4 mb-0 text-sm">
|
||||
<a class="text-muted" href="{% url 'login:login' %}" >{% trans "Back to login" %}</a>
|
||||
</div>
|
||||
|
||||
<div class="well">
|
||||
<div class="row-fluid">
|
||||
<div>
|
||||
<form action="{% url 'login:password_reset' %}" role="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_form_errors form type='non_fields' %}
|
||||
<div class="form-actions-no-box">
|
||||
<input type="submit" name="submit" value="{% trans 'Reset my password' %}" class="btn btn-primary form-control" id="submit-id-submit">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div><!-- /.row-fluid -->
|
||||
</div><!--/.well-->
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,14 +4,12 @@
|
|||
{% block login_content %}
|
||||
<div class="well">
|
||||
<div class="row-fluid">
|
||||
<h4 class="card-title text-center text-bold">{% trans 'Password reset sent' %}</h4>
|
||||
<p class="text-center text-muted mt-4 fs-7">{% trans "We've sent you an email with instructions to reset your password. If an account with the provided email exists, you should receive it shortly." %}</p>
|
||||
<p class="text-center mt-4 fs-7">{% trans "If you don't receive an email, please check the email address you entered and look in your spam folder." %}</p>
|
||||
<h2>{% trans 'Password reset sent' %}</h2>
|
||||
|
||||
<p>{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}</p>
|
||||
|
||||
<p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
|
||||
|
||||
</div><!-- /.row-fluid -->
|
||||
|
||||
<div class="text-end mt-4 mb-0 text-sm">
|
||||
<a class="text-muted" href="{% url 'login:login' %}" >{% trans "Back to login" %}</a>
|
||||
</div>
|
||||
</div><!--/.well-->
|
||||
{% endblock %}
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.contrib.auth import logout as auth_logout
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.shortcuts import redirect
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib import messages
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -40,10 +40,6 @@ class LoginView(auth_views.LoginView):
|
|||
|
||||
return redirect(self.extra_context['success_url'])
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _("Login error. Check credentials."))
|
||||
return self.render_to_response(self.get_context_data(form=form), status=401)
|
||||
|
||||
|
||||
def LogoutView(request):
|
||||
auth_logout(request)
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# Generated by Django 5.0.6 on 2025-03-05 19:53
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("lot", "0008_rename_closed_lot_archived"),
|
||||
("user", "0002_institution_algorithm"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name="lot",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("owner", "name", "type"), name="unique_institution_and_name"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 5.0.6 on 2025-03-20 17:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("lot", "0009_lot_unique_institution_and_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="lottag",
|
||||
name="order",
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,4 @@
|
|||
from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from utils.constants import (
|
||||
STR_SM_SIZE,
|
||||
|
@ -17,25 +16,10 @@ class LotTag(models.Model):
|
|||
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
inbox = models.BooleanField(default=False)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk:
|
||||
# set the order to be last
|
||||
max_order = LotTag.objects.filter(owner=self.owner).aggregate(Max('order'))['order__max']
|
||||
self.order = (max_order or 0) + 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
institution = self.owner
|
||||
order = self.order
|
||||
super().delete(*args, **kwargs)
|
||||
# Adjust the order of other instances
|
||||
LotTag.objects.filter(owner=institution, order__gt=order).update(order=models.F('order') - 1)
|
||||
|
||||
|
||||
class DeviceLot(models.Model):
|
||||
lot = models.ForeignKey("Lot", on_delete=models.CASCADE)
|
||||
|
@ -53,11 +37,6 @@ class Lot(models.Model):
|
|||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
type = models.ForeignKey(LotTag, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['owner', 'name', 'type'], name='unique_institution_and_name')
|
||||
]
|
||||
|
||||
def add(self, v):
|
||||
if DeviceLot.objects.filter(lot=self, device_id=v).exists():
|
||||
return
|
||||
|
@ -67,14 +46,6 @@ class Lot(models.Model):
|
|||
for d in DeviceLot.objects.filter(lot=self, device_id=v):
|
||||
d.delete()
|
||||
|
||||
@property
|
||||
def devices(self):
|
||||
return DeviceLot.objects.filter(lot=self)
|
||||
|
||||
def device_count(self):
|
||||
return self.devices.count()
|
||||
|
||||
|
||||
class LotProperty(Property):
|
||||
lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
|
||||
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from lot.models import Lot
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
class LotTable(tables.Table):
|
||||
select = tables.CheckBoxColumn(
|
||||
accessor='id',
|
||||
attrs={
|
||||
'th__input': {
|
||||
'id': 'select-all',
|
||||
'class': 'form-check-input'
|
||||
},
|
||||
'td__input': {
|
||||
'class': 'select-checkbox form-check-input'
|
||||
},
|
||||
'th': {'class': 'text-center'},
|
||||
'td': {'class': 'text-center'}
|
||||
},
|
||||
orderable=False
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=("dashboard:lot", {"pk": tables.A("id")}),
|
||||
verbose_name=_("Lot Name"),
|
||||
attrs={
|
||||
'th': {'class': 'text-start'},
|
||||
'td': {'class': 'fw-bold text-start'}
|
||||
}
|
||||
)
|
||||
description = tables.Column(
|
||||
verbose_name=_("Description"),
|
||||
default=_("No description"),
|
||||
attrs={
|
||||
'th': {'class': 'text-start'},
|
||||
'td': {'class': 'text-muted text-start'}
|
||||
}
|
||||
)
|
||||
archived = tables.Column(
|
||||
verbose_name=_("Status"),
|
||||
attrs={
|
||||
'th': {'class': 'text-center'},
|
||||
'td': {'class': 'text-center'}
|
||||
}
|
||||
)
|
||||
device_count = tables.Column(
|
||||
verbose_name=_("Devices"),
|
||||
accessor='device_count',
|
||||
attrs={
|
||||
'th': {'class': 'text-center'},
|
||||
'td': {'class': 'text-center'}
|
||||
}
|
||||
)
|
||||
created = tables.DateColumn(
|
||||
format="Y-m-d",
|
||||
verbose_name=_("Created On"),
|
||||
attrs={
|
||||
'th': {'class': 'text-end'},
|
||||
'td': {'class': 'text-end'}
|
||||
}
|
||||
)
|
||||
user = tables.Column(
|
||||
verbose_name=_("Created By"),
|
||||
default=_("Unknown"),
|
||||
attrs={
|
||||
'th': {'class': 'text-end'},
|
||||
'td': {'class': 'text-muted text-end'}
|
||||
}
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_name="lot_actions.html",
|
||||
verbose_name=_(""),
|
||||
attrs={
|
||||
'th': {'class': 'text-end'},
|
||||
'td': {'class': 'text-end'}
|
||||
}
|
||||
)
|
||||
|
||||
def render_archived(self, value):
|
||||
if value:
|
||||
return mark_safe('<span class="badge bg-warning"><i class="bi bi-archive-fill"></i></span>')
|
||||
return mark_safe('<span class="badge bg-success"><i class="bi bi-folder-fill"></i></span>')
|
||||
|
||||
class Meta:
|
||||
model = Lot
|
||||
fields = ("select", "archived", "name", "description", "device_count", "created", "user", "actions")
|
||||
attrs = {
|
||||
"class": "table table-hover align-middle",
|
||||
"thead": {"class": "table-light"}
|
||||
}
|
||||
order_by = ("-created",)
|
38
lot/templates/delete_lot.html
Normal file
38
lot/templates/delete_lot.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ subtitle }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
Are you sure than want remove the lot {{ object.name }} with {{ object.devices.count }} devices.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form role="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||
<div class="message">
|
||||
{% for field, error in form.errors.items %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-actions-no-box">
|
||||
<a class="btn btn-grey" href="{% url 'dashboard:unassigned' %}">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Delete' %}" />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,105 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-md-8 mb-3">
|
||||
<p class="lead text-center mb-4">
|
||||
{% trans "Are you sure you want to delete the following lot/s?" %}
|
||||
</p>
|
||||
|
||||
{% for lot in lots %}
|
||||
<div class="card shadow-sm mb-3 border-top-0">
|
||||
<span class="badge fs-6 {% if lot.devices.count > 0 %} bg-danger {% else %} bg-secondary {% endif %} border-bottom-0">
|
||||
{{ lot.devices.count }} {% trans "device/s" %}
|
||||
</span>
|
||||
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-2 badge {% if lot.archived %}bg-warning{% else %}bg-success{% endif %}">
|
||||
{% if lot.archived %}{% trans "Archived" %}{% else %}{% trans "Open" %}{% endif %}
|
||||
</span>
|
||||
|
||||
<h5 class="card-title mb-0 me-2 text-capitalize">{{ lot.name }}</h5>
|
||||
</div>
|
||||
<button class="btn btn-link p-0" type="button" data-bs-toggle="collapse" data-bs-target="#lotDetails{{ forloop.counter }}" aria-expanded="false" aria-controls="lotDetails{{ forloop.counter }}">
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="collapse" id="lotDetails{{ forloop.counter }}">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<strong>{% trans "Code" %}:</strong> {{ lot.code|default:"N/A" }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>{% trans "Description" %}:</strong> <span class="text-muted">{{ lot.description|default:"N/A" }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>{% trans "Owner" %}:</strong> {{ lot.owner.name }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>{% trans "Created by" %}:</strong> {{ lot.user|default:"N/A" }}
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<strong>{% trans "Lot Group" %}:</strong> {{ lot.type.name }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>{% trans "Created" %}:</strong> {{ lot.created|date:"Y-m-d H:i" }}
|
||||
</li>
|
||||
|
||||
<li class="list-group-item">
|
||||
<strong>{% trans "Last Updated" %}:</strong> {{ lot.updated|date:"Y-m-d H:i" }}
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if lots_with_devices %}
|
||||
<div class="alert alert-danger d-flex align-items-center justify-content-center" role="alert">
|
||||
<i class="bi bi-exclamation-circle-fill me-2"></i>
|
||||
{% trans "All associated devices will be deassigned." %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info d-flex align-items-center justify-content-center" role="alert">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
{% trans "No devices are associated with these lots." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form role="form" method="post" action="{% url 'lot:delete' %}" class="mt-4">
|
||||
{% csrf_token %}
|
||||
{% for selected_id in selected_ids %}
|
||||
<input type="hidden" name="selected_ids" value="{{ selected_id }}">
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-grid gap-3 d-md-flex justify-content-md-center">
|
||||
<a class="btn btn-outline-secondary" href="{{ request.META.HTTP_REFERER }}">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
{% translate "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash me-1"></i>
|
||||
{% translate "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,6 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
<a href="{% url 'lot:edit' record.id %}" class="btn btn-sm btn-outline-primary me-2">
|
||||
<i class="bi bi-pen"></i>
|
||||
{% trans 'Edit' %}
|
||||
</a>
|
|
@ -1,110 +1,43 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<h3>{{ subtitle }}</h3>
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
{% if show_archived %}
|
||||
<a href="?show_archived=false" class="btn btn-green-admin">
|
||||
{% trans 'Show active lots' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="?show_archived=true" class="btn btn-green-admin">
|
||||
{% trans 'Show archived lots' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Search and new lot button -->
|
||||
<div class="d-flex justify-content-end align-items-stretch mb-4">
|
||||
<form method="get" class="input-group w-100 me-3">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="{% trans 'Search by name or description...' %}"
|
||||
value="{{ search_query }}">
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-outline-secondary h-100" style="border-radius: 0 4px 4px 0;">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<a href="{% url 'lot:add' %}" class="btn btn-success d-flex align-items-center" style="white-space: nowrap;">
|
||||
<span>{% trans 'New lot' %}</span>
|
||||
<a href="{% url 'lot:add' %}" type="button" class="btn btn-green-admin">
|
||||
<i class="bi bi-plus"></i>
|
||||
{% trans 'Add new lot' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete and filter buttons -->
|
||||
<form method="get" action="{% url 'lot:delete' %}" id="bulk-action-form">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-end align-items-center mb-4">
|
||||
|
||||
<button type="submit" class="btn btn-outline-danger d-none me-3" id="delete-selected">
|
||||
<i class="bi bi-trash"></i>
|
||||
{% trans 'Delete Selected' %}
|
||||
</button>
|
||||
|
||||
<div class="btn-group" role="group" aria-label="Filter Options">
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="filterOptions"
|
||||
id="filterActive"
|
||||
autocomplete="off"
|
||||
onclick="window.location.href='?{% if search_query %}q={{ search_query }}&{% endif %}show_archived=false'"
|
||||
{% if show_archived == 'false' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary d-flex align-items-center h-100" for="filterActive">
|
||||
<i class="bi bi-filter me-2"></i>
|
||||
{% trans 'Active' %} ({{ active_count }})
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="filterOptions"
|
||||
id="filterArchived"
|
||||
autocomplete="off"
|
||||
onclick="window.location.href='?{% if search_query %}q={{ search_query }}&{% endif %}show_archived=true'"
|
||||
{% if show_archived == 'true' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary d-flex align-items-center h-100" for="filterArchived">
|
||||
<i class="bi bi-filter me-2"></i>
|
||||
{% trans 'Archived' %} ({{ archived_count }})
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="filterOptions"
|
||||
id="filterAll"
|
||||
autocomplete="off"
|
||||
onclick="window.location.href='?{% if search_query %}q={{ search_query }}&{% endif %}show_archived=both'"
|
||||
{% if show_archived == 'both' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary d-flex align-items-center h-100" for="filterAll">
|
||||
<i class="bi bi-filter me-2"></i>
|
||||
{% trans 'All Lots' %} ({{ total_count }})
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% render_table table %}
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const checkboxes = document.querySelectorAll('.select-checkbox');
|
||||
const selectAll = document.querySelector('#select-all');
|
||||
const deleteBtn = document.querySelector('#delete-selected');
|
||||
|
||||
function updateDeleteButton() {
|
||||
const checked = document.querySelectorAll('.select-checkbox:checked').length > 0;
|
||||
deleteBtn.classList.toggle('d-none', !checked);
|
||||
}
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', (e) => {
|
||||
checkboxes.forEach(checkbox => checkbox.checked = e.target.checked);
|
||||
updateDeleteButton();
|
||||
});
|
||||
}
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateDeleteButton);
|
||||
});
|
||||
|
||||
// on DOM reload (f5) check for checkboxes too and update show/hide btn
|
||||
updateDeleteButton();
|
||||
});
|
||||
</script>
|
||||
<div class="row">
|
||||
<table class= "table table-striped table-sm">
|
||||
{% for lot in lots %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}</a></td>
|
||||
<td>
|
||||
<a href="{% url 'lot:edit' lot.id %}"><i class="bi bi-pen"></i></a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'lot:delete' lot.id %}"><i class="bi bi-trash text-danger"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -22,17 +22,11 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form form layout="floating" %}
|
||||
|
||||
<div class="d-flex justify-content-start gap-3 mt-4">
|
||||
<a class="btn btn-outline-secondary" href="{{ request.META.HTTP_REFERER }}">
|
||||
<i class="bi bi-x-circle me-2"></i>
|
||||
{% translate "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-green-user">
|
||||
<i class="bi bi-save me-2"></i>
|
||||
{% translate "Save" %}
|
||||
</button>
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-actions-no-box">
|
||||
<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' %}" />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -5,7 +5,7 @@ app_name = 'lot'
|
|||
|
||||
urlpatterns = [
|
||||
path("add/", views.NewLotView.as_view(), name="add"),
|
||||
path("lots/delete/", views.DeleteLotsView.as_view(), name="delete"),
|
||||
path("delete/<int:pk>/", views.DeleteLotView.as_view(), name="delete"),
|
||||
path("edit/<int:pk>/", views.EditLotView.as_view(), name="edit"),
|
||||
path("add/devices/", views.AddToLotView.as_view(), name="add_devices"),
|
||||
path("del/devices/", views.DelToLotView.as_view(), name="del_devices"),
|
||||
|
|
180
lot/views.py
180
lot/views.py
|
@ -1,10 +1,8 @@
|
|||
from django.db import IntegrityError
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import get_object_or_404, redirect, Http404, render
|
||||
from django.shortcuts import get_object_or_404, redirect, Http404
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import Q, Count, Case, When, IntegerField
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.edit import (
|
||||
CreateView,
|
||||
|
@ -12,37 +10,15 @@ from django.views.generic.edit import (
|
|||
UpdateView,
|
||||
FormView,
|
||||
)
|
||||
from django_tables2 import SingleTableView
|
||||
from dashboard.mixins import DashboardView
|
||||
from lot.tables import LotTable
|
||||
from lot.models import Lot, LotTag, LotProperty
|
||||
from lot.forms import LotsForm
|
||||
|
||||
class LotSuccessUrlMixin():
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
|
||||
def get_success_url(self, lot_tag=None):
|
||||
try:
|
||||
if lot_tag:
|
||||
lot_group = LotTag.objects.only('id').get(
|
||||
owner=self.request.user.institution,
|
||||
name=lot_tag
|
||||
)
|
||||
else:
|
||||
lot_group = LotTag.objects.only('id').get(
|
||||
owner=self.object.owner,
|
||||
name=self.object.type
|
||||
)
|
||||
return reverse_lazy('lot:tags', args=[lot_group.id])
|
||||
|
||||
except LotTag.DoesNotExist:
|
||||
return self.success_url
|
||||
|
||||
|
||||
class NewLotView(LotSuccessUrlMixin, DashboardView, CreateView):
|
||||
class NewLotView(DashboardView, CreateView):
|
||||
template_name = "new_lot.html"
|
||||
title = _("New lot")
|
||||
breadcrumb = "lot / New lot"
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
model = Lot
|
||||
fields = (
|
||||
"type",
|
||||
|
@ -61,63 +37,36 @@ class NewLotView(LotSuccessUrlMixin, DashboardView, CreateView):
|
|||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
response = super().form_valid(form)
|
||||
messages.success(self.request, _("Lot created successfully."))
|
||||
return response
|
||||
|
||||
except IntegrityError:
|
||||
messages.error(self.request, _("Lot name is already defined."))
|
||||
return self.form_invalid(form)
|
||||
form.instance.owner = self.request.user.institution
|
||||
form.instance.user = self.request.user
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
|
||||
class DeleteLotsView(LotSuccessUrlMixin, DashboardView, TemplateView ):
|
||||
template_name = "delete_lots.html"
|
||||
title = _("Delete lot/s")
|
||||
breadcrumb = "lots / Delete"
|
||||
class DeleteLotView(DashboardView, DeleteView):
|
||||
template_name = "delete_lot.html"
|
||||
title = _("Delete lot")
|
||||
breadcrumb = "lot / Delete lot"
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
model = Lot
|
||||
fields = (
|
||||
"type",
|
||||
"name",
|
||||
"code",
|
||||
"description",
|
||||
"archived",
|
||||
)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
selected_ids = request.GET.getlist('select')
|
||||
if not selected_ids:
|
||||
messages.error(request, _("No lots selected for deletion."))
|
||||
return redirect(self.success_url)
|
||||
# check ownership
|
||||
lots_to_delete = Lot.objects.filter(
|
||||
id__in=selected_ids,
|
||||
owner=request.user.institution
|
||||
)
|
||||
context = {
|
||||
'lots': lots_to_delete,
|
||||
'lots_with_devices': any(lot.devices.exists() for lot in lots_to_delete),
|
||||
'selected_ids': selected_ids,
|
||||
'breadcrumb': self.breadcrumb,
|
||||
'title': self.title,
|
||||
}
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
selected_ids = request.POST.getlist('selected_ids')
|
||||
if not selected_ids:
|
||||
messages.error(request, _("No lots selected for deletion."))
|
||||
return redirect(self.success_url)
|
||||
|
||||
lots_to_delete = Lot.objects.filter(
|
||||
id__in=selected_ids,
|
||||
owner=request.user.institution
|
||||
)
|
||||
|
||||
lot_tag = lots_to_delete.first().type
|
||||
deleted_count = lots_to_delete.delete()
|
||||
messages.success(request, _("Lots succesfully deleted"))
|
||||
return redirect(self.get_success_url(lot_tag=lot_tag))
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
|
||||
class EditLotView(LotSuccessUrlMixin, DashboardView, UpdateView):
|
||||
class EditLotView(DashboardView, UpdateView):
|
||||
template_name = "new_lot.html"
|
||||
title = _("Edit lot")
|
||||
breadcrumb = "Lot / Edit lot"
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
model = Lot
|
||||
fields = (
|
||||
"type",
|
||||
|
@ -134,6 +83,7 @@ class EditLotView(LotSuccessUrlMixin, DashboardView, UpdateView):
|
|||
owner=self.request.user.institution,
|
||||
pk=pk,
|
||||
)
|
||||
# self.success_url = reverse_lazy('dashbiard:lot', args=[pk])
|
||||
kwargs = super().get_form_kwargs()
|
||||
return kwargs
|
||||
|
||||
|
@ -145,11 +95,6 @@ class EditLotView(LotSuccessUrlMixin, DashboardView, UpdateView):
|
|||
)
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _("Lot edited succesfully."))
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
|
||||
class AddToLotView(DashboardView, FormView):
|
||||
template_name = "list_lots.html"
|
||||
|
@ -192,73 +137,30 @@ class DelToLotView(AddToLotView):
|
|||
return response
|
||||
|
||||
|
||||
class LotsTagsView(DashboardView, SingleTableView):
|
||||
class LotsTagsView(DashboardView, TemplateView):
|
||||
template_name = "lots.html"
|
||||
title = _("Lot group")
|
||||
title = _("lots")
|
||||
breadcrumb = _("lots") + " /"
|
||||
success_url = reverse_lazy('dashboard:unassigned')
|
||||
model = Lot
|
||||
table_class = LotTable
|
||||
paginate_by = 10
|
||||
|
||||
def get_queryset(self):
|
||||
self.pk = self.kwargs.get('pk')
|
||||
self.tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk)
|
||||
self.show_archived = self.request.GET.get('show_archived', 'false')
|
||||
self.search_query = self.request.GET.get('q', '').strip()
|
||||
|
||||
queryset = Lot.objects.filter(owner=self.request.user.institution, type=self.tag).annotate(
|
||||
device_count=Count('devicelot')
|
||||
)
|
||||
|
||||
if self.show_archived == 'true':
|
||||
queryset = queryset.filter(archived=True)
|
||||
elif self.show_archived == 'false':
|
||||
queryset = queryset.filter(archived=False)
|
||||
|
||||
if self.search_query:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=self.search_query) |
|
||||
Q(description__icontains=self.search_query) |
|
||||
Q(code__icontains=self.search_query)
|
||||
)
|
||||
|
||||
sort = self.request.GET.get('sort')
|
||||
if sort:
|
||||
queryset = queryset.order_by(sort)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.pk = kwargs.get('pk')
|
||||
context = super().get_context_data(**kwargs)
|
||||
counts = self.get_counts()
|
||||
|
||||
tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk)
|
||||
self.title += " {}".format(tag.name)
|
||||
self.breadcrumb += " {}".format(tag.name)
|
||||
show_archived = self.request.GET.get('show_archived', 'false') == 'true'
|
||||
lots = Lot.objects.filter(owner=self.request.user.institution).filter(
|
||||
type=tag, archived=show_archived
|
||||
)
|
||||
context.update({
|
||||
'title': _("Lot Group") + " - " + self.tag.name,
|
||||
'breadcrumb': _("Lots") + " / " + self.tag.name,
|
||||
'show_archived': self.show_archived,
|
||||
'search_query': self.search_query,
|
||||
'archived_count': counts['archived_count'],
|
||||
'active_count': counts['active_count'],
|
||||
'total_count': counts['total_count'],
|
||||
'lots': lots,
|
||||
'title': self.title,
|
||||
'breadcrumb': self.breadcrumb,
|
||||
'show_archived': show_archived
|
||||
})
|
||||
return context
|
||||
|
||||
def get_counts(self):
|
||||
cache_key = f"lot_counts_{self.request.user.institution.id}_{self.tag.id}"
|
||||
counts = cache.get(cache_key)
|
||||
|
||||
if not counts:
|
||||
# calculate archived, open, and total count on a single query
|
||||
counts = Lot.objects.filter(owner=self.request.user.institution, type=self.tag).aggregate(
|
||||
archived_count=Count(Case(When(archived=True, then=1), output_field=IntegerField())),
|
||||
active_count=Count(Case(When(archived=False, then=1), output_field=IntegerField())),
|
||||
total_count=Count('id')
|
||||
)
|
||||
cache.set(cache_key, counts, timeout=250)
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
class LotPropertiesView(DashboardView, TemplateView):
|
||||
template_name = "properties.html"
|
||||
|
@ -307,7 +209,7 @@ class AddLotPropertyView(DashboardView, CreateView):
|
|||
def get_form_kwargs(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
self.lot = get_object_or_404(Lot, pk=pk, owner=self.request.user.institution)
|
||||
self.success_url = reverse_lazy('dashboard:properties', args=[pk])
|
||||
self.success_url = reverse_lazy('lot:properties', args=[pk])
|
||||
kwargs = super().get_form_kwargs()
|
||||
return kwargs
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ xlrd==2.0.1
|
|||
odfpy==1.4.1
|
||||
pytz==2024.2
|
||||
json-repair==0.30.0
|
||||
setuptools==65.5.1
|
||||
setuptools==75.5.0
|
||||
requests==2.32.3
|
||||
wheel==0.45.1
|
||||
markdown==3.7
|
||||
wheel==0.45.0
|
||||
|
|
|
@ -12,16 +12,7 @@ main() {
|
|||
browser="${browser:-firefox}"
|
||||
project="${project:-firefox}"
|
||||
headed="${headed:---headed}"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
npx playwright test --project "${project}" "${headed}"
|
||||
else
|
||||
#Runs playwright with specific file if provided
|
||||
#ej. ./run "tests/lots.spec.ts"
|
||||
for test_file in "$@"; do
|
||||
npx playwright test "${test_file}" --project "${project}" "${headed}"
|
||||
done
|
||||
fi
|
||||
npx playwright test --project "${project}" "${headed}"
|
||||
}
|
||||
|
||||
main "${@}"
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// TODO after the tests, put again demo.ereuse.org as default
|
||||
const TEST_SITE = process.env.TEST_SITE || 'http://127.0.0.1:8001'
|
||||
const TEST_USER = process.env.TEST_USER || 'user@example.org'
|
||||
const TEST_PASSWD = process.env.TEST_PASSWD || '1234'
|
||||
|
||||
async function login(page, date, time) {
|
||||
await page.goto(TEST_SITE);
|
||||
await page.getByPlaceholder('Email address').click();
|
||||
await page.getByPlaceholder('Email address').fill(TEST_USER);
|
||||
await page.getByPlaceholder('Password').fill(TEST_PASSWD);
|
||||
await page.getByPlaceholder('Password').press('Enter');
|
||||
}
|
||||
|
||||
// when introducing a new test, use only temporarily to just enable that test
|
||||
//
|
||||
//test.only('NEW example', async ({ page }) => {
|
||||
// await login(page);
|
||||
// test.setTimeout(0)
|
||||
// await page.pause();
|
||||
//});
|
||||
|
||||
test.only('Lot GROUP-CRUD', async ({ page }) => {
|
||||
await login(page);
|
||||
//await page.pause();
|
||||
|
||||
// create lot group
|
||||
await page.getByRole('link', { name: ' Admin' }).click();
|
||||
await page.getByRole('link', { name: 'Lot Groups' }).click();
|
||||
await page.getByRole('button', { name: 'Add' }).click();
|
||||
await page.getByRole('textbox', { name: 'Tag' }).fill('Newlotgroup');
|
||||
await page.getByRole('button', { name: 'Add Lot tag' }).click();
|
||||
await expect(page.getByText('Lot Group successfully added.')).toBeVisible();
|
||||
|
||||
//Edit lot group
|
||||
await page.getByRole('button', { name: ' Edit' }).nth(4).click();
|
||||
await page.getByRole('textbox', { name: 'Tag' }).fill('NewlotgroupEdited');
|
||||
await page.getByRole('button', { name: 'Save Changes' }).click();
|
||||
await expect(page.getByText('Lot Group updated')).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'NewlotgroupEdited' })).toBeVisible();
|
||||
|
||||
//Delete lot group
|
||||
await page.getByRole('button', { name: ' Delete' }).nth(4).click();
|
||||
await expect(page.getByText('Are you sure you want to delete this lot group? NewlotgroupEdited')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click();
|
||||
|
||||
|
||||
});
|
||||
|
||||
test('Lot group already exists (Inbox)', async ({ page }) => {
|
||||
await login(page);
|
||||
//await page.pause();
|
||||
|
||||
await page.getByRole('link', { name: ' Admin' }).click();
|
||||
await page.getByRole('link', { name: 'Lot Groups' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Add' }).click();
|
||||
await page.getByRole('textbox', { name: 'Tag' }).fill('Newgroup');
|
||||
await page.getByRole('button', { name: 'Add Lot tag' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Add' }).click();
|
||||
await page.getByRole('textbox', { name: 'Tag' }).fill('Newgroup');
|
||||
await page.getByRole('button', { name: 'Add Lot tag' }).click();
|
||||
await expect(page.getByText('The name \'Newgroup\' exist.')).toBeVisible();
|
||||
});
|
|
@ -1,71 +0,0 @@
|
|||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
// TODO after the tests, put again demo.ereuse.org as default
|
||||
const TEST_SITE = process.env.TEST_SITE || 'http://127.0.0.1:8001'
|
||||
const TEST_USER = process.env.TEST_USER || 'user@example.org'
|
||||
const TEST_PASSWD = process.env.TEST_PASSWD || '1234'
|
||||
|
||||
async function login(page, date, time) {
|
||||
await page.goto(TEST_SITE);
|
||||
await page.getByPlaceholder('Email address').click();
|
||||
await page.getByPlaceholder('Email address').fill(TEST_USER);
|
||||
await page.getByPlaceholder('Password').fill(TEST_PASSWD);
|
||||
await page.getByPlaceholder('Password').press('Enter');
|
||||
}
|
||||
|
||||
// when introducing a new test, use only temporarily to just enable that test
|
||||
//
|
||||
//test.only('NEW example', async ({ page }) => {
|
||||
// await login(page);
|
||||
// test.setTimeout(0)
|
||||
// await page.pause();
|
||||
//});
|
||||
|
||||
|
||||
test.only('Change erasure server status', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.pause();
|
||||
|
||||
await page.getByRole('link', { name: ' Evidences' }).click();
|
||||
await page.getByRole('link', { name: 'List of evidences' }).click();
|
||||
await page.getByRole('link', { name: '7928afeb-e6a4-464a-a842-' }).click();
|
||||
|
||||
await page.locator('#id_erase_server').check();
|
||||
await page.locator('#id_erase_server').uncheck();
|
||||
|
||||
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('Change TAG', async ({ page }) => {
|
||||
await login(page);
|
||||
//await page.pause();
|
||||
|
||||
await page.getByRole('link', { name: ' Evidences' }).click();
|
||||
await page.getByRole('link', { name: 'List of evidences' }).click();
|
||||
await page.getByRole('link', { name: '7928afeb-e6a4-464a-a842-' }).click();
|
||||
await page.getByRole('button', { name: 'Tag' }).click();
|
||||
await page.getByPlaceholder('Tag').click();
|
||||
await page.getByPlaceholder('Tag').fill('CUSTOMTAG');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Tag' }).click();
|
||||
await page.getByRole('link', { name: 'Delete' }).click();
|
||||
await expect(page.getByText('Evicende Tag deleted')).toBeVisible();
|
||||
|
||||
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('Download Evidence', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.pause();
|
||||
|
||||
await page.getByRole('link', { name: ' Evidences' }).click();
|
||||
await page.getByRole('link', { name: 'List of evidences' }).click();
|
||||
await page.getByRole('link', { name: '7928afeb-e6a4-464a-a842-' }).click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByRole('link', { name: 'Download File' }).click();
|
||||
const download = await downloadPromise;
|
||||
|
||||
await page.close();
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// TODO after the tests, put again demo.ereuse.org as default
|
||||
const TEST_SITE = process.env.TEST_SITE || 'http://localhost:8001'
|
||||
const TEST_USER = process.env.TEST_USER || 'user@example.org'
|
||||
const TEST_PASSWD = process.env.TEST_PASSWD || '1234'
|
||||
|
||||
// when introducing a new test, use only temporarily to just enable that test
|
||||
//
|
||||
//test.only('NEW example', async ({ page }) => {
|
||||
// await login(page);
|
||||
// test.setTimeout(0)
|
||||
// await page.pause();
|
||||
//});
|
||||
|
||||
test('Login success', async ({ page }) => {
|
||||
await page.goto(TEST_SITE);
|
||||
|
||||
//await page.pause();
|
||||
await page.getByPlaceholder('Email address').click();
|
||||
await page.getByPlaceholder('Email address').fill(TEST_USER);
|
||||
await page.getByPlaceholder('Password').fill(TEST_PASSWD);
|
||||
await page.getByPlaceholder('Password').press('Enter');
|
||||
|
||||
//checks that ui is now logged in
|
||||
await expect(page.getByRole('link', { name: ' Evidences' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Login failed', async ({ page }) => {
|
||||
await page.goto(TEST_SITE);
|
||||
|
||||
//await page.pause();
|
||||
await page.getByPlaceholder('Email address').click();
|
||||
await page.getByPlaceholder('Email address').fill(TEST_USER);
|
||||
await page.getByPlaceholder('Password').fill("incorrect password");
|
||||
await page.getByPlaceholder('Password').press('Enter');
|
||||
|
||||
await expect(page.getByText('Login error. Check')).toBeVisible();
|
||||
|
||||
|
||||
});
|
||||
|
||||
test.only('Recover Password ', async ({ page }) => {
|
||||
await page.goto(TEST_SITE);
|
||||
|
||||
await page.pause();
|
||||
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
||||
await page.getByPlaceholder('Email').click();
|
||||
|
||||
await page.getByPlaceholder('Email').fill(TEST_USER);
|
||||
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Password reset sent' })).toBeVisible();
|
||||
await page.getByRole('link', { name: 'Back to login' }).click();
|
||||
|
||||
|
||||
});
|
|
@ -1,155 +0,0 @@
|
|||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
// TODO after the tests, put again demo.ereuse.org as default
|
||||
const TEST_SITE = process.env.TEST_SITE || 'http://127.0.0.1:8001'
|
||||
const TEST_USER = process.env.TEST_USER || 'user@example.org'
|
||||
const TEST_PASSWD = process.env.TEST_PASSWD || '1234'
|
||||
|
||||
async function login(page, date, time) {
|
||||
await page.goto(TEST_SITE);
|
||||
await page.getByPlaceholder('Email address').click();
|
||||
await page.getByPlaceholder('Email address').fill(TEST_USER);
|
||||
await page.getByPlaceholder('Password').fill(TEST_PASSWD);
|
||||
await page.getByPlaceholder('Password').press('Enter');
|
||||
}
|
||||
|
||||
// when introducing a new test, use only temporarily to just enable that test
|
||||
//
|
||||
//test.only('NEW example', async ({ page }) => {
|
||||
// await login(page);
|
||||
// test.setTimeout(0)
|
||||
// await page.pause();
|
||||
//});
|
||||
|
||||
|
||||
test.only('Lot CRUD', async ({ page }) => {
|
||||
await login(page);
|
||||
//await page.pause();
|
||||
|
||||
// Create Lot
|
||||
await page.getByRole('link', { name: 'Entrada' }).click();
|
||||
await page.getByRole('link', { name: 'New lot' }).click();
|
||||
await page.getByLabel('Type').selectOption('2');
|
||||
await page.getByPlaceholder('Name').fill('Organizaci');
|
||||
await page.getByPlaceholder('Code').click();
|
||||
await page.getByPlaceholder('Code').fill('Codigo');
|
||||
await page.getByPlaceholder('Description').fill('Descripcion muy extensa de una organizacion muy extensa');
|
||||
await page.getByRole('button', { name: ' Save' }).click();
|
||||
|
||||
// Edit Lot
|
||||
await page.getByRole('link', { name: ' Edit' }).first().click();
|
||||
await page.getByPlaceholder('Name').click();
|
||||
await page.getByPlaceholder('Name').fill('Organización');
|
||||
await page.getByPlaceholder('Name').press('Enter');
|
||||
|
||||
// Delete Lot
|
||||
await page.getByRole('row', { name: ' Organización Descripcion' }).getByRole('checkbox').check();
|
||||
await page.getByRole('button', { name: ' Delete Selected' }).click();
|
||||
await page.getByRole('button', { name: '' }).click();
|
||||
await page.getByRole('button', { name: '' }).click();
|
||||
await page.getByRole('button', { name: ' Delete' }).click();
|
||||
|
||||
await page.close();
|
||||
});
|
||||
|
||||
|
||||
test('Search function', async ({ page }) => {
|
||||
//Searches for a demo loaded lot (orgC)
|
||||
await login(page);
|
||||
//await page.pause();
|
||||
|
||||
await page.getByRole('link', { name: 'Entrada' }).click();
|
||||
await page.getByPlaceholder('Search by name or description').click();
|
||||
await page.getByPlaceholder('Search by name or description').fill('orgC');
|
||||
await page.getByRole('button', { name: '' }).click();
|
||||
await page.getByRole('link', { name: 'donante-orgC' }).click();
|
||||
|
||||
});
|
||||
|
||||
test('Show archived', async ({ page }) => {
|
||||
await login(page);
|
||||
//await page.pause();
|
||||
|
||||
await page.getByRole('link', { name: 'Entrada' }).click();
|
||||
await page.getByText('Archived (1)').click();
|
||||
await page.getByRole('link', { name: 'donante-orgA' }).click();
|
||||
|
||||
});
|
||||
|
||||
test('Sort by different columns', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
//await page.pause();
|
||||
await page.getByRole('link', { name: 'Entrada' }).click();
|
||||
await page.getByText('All Lots (3)').click();
|
||||
await page.getByRole('link', { name: 'Status' }).click();
|
||||
await page.getByRole('link', { name: 'Status' }).click();
|
||||
await page.getByRole('link', { name: 'Lot Name' }).click();
|
||||
await page.getByRole('link', { name: 'Lot Name' }).click();
|
||||
await page.getByRole('link', { name: 'Description' }).click();
|
||||
await page.getByRole('link', { name: 'Description' }).click();
|
||||
await page.getByRole('link', { name: 'Devices' }).click();
|
||||
await page.getByRole('link', { name: 'Devices' }).click();
|
||||
await page.getByRole('link', { name: 'Created On' }).click();
|
||||
await page.getByRole('link', { name: 'Created On' }).click();
|
||||
await page.getByRole('link', { name: 'Created By' }).click();
|
||||
await page.getByRole('link', { name: 'Created By' }).click();
|
||||
|
||||
});
|
||||
|
||||
test('Lot already exists', async ({ page }) => {
|
||||
await login(page);
|
||||
//await page.pause();
|
||||
|
||||
// Create Lot
|
||||
await page.getByRole('link', { name: 'Entrada' }).click();
|
||||
await page.getByRole('link', { name: 'New lot' }).click();
|
||||
await page.getByLabel('Type').selectOption('2');
|
||||
await page.getByPlaceholder('Name').fill('Duplicated lot');
|
||||
await page.getByPlaceholder('Code').click();
|
||||
await page.getByPlaceholder('Code').fill('Codigo');
|
||||
await page.getByPlaceholder('Description').fill('Descripcion muy extensa de una organizacion muy extensa');
|
||||
await page.getByRole('button', { name: ' Save' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'New lot' }).click();
|
||||
await page.getByLabel('Type').selectOption('2');
|
||||
await page.getByPlaceholder('Name').fill('Duplicated lot');
|
||||
await page.getByPlaceholder('Code').click();
|
||||
await page.getByPlaceholder('Code').fill('Codigo');
|
||||
await page.getByPlaceholder('Description').fill('Descripcion muy extensa de una organizacion muy extensa');
|
||||
await page.getByRole('button', { name: ' Save' }).click();
|
||||
|
||||
await expect(page.getByText('Lot name is already defined.')).toBeVisible();
|
||||
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('Archive lot', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.pause();
|
||||
|
||||
// Create Lot
|
||||
await page.getByRole('link', { name: 'Entrada' }).click();
|
||||
await page.getByRole('link', { name: 'New lot' }).click();
|
||||
await page.getByLabel('Type').selectOption('2');
|
||||
await page.getByPlaceholder('Name').fill('Lot-to be archived');
|
||||
await page.getByRole('button', { name: ' Save' }).click();
|
||||
|
||||
await page.getByRole('link', { name: ' Edit' }).first().click();
|
||||
await page.getByLabel('Archived').check();
|
||||
await page.getByRole('button', { name: ' Save' }).click();
|
||||
await page.getByText('Archived (1)').click();
|
||||
await page.getByRole('link', { name: 'Lot-to be archived' }).click();
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('Select all and delete all', async ({ page }) => {
|
||||
await login(page);
|
||||
// await page.pause();
|
||||
|
||||
await page.getByRole('link', { name: 'Entrada' }).click();
|
||||
await page.locator('#select-all').check();
|
||||
await page.getByRole('button', { name: ' Delete Selected' }).click();
|
||||
await page.getByRole('button', { name: ' Delete' }).click();
|
||||
|
||||
});
|
|
@ -28,9 +28,19 @@ EREUSE22 = [
|
|||
"version"
|
||||
]
|
||||
|
||||
LEGACY_DPP = [
|
||||
"manufacturer",
|
||||
"model",
|
||||
"chassis",
|
||||
"serialNumber",
|
||||
"sku",
|
||||
"type",
|
||||
"version"
|
||||
]
|
||||
|
||||
ALGOS = {
|
||||
"ereuse24": EREUSE24,
|
||||
"ereuse22": EREUSE22
|
||||
"hidalgo1": HID_ALGO1,
|
||||
"legacy_dpp": LEGACY_DPP
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue