From 1327c1d3b1714ca60641b78d29a8be8dee6200a4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Feb 2020 22:03:06 +1100 Subject: [PATCH 01/11] Add API endpoint for querying part images --- InvenTree/part/api.py | 57 +++++++++++++++++++++++++---------- InvenTree/part/serializers.py | 10 ++++++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 75c8f3a60f..424f03fd0b 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from django.conf import settings -from django.db.models import Sum +from django.db.models import Sum, Count from rest_framework import status from rest_framework.response import Response @@ -23,10 +23,7 @@ import os from .models import Part, PartCategory, BomItem, PartStar from .models import PartParameter, PartParameterTemplate -from .serializers import PartSerializer, BomItemSerializer -from .serializers import CategorySerializer -from .serializers import PartStarSerializer -from .serializers import PartParameterSerializer, PartParameterTemplateSerializer +from . import serializers as part_serializers from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool @@ -53,7 +50,7 @@ class CategoryList(generics.ListCreateAPIView): """ queryset = PartCategory.objects.all() - serializer_class = CategorySerializer + serializer_class = part_serializers.CategorySerializer permission_classes = [ permissions.IsAuthenticated, @@ -83,14 +80,40 @@ class CategoryList(generics.ListCreateAPIView): class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): """ API endpoint for detail view of a single PartCategory object """ - serializer_class = CategorySerializer + serializer_class = part_serializers.CategorySerializer queryset = PartCategory.objects.all() +class PartThumbs(generics.ListAPIView): + """ API endpoint for retrieving information on available Part thumbnails """ + + serializer_class = part_serializers.PartThumbSerializer + + def list(self, reguest, *args, **kwargs): + """ + Serialize the available Part images. + - Images may be used for multiple parts! + """ + + # Get all Parts which have an associated image + queryset = Part.objects.all().exclude(image='') + + data = queryset.values( + 'image', + ).annotate(count=Count('image')).order_by('-count') + + print("Parts with img:", queryset.count()) + + print(data) + + return Response(data) + + class PartDetail(generics.RetrieveUpdateAPIView): """ API endpoint for detail view of a single Part object """ + queryset = Part.objects.all() - serializer_class = PartSerializer + serializer_class = part_serializers.PartSerializer permission_classes = [ permissions.IsAuthenticated, @@ -104,12 +127,12 @@ class PartList(generics.ListCreateAPIView): - POST: Create a new Part object """ - serializer_class = PartSerializer + serializer_class = part_serializers.PartSerializer def list(self, request, *args, **kwargs): """ Instead of using the DRF serialiser to LIST, - we serialize the objects manuually. + we serialize the objects manually. This turns out to be significantly faster. """ @@ -218,7 +241,7 @@ class PartStarDetail(generics.RetrieveDestroyAPIView): """ API endpoint for viewing or removing a PartStar object """ queryset = PartStar.objects.all() - serializer_class = PartStarSerializer + serializer_class = part_serializers.PartStarSerializer class PartStarList(generics.ListCreateAPIView): @@ -229,7 +252,7 @@ class PartStarList(generics.ListCreateAPIView): """ queryset = PartStar.objects.all() - serializer_class = PartStarSerializer + serializer_class = part_serializers.PartStarSerializer def create(self, request, *args, **kwargs): @@ -271,7 +294,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView): """ queryset = PartParameterTemplate.objects.all() - serializer_class = PartParameterTemplateSerializer + serializer_class = part_serializers.PartParameterTemplateSerializer permission_classes = [ permissions.IsAuthenticated, @@ -294,7 +317,7 @@ class PartParameterList(generics.ListCreateAPIView): """ queryset = PartParameter.objects.all() - serializer_class = PartParameterSerializer + serializer_class = part_serializers.PartParameterSerializer permission_classes = [ permissions.IsAuthenticated, @@ -317,7 +340,7 @@ class BomList(generics.ListCreateAPIView): - POST: Create a new BomItem object """ - serializer_class = BomItemSerializer + serializer_class = part_serializers.BomItemSerializer def get_serializer(self, *args, **kwargs): @@ -360,7 +383,7 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView): """ API endpoint for detail view of a single BomItem object """ queryset = BomItem.objects.all() - serializer_class = BomItemSerializer + serializer_class = part_serializers.BomItemSerializer permission_classes = [ permissions.IsAuthenticated, @@ -424,6 +447,8 @@ part_api_urls = [ url(r'^star/', include(part_star_api_urls)), url(r'^parameter/', include(part_param_api_urls)), + url(r'^thumbs/', PartThumbs.as_view(), name='api-part-thumbs'), + url(r'^(?P\d+)/?', PartDetail.as_view(), name='api-part-detail'), url(r'^.*$', PartList.as_view(), name='api-part-list'), diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index bdeead670d..3e1ed6949a 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -30,6 +30,16 @@ class CategorySerializer(InvenTreeModelSerializer): ] +class PartThumbSerializer(serializers.Serializer): + """ + Serializer for the 'image' field of the Part model. + Used to serve and display existing Part images. + """ + + image = serializers.URLField(read_only=True) + count = serializers.IntegerField(read_only=True) + + class PartBriefSerializer(InvenTreeModelSerializer): """ Serializer for Part (brief detail) """ From a82e21933639deb6010c114b784c5895f7721981 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Feb 2020 22:10:06 +1100 Subject: [PATCH 02/11] Add translatable strings for part views --- InvenTree/locale/de/LC_MESSAGES/django.po | 198 +++++++++++++++++++--- InvenTree/locale/en/LC_MESSAGES/django.po | 153 ++++++++++++++--- InvenTree/locale/es/LC_MESSAGES/django.po | 153 ++++++++++++++--- InvenTree/part/views.py | 63 ++++--- 4 files changed, 482 insertions(+), 85 deletions(-) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 151190ca0e..fc53b0aed0 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-02-03 10:28+0000\n" +"POT-Creation-Date: 2020-02-10 11:09+0000\n" "PO-Revision-Date: 2020-02-02 08:07+0100\n" "Last-Translator: Christian Schlüter \n" "Language-Team: C \n" @@ -706,7 +706,7 @@ msgstr "Link auf externe Seite" msgid "Order notes" msgstr "Bestell-Notizen" -#: order/models.py:159 order/models.py:210 part/views.py:1067 +#: order/models.py:159 order/models.py:210 part/views.py:1080 #: stock/models.py:440 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" @@ -1277,56 +1277,208 @@ msgstr "Tracking" msgid "Attachments" msgstr "Anhänge" +#: part/views.py:77 +#, fuzzy +#| msgid "Add Attachment" +msgid "Added attachment" +msgstr "Anhang hinzufügen" + +#: part/views.py:119 +#, fuzzy +#| msgid "Part Attachments" +msgid "Part attachment updated" +msgstr "Anhänge" + #: part/views.py:196 #, python-brace-format msgid "Set category for {n} parts" msgstr "Kategorie für {n} Teile setzen" -#: part/views.py:808 +#: part/views.py:306 +#, fuzzy +#| msgid "Supplier part" +msgid "Copied part" +msgstr "Zulieferer-Teil" + +#: part/views.py:414 +#, fuzzy +#| msgid "Create new Stock Item" +msgid "Create new part" +msgstr "Neues Lagerobjekt hinzufügen" + +#: part/views.py:419 +#, fuzzy +#| msgid "Created new stock item" +msgid "Created new part" +msgstr "Neues Lagerobjekt erstellt" + +#: part/views.py:609 +msgid "Upload Part Image" +msgstr "" + +#: part/views.py:614 +msgid "Updated part image" +msgstr "" + +#: part/views.py:623 +#, fuzzy +#| msgid "Select part" +msgid "Select Part Image" +msgstr "Teil auswählen" + +#: part/views.py:627 +#, fuzzy +#| msgid "Select part" +msgid "Selected part image" +msgstr "Teil auswählen" + +#: part/views.py:637 +#, fuzzy +#| msgid "Edit notes" +msgid "Edit Part Properties" +msgstr "Bermerkungen bearbeiten" + +#: part/views.py:659 +msgid "Validate BOM" +msgstr "" + +#: part/views.py:821 msgid "No BOM file provided" msgstr "Keine Stückliste angegeben" -#: part/views.py:1069 +#: part/views.py:1082 msgid "Enter a valid quantity" msgstr "Bitte eine gültige Anzahl eingeben" -#: part/views.py:1093 part/views.py:1096 +#: part/views.py:1106 part/views.py:1109 msgid "Select valid part" msgstr "Bitte ein gültiges Teil auswählen" -#: part/views.py:1102 +#: part/views.py:1115 msgid "Duplicate part selected" msgstr "Teil doppelt ausgewählt" -#: part/views.py:1130 +#: part/views.py:1143 msgid "Select a part" msgstr "Teil auswählen" -#: part/views.py:1134 +#: part/views.py:1147 msgid "Specify quantity" msgstr "Anzahl angeben" -#: stock/forms.py:92 +#: part/views.py:1324 +#, fuzzy +#| msgid "Confirm part creation" +msgid "Confirm Part Deletion" +msgstr "Erstellen des Teils bestätigen" + +#: part/views.py:1331 +msgid "Part was deleted" +msgstr "" + +#: part/views.py:1340 +#, fuzzy +#| msgid "Part packaging" +msgid "Part Pricing" +msgstr "Teile-Packaging" + +#: part/views.py:1462 +#, fuzzy +#| msgid "Parameter Template" +msgid "Create Part Parameter Template" +msgstr "Parameter Vorlage" + +#: part/views.py:1470 +#, fuzzy +#| msgid "Parameter Template" +msgid "Edit Part Parameter Template" +msgstr "Parameter Vorlage" + +#: part/views.py:1477 +#, fuzzy +#| msgid "Parameter Template" +msgid "Delete Part Parameter Template" +msgstr "Parameter Vorlage" + +#: part/views.py:1485 +msgid "Create Part Parameter" +msgstr "" + +#: part/views.py:1535 +#, fuzzy +#| msgid "Edit attachment" +msgid "Edit Part Parameter" +msgstr "Anhang bearbeiten" + +#: part/views.py:1549 +#, fuzzy +#| msgid "Delete attachment" +msgid "Delete Part Parameter" +msgstr "Anhang löschen" + +#: part/views.py:1565 +#, fuzzy +#| msgid "Part category" +msgid "Edit Part Category" +msgstr "Teile-Kategorie" + +#: part/views.py:1600 +#, fuzzy +#| msgid "Select part category" +msgid "Delete Part Category" +msgstr "Teilekategorie wählen" + +#: part/views.py:1606 +#, fuzzy +#| msgid "Part category" +msgid "Part category was deleted" +msgstr "Teile-Kategorie" + +#: part/views.py:1614 +#, fuzzy +#| msgid "Select part category" +msgid "Create new part category" +msgstr "Teilekategorie wählen" + +#: part/views.py:1665 +#, fuzzy +#| msgid "Created new stock item" +msgid "Create BOM item" +msgstr "Neues Lagerobjekt erstellt" + +#: part/views.py:1731 +#, fuzzy +#| msgid "Edit Stock Item" +msgid "Edit BOM item" +msgstr "Lagerobjekt bearbeiten" + +#: part/views.py:1779 +#, fuzzy +#| msgid "Confirm build completion" +msgid "Confim BOM item deletion" +msgstr "Bau-Fertigstellung bestätigen" + +#: stock/forms.py:91 msgid "File Format" msgstr "Dateiformat" -#: stock/forms.py:92 +#: stock/forms.py:91 msgid "Select output file format" msgstr "Ausgabe-Dateiformat auswählen" -#: stock/forms.py:94 +#: stock/forms.py:93 msgid "Include stock items in sub locations" msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen" -#: stock/forms.py:127 +#: stock/forms.py:126 msgid "Destination stock location" msgstr "Ziel-Lagerbestand" -#: stock/forms.py:133 +#: stock/forms.py:132 msgid "Confirm movement of stock items" msgstr "Bewegung der Lagerobjekte bestätigen" -#: stock/forms.py:135 +#: stock/forms.py:134 msgid "Set the destination as the default location for selected parts" msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile" @@ -1652,27 +1804,33 @@ msgstr "Ungültige Menge" msgid "Invalid part selection" msgstr "Ungültige Teileauswahl" -#: stock/views.py:925 +#: stock/views.py:910 +#, fuzzy, python-brace-format +#| msgid "Created new stock item" +msgid "Created {n} new stock items" +msgstr "Neues Lagerobjekt erstellt" + +#: stock/views.py:927 stock/views.py:940 msgid "Created new stock item" msgstr "Neues Lagerobjekt erstellt" -#: stock/views.py:942 +#: stock/views.py:957 msgid "Delete Stock Location" msgstr "Standort löschen" -#: stock/views.py:955 +#: stock/views.py:970 msgid "Delete Stock Item" msgstr "Lagerobjekt löschen" -#: stock/views.py:966 +#: stock/views.py:981 msgid "Delete Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag löschen" -#: stock/views.py:983 +#: stock/views.py:998 msgid "Edit Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag bearbeiten" -#: stock/views.py:992 +#: stock/views.py:1007 msgid "Add Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag hinzufügen" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index 3b7c7af271..930ff589f8 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-02-03 10:28+0000\n" +"POT-Creation-Date: 2020-02-10 11:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -675,7 +675,7 @@ msgstr "" msgid "Order notes" msgstr "" -#: order/models.py:159 order/models.py:210 part/views.py:1067 +#: order/models.py:159 order/models.py:210 part/views.py:1080 #: stock/models.py:440 msgid "Quantity must be greater than zero" msgstr "" @@ -1242,56 +1242,164 @@ msgstr "" msgid "Attachments" msgstr "" +#: part/views.py:77 +msgid "Added attachment" +msgstr "" + +#: part/views.py:119 +msgid "Part attachment updated" +msgstr "" + #: part/views.py:196 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:808 +#: part/views.py:306 +msgid "Copied part" +msgstr "" + +#: part/views.py:414 +msgid "Create new part" +msgstr "" + +#: part/views.py:419 +msgid "Created new part" +msgstr "" + +#: part/views.py:609 +msgid "Upload Part Image" +msgstr "" + +#: part/views.py:614 +msgid "Updated part image" +msgstr "" + +#: part/views.py:623 +msgid "Select Part Image" +msgstr "" + +#: part/views.py:627 +msgid "Selected part image" +msgstr "" + +#: part/views.py:637 +msgid "Edit Part Properties" +msgstr "" + +#: part/views.py:659 +msgid "Validate BOM" +msgstr "" + +#: part/views.py:821 msgid "No BOM file provided" msgstr "" -#: part/views.py:1069 +#: part/views.py:1082 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1093 part/views.py:1096 +#: part/views.py:1106 part/views.py:1109 msgid "Select valid part" msgstr "" -#: part/views.py:1102 +#: part/views.py:1115 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1130 +#: part/views.py:1143 msgid "Select a part" msgstr "" -#: part/views.py:1134 +#: part/views.py:1147 msgid "Specify quantity" msgstr "" -#: stock/forms.py:92 +#: part/views.py:1324 +msgid "Confirm Part Deletion" +msgstr "" + +#: part/views.py:1331 +msgid "Part was deleted" +msgstr "" + +#: part/views.py:1340 +msgid "Part Pricing" +msgstr "" + +#: part/views.py:1462 +msgid "Create Part Parameter Template" +msgstr "" + +#: part/views.py:1470 +msgid "Edit Part Parameter Template" +msgstr "" + +#: part/views.py:1477 +msgid "Delete Part Parameter Template" +msgstr "" + +#: part/views.py:1485 +msgid "Create Part Parameter" +msgstr "" + +#: part/views.py:1535 +msgid "Edit Part Parameter" +msgstr "" + +#: part/views.py:1549 +msgid "Delete Part Parameter" +msgstr "" + +#: part/views.py:1565 +msgid "Edit Part Category" +msgstr "" + +#: part/views.py:1600 +msgid "Delete Part Category" +msgstr "" + +#: part/views.py:1606 +msgid "Part category was deleted" +msgstr "" + +#: part/views.py:1614 +msgid "Create new part category" +msgstr "" + +#: part/views.py:1665 +msgid "Create BOM item" +msgstr "" + +#: part/views.py:1731 +msgid "Edit BOM item" +msgstr "" + +#: part/views.py:1779 +msgid "Confim BOM item deletion" +msgstr "" + +#: stock/forms.py:91 msgid "File Format" msgstr "" -#: stock/forms.py:92 +#: stock/forms.py:91 msgid "Select output file format" msgstr "" -#: stock/forms.py:94 +#: stock/forms.py:93 msgid "Include stock items in sub locations" msgstr "" -#: stock/forms.py:127 +#: stock/forms.py:126 msgid "Destination stock location" msgstr "" -#: stock/forms.py:133 +#: stock/forms.py:132 msgid "Confirm movement of stock items" msgstr "" -#: stock/forms.py:135 +#: stock/forms.py:134 msgid "Set the destination as the default location for selected parts" msgstr "" @@ -1610,27 +1718,32 @@ msgstr "" msgid "Invalid part selection" msgstr "" -#: stock/views.py:925 +#: stock/views.py:910 +#, python-brace-format +msgid "Created {n} new stock items" +msgstr "" + +#: stock/views.py:927 stock/views.py:940 msgid "Created new stock item" msgstr "" -#: stock/views.py:942 +#: stock/views.py:957 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:955 +#: stock/views.py:970 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:966 +#: stock/views.py:981 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:983 +#: stock/views.py:998 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:992 +#: stock/views.py:1007 msgid "Add Stock Tracking Entry" msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index 3b7c7af271..930ff589f8 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-02-03 10:28+0000\n" +"POT-Creation-Date: 2020-02-10 11:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -675,7 +675,7 @@ msgstr "" msgid "Order notes" msgstr "" -#: order/models.py:159 order/models.py:210 part/views.py:1067 +#: order/models.py:159 order/models.py:210 part/views.py:1080 #: stock/models.py:440 msgid "Quantity must be greater than zero" msgstr "" @@ -1242,56 +1242,164 @@ msgstr "" msgid "Attachments" msgstr "" +#: part/views.py:77 +msgid "Added attachment" +msgstr "" + +#: part/views.py:119 +msgid "Part attachment updated" +msgstr "" + #: part/views.py:196 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:808 +#: part/views.py:306 +msgid "Copied part" +msgstr "" + +#: part/views.py:414 +msgid "Create new part" +msgstr "" + +#: part/views.py:419 +msgid "Created new part" +msgstr "" + +#: part/views.py:609 +msgid "Upload Part Image" +msgstr "" + +#: part/views.py:614 +msgid "Updated part image" +msgstr "" + +#: part/views.py:623 +msgid "Select Part Image" +msgstr "" + +#: part/views.py:627 +msgid "Selected part image" +msgstr "" + +#: part/views.py:637 +msgid "Edit Part Properties" +msgstr "" + +#: part/views.py:659 +msgid "Validate BOM" +msgstr "" + +#: part/views.py:821 msgid "No BOM file provided" msgstr "" -#: part/views.py:1069 +#: part/views.py:1082 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1093 part/views.py:1096 +#: part/views.py:1106 part/views.py:1109 msgid "Select valid part" msgstr "" -#: part/views.py:1102 +#: part/views.py:1115 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1130 +#: part/views.py:1143 msgid "Select a part" msgstr "" -#: part/views.py:1134 +#: part/views.py:1147 msgid "Specify quantity" msgstr "" -#: stock/forms.py:92 +#: part/views.py:1324 +msgid "Confirm Part Deletion" +msgstr "" + +#: part/views.py:1331 +msgid "Part was deleted" +msgstr "" + +#: part/views.py:1340 +msgid "Part Pricing" +msgstr "" + +#: part/views.py:1462 +msgid "Create Part Parameter Template" +msgstr "" + +#: part/views.py:1470 +msgid "Edit Part Parameter Template" +msgstr "" + +#: part/views.py:1477 +msgid "Delete Part Parameter Template" +msgstr "" + +#: part/views.py:1485 +msgid "Create Part Parameter" +msgstr "" + +#: part/views.py:1535 +msgid "Edit Part Parameter" +msgstr "" + +#: part/views.py:1549 +msgid "Delete Part Parameter" +msgstr "" + +#: part/views.py:1565 +msgid "Edit Part Category" +msgstr "" + +#: part/views.py:1600 +msgid "Delete Part Category" +msgstr "" + +#: part/views.py:1606 +msgid "Part category was deleted" +msgstr "" + +#: part/views.py:1614 +msgid "Create new part category" +msgstr "" + +#: part/views.py:1665 +msgid "Create BOM item" +msgstr "" + +#: part/views.py:1731 +msgid "Edit BOM item" +msgstr "" + +#: part/views.py:1779 +msgid "Confim BOM item deletion" +msgstr "" + +#: stock/forms.py:91 msgid "File Format" msgstr "" -#: stock/forms.py:92 +#: stock/forms.py:91 msgid "Select output file format" msgstr "" -#: stock/forms.py:94 +#: stock/forms.py:93 msgid "Include stock items in sub locations" msgstr "" -#: stock/forms.py:127 +#: stock/forms.py:126 msgid "Destination stock location" msgstr "" -#: stock/forms.py:133 +#: stock/forms.py:132 msgid "Confirm movement of stock items" msgstr "" -#: stock/forms.py:135 +#: stock/forms.py:134 msgid "Set the destination as the default location for selected parts" msgstr "" @@ -1610,27 +1718,32 @@ msgstr "" msgid "Invalid part selection" msgstr "" -#: stock/views.py:925 +#: stock/views.py:910 +#, python-brace-format +msgid "Created {n} new stock items" +msgstr "" + +#: stock/views.py:927 stock/views.py:940 msgid "Created new stock item" msgstr "" -#: stock/views.py:942 +#: stock/views.py:957 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:955 +#: stock/views.py:970 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:966 +#: stock/views.py:981 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:983 +#: stock/views.py:998 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:992 +#: stock/views.py:1007 msgid "Add Stock Tracking Entry" msgstr "" diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index f36cedf48e..722588b51a 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -74,7 +74,7 @@ class PartAttachmentCreate(AjaxCreateView): def get_data(self): return { - 'success': 'Added attachment' + 'success': _('Added attachment') } def get_initial(self): @@ -116,7 +116,7 @@ class PartAttachmentEdit(AjaxUpdateView): def get_data(self): return { - 'success': 'Part attachment updated' + 'success': _('Part attachment updated') } def get_form(self): @@ -303,7 +303,7 @@ class PartDuplicate(AjaxCreateView): def get_data(self): return { - 'success': 'Copied part' + 'success': _('Copied part') } def get_part_to_copy(self): @@ -411,12 +411,12 @@ class PartCreate(AjaxCreateView): model = Part form_class = part_forms.EditPartForm - ajax_form_title = 'Create new part' + ajax_form_title = _('Create new part') ajax_template_name = 'part/create_part.html' def get_data(self): return { - 'success': "Created new part", + 'success': _("Created new part"), } def get_category_id(self): @@ -606,12 +606,25 @@ class PartImage(AjaxUpdateView): model = Part ajax_template_name = 'modal_form.html' - ajax_form_title = 'Upload Part Image' + ajax_form_title = _('Upload Part Image') form_class = part_forms.PartImageForm def get_data(self): return { - 'success': 'Updated part image', + 'success': _('Updated part image'), + } + + +class PartImageSelect(AjaxUpdateView): + """ View for selecting Part image from existing images. """ + + model = Part + ajax_template_name = 'part/select_image.html' + ajax_form_title = _('Select Part Image') + + def get_data(self): + return { + 'success': _('Selected part image') } @@ -621,7 +634,7 @@ class PartEdit(AjaxUpdateView): model = Part form_class = part_forms.EditPartForm ajax_template_name = 'modal_form.html' - ajax_form_title = 'Edit Part Properties' + ajax_form_title = _('Edit Part Properties') context_object_name = 'part' def get_form(self): @@ -643,7 +656,7 @@ class BomValidate(AjaxUpdateView): """ Modal form view for validating a part BOM """ model = Part - ajax_form_title = "Validate BOM" + ajax_form_title = _("Validate BOM") ajax_template_name = 'part/bom_validate.html' context_object_name = 'part' form_class = part_forms.BomValidateForm @@ -1308,14 +1321,14 @@ class PartDelete(AjaxDeleteView): model = Part ajax_template_name = 'part/partial_delete.html' - ajax_form_title = 'Confirm Part Deletion' + ajax_form_title = _('Confirm Part Deletion') context_object_name = 'part' success_url = '/part/' def get_data(self): return { - 'danger': 'Part was deleted', + 'danger': _('Part was deleted'), } @@ -1324,7 +1337,7 @@ class PartPricing(AjaxView): model = Part ajax_template_name = "part/part_pricing.html" - ajax_form_title = "Part Pricing" + ajax_form_title = _("Part Pricing") form_class = part_forms.PartPriceForm def get_part(self): @@ -1446,7 +1459,7 @@ class PartParameterTemplateCreate(AjaxCreateView): model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm - ajax_form_title = 'Create Part Parameter Template' + ajax_form_title = _('Create Part Parameter Template') class PartParameterTemplateEdit(AjaxUpdateView): @@ -1454,14 +1467,14 @@ class PartParameterTemplateEdit(AjaxUpdateView): model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm - ajax_form_title = 'Edit Part Parameter Template' + ajax_form_title = _('Edit Part Parameter Template') class PartParameterTemplateDelete(AjaxDeleteView): """ View for deleting an existing PartParameterTemplate """ model = PartParameterTemplate - ajax_form_title = "Delete Part Parameter Template" + ajax_form_title = _("Delete Part Parameter Template") class PartParameterCreate(AjaxCreateView): @@ -1469,7 +1482,7 @@ class PartParameterCreate(AjaxCreateView): model = PartParameter form_class = part_forms.EditPartParameterForm - ajax_form_title = 'Create Part Parameter' + ajax_form_title = _('Create Part Parameter') def get_initial(self): @@ -1519,7 +1532,7 @@ class PartParameterEdit(AjaxUpdateView): model = PartParameter form_class = part_forms.EditPartParameterForm - ajax_form_title = 'Edit Part Parameter' + ajax_form_title = _('Edit Part Parameter') def get_form(self): @@ -1533,7 +1546,7 @@ class PartParameterDelete(AjaxDeleteView): model = PartParameter ajax_template_name = 'part/param_delete.html' - ajax_form_title = 'Delete Part Parameter' + ajax_form_title = _('Delete Part Parameter') class CategoryDetail(DetailView): @@ -1549,7 +1562,7 @@ class CategoryEdit(AjaxUpdateView): model = PartCategory form_class = part_forms.EditCategoryForm ajax_template_name = 'modal_form.html' - ajax_form_title = 'Edit Part Category' + ajax_form_title = _('Edit Part Category') def get_context_data(self, **kwargs): context = super(CategoryEdit, self).get_context_data(**kwargs).copy() @@ -1584,13 +1597,13 @@ class CategoryDelete(AjaxDeleteView): """ Delete view to delete a PartCategory """ model = PartCategory ajax_template_name = 'part/category_delete.html' - ajax_form_title = 'Delete Part Category' + ajax_form_title = _('Delete Part Category') context_object_name = 'category' success_url = '/part/' def get_data(self): return { - 'danger': 'Part category was deleted', + 'danger': _('Part category was deleted'), } @@ -1598,7 +1611,7 @@ class CategoryCreate(AjaxCreateView): """ Create view to make a new PartCategory """ model = PartCategory ajax_form_action = reverse_lazy('category-create') - ajax_form_title = 'Create new part category' + ajax_form_title = _('Create new part category') ajax_template_name = 'modal_form.html' form_class = part_forms.EditCategoryForm @@ -1649,7 +1662,7 @@ class BomItemCreate(AjaxCreateView): model = BomItem form_class = part_forms.EditBomItemForm ajax_template_name = 'modal_form.html' - ajax_form_title = 'Create BOM item' + ajax_form_title = _('Create BOM item') def get_form(self): """ Override get_form() method to reduce Part selection options. @@ -1715,7 +1728,7 @@ class BomItemEdit(AjaxUpdateView): model = BomItem form_class = part_forms.EditBomItemForm ajax_template_name = 'modal_form.html' - ajax_form_title = 'Edit BOM item' + ajax_form_title = _('Edit BOM item') def get_form(self): """ Override get_form() method to filter part selection options @@ -1763,4 +1776,4 @@ class BomItemDelete(AjaxDeleteView): model = BomItem ajax_template_name = 'part/bom-delete.html' context_object_name = 'item' - ajax_form_title = 'Confim BOM item deletion' + ajax_form_title = _('Confim BOM item deletion') From 17c10da10eba6db82a92204e75ebe04adc962424 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Feb 2020 22:57:36 +1100 Subject: [PATCH 03/11] Display existing images in a form --- InvenTree/InvenTree/static/css/inventree.css | 18 ++++++++++ InvenTree/part/templates/part/part_base.html | 34 +++++++++++++++++-- .../part/templates/part/select_image.html | 20 +++++++++++ InvenTree/part/urls.py | 3 +- InvenTree/part/views.py | 8 +++-- 5 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 InvenTree/part/templates/part/select_image.html diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index b920205ca3..6dc6ad4822 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -183,6 +183,24 @@ -webkit-opacity: 10%; } +/* grid display for part images */ + +.table-img-grid tr { + display: inline; +} + +.table-img-grid td { + padding: 10px; + margin: 10px; +} + +.table-img-grid .grid-image { + + height: 128px; + width: 128px; + object-fit: contain; + background: #eee; +} .btn-glyph { padding-left: 6px; diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 70640027ce..13d82c573b 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -28,13 +28,14 @@

@@ -163,7 +164,7 @@ enableDragAndDrop( '#part-thumb', - "{% url 'part-image' part.id %}", + "{% url 'part-image-upload' part.id %}", { label: 'image', success: function(data, status, xhr) { @@ -210,13 +211,40 @@ $("#part-thumb").click(function() { launchModalForm( - "{% url 'part-image' part.id %}", + "{% url 'part-image-upload' part.id %}", { reload: true } ); }); + function onSelectImage(response) { + + $("#modal-form").find("#image-select-table").bootstrapTable({ + pagination: true, + pageSize: 25, + url: "{% url 'api-part-thumbs' %}", + showHeader: false, + columns: [ + { + field: 'image', + title: 'Image', + formatter: function(value, row, index, field) { + return "" + } + } + ], + }); + } + + $("#part-image-select").click(function() { + launchModalForm("{% url 'part-image-select' part.id %}", + { + reload: true, + after_render: onSelectImage + }); + }); + $("#part-edit").click(function() { launchModalForm( "{% url 'part-edit' part.id %}", diff --git a/InvenTree/part/templates/part/select_image.html b/InvenTree/part/templates/part/select_image.html new file mode 100644 index 0000000000..851688bf05 --- /dev/null +++ b/InvenTree/part/templates/part/select_image.html @@ -0,0 +1,20 @@ +{% extends "modal_form.html" %} + +{% block pre_form_content %} + +{{ block.super }} + +Select from existing images. + +{% endblock %} + +{% block form %} +
+{% csrf_token %} +{% load crispy_forms_tags %} + + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 0a9adefe8f..48e647302a 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -57,7 +57,8 @@ part_detail_urls = [ url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), # Normal thumbnail with form - url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'), + url(r'^thumbnail/?', views.PartImageUpload.as_view(), name='part-image-upload'), + url(r'^thumb-select/?', views.PartImageSelect.as_view(), name='part-image-select'), # Any other URLs go to the part detail page url(r'^.*$', views.PartDetail.as_view(), name='part-detail'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 722588b51a..dbc57ece59 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -601,8 +601,8 @@ class PartQRCode(QRCodeView): return None -class PartImage(AjaxUpdateView): - """ View for uploading Part image """ +class PartImageUpload(AjaxUpdateView): + """ View for uploading a new Part image """ model = Part ajax_template_name = 'modal_form.html' @@ -622,6 +622,10 @@ class PartImageSelect(AjaxUpdateView): ajax_template_name = 'part/select_image.html' ajax_form_title = _('Select Part Image') + fields = [ + 'image', + ] + def get_data(self): return { 'success': _('Selected part image') From 725eb3c53891e5b9e9086deda3793e182d1009b6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Feb 2020 23:04:58 +1100 Subject: [PATCH 04/11] Do not duplicate images when copying a part - Simply reference the existing image --- InvenTree/part/models.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index fb270f06f0..7d33a782de 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -136,18 +136,9 @@ def rename_part_image(instance, filename): """ base = 'part_images' + fname = os.path.basename(filename) - if filename.count('.') > 0: - ext = filename.split('.')[-1] - else: - ext = '' - - fn = 'part_{pk}_img'.format(pk=instance.pk) - - if ext: - fn += '.' + ext - - return os.path.join(base, fn) + return os.path.join(base, fname) def match_part_names(match, threshold=80, reverse=True, compare_length=False): @@ -832,10 +823,8 @@ class Part(models.Model): # Copy the part image if kwargs.get('image', True): if other.image: - image_file = ContentFile(other.image.read()) - image_file.name = rename_part_image(self, other.image.url) - - self.image = image_file + # Reference the other image from this Part + self.image = other.image # Copy the BOM data if kwargs.get('bom', False): From 534b60d4b84b58a0cd024db8a3dea040cb75e6c0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Feb 2020 23:43:41 +1100 Subject: [PATCH 05/11] Print out MEDIA_ROOT directory if in debug mode --- InvenTree/InvenTree/settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index e5f103f2a9..e7f73fcec6 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -308,7 +308,10 @@ STATICFILES_DIRS = [ MEDIA_URL = '/media/' # The filesystem location for served static files -MEDIA_ROOT = CONFIG.get('media_root', os.path.join(BASE_DIR, 'media')) +MEDIA_ROOT = os.path.abspath(CONFIG.get('media_root', os.path.join(BASE_DIR, 'media'))) + +if DEBUG: + print("MEDIA_ROOT:", MEDIA_ROOT) # crispy forms use the bootstrap templates CRISPY_TEMPLATE_PACK = 'bootstrap' From d4fe83170f12e9d17f14d3844e2f24e807fd0586 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Feb 2020 23:48:45 +1100 Subject: [PATCH 06/11] Select existing image and upload successfully --- InvenTree/part/api.py | 7 ++-- InvenTree/part/models.py | 1 - InvenTree/part/templates/part/part_base.html | 15 +++++++++ .../part/templates/part/select_image.html | 7 ++-- InvenTree/part/views.py | 33 ++++++++++++++++--- 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 424f03fd0b..b55e027152 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -98,20 +98,17 @@ class PartThumbs(generics.ListAPIView): # Get all Parts which have an associated image queryset = Part.objects.all().exclude(image='') + # Return the most popular parts first data = queryset.values( 'image', ).annotate(count=Count('image')).order_by('-count') - print("Parts with img:", queryset.count()) - - print(data) - return Response(data) class PartDetail(generics.RetrieveUpdateAPIView): """ API endpoint for detail view of a single Part object """ - + queryset = Part.objects.all() serializer_class = part_serializers.PartSerializer diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 7d33a782de..4109946dce 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -12,7 +12,6 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.conf import settings -from django.core.files.base import ContentFile from django.db import models, transaction from django.db.models import Sum from django.db.models import prefetch_related_objects diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 13d82c573b..cecf4244ea 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -218,14 +218,22 @@ ); }); + function onSelectImage(response) { + // Callback when the image-selection modal form is displayed + // Populate the form with image data (requested via AJAX) $("#modal-form").find("#image-select-table").bootstrapTable({ pagination: true, pageSize: 25, url: "{% url 'api-part-thumbs' %}", showHeader: false, + clickToSelect: true, + singleSelect: true, columns: [ + { + checkbox: true, + }, { field: 'image', title: 'Image', @@ -234,6 +242,13 @@ } } ], + onCheck: function(row, element) { + + // Update the selected image in the form + var ipt = $("#modal-form").find("#image-input"); + ipt.val(row.image); + + } }); } diff --git a/InvenTree/part/templates/part/select_image.html b/InvenTree/part/templates/part/select_image.html index 851688bf05..5e7667824d 100644 --- a/InvenTree/part/templates/part/select_image.html +++ b/InvenTree/part/templates/part/select_image.html @@ -4,14 +4,15 @@ {{ block.super }} -Select from existing images. {% endblock %} {% block form %}
-{% csrf_token %} -{% load crispy_forms_tags %} + {% csrf_token %} + {% load crispy_forms_tags %} + +
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index dbc57ece59..24717d55bb 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -14,6 +14,9 @@ from django.urls import reverse, reverse_lazy from django.views.generic import DetailView, ListView, FormView, UpdateView from django.forms.models import model_to_dict from django.forms import HiddenInput, CheckboxInput +from django.conf import settings + +import os from fuzzywuzzy import fuzz from decimal import Decimal @@ -626,10 +629,32 @@ class PartImageSelect(AjaxUpdateView): 'image', ] - def get_data(self): - return { - 'success': _('Selected part image') - } + def post(self, request, *args, **kwargs): + + part = self.get_object() + form = self.get_form() + + img = request.POST.get('image', '') + + img = os.path.basename(img) + + data = {} + + if img: + img_path = os.path.join(settings.MEDIA_ROOT, 'part_images', img) + + # Ensure that the image already exists + if os.path.exists(img_path): + + part.image = os.path.join('part_images', img) + part.save() + + data['success'] = _('Updated part image') + + if 'success' not in data: + data['error'] = _('Part image not found') + + return self.renderJsonResponse(request, form, data) class PartEdit(AjaxUpdateView): From e0e996a6c390a849484879ec27743ca3d545aa37 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Feb 2020 00:00:03 +1100 Subject: [PATCH 07/11] Add buttons to select or upload part images --- InvenTree/part/templates/part/part_base.html | 26 +++++-------------- InvenTree/part/templates/part/part_thumb.html | 20 ++++++++++++++ 2 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 InvenTree/part/templates/part/part_thumb.html diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index cecf4244ea..27e5aedb83 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -25,18 +25,7 @@
-
-
-
- -
- Select Part Image -
+ {% include "part/part_thumb.html" %}

{{ part.full_name }} @@ -209,13 +198,12 @@ }); }); - $("#part-thumb").click(function() { - launchModalForm( - "{% url 'part-image-upload' part.id %}", - { - reload: true - } - ); + $("#part-image-upload").click(function() { + launchModalForm("{% url 'part-image-upload' part.id %}", + { + reload: true + } + ); }); diff --git a/InvenTree/part/templates/part/part_thumb.html b/InvenTree/part/templates/part/part_thumb.html new file mode 100644 index 0000000000..e99b5106c0 --- /dev/null +++ b/InvenTree/part/templates/part/part_thumb.html @@ -0,0 +1,20 @@ +{% load static %} +{% load i18n %} + +
+
+
+ +
+
+
+ + +
+
+
\ No newline at end of file From 8ea1086b03c9c1d62857ad7aed164bdd763a5379 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Feb 2020 00:28:46 +1100 Subject: [PATCH 08/11] Make thumb buttons only visible on mouseover --- InvenTree/InvenTree/static/css/inventree.css | 14 ++++++++++++++ InvenTree/part/templates/part/part_thumb.html | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 6dc6ad4822..6c89ff7265 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -229,6 +229,20 @@ object-fit: contain; } +.part-thumb-container:hover .part-thumb-overlay { + opacity: 1; +} + +.part-thumb-overlay { + position: absolute; + top: 0; + left: 0; + opacity: 0; + transition: .25s ease; + padding: 15px; + margin: 5px; +} + .checkbox { margin-left: 20px; } diff --git a/InvenTree/part/templates/part/part_thumb.html b/InvenTree/part/templates/part/part_thumb.html index e99b5106c0..04efdd383f 100644 --- a/InvenTree/part/templates/part/part_thumb.html +++ b/InvenTree/part/templates/part/part_thumb.html @@ -2,7 +2,7 @@ {% load i18n %}
-
+
-
+
- +
From dee47bdea890385a659b8aff75b9b4ed51c139b7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Feb 2020 00:29:29 +1100 Subject: [PATCH 09/11] Prevent django_cleanup from deleting part thumbs that are used elsewhere - Will need to implement a method for automatically deleting part thumbs... --- InvenTree/part/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4109946dce..52dfa96ba5 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -23,6 +23,8 @@ from django.dispatch import receiver from markdownx.models import MarkdownxField +from django_cleanup import cleanup + from mptt.models import TreeForeignKey from datetime import datetime @@ -191,6 +193,7 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False): return matches +@cleanup.ignore class Part(models.Model): """ The Part object represents an abstract part, the 'concept' of an actual entity. From 77c950a7295321b180d2b95beee7f4425ab41ee3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Feb 2020 00:39:02 +1100 Subject: [PATCH 10/11] Fixed unit tests --- InvenTree/part/test_part.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 28ee93d976..2d3e5408bd 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -23,7 +23,7 @@ class TemplateTagTest(TestCase): def test_hash(self): hash = inventree_extras.inventree_commit_hash() - self.assertEqual(len(hash), 7) + self.assertGreater(len(hash), 5) def test_date(self): d = inventree_extras.inventree_commit_date() @@ -68,11 +68,8 @@ class PartTest(TestCase): def test_rename_img(self): img = rename_part_image(self.R1, 'hello.png') - self.assertEqual(img, os.path.join('part_images', 'part_3_img.png')) - - img = rename_part_image(self.R2, 'test') - self.assertEqual(img, os.path.join('part_images', 'part_4_img')) - + self.assertEqual(img, os.path.join('part_images', 'hello.png')) + def test_stock(self): # No stock of any resistors res = Part.objects.filter(description__contains='resistor') From 55aa63dab4084b269bdd58586e5fe8241204521f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Feb 2020 20:27:06 +1100 Subject: [PATCH 11/11] Override save() method for Part model - Delete old thumbnails if they are no longer being used --- InvenTree/part/models.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 52dfa96ba5..3a59db0cc6 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -230,6 +230,26 @@ class Part(models.Model): verbose_name = "Part" verbose_name_plural = "Parts" + def save(self, *args, **kwargs): + """ + Overrides the save() function for the Part model. + If the part image has been updated, + then check if the "old" (previous) image is still used by another part. + If not, it is considered "orphaned" and will be deleted. + """ + + if self.pk: + previous = Part.objects.get(pk=self.pk) + + if previous.image and not self.image == previous.image: + # Are there any (other) parts which reference the image? + n_refs = Part.objects.filter(image=previous.image).exclude(pk=self.pk).count() + + if n_refs == 0: + previous.image.delete(save=False) + + super().save(*args, **kwargs) + def __str__(self): return "{n} - {d}".format(n=self.full_name, d=self.description)