2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-12 02:02:59 +00:00

Merge pull request from SchrodingersGat/qrcode-glyph

Part Starring
This commit is contained in:
Oliver 2019-05-05 11:47:16 +10:00 committed by GitHub
commit c710171cd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 331 additions and 40 deletions

@ -328,11 +328,15 @@ class IndexView(TemplateView):
def get_context_data(self, **kwargs):
context = super(TemplateView, self).get_context_data(**kwargs)
context['starred'] = [star.part for star in self.request.user.starred_parts.all()]
# Generate a list of orderable parts which have stock below their minimum values
# TODO - Is there a less expensive way to get these from the database
context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()]
# Generate a list of buildable parts which have stock below their minimum values
# TODO - Is there a less expensive way to get these from the database
context['to_build'] = [part for part in Part.objects.filter(buildable=True) if part.need_to_restock()]
return context

@ -2,7 +2,7 @@ from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import PartCategory, Part
from .models import PartAttachment
from .models import PartAttachment, PartStar
from .models import SupplierPart
from .models import BomItem
@ -22,6 +22,11 @@ class PartAttachmentAdmin(admin.ModelAdmin):
list_display = ('part', 'attachment', 'comment')
class PartStarAdmin(admin.ModelAdmin):
list_display = ('part', 'user')
class BomItemAdmin(ImportExportModelAdmin):
list_display = ('part', 'sub_part', 'quantity')
@ -42,5 +47,6 @@ class ParameterAdmin(admin.ModelAdmin):
admin.site.register(Part, PartAdmin)
admin.site.register(PartCategory, PartCategoryAdmin)
admin.site.register(PartAttachment, PartAttachmentAdmin)
admin.site.register(PartStar, PartStarAdmin)
admin.site.register(BomItem, BomItemAdmin)
admin.site.register(SupplierPart, SupplierPartAdmin)

@ -6,18 +6,22 @@ Provides a JSON API for the Part app
from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status
from rest_framework.response import Response
from rest_framework import filters
from rest_framework import generics, permissions
from django.db.models import Q
from django.conf.urls import url, include
from .models import Part, PartCategory, BomItem
from .models import Part, PartCategory, BomItem, PartStar
from .models import SupplierPart, SupplierPriceBreak
from .serializers import PartSerializer, BomItemSerializer
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
from .serializers import CategorySerializer
from .serializers import PartStarSerializer
from InvenTree.views import TreeSerializer
@ -150,8 +154,57 @@ class PartList(generics.ListCreateAPIView):
]
class PartStarDetail(generics.RetrieveDestroyAPIView):
""" API endpoint for viewing or removing a PartStar object """
queryset = PartStar.objects.all()
serializer_class = PartStarSerializer
class PartStarList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartStar objects.
- GET: Return list of PartStar objects
- POST: Create a new PartStar object
"""
queryset = PartStar.objects.all()
serializer_class = PartStarSerializer
def create(self, request, *args, **kwargs):
# Override the user field (with the logged-in user)
data = request.data.copy()
data['user'] = str(request.user.id)
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter
]
filter_fields = [
'part',
'user',
]
search_fields = [
'partname'
]
class BomList(generics.ListCreateAPIView):
""" API endpoing for accessing a list of BomItem objects
""" API endpoint for accessing a list of BomItem objects.
- GET: Return list of BomItem objects
- POST: Create a new BomItem object
@ -267,12 +320,21 @@ supplier_part_api_urls = [
url(r'^.*$', SupplierPartList.as_view(), name='api-part-supplier-list'),
]
part_star_api_urls = [
url(r'^(?P<pk>\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'),
# Catchall
url(r'^.*$', PartStarList.as_view(), name='api-part-star-list'),
]
part_api_urls = [
url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),
url(r'^category/', include(cat_api_urls)),
url(r'^supplier/', include(supplier_part_api_urls)),
url(r'^star/', include(part_star_api_urls)),
url(r'^price-break/?', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'),
url(r'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'),

@ -0,0 +1,24 @@
# Generated by Django 2.2 on 2019-05-04 22:45
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('part', '0015_partcategory_default_location'),
]
operations = [
migrations.CreateModel(
name='PartStar',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_users', to='part.Part')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='starred_parts', to=settings.AUTH_USER_MODEL)),
],
),
]

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-04 22:48
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('part', '0016_partstar'),
]
operations = [
migrations.AlterUniqueTogether(
name='partstar',
unique_together={('part', 'user')},
),
]

@ -18,6 +18,7 @@ from django.urls import reverse
from django.db import models
from django.core.validators import MinValueValidator
from django.contrib.auth.models import User
from django.db.models.signals import pre_delete
from django.dispatch import receiver
@ -246,6 +247,15 @@ class Part(models.Model):
return total
def isStarredBy(self, user):
""" Return True if this part has been starred by a particular user """
try:
PartStar.objects.get(part=self, user=user)
return True
except PartStar.DoesNotExist:
return False
def need_to_restock(self):
""" Return True if this part needs to be restocked
(either by purchasing or building).
@ -427,6 +437,21 @@ class PartAttachment(models.Model):
return os.path.basename(self.attachment.name)
class PartStar(models.Model):
""" A PartStar object creates a relationship between a User and a Part.
It is used to designate a Part as 'starred' (or favourited) for a given User,
so that the user can track a list of their favourite parts.
"""
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='starred_users')
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='starred_parts')
class Meta:
unique_together = ['part', 'user']
class BomItem(models.Model):
""" A BomItem links a part to its component items.
A part can have a BOM (bill of materials) which defines

@ -4,8 +4,10 @@ JSON serializers for Part app
from rest_framework import serializers
from .models import Part, PartCategory, BomItem
from .models import Part, PartStar
from .models import SupplierPart, SupplierPriceBreak
from .models import PartCategory
from .models import BomItem
from InvenTree.serializers import InvenTreeModelSerializer
@ -75,6 +77,23 @@ class PartSerializer(serializers.ModelSerializer):
]
class PartStarSerializer(InvenTreeModelSerializer):
""" Serializer for a PartStar object """
partname = serializers.CharField(source='part.name', read_only=True)
username = serializers.CharField(source='user.username', read_only=True)
class Meta:
model = PartStar
fields = [
'pk',
'part',
'partname',
'user',
'username',
]
class BomItemSerializer(InvenTreeModelSerializer):
""" Serializer for BomItem object """

@ -23,7 +23,6 @@
{% else %}
<li><a href="#" id='activate-part' title='Activate part'>Activate</a></li>
{% endif %}
<li><a href='#' id='show-qr-code' title='Generate QR Code'>Show QR Code</a></li>
</ul>
</div>
</h3>
@ -126,15 +125,6 @@
{% block js_ready %}
{{ block.super }}
$("#show-qr-code").click(function() {
launchModalForm(
"{% url 'part-qr' part.id %}",
{
no_post: true,
}
);
});
$("#duplicate-part").click(function() {
launchModalForm(

@ -1,5 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% block sidenav %}
<div id='part-tree'></div>
{% endblock %}
@ -14,6 +16,11 @@
{% endblock %}
{% block js_load %}
{{ block.super }}
<script type='text/javascript' src="{% static 'script/inventree/part.js' %}"></script>
{% endblock %}
{% block js_ready %}
{{ block.super }}
loadTree("{% url 'api-part-tree' %}",

@ -21,20 +21,34 @@
{% endif %}/>
</div>
<div class="media-body">
<h4>{{ part.name }}{% if part.active == False %} <i>- INACTIVE</i>{% endif %}</h4>
<h4>
{{ part.name }}
</h4>
<p><i>{{ part.description }}</i></p>
{% if part.IPN %}
<tr>
<td>IPN</td>
<td>{{ part.IPN }}</td>
</tr>
{% endif %}
{% if part.URL %}
<tr>
<td>URL</td>
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr>
{% endif %}
<p>
<div class='btn-group'>
{% include "qr_button.html" %}
<button type='button' class='btn btn-default btn-glyph' id='toggle-starred' title='Star this part'>
<span id='part-star-icon' class='starred-part glyphicon {% if starred %}glyphicon-star{% else %}glyphicon-star-empty{% endif %}'/>
</button>
</div>
</p>
<table class='table table-condensed'>
{% if part.IPN %}
<tr>
<td>IPN</td>
<td>{{ part.IPN }}</td>
</tr>
{% endif %}
{% if part.URL %}
<tr>
<td>URL</td>
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr>
{% endif %}
<tr>
</tr>
</table>
</div>
</div>
</div>
@ -82,6 +96,26 @@
{% block js_ready %}
{{ block.super }}
$("#show-qr-code").click(function() {
launchModalForm(
"{% url 'part-qr' part.id %}",
{
no_post: true,
}
);
});
$("#toggle-starred").click(function() {
toggleStar({
part: {{ part.id }},
user: {{ user.id }},
button: '#part-star-icon'
});
});
$('#toggle-starred').click(function() {
});
$("#part-thumb").click(function() {
launchModalForm(
"{% url 'part-image' part.id %}",

@ -232,6 +232,10 @@ class PartDetail(DetailView):
else:
context['editing_enabled'] = 0
part = self.get_object()
context['starred'] = part.isStarredBy(self.request.user)
return context

@ -2,6 +2,21 @@
float: left;
}
.glyphicon {
font-size: 20px;
}
.starred-part {
color: #ffcc00;
}
.btn-glyph {
padding-left: 6px;
padding-right: 6px;
padding-top: 3px;
padding-bottom: 2px;
}
.badge {
float: right;
background-color: #777;

@ -43,9 +43,6 @@ function inventreeGet(url, filters={}, options={}) {
}
function inventreeUpdate(url, data={}, options={}) {
if ('final' in options && options.final) {
data["_is_final"] = true;
}
var method = options.method || 'PUT';
@ -63,8 +60,7 @@ function inventreeUpdate(url, data={}, options={}) {
dataType: 'json',
contentType: 'application/json',
success: function(response, status) {
response['_status_code'] = status;
console.log('UPDATE object to ' + url + ' - result = ' + status);
console.log(method + ' - ' + url + ' : result = ' + status);
if (options.success) {
options.success(response, status);
}

@ -16,4 +16,60 @@ function getPartList(filters={}, options={}) {
function getBomList(filters={}, options={}) {
return inventreeGet('/api/bom/', filters, options);
}
function toggleStar(options) {
/* Toggle the 'starred' status of a part.
* Performs AJAX queries and updates the display on the button.
*
* options:
* - button: ID of the button (default = '#part-star-icon')
* - part: pk of the part object
* - user: pk of the user
*/
var url = '/api/part/star/';
inventreeGet(
url,
{
part: options.part,
user: options.user,
},
{
success: function(response) {
if (response.length == 0) {
// Zero length response = star does not exist
// So let's add one!
inventreeUpdate(
url,
{
part: options.part,
user: options.user,
},
{
method: 'POST',
success: function(response, status) {
$(options.button).removeClass('glyphicon-star-empty').addClass('glyphicon-star');
},
}
);
} else {
var pk = response[0].pk;
// There IS a star (delete it!)
inventreeUpdate(
url + pk + "/",
{
},
{
method: 'DELETE',
success: function(response, status) {
$(options.button).removeClass('glyphicon-star').addClass('glyphicon-star-empty');
},
}
);
}
},
}
);
}

@ -6,6 +6,11 @@
<div class='col-sm-6'>
<h3>Stock Item Details</h3>
<p><i>{{ item.quantity }} &times {{ item.part.name }}</i></p>
<p>
<div class='btn-group'>
{% include "qr_button.html" %}
</div>
</p>
</div>
<div class='col-sm-6'>
<h3>
@ -23,9 +28,6 @@
<li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li>
{% endif %}
<li><a href="#" id='stock-delete' title='Delete stock item'>Delete stock item</a></li>
<hr>
<li><a href="#" id='item-qr-code' title='Generate QR code'>Show QR code</a></li>
</ul>
</div>
</div>
</h3>
@ -145,7 +147,7 @@
});
});
$("#item-qr-code").click(function() {
$("#show-qr-code").click(function() {
launchModalForm("{% url 'stock-item-qr' item.id %}",
{
no_post: true,

@ -7,6 +7,11 @@
{% if location %}
<h3>{{ location.name }}</h3>
<p>{{ location.description }}</p>
<p>
<div class='btn-group'>
{% include "qr_button.html" %}
</div>
</p>
{% else %}
<h3>Stock</h3>
<p>All stock items</p>
@ -23,8 +28,6 @@
<ul class="dropdown-menu">
<li><a href="#" id='location-edit' title='Edit stock location'>Edit</a></li>
<li><a href="#" id='location-delete' title='Delete stock location'>Delete</a></li>
<hr>
<li><a href="#" id='location-qr-code' title='Generate QR code'>Show QR code</a></li>
</ul>
</div>
{% endif %}
@ -101,7 +104,7 @@
return false;
});
$('#location-qr-code').click(function() {
$('#show-qr-code').click(function() {
launchModalForm("{% url 'stock-location-qr' location.id %}",
{
no_post: true,

@ -3,6 +3,8 @@
{% block content %}
<h3>InvenTree</h3>
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %}
{% if to_order %}
{% include "InvenTree/parts_to_order.html" with collapse_id="order" %}
{% endif %}
@ -19,4 +21,9 @@
{% block js_ready %}
{{ block.super }}
$("#to-build-table").bootstrapTable();
$("#to-order-table").bootstrapTable();
$("#starred-parts-table").bootstrapTable();
{% endblock %}

@ -1,5 +1,6 @@
{% extends "collapse.html" %}
{% block collapse_title %}
<span class='glyphicon glyphicon-wrench'></span>
Parts to Build<span class='badge'>{{ to_build | length }}</span>
{% endblock %}

@ -1,5 +1,6 @@
{% extends "collapse.html" %}
{% block collapse_title %}
<span class='glyphicon glyphicon-shopping-cart'></span>
Parts to Order<span class='badge'>{{ to_order | length }}</span>
{% endblock %}

@ -0,0 +1,15 @@
{% extends "collapse.html" %}
{% block collapse_title %}
<span class='glyphicon glyphicon-star'></span>
Starred Parts<span class='badge'>{{ starred | length }}</span>
{% endblock %}
{% block collapse_heading %}
You have {{ starred | length }} favourite parts
{% endblock %}
{% block collapse_content %}
{% include "required_part_table.html" with parts=starred table_id="starred-parts-table" %}
{% endblock %}

@ -0,0 +1 @@
<button type='button' class='btn btn-default btn-glyph' id='show-qr-code' title='Show QR code'><span class='glyphicon glyphicon-qrcode'></span></button>