Merge pull request #288 from SchrodingersGat/tree-badges

Tree badges
This commit is contained in:
Oliver 2019-05-09 23:06:32 +10:00 committed by GitHub
commit 793b4f8063
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 142 additions and 26 deletions

View File

@ -41,6 +41,17 @@ class InvenTreeTree(models.Model):
null=True,
related_name='children')
@property
def item_count(self):
""" Return the number of items which exist *under* this node in the tree.
Here an 'item' is considered to be the 'leaf' at the end of each branch,
and the exact nature here will depend on the class implementation.
The default implementation returns zero
"""
return 0
def getUniqueParents(self, unique=None):
""" Return a flat set of all parent items that exist above this node.
If any parents are repeated (which would be very bad!), the process is halted

View File

@ -28,11 +28,22 @@ class TreeSerializer(views.APIView):
Ref: https://github.com/jonmiles/bootstrap-treeview
"""
@property
def root_url(self):
""" Return the root URL for the tree. Implementation is class dependent.
Default implementation returns #
"""
return '#'
def itemToJson(self, item):
data = {
'pk': item.id,
'text': item.name,
'href': item.get_absolute_url(),
'tags': [item.item_count],
}
if item.has_children:
@ -51,12 +62,18 @@ class TreeSerializer(views.APIView):
nodes = []
top_count = 0
for item in top_items:
nodes.append(self.itemToJson(item))
top_count += item.item_count
top = {
'pk': None,
'text': self.title,
'href': self.root_url,
'nodes': nodes,
'tags': [top_count],
}
response = {

View File

@ -14,6 +14,7 @@ from rest_framework import generics, permissions
from django.db.models import Q
from django.conf.urls import url, include
from django.urls import reverse
from .models import Part, PartCategory, BomItem, PartStar
from .models import SupplierPart, SupplierPriceBreak
@ -30,6 +31,10 @@ class PartCategoryTree(TreeSerializer):
title = "Parts"
model = PartCategory
@property
def root_url(self):
return reverse('part-index')
class CategoryList(generics.ListCreateAPIView):

View File

@ -47,18 +47,18 @@ class PartCategory(InvenTreeTree):
verbose_name = "Part Category"
verbose_name_plural = "Part Categories"
@property
def item_count(self):
return self.partcount
@property
def partcount(self):
""" Return the total part count under this category
(including children of child categories)
"""
count = self.parts.count()
for child in self.children.all():
count += child.partcount
return count
return len(Part.objects.filter(category__in=self.getUniqueChildren(),
active=True))
@property
def has_parts(self):

View File

@ -1,7 +1,7 @@
<div class='navigation'>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li><a href='#' id='toggle-part-tree'><b>+</b></a></li>
<li><a href='#' id='toggle-part-tree'><b><span class='glyphicon glyphicon-small glyphicon-th-list'></span></b></a></li>
<li class="breadcrumb-item{% if category is None %} active" aria-current="page{% endif %}"><a href="/part/">Parts</a></li>
{% if category %}
{% for path_item in category.parentpath %}

View File

@ -34,7 +34,11 @@ InvenTree | Part List
{% block js_ready %}
{{ block.super }}
loadTree("{% url 'api-part-tree' %}",
"#part-tree");
"#part-tree",
{
name: 'part',
}
);
$("#toggle-part-tree").click(function() {
toggleSideNav("#sidenav");

View File

@ -19,6 +19,14 @@
font-size: 11px;
}
.treeview .badge {
font-size: 10px;
}
.treeview .list-group-item {
padding: 6px 12px;
}
/* Force select2 elements in modal forms to be full width */
.select-full-width {
width: 100%;
@ -113,13 +121,26 @@
.navigation {
}
.breadcrump {
margin-bottom: 5px;
}
.inventree-body {
width: 100%;
padding: 5px;
margin: 10px;
}
.inventree-pre-content {
width: 100%;
clear: both;
}
.inventree-content {
padding-left: 5px;
padding-right: 5px;
padding-top: 15px;
margin-right: 50px;
margin-left: 50px;
width: 100%;
padding-top: 5px;
width: auto;
transition: 0.1s;
}
@ -165,7 +186,7 @@
position: fixed; /* Stay in place */
background-color: #fff; /* Black*/
overflow-x: hidden; /* Disable horizontal scroll */
//transition: 0.1s; /* 0.5 second transition effect to slide in the sidenav */
transition: 0.1s; /* 0.5 second transition effect to slide in the sidenav */
}
.wrapper {

View File

@ -1,4 +1,26 @@
function loadTree(url, tree, data) {
function loadTree(url, tree, options={}) {
/* Load the side-nav tree view
Args:
url: URL to request tree data
tree: html ref to treeview
options:
data: data object to pass to the AJAX request
selected: ID of currently selected item
name: name of the tree
*/
var data = {};
if (options.data) {
data = options.data;
}
var key = "inventree-sidenav-items-";
if (options.name) {
key += options.name;
}
$.ajax({
url: url,
@ -9,14 +31,17 @@ function loadTree(url, tree, data) {
if (response.tree) {
$(tree).treeview({
data: response.tree,
enableLinks: true
enableLinks: true,
showTags: true,
});
var saved_exp = sessionStorage.getItem('inventree-sidenav-expanded-items').split(",");
if (sessionStorage.getItem(key)) {
var saved_exp = sessionStorage.getItem(key).split(",");
// Automatically expand the desired notes
for (var q = 0; q < saved_exp.length; q++) {
$(tree).treeview('expandNode', parseInt(saved_exp[q]));
// Automatically expand the desired notes
for (var q = 0; q < saved_exp.length; q++) {
$(tree).treeview('expandNode', parseInt(saved_exp[q]));
}
}
// Setup a callback whenever a node is toggled
@ -32,7 +57,7 @@ function loadTree(url, tree, data) {
}
// Save the expanded nodes
sessionStorage.setItem('inventree-sidenav-expanded-items', exp);
sessionStorage.setItem(key, exp);
});
}
},
@ -43,6 +68,7 @@ function loadTree(url, tree, data) {
}
function openSideNav() {
document.getElementById("sidenav").style.display = "block";
document.getElementById("sidenav").style.width = "250px";
document.getElementById("inventree-content").style.marginLeft = "270px";
@ -52,8 +78,9 @@ function openSideNav() {
}
function closeSideNav() {
document.getElementById("sidenav").style.display = "none";
document.getElementById("sidenav").style.width = "0";
document.getElementById("inventree-content").style.marginLeft = "50px";
document.getElementById("inventree-content").style.marginLeft = "0px";
sessionStorage.setItem('inventree-sidenav-state', 'closed');
}

View File

@ -6,6 +6,7 @@ from django_filters.rest_framework import FilterSet, DjangoFilterBackend
from django_filters import NumberFilter
from django.conf.urls import url, include
from django.urls import reverse
from django.db.models import Q
from .models import StockLocation, StockItem
@ -27,6 +28,10 @@ class StockCategoryTree(TreeSerializer):
title = 'Stock'
model = StockLocation
@property
def root_url(self):
return reverse('stock-index')
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" API detail endpoint for Stock object

View File

@ -48,6 +48,18 @@ class StockLocation(InvenTreeTree):
}
)
@property
def stock_item_count(self):
""" Return the number of StockItem objects which live in or under this category
"""
return len(StockItem.objects.filter(location__in=self.getUniqueChildren()))
@property
def item_count(self):
return self.stock_item_count
@receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log')
def before_delete_stock_location(sender, instance, using, **kwargs):

View File

@ -1,7 +1,7 @@
<div class="navigation">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li><a href='#' id='toggle-stock-tree'><b>+</b></a></li>
<li><a href='#' title='Toggle Stock Tree' id='toggle-stock-tree'><b><span class='glyphicon glyphicon-small glyphicon-th-list'></span></b></a></li>
<li class="breadcrumb-item{% if location is None %} active" aria-current="page{% endif %}"><a href="/stock/">Stock</a></li>
{% if location %}
{% for path_item in location.parentpath %}

View File

@ -33,7 +33,12 @@ InvenTree | Stock
initSideNav();
{{ block.super }}
loadTree("{% url 'api-stock-tree' %}",
"#stock-tree");
"#stock-tree",
{
name: 'stock',
selected: 'elab',
}
);
$("#toggle-stock-tree").click(function() {
toggleSideNav("#sidenav");

View File

@ -52,19 +52,26 @@ InvenTree
<div class='main body wrapper'>
<div class='inventree-body'>
<div class='containter-fluid inventree-pre-content'>
{% block pre_content %}
{% endblock %}
</div>
<div class='sidenav' id='sidenav'>
{% block sidenav %}
{% endblock %}
</div>
<div class="container container-fluid inventree-content" id='inventree-content'>
{% block pre_content %}
{% endblock %}
{% block content %}
<!-- Each view fills in here.. -->
{% endblock %}
{% block post_content %}
{% endblock %}
</div>
{% include 'modals.html' %}
@ -72,6 +79,8 @@ InvenTree
{% include 'notification.html' %}
</div>
</div>
<!-- Scripts -->
<script type="text/javascript" src="{% static 'script/jquery_3.3.1_jquery.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/jquery.form.min.js' %}"></script>