diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 15926715f6..6118f00fd0 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -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 diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 56bf290739..11a5b2c28a 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -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) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 27a5b24894..9db2bf102f 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -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\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\d+)/', PartDetail.as_view(), name='api-part-detail'), diff --git a/InvenTree/part/migrations/0016_partstar.py b/InvenTree/part/migrations/0016_partstar.py new file mode 100644 index 0000000000..baa5c83d5b --- /dev/null +++ b/InvenTree/part/migrations/0016_partstar.py @@ -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)), + ], + ), + ] diff --git a/InvenTree/part/migrations/0017_auto_20190505_0848.py b/InvenTree/part/migrations/0017_auto_20190505_0848.py new file mode 100644 index 0000000000..90162132f9 --- /dev/null +++ b/InvenTree/part/migrations/0017_auto_20190505_0848.py @@ -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')}, + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d406382856..e5497faa4c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -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 diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index e5c23b730a..847b3bb1a8 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -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 """ diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 789a40b166..a99839d34a 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -23,7 +23,6 @@ {% else %}
  • Activate
  • {% endif %} -
  • Show QR Code
  • @@ -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( diff --git a/InvenTree/part/templates/part/part_app_base.html b/InvenTree/part/templates/part/part_app_base.html index 30c9b30b6d..8c034dfa6e 100644 --- a/InvenTree/part/templates/part/part_app_base.html +++ b/InvenTree/part/templates/part/part_app_base.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% load static %} + {% block sidenav %}
    {% endblock %} @@ -14,6 +16,11 @@ {% endblock %} +{% block js_load %} +{{ block.super }} + +{% endblock %} + {% block js_ready %} {{ block.super }} loadTree("{% url 'api-part-tree' %}", diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index de1ab2c6e0..5dc7add027 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -21,20 +21,34 @@ {% endif %}/>
    -

    {{ part.name }}{% if part.active == False %} - INACTIVE{% endif %}

    +

    + {{ part.name }} +

    {{ part.description }}

    - {% if part.IPN %} - - IPN - {{ part.IPN }} - - {% endif %} - {% if part.URL %} - - URL - {{ part.URL }} - - {% endif %} +

    +

    + {% include "qr_button.html" %} + +
    +

    + + {% if part.IPN %} + + + + + {% endif %} + {% if part.URL %} + + + + + {% endif %} + + +
    IPN{{ part.IPN }}
    URL{{ part.URL }}
    @@ -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 %}", diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6ba810b67d..c97808888e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -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 diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 98a4e04de4..3a9c1be184 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -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; diff --git a/InvenTree/static/script/inventree/api.js b/InvenTree/static/script/inventree/api.js index 530e816f68..27feec8b79 100644 --- a/InvenTree/static/script/inventree/api.js +++ b/InvenTree/static/script/inventree/api.js @@ -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); } diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index 053355113b..7682d5a6c1 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -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'); + }, + } + ); + } + }, + } + ); } \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index ca38ea29ef..daea4ddf24 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -6,6 +6,11 @@

    Stock Item Details

    {{ item.quantity }} × {{ item.part.name }}

    +

    +

    + {% include "qr_button.html" %} +
    +

    @@ -23,9 +28,6 @@
  • Stocktake
  • {% endif %}
  • Delete stock item
  • -
    -
  • Show QR code
  • -

    @@ -145,7 +147,7 @@ }); }); - $("#item-qr-code").click(function() { + $("#show-qr-code").click(function() { launchModalForm("{% url 'stock-item-qr' item.id %}", { no_post: true, diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 20769669a2..fbb46b6e18 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -7,6 +7,11 @@ {% if location %}

    {{ location.name }}

    {{ location.description }}

    +

    +

    + {% include "qr_button.html" %} +
    +

    {% else %}

    Stock

    All stock items

    @@ -23,8 +28,6 @@ {% 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, diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 91cac4c18c..12019ca7b0 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -3,6 +3,8 @@ {% block content %}

    InvenTree

    +{% 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 %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/parts_to_build.html b/InvenTree/templates/InvenTree/parts_to_build.html index e9c1dcdee1..df9199853b 100644 --- a/InvenTree/templates/InvenTree/parts_to_build.html +++ b/InvenTree/templates/InvenTree/parts_to_build.html @@ -1,5 +1,6 @@ {% extends "collapse.html" %} {% block collapse_title %} + Parts to Build{{ to_build | length }} {% endblock %} diff --git a/InvenTree/templates/InvenTree/parts_to_order.html b/InvenTree/templates/InvenTree/parts_to_order.html index b68eda7e14..c9b18606d0 100644 --- a/InvenTree/templates/InvenTree/parts_to_order.html +++ b/InvenTree/templates/InvenTree/parts_to_order.html @@ -1,5 +1,6 @@ {% extends "collapse.html" %} {% block collapse_title %} + Parts to Order{{ to_order | length }} {% endblock %} diff --git a/InvenTree/templates/InvenTree/starred_parts.html b/InvenTree/templates/InvenTree/starred_parts.html new file mode 100644 index 0000000000..eebc251666 --- /dev/null +++ b/InvenTree/templates/InvenTree/starred_parts.html @@ -0,0 +1,15 @@ +{% extends "collapse.html" %} +{% block collapse_title %} + +Starred Parts{{ starred | length }} +{% 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 %} \ No newline at end of file diff --git a/InvenTree/templates/qr_button.html b/InvenTree/templates/qr_button.html new file mode 100644 index 0000000000..7aafd834bc --- /dev/null +++ b/InvenTree/templates/qr_button.html @@ -0,0 +1 @@ + \ No newline at end of file