Compare commits

..

176 commits

Author SHA1 Message Date
Sergio Giménez Antón d4f971bfa3 Merge remote-tracking branch 'refs/remotes/origin/feature/f31-device-enviromental-impact' into feature/f31-device-enviromental-impact 2025-03-18 19:59:09 +01:00
Sergio Giménez Antón fcd6e13bf9 Add UI polishing for the demo 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón e692417c73 Fix get_power_hours_from_components 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón 9600a6064f Improve example docs 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón b83afa6f12 Add algorithm docs 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón c6f63d4d44 Make env algorithms python packages 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón b2bf894338 Update template with new structure 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón fdc375f804 f31: Initial implementation for environmental impact calculator 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón f26935b617 Very initial impleentation of co2 consumption 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón 01bb822aa5 Add both impact and dpp in the view context 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 5c7fc8fd47 fix rebase from main 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas a7c19ac93e add inxi in parsing and show in details of devs 2025-03-18 19:58:32 +01:00
pedro de1f090694 docker entrypoint: bugfix when DPP env var unbound 2025-03-18 19:58:32 +01:00
pedro a876acc814 docker entrypoint: adapt it to DPP env var 2025-03-18 19:58:32 +01:00
pedro a8884ea012 propagate DPP env var to docker 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 4b1fb26c67 activate/deactivate DPP from env 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 03bdd4818b convert jsonld in credentials for dpps 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 1f93c88bc8 drop loggers 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 573603a6ea fix register dpp 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 3d49db9436 fix did dpp 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas d4d0a35e4a debug timestamp 2025-03-18 19:58:32 +01:00
pedro d8dc37ba94 bugfix attempt verifyProof
co-authored with cayo
2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 8e29ef4bf5 more and more debug 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 62006eb4e3 more debug 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas e42f2c3ea3 fix call to proofs 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 911388718d dpp for proofs 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 3d744e7945 dpp for proofs 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 277a7606e2 drop actions for dpp 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas fe1d020618 drop actions for dpp 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas c8ddec6942 debug in proof call 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 886cf20565 fix cors origin 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 871ed179fb fix json call to chid 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas eb796de4d3 fix phid hash list 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 4b9bcb054e fix phid hash list 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 55e018ad51 fix phid 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 782fc4a541 fix 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 85011076e8 new document and out device and components 2025-03-18 19:58:32 +01:00
pedro 0fc50d7187 comment logger trace when DEBUG
is it necessary?
2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 746a692118 remove flask sintax for django sintax 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas a3613732a9 remove flask sintax for django sintax 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 711fb9e171 add_services 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas d44310bcfd add result for dpp and for chid 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 60c618ce09 fix new document json 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 4e69062452 fix get_result 2025-03-18 19:58:32 +01:00
pedro 490144fd86 dhub settings: bugfix wrong DLT TOKEN 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas e6995e74e0 fix get_result for get correct document 2025-03-18 19:58:32 +01:00
pedro 1fe20e11b8 progress on making it work
still fails
2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 1267f388c1 remove pdb 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 369dc83154 get_result for json 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 3d0527edf1 view dpp page 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 0bbc3475c2 fix 2025-03-18 19:58:32 +01:00
pedro 13a74b133d no sudo in docker-reset, all is with user 1000 2025-03-18 19:58:32 +01:00
pedro 4f38cdf5b1 dh-django dockerfile: use uid 1000 (app)
at least temporarily
2025-03-18 19:58:32 +01:00
pedro 6bb31c40ff dh docker: bugfix wrong usage of up_snapshots 2025-03-18 19:58:32 +01:00
pedro 6fd9792d78 dh dockerfile: add time debpkg 2025-03-18 19:58:32 +01:00
pedro c27b2c2263 dh docker: bugfix wrong path in rm prev snapshots 2025-03-18 19:58:32 +01:00
pedro 97c74ca9bb dh docker: create institution before first dlt usr 2025-03-18 19:58:32 +01:00
pedro 51cbd2bf62 bugfix logger 2025-03-18 19:58:32 +01:00
pedro 93a70ed031 logger: bugfix function name changed for highlight 2025-03-18 19:58:32 +01:00
pedro 83885ceb84 dh docker: cleanup other snapshots when dpp/dlt 2025-03-18 19:58:32 +01:00
pedro 79df0100b1 dh docker: first migrate, then config 2025-03-18 19:58:32 +01:00
pedro ccdd292a97 utils/logger: ensure msgs are logged 2025-03-18 19:58:32 +01:00
pedro 518866bbc9 logger: improve error handling 2025-03-18 19:58:32 +01:00
pedro 4533395e3b dpp/dlt: fix typo 2025-03-18 19:58:32 +01:00
pedro 1a30fa75dc dpp/dlt: fix typo 2025-03-18 19:58:32 +01:00
pedro 9a3a5fe638 dpp/dlt: fix typo 2025-03-18 19:58:32 +01:00
pedro d085d448a9 dh docker entrypoint: use appropriate new env vars 2025-03-18 19:58:32 +01:00
pedro 0974e074d2 docker: remove unused vars in django
were used in the flask app devicehub-teal
2025-03-18 19:58:32 +01:00
pedro b707d78595 docker entrypoint: make DB_* optional 2025-03-18 19:58:32 +01:00
pedro 6c35210d4e docker devicehub-django entrypoint 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas d614fd4756 fix 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas cd93e6dafa fix 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas a81172ad8e add memberFederated model 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 73a72a7d15 add did view 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas bdf029abbd add commands for setup to dlt 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 893bf0c14d . 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 96b0a0ad80 register device and dpp in dlt and dpp api 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas eeea0b3879 first base for dpp 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas c30416d038 fix parsing with credentials 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 1757f70963 fix parsing 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas d7bd47554a fix get_hid 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas ba2ddecc11 fix component empty 2025-03-18 19:58:32 +01:00
Cayo Puigdefabregas 59bdab7776 add inxi in parsing and show in details of devs 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón 5b90d7a648 [WIP] Add button for exporting to PDF 2025-03-18 19:58:32 +01:00
sergio_gimenez 9dc7a66ffd Initial view of the enviromental impact without calculations 2025-03-18 19:58:32 +01:00
Sergio Giménez Antón 01dbe005e4 Add UI polishing for the demo 2025-02-26 18:44:50 +01:00
Sergio Giménez Antón 448287248e Fix get_power_hours_from_components 2025-02-26 18:20:32 +01:00
Sergio Giménez Antón e6223420d2 Improve example docs 2025-02-26 17:36:21 +01:00
Sergio Giménez Antón 6ee0184f66 Add algorithm docs 2025-02-25 09:51:04 +01:00
Sergio Giménez Antón 8f206340f3 Make env algorithms python packages 2025-02-25 08:34:46 +01:00
Sergio Giménez Antón 324eaa215c Update template with new structure 2025-02-25 08:26:45 +01:00
Sergio Giménez Antón 0da3e15a03 Merge branch 'main' into feature/f31-device-enviromental-impact 2025-02-25 07:39:45 +01:00
Sergio Giménez Antón bd4f6b7d56 f31: Initial implementation for environmental impact calculator 2025-01-07 08:06:29 +01:00
Sergio Giménez Antón f9c9c9dd7c Very initial impleentation of co2 consumption 2024-12-17 09:58:20 +01:00
Sergio Giménez Antón 60ccbec369 Merge branch 'main' into feature/f31-device-enviromental-impact 2024-12-17 08:03:31 +01:00
Sergio Giménez Antón 3fb0961815 Add both impact and dpp in the view context 2024-12-16 09:01:05 +01:00
Sergio Giménez Antón 447946a576 Merge branch 'inxi' into feature/f31-device-enviromental-impact 2024-12-16 08:55:00 +01:00
Cayo Puigdefabregas 5d190d07a3 fix rebase from main 2024-12-12 17:11:05 +01:00
Cayo Puigdefabregas d1abb206e8 add inxi in parsing and show in details of devs 2024-12-11 17:41:05 +01:00
pedro 85bae67189 docker entrypoint: bugfix when DPP env var unbound 2024-12-11 17:12:58 +01:00
pedro d429485651 docker entrypoint: adapt it to DPP env var 2024-12-11 17:12:58 +01:00
pedro 07c25f4a92 propagate DPP env var to docker 2024-12-11 17:12:58 +01:00
Cayo Puigdefabregas 14277c17cb activate/deactivate DPP from env 2024-12-11 17:12:56 +01:00
Cayo Puigdefabregas f7051c3130 convert jsonld in credentials for dpps 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 09be1a2f74 drop loggers 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas a3dd5d9639 fix register dpp 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 3f5460b81f fix did dpp 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas bf7975bc24 debug timestamp 2024-12-11 17:09:25 +01:00
pedro 8e128557c0 bugfix attempt verifyProof
co-authored with cayo
2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 25e7e85548 more and more debug 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas ba126491be more debug 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 81e7ba267d fix call to proofs 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 1e08f0fc0c dpp for proofs 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas ebabb6b228 dpp for proofs 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 4954199610 drop actions for dpp 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas e84b72c70b drop actions for dpp 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 99435fff85 debug in proof call 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 6c0e77891f fix cors origin 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas a2d859494b fix json call to chid 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas ea6d990e56 fix phid hash list 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 612737d46c fix phid hash list 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 30be57ee25 fix phid 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 88bdabb64f fix 2024-12-11 17:09:25 +01:00
Cayo Puigdefabregas 96268c8caf new document and out device and components 2024-12-11 17:09:23 +01:00
pedro 7ed05f0932 comment logger trace when DEBUG
is it necessary?
2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas b652d7d452 remove flask sintax for django sintax 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 04ecb4f2f1 remove flask sintax for django sintax 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 1613eaaa44 add_services 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 06264558df add result for dpp and for chid 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 80b4c3b4ca fix new document json 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas e2078c7bde fix get_result 2024-12-11 17:04:20 +01:00
pedro cfae9d4ec9 dhub settings: bugfix wrong DLT TOKEN 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 578fa73fe5 fix get_result for get correct document 2024-12-11 17:04:20 +01:00
pedro f1d57ff618 progress on making it work
still fails
2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 3cf8ceb5d3 remove pdb 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas b56dc0dfda get_result for json 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 1c58bff515 view dpp page 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas e6c1ede93c fix 2024-12-11 17:04:20 +01:00
pedro 371845971c no sudo in docker-reset, all is with user 1000 2024-12-11 17:04:20 +01:00
pedro b4efcfb171 dh-django dockerfile: use uid 1000 (app)
at least temporarily
2024-12-11 17:04:20 +01:00
pedro ac0d36ea6f dh docker: bugfix wrong usage of up_snapshots 2024-12-11 17:04:20 +01:00
pedro 6a3a2b3a2b dh dockerfile: add time debpkg 2024-12-11 17:04:20 +01:00
pedro 850678fbe4 dh docker: bugfix wrong path in rm prev snapshots 2024-12-11 17:04:20 +01:00
pedro f43aaf6ac6 dh docker: create institution before first dlt usr 2024-12-11 17:04:20 +01:00
pedro 355ed08561 bugfix logger 2024-12-11 17:04:20 +01:00
pedro d0e46aa0b0 logger: bugfix function name changed for highlight 2024-12-11 17:04:20 +01:00
pedro 771b216a31 dh docker: cleanup other snapshots when dpp/dlt 2024-12-11 17:04:20 +01:00
pedro 263eacda99 add dpp 2024-12-11 17:04:20 +01:00
pedro 8fcd20f609 dh docker: first migrate, then config 2024-12-11 17:04:20 +01:00
pedro 15fb5d3739 utils/logger: ensure msgs are logged 2024-12-11 17:04:20 +01:00
pedro d7ff3c2798 logger: improve error handling 2024-12-11 17:04:20 +01:00
pedro 0e0ad400c2 dpp/dlt: fix typo 2024-12-11 17:04:20 +01:00
pedro 367d3a7f87 dpp/dlt: fix typo 2024-12-11 17:04:20 +01:00
pedro c90ed58ea0 dpp/dlt: fix typo 2024-12-11 17:04:20 +01:00
pedro 45629db102 dh docker entrypoint: use appropriate new env vars 2024-12-11 17:04:20 +01:00
pedro 1e29f9562d docker: remove unused vars in django
were used in the flask app devicehub-teal
2024-12-11 17:04:20 +01:00
pedro d0cac9d1d9 docker entrypoint: make DB_* optional 2024-12-11 17:04:20 +01:00
pedro 8b4d1f51f6 docker devicehub-django entrypoint 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 34ea4bedfc fix 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas fe429e7db6 fix 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas caf2606fd9 add memberFederated model 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 73d478f517 add did view 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 0f03171076 add commands for setup to dlt 2024-12-11 17:04:20 +01:00
pedro bfdcb33538 docker: add dpp python dep ereuseapitest 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas 271ac83d71 . 2024-12-11 17:04:20 +01:00
Cayo Puigdefabregas f7b2687ca2 register device and dpp in dlt and dpp api 2024-12-11 17:04:11 +01:00
Cayo Puigdefabregas 1dad22c3d3 first base for dpp 2024-12-11 17:02:26 +01:00
Cayo Puigdefabregas 7de6d69a6c fix parsing with credentials 2024-12-05 19:23:53 +01:00
Sergio Giménez Antón fa5b9eec67 Merge branch 'inxi' into feature/f31-device-enviromental-impact 2024-12-05 09:18:03 +01:00
Cayo Puigdefabregas 7fd42db3e4 fix parsing 2024-12-03 16:37:56 +01:00
Cayo Puigdefabregas bed40d3ee0 fix get_hid 2024-11-20 18:41:59 +01:00
Cayo Puigdefabregas 9553ed6a4c fix component empty 2024-11-20 18:35:27 +01:00
Sergio Giménez Antón f3c9297ffd [WIP] Add button for exporting to PDF 2024-11-19 08:27:44 +01:00
Sergio Giménez cb6c7f6fda Merge branch 'main' into feature/f31-device-enviromental-impact 2024-11-16 16:37:30 +01:00
Cayo Puigdefabregas a0276f439e add inxi in parsing and show in details of devs 2024-11-15 12:47:08 +01:00
sergio_gimenez a4d361ff9b Initial view of the enviromental impact without calculations 2024-11-07 08:15:42 +01:00
60 changed files with 1208 additions and 4241 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,61 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col">
<h3>{{ object.id }}</h3>
</div>
</div>
<div class="row">
<div class="col">
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items">
<a href="{% url 'evidence:details' object.uuid %}" class="nav-link">{% trans "Devices" %}</a>
</li>
<li class="nav-items">
<a href="{% url 'evidence:details' object.uuid %}#tag" class="nav-link">{% trans "Tag" %}</a>
</li>
<li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#erase_server">{% trans "Erase Server" %}</button>
</li>
<li class="nav-items">
<a href="{% url 'evidence:download' object.uuid %}" class="nav-link">{% trans "Download File" %}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content pt-2">
<div class="tab-pane fade show active" id="erase_server">
{% load django_bootstrap5 %}
<div class="list-group col-6">
<form role="form" method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
<div class="message">
{% for field, error in form.errors.items %}
{{ error }}<br />
{% endfor %}
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
</div>
</div>
{% endif %}
{% bootstrap_form form %}
<div class="container">
<div class="row">
<div class="col">
<a class="btn btn-grey" href="">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View file

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

View file

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

View file

@ -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">&times;</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>

View file

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

View file

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

View file

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

View file

@ -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"
),
),
]

View file

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

View file

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

View file

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

View 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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 "${@}"

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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();
});

View file

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