mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #230 from SchrodingersGat/qrcode-glyph
Part Starring
This commit is contained in:
commit
c710171cd8
@ -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'),
|
||||
|
24
InvenTree/part/migrations/0016_partstar.py
Normal file
24
InvenTree/part/migrations/0016_partstar.py
Normal file
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
19
InvenTree/part/migrations/0017_auto_20190505_0848.py
Normal file
19
InvenTree/part/migrations/0017_auto_20190505_0848.py
Normal file
@ -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 }} × {{ 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 %}
|
||||
|
||||
|
15
InvenTree/templates/InvenTree/starred_parts.html
Normal file
15
InvenTree/templates/InvenTree/starred_parts.html
Normal file
@ -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 %}
|
1
InvenTree/templates/qr_button.html
Normal file
1
InvenTree/templates/qr_button.html
Normal file
@ -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>
|
Loading…
Reference in New Issue
Block a user