Compare commits
10 commits
b256d12fee
...
441d345085
Author | SHA1 | Date | |
---|---|---|---|
441d345085 | |||
be40cd489f | |||
ff090fbb3c | |||
41262ee1d9 | |||
2d7dd98b5b | |||
5ae40a9cca | |||
2abe2854f8 | |||
a35537d52c | |||
558033e67e | |||
34e845d14d |
87
Readme.md
|
@ -20,12 +20,12 @@ As configured by default you will need the following:
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
This is a standard Django 5.0 application, if you know how to deploy those the
|
This is a standard Django 5.1 application, if you know how to deploy those the
|
||||||
following might sound familiar:
|
following might sound familiar:
|
||||||
|
|
||||||
1. Checkout repository: `git clone https://github.com/dunkelstern/inventory.git`
|
1. Checkout repository: `git clone https://github.com/dunkelstern/inventory.git`
|
||||||
2. Change to checkout: `cd inventory`
|
2. Change to checkout: `cd inventory`
|
||||||
3. Install virtualenv and dependencies: `poetry install`
|
3. Install virtualenv and dependencies: `poetry install --no-root`
|
||||||
4. Migrate the Database: `poetry run python manage.py migrate`
|
4. Migrate the Database: `poetry run python manage.py migrate`
|
||||||
5. Create an admin user: `poetry run python manage.py createsuperuser`
|
5. Create an admin user: `poetry run python manage.py createsuperuser`
|
||||||
6. Run the server
|
6. Run the server
|
||||||
|
@ -43,16 +43,35 @@ go to `http://localhost:8000` to enter the inventory management system directly
|
||||||
2. For editing parts the Django admin interface is used, so edit-links will only
|
2. For editing parts the Django admin interface is used, so edit-links will only
|
||||||
appear if the currently logged in user is a `staff` user (set the checkbox
|
appear if the currently logged in user is a `staff` user (set the checkbox
|
||||||
in the admin area).
|
in the admin area).
|
||||||
|
3. If you want to change the default number of items on paginated views you can
|
||||||
|
set the page size in the settings by providing a parameter `PAGE_SIZE`
|
||||||
|
4. If you want to run this as a systemd service see the
|
||||||
|
[the service file](inventory.service) in the root of this repository for an
|
||||||
|
example.
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
|
#### Login
|
||||||
|
|
||||||
|
To be able to list all parts you'll need to login. You basically have three
|
||||||
|
levels of permissions:
|
||||||
|
|
||||||
|
- Normal Users
|
||||||
|
- Staff Users
|
||||||
|
- Admin Users
|
||||||
|
|
||||||
|
Normal users can view all parts and search, Staff users may edit in addition.
|
||||||
|
Admin users can create Users and do everything (like adding new layouts, etc.).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
#### Overview Page
|
#### Overview Page
|
||||||
|
|
||||||
here we have a layer of containers, you may nest multiple containers into each
|
here we have a layer of containers, you may nest multiple containers into each
|
||||||
other, for example to define a cupboard which contains multiple boxes of parts,
|
other, for example to define a cupboard which contains multiple boxes of parts,
|
||||||
or multiple rooms in your workshop that contain cupboards, etc.
|
or multiple rooms in your workshop that contain cupboards, etc.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Box View
|
#### Box View
|
||||||
|
|
||||||
|
@ -61,9 +80,9 @@ compartments, number of items per compartment and layout of compartments
|
||||||
themselves) all by yourself in the admin backend, by default the database comes
|
themselves) all by yourself in the admin backend, by default the database comes
|
||||||
with an assortment of Ikea and Raaco sorter boxes.
|
with an assortment of Ikea and Raaco sorter boxes.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The Overview and Box views are designed to be used on a touch-screen and the HTML,
|
The Overview and Box views are designed to be used on a touch-screen and the HTML,
|
||||||
CSS and Javascript are designed to work on older Hardware (Apple iOS 9 has been
|
CSS and Javascript are designed to work on older Hardware (Apple iOS 9 has been
|
||||||
|
@ -74,7 +93,7 @@ tested at lowest, so this works from iPad 2 up to the newest pro).
|
||||||
This is the detail view of a part, this is useful to find all parts by manufacturer
|
This is the detail view of a part, this is useful to find all parts by manufacturer
|
||||||
or distributor, or when a part has multiple datasheets.
|
or distributor, or when a part has multiple datasheets.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Part edit view
|
#### Part edit view
|
||||||
|
|
||||||
|
@ -82,4 +101,58 @@ Editing is done on the standard Django admin interface, so all users that have n
|
||||||
*staff* privileges only can view all parts, all with *staff* privileges have access
|
*staff* privileges only can view all parts, all with *staff* privileges have access
|
||||||
to the django admin backend and can edit parts too.
|
to the django admin backend and can edit parts too.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
#### Search function
|
||||||
|
|
||||||
|
If you click on the loupe symbol on top you'll get a popup searchbox for a fulltext
|
||||||
|
search through all parts.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The search results contain a link to the container the object is stored in and if
|
||||||
|
you click that link the compartment will be hilighted so you can find the part faster:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can also reach the first datasheet directly from the search results by clicking
|
||||||
|
on the icon in front of the description text.
|
||||||
|
|
||||||
|
#### Tag cloud
|
||||||
|
|
||||||
|
If you select the tag icon in the header bar you will get a dynamically searchable
|
||||||
|
tag cloud if you do not know a search term exactly or if you need a group of parts
|
||||||
|
(e.g. give me all transistors of any type).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The detail view of a tag will list all items with that tag, as well as all containers
|
||||||
|
or footprints that have been assigned this tag:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Editing is in Django backend:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Other options
|
||||||
|
|
||||||
|
You can browse your parts inventory by distributor or part manufacturer if you want:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The distributor views have a convenient search box if a parametrized search link has
|
||||||
|
been set up in the backend.
|
||||||
|
|
||||||
|
This link will be used to link to a part directly if a part has a distributor part
|
||||||
|
number saved.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
BIN
docs/distributor.jpeg
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
docs/distributor_detail.jpeg
Normal file
After Width: | Height: | Size: 231 KiB |
BIN
docs/edit_area.jpeg
Normal file
After Width: | Height: | Size: 237 KiB |
BIN
docs/edit_box.jpeg
Normal file
After Width: | Height: | Size: 163 KiB |
BIN
docs/edit_distributor.jpeg
Normal file
After Width: | Height: | Size: 177 KiB |
BIN
docs/edit_manufacturer.jpeg
Normal file
After Width: | Height: | Size: 122 KiB |
BIN
docs/edit_part.jpeg
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
docs/edit_tag.jpeg
Normal file
After Width: | Height: | Size: 167 KiB |
Before Width: | Height: | Size: 253 KiB |
Before Width: | Height: | Size: 240 KiB |
Before Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 554 KiB |
Before Width: | Height: | Size: 85 KiB |
BIN
docs/login.jpeg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/main_area.jpeg
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
docs/manufacturer.jpeg
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
docs/manufacturer_detail.jpeg
Normal file
After Width: | Height: | Size: 204 KiB |
BIN
docs/part.jpeg
Normal file
After Width: | Height: | Size: 239 KiB |
BIN
docs/search.jpeg
Normal file
After Width: | Height: | Size: 234 KiB |
BIN
docs/search_hilight.jpeg
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
docs/smd_box.jpeg
Normal file
After Width: | Height: | Size: 299 KiB |
BIN
docs/smd_box_marker.jpeg
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
docs/tag_detail.jpeg
Normal file
After Width: | Height: | Size: 218 KiB |
BIN
docs/tags.jpeg
Normal file
After Width: | Height: | Size: 314 KiB |
|
@ -46,6 +46,10 @@ td.disabled {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-hilight {
|
||||||
|
background-color: #ffa0a0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.cell:nth-child(2n) {
|
.cell:nth-child(2n) {
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
@ -142,3 +146,25 @@ td.disabled {
|
||||||
.dock-top {
|
.dock-top {
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.cell .form_factor {
|
||||||
|
font-size: 75%;
|
||||||
|
top: 3px;
|
||||||
|
right: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell .price {
|
||||||
|
font-size: 75%;
|
||||||
|
bottom: 3px;
|
||||||
|
left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell .shop {
|
||||||
|
bottom: 3px;
|
||||||
|
right: 7px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -88,14 +88,18 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 .small {
|
h1 .small {
|
||||||
font-size: 12pt;
|
font-size: 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: #808080;
|
color: #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 .small {
|
h2 .small {
|
||||||
font-size: 12pt;
|
display: inline-block;
|
||||||
|
max-width: 400px;
|
||||||
|
font-size: 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav h2 {
|
nav h2 {
|
||||||
|
@ -135,10 +139,18 @@ main {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
body {
|
body {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
body {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 .small {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img.icon {
|
img.icon {
|
||||||
|
@ -207,7 +219,6 @@ ul.tag-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
max-width: 500px;
|
|
||||||
text-indent: 0;
|
text-indent: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
@ -292,3 +303,22 @@ table.list tbody td {
|
||||||
.right {
|
.right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#search-container {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #292981;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid white;
|
||||||
|
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result em {
|
||||||
|
color: #c00000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result .small {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
|
1
inventory/static/inventory/img/search.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>
|
After Width: | Height: | Size: 463 B |
|
@ -23,6 +23,29 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="icon-nav">
|
<ul class="icon-nav">
|
||||||
|
<li>
|
||||||
|
<script>
|
||||||
|
function showSearch(e) {
|
||||||
|
const searchBox = document.getElementById("search-container");
|
||||||
|
if ((searchBox.style.display === '') || (searchBox.style.display === 'none')) {
|
||||||
|
searchBox.style.display = 'block';
|
||||||
|
searchBox.getElementsByTagName('input')[0].focus();
|
||||||
|
} else {
|
||||||
|
searchBox.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<a href="{% url 'search' %}" onclick="showSearch(event)"><img class="icon" title="Search" src="{% static "inventory/img/search.svg" %}"></a>
|
||||||
|
<div id="search-container">
|
||||||
|
<form action="{% url 'search' %}" method="get">
|
||||||
|
<input name="q" id="search" type="text" placeholder="Search">
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'manufacturer-list' %}"><img class="icon" title="Manufacturers" src="{% static "inventory/img/manufacturer.svg" %}"></a>
|
<a href="{% url 'manufacturer-list' %}"><img class="icon" title="Manufacturers" src="{% static "inventory/img/manufacturer.svg" %}"></a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -33,7 +56,7 @@
|
||||||
<a href="{% url 'tag-list' %}"><img class="icon" title="Tags" src="{% static "inventory/img/tags.svg" %}"></a>
|
<a href="{% url 'tag-list' %}"><img class="icon" title="Tags" src="{% static "inventory/img/tags.svg" %}"></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'workshop-list' %}"><img class="icon" title="Workshops" src="{% static "inventory/img/workshop.svg" %}"></a>
|
<a href="{% url 'index' %}"><img class="icon" title="Workshops" src="{% static "inventory/img/workshop.svg" %}"></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<h3>Items</h3>
|
<h3>Items</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in box.item_related.all %}
|
{% for item in box.item_related.all %}
|
||||||
<li><a href="{% url 'item-detail' item.id %}" title="{{ item.description }}">{{ item.name }}</a></li>
|
<li {% if hilight == item.id %}class="hilighted"{% endif %}><a href="{% url 'item-detail' item.id %}" title="{{ item.description }}">{{ item.name }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<td rowspan="2">
|
<td rowspan="2">
|
||||||
<div class="compartments-vertical">
|
<div class="compartments-vertical">
|
||||||
{% for compartment in layouted.0.0 %}
|
{% for compartment in layouted.0.0 %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
<td class="triple-height" colspan="3">
|
<td class="triple-height" colspan="3">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in layouted.0.1 %}
|
{% for compartment in layouted.0.1 %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<td class="triple-height">
|
<td class="triple-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in layouted.0 %}
|
{% for compartment in layouted.0 %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
<td rowspan="2">
|
<td rowspan="2">
|
||||||
<div class="compartments-vertical">
|
<div class="compartments-vertical">
|
||||||
{% for compartment in layouted.1.0 %}
|
{% for compartment in layouted.1.0 %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
<td class="double-height">
|
<td class="double-height">
|
||||||
<div class="compartments">
|
<div class="compartments">
|
||||||
{% for compartment in item %}
|
{% for compartment in item %}
|
||||||
{% include "inventory/cell.html" with item=compartment %}
|
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load admin_urls %}
|
{% load admin_urls %}
|
||||||
|
<div class="cell{% if hilight == item.id %} search-hilight{% endif %}">
|
||||||
<div class="cell">
|
|
||||||
{% if item.name %}
|
{% if item.name %}
|
||||||
{% if item.metadata.package %}
|
{% if item.metadata.package %}
|
||||||
<div class="package">{{ item.metadata.package }}</div>
|
<div class="package">{{ item.metadata.package }}</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<div class="pagination" id="paginator_top">
|
<div class="pagination" id="{{ id }}">
|
||||||
{% if paginator.has_previous %}
|
{% if paginator.has_previous %}
|
||||||
<a href="{{ url }}?{{ param }}_page=1#{{ id }}"><img src="{% static 'inventory/img/first.svg' %}" class="icon" title="First page"></a>
|
<a href="{{ url }}?{{ param }}_page=1#{{ id }}"><img src="{% static 'inventory/img/first.svg' %}" class="icon" title="First page"></a>
|
||||||
<a href="{{ url }}?{{ param }}_page={{ paginator.previous_page_number }}#{{ id }}"><img src="{% static 'inventory/img/previous.svg' %}" class="icon" title="Previous page"></a>
|
<a href="{{ url }}?{{ param }}_page={{ paginator.previous_page_number }}#{{ id }}"><img src="{% static 'inventory/img/previous.svg' %}" class="icon" title="Previous page"></a>
|
||||||
|
|
15
inventory/templates/inventory/search.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Search{% endblock %}
|
||||||
|
|
||||||
|
{% block header_bar %}
|
||||||
|
Search
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="get" action="{% url 'search' %}">
|
||||||
|
<input type="text" id="search" name="q">
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
43
inventory/templates/inventory/search_result.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load hilight %}
|
||||||
|
|
||||||
|
{% block title %}Search{% endblock %}
|
||||||
|
|
||||||
|
{% block header_bar %}
|
||||||
|
Search
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="get" action="{% url 'search' %}">
|
||||||
|
<input type="text" id="search" name="q" value="{{ q }}">
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Search result for '{{ q }}'</h2>
|
||||||
|
|
||||||
|
{% for result in results %}
|
||||||
|
<div class="search-result">
|
||||||
|
<h3><a href="{{ result.url }}">{{ result.title | hilight:tokens }}</a></h3>
|
||||||
|
{{ result.text | safe }}
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p>Noting found</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if pages > 1 %}
|
||||||
|
<div class="pagination">
|
||||||
|
{% if page > 1 %}
|
||||||
|
<a href="{% url 'search' %}?q={{ q }}&page=1"><img src="{% static 'inventory/img/first.svg' %}" class="icon" title="First page"></a>
|
||||||
|
<a href="{% url 'search' %}?q={{ q }}&page={{ page | add:'-1'}}"><img src="{% static 'inventory/img/previous.svg' %}" class="icon" title="Previous page"></a>
|
||||||
|
{% endif %}
|
||||||
|
{{ page }}/{{ pages }}
|
||||||
|
{% if page < pages %}
|
||||||
|
<a href="{% url 'search' %}?q={{ q }}&page={{ page | add:'1'}}"><img src="{% static 'inventory/img/next.svg' %}" class="icon" title="Next page"></a>
|
||||||
|
<a href="{% url 'search' %}?q={{ q }}&page={{ pages }}"><img src="{% static 'inventory/img/last.svg' %}" class="icon" title="Last page"></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
17
inventory/templates/inventory/search_result_item.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load hilight %}
|
||||||
|
<p>
|
||||||
|
{% if item.documentation.all %}
|
||||||
|
<a class="datasheet" href="{{ item.documentation.all.0.file.url }}"><img class="icon" src="{% static "inventory/img/datasheet.svg" %}"></a>
|
||||||
|
{% endif %}
|
||||||
|
{{ item | hilight:tokens }}
|
||||||
|
<span class="small">{{item.form_factor.name}}</span>
|
||||||
|
</p>
|
||||||
|
<p>Contained in: <a href="{{ item.container_url }}?hilight={{ item.id }}">{{ item.container.display_name }}</a></p>
|
||||||
|
<ul class="tag-list">
|
||||||
|
{% for tag in item.all_tags %}
|
||||||
|
<li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
|
||||||
|
{% empty %}
|
||||||
|
No tags
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
|
@ -5,13 +5,13 @@
|
||||||
{% block title %}Tags{% endblock %}
|
{% block title %}Tags{% endblock %}
|
||||||
|
|
||||||
{% block header_bar %}
|
{% block header_bar %}
|
||||||
Tags
|
Inventory Management - Tags
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script>
|
<script>
|
||||||
function updateSearch(e) {
|
function updateSearch(e) {
|
||||||
const input = document.getElementById('search');
|
const input = document.getElementById('tag-search');
|
||||||
const text = input.value.toLowerCase();
|
const text = input.value.toLowerCase();
|
||||||
|
|
||||||
if (text.length >= 2) {
|
if (text.length >= 2) {
|
||||||
|
@ -39,8 +39,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<form>
|
<form>
|
||||||
<label for="search">Search:</label>
|
<input type="text" id="tag-search" name="tag-search" placeholder="Search" oninput="updateSearch(event)">
|
||||||
<input type="text" id="search" name="search" oninput="updateSearch(event)">
|
|
||||||
<button onclick="updateSearch(event)">Search</button>
|
<button onclick="updateSearch(event)">Search</button>
|
||||||
</form>
|
</form>
|
||||||
<ul class="tag-list" id="tagcloud">
|
<ul class="tag-list" id="tagcloud">
|
||||||
|
|
|
@ -33,7 +33,8 @@ from .views import (
|
||||||
DistributorView,
|
DistributorView,
|
||||||
ManufacturerView,
|
ManufacturerView,
|
||||||
IndexView,
|
IndexView,
|
||||||
TagView
|
TagView,
|
||||||
|
SearchView
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -52,5 +53,6 @@ urlpatterns = [
|
||||||
path('distributor/<int:pk>', DistributorView.as_view(), name='distributor-detail'),
|
path('distributor/<int:pk>', DistributorView.as_view(), name='distributor-detail'),
|
||||||
path('tags', TagListView.as_view(), name='tag-list'),
|
path('tags', TagListView.as_view(), name='tag-list'),
|
||||||
path('tag/<int:pk>', TagView.as_view(), name='tag-detail'),
|
path('tag/<int:pk>', TagView.as_view(), name='tag-detail'),
|
||||||
|
path('search', SearchView.as_view(), name='search'),
|
||||||
path('', IndexView.as_view(), name='index')
|
path('', IndexView.as_view(), name='index')
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,6 +6,7 @@ from .manufacturer import ManufacturerView, ManufacturerListView
|
||||||
from .workshop import WorkshopView, WorkshopListView
|
from .workshop import WorkshopView, WorkshopListView
|
||||||
from .index import IndexView
|
from .index import IndexView
|
||||||
from .tag import TagListView, TagView
|
from .tag import TagListView, TagView
|
||||||
|
from .search import SearchView
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
AreaView, AreaListView,
|
AreaView, AreaListView,
|
||||||
|
@ -15,5 +16,6 @@ __all__ = [
|
||||||
ManufacturerView, ManufacturerListView,
|
ManufacturerView, ManufacturerListView,
|
||||||
WorkshopView, WorkshopListView,
|
WorkshopView, WorkshopListView,
|
||||||
IndexView,
|
IndexView,
|
||||||
TagView, TagListView
|
TagView, TagListView,
|
||||||
|
SearchView
|
||||||
]
|
]
|
|
@ -49,9 +49,11 @@ class BoxView(CanBeIndexMixin, DetailView):
|
||||||
return result, idx
|
return result, idx
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
hilighted = int(request.GET.get('hilight', "0"))
|
||||||
self.object = cast(Box, self.get_object())
|
self.object = cast(Box, self.get_object())
|
||||||
context = self.get_context_data(object=self.object)
|
context = self.get_context_data(object=self.object)
|
||||||
context['layouted'], _ = self.layout(self.object.item_related.all().order_by('index'), self.object.layout.data)
|
context['layouted'], _ = self.layout(self.object.item_related.all().order_by('index'), self.object.layout.data)
|
||||||
|
context['hilight'] = hilighted
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
|
||||||
|
|
84
inventory/views/search.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.generic import View
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from re import finditer
|
||||||
|
|
||||||
|
from inventory.models import Item
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class SearchView(View):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
page = int(request.GET.get('page', "1"))
|
||||||
|
query = request.GET.get('q', None)
|
||||||
|
if not query:
|
||||||
|
return render(request, "inventory/search.html")
|
||||||
|
|
||||||
|
results: list[dict[str, str]] = []
|
||||||
|
|
||||||
|
tokens = query.split(" ")
|
||||||
|
tokens = map(str.lower, map(str.strip, tokens))
|
||||||
|
|
||||||
|
q: Q = None
|
||||||
|
item_template = get_template("inventory/search_result_item.html")
|
||||||
|
t: list[str] = []
|
||||||
|
for token in tokens:
|
||||||
|
combiner = 'or'
|
||||||
|
if token.startswith("+"):
|
||||||
|
token = token[1:]
|
||||||
|
combiner = 'and'
|
||||||
|
elif token.startswith("-"):
|
||||||
|
token = token[1:]
|
||||||
|
combiner = 'and not'
|
||||||
|
|
||||||
|
t.append(token)
|
||||||
|
|
||||||
|
q1 = Q(name__icontains=f'{token}')
|
||||||
|
q2 = Q(description__icontains=f'{token}')
|
||||||
|
q3 = Q(tags__name__icontains=f'{token}')
|
||||||
|
q4 = Q(tags__description__icontains=f'{token}')
|
||||||
|
q5 = Q(form_factor__tags__name__icontains=f'{token}')
|
||||||
|
q6 = Q(form_factor__tags__description__icontains=f'{token}')
|
||||||
|
|
||||||
|
qx = q1 | q2 | q3 | q4 | q5 | q6
|
||||||
|
|
||||||
|
if q == None:
|
||||||
|
q = qx
|
||||||
|
elif combiner == 'or':
|
||||||
|
q = q | qx
|
||||||
|
elif combiner == 'and':
|
||||||
|
q = q & qx
|
||||||
|
elif combiner == 'and not':
|
||||||
|
q = q & ~qx
|
||||||
|
items = Item.objects.filter(q).select_related("container", "form_factor").prefetch_related("tags", "documentation").distinct()
|
||||||
|
|
||||||
|
# FIXME: Rank search results
|
||||||
|
|
||||||
|
paginator = Paginator(items, getattr(settings, "PAGE_SIZE", 10))
|
||||||
|
|
||||||
|
for item in paginator.get_page(page):
|
||||||
|
text = item_template.render({
|
||||||
|
"item": item,
|
||||||
|
"tokens": t
|
||||||
|
})
|
||||||
|
result = {
|
||||||
|
"title": item.name,
|
||||||
|
"text": text,
|
||||||
|
"url": reverse('item-detail', kwargs={"pk": item.pk})
|
||||||
|
}
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
return render(request, "inventory/search_result.html", {
|
||||||
|
"q": query,
|
||||||
|
"results": results,
|
||||||
|
"tokens": t,
|
||||||
|
"page": page,
|
||||||
|
"pages": paginator.num_pages
|
||||||
|
})
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "inventory"
|
name = "inventory"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Johannes Schriewer <hallo@dunkelstern.de>"]
|
authors = ["Johannes Schriewer <hallo@dunkelstern.de>"]
|
||||||
|
|
||||||
|
|