mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Merge remote-tracking branch 'inventree/master' into webp-support
This commit is contained in:
		@@ -1,6 +1,3 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from import_export.admin import ImportExportModelAdmin
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,6 @@
 | 
			
		||||
Provides a JSON API for the Part app
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from django.urls import include, path, re_path
 | 
			
		||||
@@ -44,6 +41,7 @@ from stock.models import StockItem, StockLocation
 | 
			
		||||
from common.models import InvenTreeSetting
 | 
			
		||||
from build.models import Build, BuildItem
 | 
			
		||||
import order.models
 | 
			
		||||
from plugin.serializers import MetadataSerializer
 | 
			
		||||
 | 
			
		||||
from . import serializers as part_serializers
 | 
			
		||||
 | 
			
		||||
@@ -203,6 +201,15 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CategoryMetadata(generics.RetrieveUpdateAPIView):
 | 
			
		||||
    """API endpoint for viewing / updating PartCategory metadata"""
 | 
			
		||||
 | 
			
		||||
    def get_serializer(self, *args, **kwargs):
 | 
			
		||||
        return MetadataSerializer(PartCategory, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    queryset = PartCategory.objects.all()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CategoryParameterList(generics.ListAPIView):
 | 
			
		||||
    """ API endpoint for accessing a list of PartCategoryParameterTemplate objects.
 | 
			
		||||
 | 
			
		||||
@@ -587,6 +594,17 @@ class PartScheduling(generics.RetrieveAPIView):
 | 
			
		||||
        return Response(schedule)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartMetadata(generics.RetrieveUpdateAPIView):
 | 
			
		||||
    """
 | 
			
		||||
    API endpoint for viewing / updating Part metadata
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get_serializer(self, *args, **kwargs):
 | 
			
		||||
        return MetadataSerializer(Part, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    queryset = Part.objects.all()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartSerialNumberDetail(generics.RetrieveAPIView):
 | 
			
		||||
    """
 | 
			
		||||
    API endpoint for returning extra serial number information about a particular part
 | 
			
		||||
@@ -1912,7 +1930,15 @@ part_api_urls = [
 | 
			
		||||
        re_path(r'^tree/', CategoryTree.as_view(), name='api-part-category-tree'),
 | 
			
		||||
        re_path(r'^parameters/', CategoryParameterList.as_view(), name='api-part-category-parameter-list'),
 | 
			
		||||
 | 
			
		||||
        re_path(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
 | 
			
		||||
        # Category detail endpoints
 | 
			
		||||
        re_path(r'^(?P<pk>\d+)/', include([
 | 
			
		||||
 | 
			
		||||
            re_path(r'^metadata/', CategoryMetadata.as_view(), name='api-part-category-metadata'),
 | 
			
		||||
 | 
			
		||||
            # PartCategory detail endpoint
 | 
			
		||||
            re_path(r'^.*$', CategoryDetail.as_view(), name='api-part-category-detail'),
 | 
			
		||||
        ])),
 | 
			
		||||
 | 
			
		||||
        path('', CategoryList.as_view(), name='api-part-category-list'),
 | 
			
		||||
    ])),
 | 
			
		||||
 | 
			
		||||
@@ -1973,6 +1999,9 @@ part_api_urls = [
 | 
			
		||||
        # Endpoint for validating a BOM for the specific Part
 | 
			
		||||
        re_path(r'^bom-validate/', PartValidateBOM.as_view(), name='api-part-bom-validate'),
 | 
			
		||||
 | 
			
		||||
        # Part metadata
 | 
			
		||||
        re_path(r'^metadata/', PartMetadata.as_view(), name='api-part-metadata'),
 | 
			
		||||
 | 
			
		||||
        # Part detail endpoint
 | 
			
		||||
        re_path(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
 | 
			
		||||
    ])),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from django.db.utils import OperationalError, ProgrammingError
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,6 @@
 | 
			
		||||
Django Forms for interacting with Part objects
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								InvenTree/part/migrations/0076_auto_20220516_0819.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								InvenTree/part/migrations/0076_auto_20220516_0819.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.13 on 2022-05-16 08:19
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('part', '0075_auto_20211128_0151'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='part',
 | 
			
		||||
            name='metadata',
 | 
			
		||||
            field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='partcategory',
 | 
			
		||||
            name='metadata',
 | 
			
		||||
            field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
Part database model definitions
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
import decimal
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
@@ -46,29 +44,28 @@ from common.models import InvenTreeSetting
 | 
			
		||||
 | 
			
		||||
from InvenTree import helpers
 | 
			
		||||
from InvenTree import validators
 | 
			
		||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment, DataImportMixin
 | 
			
		||||
from InvenTree.fields import InvenTreeURLField
 | 
			
		||||
from InvenTree.helpers import decimal2string, normalize, decimal2money
 | 
			
		||||
 | 
			
		||||
import InvenTree.ready
 | 
			
		||||
import InvenTree.tasks
 | 
			
		||||
 | 
			
		||||
from InvenTree.fields import InvenTreeURLField
 | 
			
		||||
from InvenTree.helpers import decimal2string, normalize, decimal2money
 | 
			
		||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment, DataImportMixin
 | 
			
		||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
 | 
			
		||||
 | 
			
		||||
import common.models
 | 
			
		||||
from build import models as BuildModels
 | 
			
		||||
from order import models as OrderModels
 | 
			
		||||
from company.models import SupplierPart
 | 
			
		||||
from stock import models as StockModels
 | 
			
		||||
 | 
			
		||||
import common.models
 | 
			
		||||
 | 
			
		||||
import part.settings as part_settings
 | 
			
		||||
from stock import models as StockModels
 | 
			
		||||
from plugin.models import MetadataMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("inventree")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartCategory(InvenTreeTree):
 | 
			
		||||
class PartCategory(MetadataMixin, InvenTreeTree):
 | 
			
		||||
    """ PartCategory provides hierarchical organization of Part objects.
 | 
			
		||||
 | 
			
		||||
    Attributes:
 | 
			
		||||
@@ -327,7 +324,7 @@ class PartManager(TreeManager):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cleanup.ignore
 | 
			
		||||
class Part(MPTTModel):
 | 
			
		||||
class Part(MetadataMixin, MPTTModel):
 | 
			
		||||
    """ The Part object represents an abstract part, the 'concept' of an actual entity.
 | 
			
		||||
 | 
			
		||||
    An actual physical instance of a Part is a StockItem which is treated separately.
 | 
			
		||||
@@ -444,7 +441,7 @@ class Part(MPTTModel):
 | 
			
		||||
            previous = Part.objects.get(pk=self.pk)
 | 
			
		||||
 | 
			
		||||
            # Image has been changed
 | 
			
		||||
            if previous.image is not None and not self.image == previous.image:
 | 
			
		||||
            if previous.image is not None and 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()
 | 
			
		||||
@@ -2293,12 +2290,13 @@ def after_save_part(sender, instance: Part, created, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    Function to be executed after a Part is saved
 | 
			
		||||
    """
 | 
			
		||||
    from part import tasks as part_tasks
 | 
			
		||||
 | 
			
		||||
    if not created and not InvenTree.ready.isImportingData():
 | 
			
		||||
        # Check part stock only if we are *updating* the part (not creating it)
 | 
			
		||||
 | 
			
		||||
        # Run this check in the background
 | 
			
		||||
        InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance)
 | 
			
		||||
        InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartAttachment(InvenTreeAttachment):
 | 
			
		||||
@@ -2895,7 +2893,7 @@ class BomItem(models.Model, DataImportMixin):
 | 
			
		||||
 | 
			
		||||
                # If the sub_part is 'trackable' then the 'quantity' field must be an integer
 | 
			
		||||
                if self.sub_part.trackable:
 | 
			
		||||
                    if not self.quantity == int(self.quantity):
 | 
			
		||||
                    if self.quantity != int(self.quantity):
 | 
			
		||||
                        raise ValidationError({
 | 
			
		||||
                            "quantity": _("Quantity must be integer value for trackable parts")
 | 
			
		||||
                        })
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,6 @@
 | 
			
		||||
User-configurable settings for the Part app
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from common.models import InvenTreeSetting
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,3 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
@@ -49,6 +46,6 @@ def notify_low_stock_if_required(part: part.models.Part):
 | 
			
		||||
    for p in parts:
 | 
			
		||||
        if p.is_part_low_on_stock():
 | 
			
		||||
            InvenTree.tasks.offload_task(
 | 
			
		||||
                'part.tasks.notify_low_stock',
 | 
			
		||||
                notify_low_stock,
 | 
			
		||||
                p
 | 
			
		||||
            )
 | 
			
		||||
 
 | 
			
		||||
@@ -589,32 +589,15 @@
 | 
			
		||||
            // Get a list of the selected BOM items
 | 
			
		||||
            var rows = $("#bom-table").bootstrapTable('getSelections');
 | 
			
		||||
 | 
			
		||||
            // TODO - In the future, display (in the dialog) which items are going to be deleted
 | 
			
		||||
            if (rows.length == 0) {
 | 
			
		||||
                rows = $('#bom-table').bootstrapTable('getData');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            showQuestionDialog(
 | 
			
		||||
                '{% trans "Delete selected BOM items?" %}',
 | 
			
		||||
                '{% trans "All selected BOM items will be deleted" %}',
 | 
			
		||||
                {
 | 
			
		||||
                    accept: function() {
 | 
			
		||||
 | 
			
		||||
                        // Keep track of each DELETE request
 | 
			
		||||
                        var requests = [];
 | 
			
		||||
 | 
			
		||||
                        rows.forEach(function(row) {
 | 
			
		||||
                            requests.push(
 | 
			
		||||
                                inventreeDelete(
 | 
			
		||||
                                    `/api/bom/${row.pk}/`,
 | 
			
		||||
                                )
 | 
			
		||||
                            );
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        // Wait for *all* the requests to complete
 | 
			
		||||
                        $.when.apply($, requests).done(function() {
 | 
			
		||||
                            location.reload();
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
            deleteBomItems(rows, {
 | 
			
		||||
                success: function() {
 | 
			
		||||
                    $('#bom-table').bootstrapTable('refresh');
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $('#bom-upload').click(function() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,3 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
import PIL
 | 
			
		||||
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
@@ -21,6 +18,85 @@ import build.models
 | 
			
		||||
import order.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartCategoryAPITest(InvenTreeAPITestCase):
 | 
			
		||||
    """Unit tests for the PartCategory API"""
 | 
			
		||||
 | 
			
		||||
    fixtures = [
 | 
			
		||||
        'category',
 | 
			
		||||
        'part',
 | 
			
		||||
        'location',
 | 
			
		||||
        'bom',
 | 
			
		||||
        'company',
 | 
			
		||||
        'test_templates',
 | 
			
		||||
        'manufacturer_part',
 | 
			
		||||
        'supplier_part',
 | 
			
		||||
        'order',
 | 
			
		||||
        'stock',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    roles = [
 | 
			
		||||
        'part.change',
 | 
			
		||||
        'part.add',
 | 
			
		||||
        'part.delete',
 | 
			
		||||
        'part_category.change',
 | 
			
		||||
        'part_category.add',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def test_category_list(self):
 | 
			
		||||
 | 
			
		||||
        # List all part categories
 | 
			
		||||
        url = reverse('api-part-category-list')
 | 
			
		||||
 | 
			
		||||
        response = self.get(url, expected_code=200)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(len(response.data), 8)
 | 
			
		||||
 | 
			
		||||
        # Filter by parent, depth=1
 | 
			
		||||
        response = self.get(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'parent': 1,
 | 
			
		||||
                'cascade': False,
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=200
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(len(response.data), 3)
 | 
			
		||||
 | 
			
		||||
        # Filter by parent, cascading
 | 
			
		||||
        response = self.get(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'parent': 1,
 | 
			
		||||
                'cascade': True,
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=200,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(len(response.data), 5)
 | 
			
		||||
 | 
			
		||||
    def test_category_metadata(self):
 | 
			
		||||
        """Test metadata endpoint for the PartCategory"""
 | 
			
		||||
 | 
			
		||||
        cat = PartCategory.objects.get(pk=1)
 | 
			
		||||
 | 
			
		||||
        cat.metadata = {
 | 
			
		||||
            'foo': 'bar',
 | 
			
		||||
            'water': 'melon',
 | 
			
		||||
            'abc': 'xyz',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        cat.set_metadata('abc', 'ABC')
 | 
			
		||||
 | 
			
		||||
        response = self.get(reverse('api-part-category-metadata', kwargs={'pk': 1}), expected_code=200)
 | 
			
		||||
 | 
			
		||||
        metadata = response.data['metadata']
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(metadata['foo'], 'bar')
 | 
			
		||||
        self.assertEqual(metadata['water'], 'melon')
 | 
			
		||||
        self.assertEqual(metadata['abc'], 'ABC')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartOptionsAPITest(InvenTreeAPITestCase):
 | 
			
		||||
    """
 | 
			
		||||
    Tests for the various OPTIONS endpoints in the /part/ API
 | 
			
		||||
@@ -1026,6 +1102,59 @@ class PartDetailTests(InvenTreeAPITestCase):
 | 
			
		||||
        self.assertEqual(data['in_stock'], 9000)
 | 
			
		||||
        self.assertEqual(data['unallocated_stock'], 9000)
 | 
			
		||||
 | 
			
		||||
    def test_part_metadata(self):
 | 
			
		||||
        """
 | 
			
		||||
        Tests for the part metadata endpoint
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        url = reverse('api-part-metadata', kwargs={'pk': 1})
 | 
			
		||||
 | 
			
		||||
        part = Part.objects.get(pk=1)
 | 
			
		||||
 | 
			
		||||
        # Metadata is initially null
 | 
			
		||||
        self.assertIsNone(part.metadata)
 | 
			
		||||
 | 
			
		||||
        part.metadata = {'foo': 'bar'}
 | 
			
		||||
        part.save()
 | 
			
		||||
 | 
			
		||||
        response = self.get(url, expected_code=200)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(response.data['metadata']['foo'], 'bar')
 | 
			
		||||
 | 
			
		||||
        # Add more data via the API
 | 
			
		||||
        # Using the 'patch' method causes the new data to be merged in
 | 
			
		||||
        self.patch(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'metadata': {
 | 
			
		||||
                    'hello': 'world',
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=200
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        part.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(part.metadata['foo'], 'bar')
 | 
			
		||||
        self.assertEqual(part.metadata['hello'], 'world')
 | 
			
		||||
 | 
			
		||||
        # Now, issue a PUT request (existing data will be replacted)
 | 
			
		||||
        self.put(
 | 
			
		||||
            url,
 | 
			
		||||
            {
 | 
			
		||||
                'metadata': {
 | 
			
		||||
                    'x': 'y'
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            expected_code=200
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        part.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
        self.assertFalse('foo' in part.metadata)
 | 
			
		||||
        self.assertFalse('hello' in part.metadata)
 | 
			
		||||
        self.assertEqual(part.metadata['x'], 'y')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PartAPIAggregationTest(InvenTreeAPITestCase):
 | 
			
		||||
    """
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,4 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,5 @@
 | 
			
		||||
# Tests for Part Parameters
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.test import TestCase, TransactionTestCase
 | 
			
		||||
import django.core.exceptions as django_exceptions
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,5 @@
 | 
			
		||||
# Tests for the Part model
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
from allauth.account.models import EmailAddress
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
@@ -210,7 +207,7 @@ class PartTest(TestCase):
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            part_2.validate_unique()
 | 
			
		||||
 | 
			
		||||
    def test_metadata(self):
 | 
			
		||||
    def test_attributes(self):
 | 
			
		||||
        self.assertEqual(self.r1.name, 'R_2K2_0805')
 | 
			
		||||
        self.assertEqual(self.r1.get_absolute_url(), '/part/3/')
 | 
			
		||||
 | 
			
		||||
@@ -256,6 +253,24 @@ class PartTest(TestCase):
 | 
			
		||||
        self.assertEqual(float(self.r1.get_internal_price(1)), 0.08)
 | 
			
		||||
        self.assertEqual(float(self.r1.get_internal_price(10)), 0.5)
 | 
			
		||||
 | 
			
		||||
    def test_metadata(self):
 | 
			
		||||
        """Unit tests for the Part metadata field"""
 | 
			
		||||
 | 
			
		||||
        p = Part.objects.get(pk=1)
 | 
			
		||||
        self.assertIsNone(p.metadata)
 | 
			
		||||
 | 
			
		||||
        self.assertIsNone(p.get_metadata('test'))
 | 
			
		||||
        self.assertEqual(p.get_metadata('test', backup_value=123), 123)
 | 
			
		||||
 | 
			
		||||
        # Test update via the set_metadata() method
 | 
			
		||||
        p.set_metadata('test', 3)
 | 
			
		||||
        self.assertEqual(p.get_metadata('test'), 3)
 | 
			
		||||
 | 
			
		||||
        for k in ['apple', 'banana', 'carrot', 'carrot', 'banana']:
 | 
			
		||||
            p.set_metadata(k, k)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(len(p.metadata.keys()), 4)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTemplateTest(TestCase):
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,6 @@
 | 
			
		||||
Django views for interacting with Part app
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.core.files.base import ContentFile
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
@@ -628,7 +625,7 @@ class PartImageDownloadFromURL(AjaxUpdateView):
 | 
			
		||||
        self.response = response
 | 
			
		||||
 | 
			
		||||
        # Check for valid response code
 | 
			
		||||
        if not response.status_code == 200:
 | 
			
		||||
        if response.status_code != 200:
 | 
			
		||||
            form.add_error('url', _('Invalid response: {code}').format(code=response.status_code))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user