diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 1d0e70908d..89ea434d0c 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -583,4 +583,4 @@ class DataFileExtractSerializer(serializers.Serializer): def save(self): """No "save" action for this serializer.""" - ... + pass diff --git a/InvenTree/part/__init__.py b/InvenTree/part/__init__.py index 1416c9a025..1412ff8f11 100644 --- a/InvenTree/part/__init__.py +++ b/InvenTree/part/__init__.py @@ -1,8 +1 @@ -"""The Part module is responsible for Part management. - -It includes models for: - -- PartCategory -- Part -- BomItem -""" +"""The Part module is responsible for Part management.""" diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 81e408b30d..9c1648e616 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -1,3 +1,5 @@ +"""Admin class definitions for the 'part' app""" + from django.contrib import admin import import_export.widgets as widgets @@ -38,6 +40,7 @@ class PartResource(ModelResource): building = Field(attribute='quantity_being_built', readonly=True, widget=widgets.IntegerWidget()) class Meta: + """Metaclass definition""" model = models.Part skip_unchanged = True report_skipped = False @@ -61,8 +64,17 @@ class PartResource(ModelResource): return query + def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): + """Rebuild MPTT tree structure after importing Part data""" + + super().after_import(dataset, result, using_transactions, dry_run, **kwargs) + + # Rebuild the Part tree(s) + models.Part.objects.rebuild() + class PartAdmin(ImportExportModelAdmin): + """Admin class for the Part model""" resource_class = PartResource @@ -90,6 +102,7 @@ class PartCategoryResource(ModelResource): default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation)) class Meta: + """Metaclass definition""" model = models.PartCategory skip_unchanged = True report_skipped = False @@ -102,6 +115,7 @@ class PartCategoryResource(ModelResource): ] def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): + """Rebuild MPTT tree structure after importing PartCategory data""" super().after_import(dataset, result, using_transactions, dry_run, **kwargs) @@ -110,6 +124,7 @@ class PartCategoryResource(ModelResource): class PartCategoryAdmin(ImportExportModelAdmin): + """Admin class for the PartCategory model""" resource_class = PartCategoryResource @@ -127,27 +142,15 @@ class PartRelatedAdmin(admin.ModelAdmin): class PartAttachmentAdmin(admin.ModelAdmin): + """Admin class for the PartAttachment model""" list_display = ('part', 'attachment', 'comment') autocomplete_fields = ('part',) -class PartStarAdmin(admin.ModelAdmin): - - list_display = ('part', 'user') - - autocomplete_fields = ('part',) - - -class PartCategoryStarAdmin(admin.ModelAdmin): - - list_display = ('category', 'user') - - autocomplete_fields = ('category',) - - class PartTestTemplateAdmin(admin.ModelAdmin): + """Admin class for the PartTestTemplate model""" list_display = ('part', 'test_name', 'required') @@ -193,7 +196,7 @@ class BomItemResource(ModelResource): return float(item.quantity) def before_export(self, queryset, *args, **kwargs): - + """Perform before exporting data""" self.is_importing = kwargs.get('importing', False) def get_fields(self, **kwargs): @@ -229,6 +232,7 @@ class BomItemResource(ModelResource): return fields class Meta: + """Metaclass definition""" model = models.BomItem skip_unchanged = True report_skipped = False @@ -243,6 +247,7 @@ class BomItemResource(ModelResource): class BomItemAdmin(ImportExportModelAdmin): + """Admin class for the BomItem model""" resource_class = BomItemResource @@ -254,6 +259,8 @@ class BomItemAdmin(ImportExportModelAdmin): class ParameterTemplateAdmin(ImportExportModelAdmin): + """Admin class for the PartParameterTemplate model""" + list_display = ('name', 'units') search_fields = ('name', 'units') @@ -271,6 +278,7 @@ class ParameterResource(ModelResource): template_name = Field(attribute='template__name', readonly=True) class Meta: + """Metaclass definition""" model = models.PartParameter skip_unchanged = True report_skipped = False @@ -278,6 +286,7 @@ class ParameterResource(ModelResource): class ParameterAdmin(ImportExportModelAdmin): + """Admin class for the PartParameter model""" resource_class = ParameterResource @@ -287,21 +296,26 @@ class ParameterAdmin(ImportExportModelAdmin): class PartCategoryParameterAdmin(admin.ModelAdmin): + """Admin class for the PartCategoryParameterTemplate model""" autocomplete_fields = ('category', 'parameter_template',) class PartSellPriceBreakAdmin(admin.ModelAdmin): + """Admin class for the PartSellPriceBreak model""" class Meta: + """Metaclass definition""" model = models.PartSellPriceBreak list_display = ('part', 'quantity', 'price',) class PartInternalPriceBreakAdmin(admin.ModelAdmin): + """Admin class for the PartInternalPriceBreak model""" class Meta: + """Metaclass definition""" model = models.PartInternalPriceBreak list_display = ('part', 'quantity', 'price',) @@ -313,8 +327,6 @@ admin.site.register(models.Part, PartAdmin) admin.site.register(models.PartCategory, PartCategoryAdmin) admin.site.register(models.PartRelated, PartRelatedAdmin) admin.site.register(models.PartAttachment, PartAttachmentAdmin) -admin.site.register(models.PartStar, PartStarAdmin) -admin.site.register(models.PartCategoryStar, PartCategoryStarAdmin) admin.site.register(models.BomItem, BomItemAdmin) admin.site.register(models.PartParameterTemplate, ParameterTemplateAdmin) admin.site.register(models.PartParameter, ParameterAdmin) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 569a0491f2..8004cf1326 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -49,7 +49,7 @@ class CategoryList(generics.ListCreateAPIView): serializer_class = part_serializers.CategorySerializer def get_serializer_context(self): - + """Add extra context data to the serializer for the PartCategoryList endpoint""" ctx = super().get_serializer_context() try: @@ -161,7 +161,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): queryset = PartCategory.objects.all() def get_serializer_context(self): - + """Add extra context to the serializer for the CategoryDetail endpoint""" ctx = super().get_serializer_context() try: @@ -173,7 +173,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): return ctx def update(self, request, *args, **kwargs): - + """Perform 'update' function and mark this part as 'starred' (or not)""" if 'starred' in request.data: starred = str2bool(request.data.get('starred', False)) @@ -188,6 +188,7 @@ class CategoryMetadata(generics.RetrieveUpdateAPIView): """API endpoint for viewing / updating PartCategory metadata.""" def get_serializer(self, *args, **kwargs): + """Return a MetadataSerializer pointing to the referenced PartCategory instance""" return MetadataSerializer(PartCategory, *args, **kwargs) queryset = PartCategory.objects.all() @@ -370,7 +371,7 @@ class PartThumbs(generics.ListAPIView): serializer_class = part_serializers.PartThumbSerializer def get_queryset(self): - + """Return a queryset which exlcudes any parts without images""" queryset = super().get_queryset() # Get all Parts which have an associated image @@ -432,7 +433,7 @@ class PartScheduling(generics.RetrieveAPIView): queryset = Part.objects.all() def retrieve(self, request, *args, **kwargs): - + """Return scheduling information for the referenced Part instance""" today = datetime.datetime.now().date() part = self.get_object() @@ -555,6 +556,7 @@ class PartMetadata(generics.RetrieveUpdateAPIView): """API endpoint for viewing / updating Part metadata.""" def get_serializer(self, *args, **kwargs): + """Returns a MetadataSerializer instance pointing to the referenced Part""" return MetadataSerializer(Part, *args, **kwargs) queryset = Part.objects.all() @@ -566,7 +568,7 @@ class PartSerialNumberDetail(generics.RetrieveAPIView): queryset = Part.objects.all() def retrieve(self, request, *args, **kwargs): - + """Return serial number information for the referenced Part instance""" part = self.get_object() # Calculate the "latest" serial number @@ -592,7 +594,7 @@ class PartCopyBOM(generics.CreateAPIView): serializer_class = part_serializers.PartCopyBOMSerializer def get_serializer_context(self): - + """Add custom information to the serializer context for this endpoint""" ctx = super().get_serializer_context() try: @@ -607,8 +609,10 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView): """API endpoint for 'validating' the BOM for a given Part.""" class BOMValidateSerializer(serializers.ModelSerializer): + """Simple serializer class for validating a single BomItem instance""" class Meta: + """Metaclass defines serializer fields""" model = Part fields = [ 'checksum', @@ -628,6 +632,7 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView): ) def validate_valid(self, valid): + """Check that the 'valid' input was flagged""" if not valid: raise ValidationError(_('This option must be selected')) @@ -636,7 +641,7 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView): serializer_class = BOMValidateSerializer def update(self, request, *args, **kwargs): - + """Validate the referenced BomItem instance""" part = self.get_object() partial = kwargs.pop('partial', False) @@ -660,6 +665,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): starred_parts = None def get_queryset(self, *args, **kwargs): + """Return an annotated queryset object for the PartDetail endpoint""" queryset = super().get_queryset(*args, **kwargs) queryset = part_serializers.PartSerializer.annotate_queryset(queryset) @@ -667,7 +673,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): return queryset def get_serializer(self, *args, **kwargs): - + """Return a serializer instance for the PartDetail endpoint""" # By default, include 'category_detail' information in the detail view try: kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', True)) @@ -687,7 +693,11 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): return self.serializer_class(*args, **kwargs) def destroy(self, request, *args, **kwargs): - # Retrieve part + """Delete a Part instance via the API + + - If the part is 'active' it cannot be deleted + - It must first be marked as 'inactive' + """ part = Part.objects.get(pk=int(kwargs['pk'])) # Check if inactive if not part.active: @@ -695,7 +705,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): return super(PartDetail, self).destroy(request, *args, **kwargs) else: # Return 405 error - message = f'Part \'{part.name}\' (pk = {part.pk}) is active: cannot delete' + message = 'Part is active: cannot delete' return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED, data=message) def update(self, request, *args, **kwargs): @@ -723,7 +733,7 @@ class PartFilter(rest_filters.FilterSet): has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn') def filter_has_ipn(self, queryset, name, value): - + """Filter by whether the Part has an IPN (internal part number) or not""" value = str2bool(value) if value: @@ -768,7 +778,7 @@ class PartFilter(rest_filters.FilterSet): has_stock = rest_filters.BooleanFilter(label='Has stock', method='filter_has_stock') def filter_has_stock(self, queryset, name, value): - + """Filter by whether the Part has any stock""" value = str2bool(value) if value: @@ -782,7 +792,7 @@ class PartFilter(rest_filters.FilterSet): unallocated_stock = rest_filters.BooleanFilter(label='Unallocated stock', method='filter_unallocated_stock') def filter_unallocated_stock(self, queryset, name, value): - + """Filter by whether the Part has unallocated stock""" value = str2bool(value) if value: @@ -837,7 +847,7 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView): starred_parts = None def get_serializer(self, *args, **kwargs): - + """Return a serializer instance for this endpoint""" # Ensure the request context is passed through kwargs['context'] = self.get_serializer_context() @@ -859,6 +869,7 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) def download_queryset(self, queryset, export_format): + """Download the filtered queryset as a data file""" dataset = PartResource().export(queryset=queryset) filedata = dataset.export(export_format) @@ -1077,17 +1088,14 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def get_queryset(self, *args, **kwargs): - + """Return an annotated queryset object""" queryset = super().get_queryset(*args, **kwargs) queryset = part_serializers.PartSerializer.annotate_queryset(queryset) return queryset def filter_queryset(self, queryset): - """Perform custom filtering of the queryset. - - We overide the DRF filter_fields here because - """ + """Perform custom filtering of the queryset""" params = self.request.query_params queryset = super().filter_queryset(queryset) @@ -1345,7 +1353,7 @@ class PartRelatedList(generics.ListCreateAPIView): serializer_class = part_serializers.PartRelationSerializer def filter_queryset(self, queryset): - + """Custom queryset filtering""" queryset = super().filter_queryset(queryset) params = self.request.query_params @@ -1478,8 +1486,7 @@ class BomFilter(rest_filters.FilterSet): validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated') def filter_validated(self, queryset, name, value): - - # Work out which lines have actually been validated + """Filter by which lines have actually been validated""" pks = [] value = str2bool(value) @@ -1512,6 +1519,7 @@ class BomList(generics.ListCreateAPIView): filterset_class = BomFilter def list(self, request, *args, **kwargs): + """Return serialized list response for this endpoint""" queryset = self.filter_queryset(self.get_queryset()) @@ -1537,6 +1545,13 @@ class BomList(generics.ListCreateAPIView): return Response(data) def get_serializer(self, *args, **kwargs): + """Return the serializer instance for this API endpoint + + If requested, extra detail fields are annotated to the queryset: + - part_detail + - sub_part_detail + - include_pricing + """ # Do we wish to include extra detail? try: @@ -1561,7 +1576,7 @@ class BomList(generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) def get_queryset(self, *args, **kwargs): - + """Return the queryset object for this endpoint""" queryset = super().get_queryset(*args, **kwargs) queryset = self.get_serializer_class().setup_eager_loading(queryset) @@ -1570,7 +1585,7 @@ class BomList(generics.ListCreateAPIView): return queryset def filter_queryset(self, queryset): - + """Custom query filtering for the BomItem list API""" queryset = super().filter_queryset(queryset) params = self.request.query_params @@ -1766,7 +1781,7 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = part_serializers.BomItemSerializer def get_queryset(self, *args, **kwargs): - + """Prefetch related fields for this queryset""" queryset = super().get_queryset(*args, **kwargs) queryset = self.get_serializer_class().setup_eager_loading(queryset) @@ -1778,9 +1793,8 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView): class BomItemValidate(generics.UpdateAPIView): """API endpoint for validating a BomItem.""" - # Very simple serializers class BomItemValidationSerializer(serializers.Serializer): - + """Simple serializer for passing a single boolean field""" valid = serializers.BooleanField(default=False) queryset = BomItem.objects.all() diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index dff059d92a..9a8ce076b5 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -1,3 +1,5 @@ +"""part app specification""" + import logging from django.apps import AppConfig @@ -9,6 +11,7 @@ logger = logging.getLogger("inventree") class PartConfig(AppConfig): + """Config class for the 'part' app""" name = 'part' def ready(self): diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 965f476e21..f4c4810d37 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -5,7 +5,6 @@ from django.utils.translation import gettext_lazy as _ from mptt.fields import TreeNodeChoiceField -import common.models from common.forms import MatchItemForm from InvenTree.fields import RoundingDecimalFormField from InvenTree.forms import HelperForm @@ -16,20 +15,6 @@ from .models import (Part, PartCategory, PartCategoryParameterTemplate, PartSellPriceBreak) -class PartModelChoiceField(forms.ModelChoiceField): - """Extending string representation of Part instance with available stock.""" - - def label_from_instance(self, part): - - label = str(part) - - # Optionally display available part quantity - if common.models.InvenTreeSetting.get_setting('PART_SHOW_QUANTITY_IN_FORMS'): - label += f" - {part.available_stock}" - - return label - - class PartImageDownloadForm(HelperForm): """Form for downloading an image from a URL.""" @@ -40,6 +25,7 @@ class PartImageDownloadForm(HelperForm): ) class Meta: + """Metaclass defines fields for this form""" model = Part fields = [ 'url', @@ -78,6 +64,7 @@ class EditPartParameterTemplateForm(HelperForm): """Form for editing a PartParameterTemplate object.""" class Meta: + """Metaclass defines fields for this form""" model = PartParameterTemplate fields = [ 'name', @@ -97,6 +84,7 @@ class EditCategoryParameterTemplateForm(HelperForm): help_text=_('Add parameter template to all categories')) class Meta: + """Metaclass defines fields for this form""" model = PartCategoryParameterTemplate fields = [ 'category', @@ -118,6 +106,7 @@ class PartPriceForm(forms.Form): ) class Meta: + """Metaclass defines fields for this form""" model = Part fields = [ 'quantity', @@ -130,6 +119,7 @@ class EditPartSalePriceBreakForm(HelperForm): quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity')) class Meta: + """Metaclass defines fields for this form""" model = PartSellPriceBreak fields = [ 'part', @@ -144,6 +134,7 @@ class EditPartInternalPriceBreakForm(HelperForm): quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity')) class Meta: + """Metaclass defines fields for this form""" model = PartInternalPriceBreak fields = [ 'part', diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8834756b29..29a8f8ec13 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -103,12 +103,15 @@ class PartCategory(MetadataMixin, InvenTreeTree): @staticmethod def get_api_url(): + """Return the API url associated with the PartCategory model""" return reverse('api-part-category-list') def get_absolute_url(self): + """Return the web URL associated with the detail view for this PartCategory instance""" return reverse('category-detail', kwargs={'pk': self.id}) class Meta: + """Metaclass defines extra model properties""" verbose_name = _("Part Category") verbose_name_plural = _("Part Categories") @@ -131,6 +134,7 @@ class PartCategory(MetadataMixin, InvenTreeTree): @property def item_count(self): + """Return the number of parts contained in this PartCategory""" return self.partcount() def partcount(self, cascade=True, active=False): @@ -284,7 +288,7 @@ class PartManager(TreeManager): """ def get_queryset(self): - + """Perform default prefetch operations when accessing Part model from the database""" return super().get_queryset().prefetch_related( 'category', 'category__parent', @@ -333,6 +337,7 @@ class Part(MetadataMixin, MPTTModel): objects = PartManager() class Meta: + """Metaclass defines extra model properties""" verbose_name = _("Part") verbose_name_plural = _("Parts") ordering = ['name', ] @@ -341,12 +346,13 @@ class Part(MetadataMixin, MPTTModel): ] class MPTTMeta: + """MPTT metaclass definitions""" # For legacy reasons the 'variant_of' field is used to indicate the MPTT parent parent_attr = 'variant_of' @staticmethod def get_api_url(): - + """Return the list API endpoint URL associated with the Part model""" return reverse('api-part-list') def api_instance_filters(self): @@ -450,6 +456,7 @@ class Part(MetadataMixin, MPTTModel): pass def __str__(self): + """Return a string representation of the Part (for use in the admin interface)""" return f"{self.full_name} - {self.description}" def get_parts_in_bom(self, **kwargs): @@ -665,15 +672,6 @@ class Part(MetadataMixin, MPTTModel): return ' | '.join(elements) - def set_category(self, category): - - # Ignore if the category is already the same - if self.category == category: - return - - self.category = category - self.save() - def get_absolute_url(self): """Return the web URL for viewing this part.""" return reverse('part-detail', kwargs={'pk': self.id}) @@ -956,6 +954,7 @@ class Part(MetadataMixin, MPTTModel): @property def category_path(self): + """Return the category path of this Part instance""" if self.category: return self.category.pathstring return '' @@ -1442,6 +1441,7 @@ class Part(MetadataMixin, MPTTModel): @property def has_bom(self): + """Return True if this Part instance has any BOM items""" return self.get_bom_items().count() > 0 def get_trackable_parts(self): @@ -1605,7 +1605,17 @@ class Part(MetadataMixin, MPTTModel): return "{a} - {b}".format(a=min_price, b=max_price) def get_supplier_price_range(self, quantity=1): + """Return the supplier price range of this part: + - Checks if there is any supplier pricing information associated with this Part + - Iterate through available supplier pricing and select (min, max) + - Returns tuple of (min, max) + + Arguments: + quantity: Quantity at which to calculate price (default=1) + + Returns: (min, max) tuple or (None, None) if no supplier pricing available + """ min_price = None max_price = None @@ -1719,6 +1729,7 @@ class Part(MetadataMixin, MPTTModel): @property def has_price_breaks(self): + """Return True if this part has sale price breaks""" return self.price_breaks.count() > 0 @property @@ -1728,6 +1739,7 @@ class Part(MetadataMixin, MPTTModel): @property def unit_pricing(self): + """Returns the price of this Part at quantity=1""" return self.get_price(1) def add_price_break(self, quantity, price): @@ -1748,10 +1760,12 @@ class Part(MetadataMixin, MPTTModel): ) def get_internal_price(self, quantity, moq=True, multiples=True, currency=None): + """Return the internal price of this Part at the specified quantity""" return common.models.get_price(self, quantity, moq, multiples, currency, break_name='internal_price_breaks') @property def has_internal_price_breaks(self): + """Return True if this Part has internal pricing information""" return self.internal_price_breaks.count() > 0 @property @@ -1759,11 +1773,12 @@ class Part(MetadataMixin, MPTTModel): """Return the associated price breaks in the correct order.""" return self.internalpricebreaks.order_by('quantity').all() - @property - def internal_unit_pricing(self): - return self.get_internal_price(1) - def get_purchase_price(self, quantity): + """Calculate the purchase price for this part at the specified quantity + + - Looks at available supplier pricing data + - Calculates the price base on the closest price point + """ currency = currency_code_default() try: prices = [convert_money(item.purchase_price, currency).amount for item in self.stock_items.all() if item.purchase_price] @@ -1843,7 +1858,7 @@ class Part(MetadataMixin, MPTTModel): @transaction.atomic def copy_parameters_from(self, other, **kwargs): - + """Copy all parameter values from another Part instance""" clear = kwargs.get('clear', True) if clear: @@ -1920,12 +1935,9 @@ class Part(MetadataMixin, MPTTModel): return tests def getRequiredTests(self): - # Return the tests which are required by this part + """Return the tests which are required by this part""" return self.getTestTemplates(required=True) - def requiredTestCount(self): - return self.getRequiredTests().count() - @property def attachment_count(self): """Count the number of attachments for this part. @@ -2093,6 +2105,7 @@ class Part(MetadataMixin, MPTTModel): @property def related_count(self): + """Return the number of 'related parts' which point to this Part""" return len(self.get_related_parts()) def is_part_low_on_stock(self): @@ -2117,9 +2130,11 @@ class PartAttachment(InvenTreeAttachment): @staticmethod def get_api_url(): + """Return the list API endpoint URL associated with the PartAttachment model""" return reverse('api-part-attachment-list') def getSubdir(self): + """Returns the media subdirectory where part attachments are stored""" return os.path.join("part_files", str(self.part.id)) part = models.ForeignKey(Part, on_delete=models.CASCADE, @@ -2131,6 +2146,7 @@ class PartSellPriceBreak(common.models.PriceBreak): @staticmethod def get_api_url(): + """Return the list API endpoint URL associated with the PartSellPriceBreak model""" return reverse('api-part-sale-price-list') part = models.ForeignKey( @@ -2141,6 +2157,7 @@ class PartSellPriceBreak(common.models.PriceBreak): ) class Meta: + """Metaclass providing extra model definition""" unique_together = ('part', 'quantity') @@ -2149,6 +2166,7 @@ class PartInternalPriceBreak(common.models.PriceBreak): @staticmethod def get_api_url(): + """Return the list API endpoint URL associated with the PartInternalPriceBreak model""" return reverse('api-part-internal-price-list') part = models.ForeignKey( @@ -2158,6 +2176,7 @@ class PartInternalPriceBreak(common.models.PriceBreak): ) class Meta: + """Metaclass providing extra model definition""" unique_together = ('part', 'quantity') @@ -2176,6 +2195,7 @@ class PartStar(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('User'), related_name='starred_parts') class Meta: + """Metaclass providing extra model definition""" unique_together = [ 'part', 'user' @@ -2195,6 +2215,7 @@ class PartCategoryStar(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('User'), related_name='starred_categories') class Meta: + """Metaclass providing extra model definition""" unique_together = [ 'category', 'user', @@ -2216,16 +2237,17 @@ class PartTestTemplate(models.Model): @staticmethod def get_api_url(): + """Return the list API endpoint URL associated with the PartTestTemplate model""" return reverse('api-part-test-template-list') def save(self, *args, **kwargs): - + """Enforce 'clean' operation when saving a PartTestTemplate instance""" self.clean() super().save(*args, **kwargs) def clean(self): - + """Clean fields for the PartTestTemplate model""" self.test_name = self.test_name.strip() self.validate_unique() @@ -2320,9 +2342,11 @@ class PartParameterTemplate(models.Model): @staticmethod def get_api_url(): + """Return the list API endpoint URL associated with the PartParameterTemplate model""" return reverse('api-part-parameter-template-list') def __str__(self): + """Return a string representation of a PartParameterTemplate instance""" s = str(self.name) if self.units: s += " ({units})".format(units=self.units) @@ -2368,10 +2392,11 @@ class PartParameter(models.Model): @staticmethod def get_api_url(): + """Return the list API endpoint URL associated with the PartParameter model""" return reverse('api-part-parameter-list') def __str__(self): - # String representation of a PartParameter (used in the admin interface) + """String representation of a PartParameter (used in the admin interface)""" return "{part} : {param} = {data}{units}".format( part=str(self.part.full_name), param=str(self.template.name), @@ -2380,6 +2405,7 @@ class PartParameter(models.Model): ) class Meta: + """Metaclass providing extra model definition""" # Prevent multiple instances of a parameter for a single part unique_together = ('part', 'template') @@ -2391,6 +2417,7 @@ class PartParameter(models.Model): @classmethod def create(cls, part, template, data, save=False): + """Custom save method for the PartParameter class""" part_parameter = cls(part=part, template=template, data=data) if save: part_parameter.save() @@ -2408,6 +2435,7 @@ class PartCategoryParameterTemplate(models.Model): """ class Meta: + """Metaclass providing extra model definition""" constraints = [ UniqueConstraint(fields=['category', 'parameter_template'], name='unique_category_parameter_template_pair') @@ -2438,7 +2466,7 @@ class PartCategoryParameterTemplate(models.Model): help_text=_('Default Parameter Value')) -class BomItem(models.Model, DataImportMixin): +class BomItem(DataImportMixin, models.Model): """A BomItem links a part to its component items. A part can have a BOM (bill of materials) which defines @@ -2492,6 +2520,7 @@ class BomItem(models.Model, DataImportMixin): @staticmethod def get_api_url(): + """Return the list API endpoint URL associated with the BomItem model""" return reverse('api-bom-list') def get_valid_parts_for_allocation(self, allow_variants=True, allow_substitutes=True): @@ -2546,7 +2575,7 @@ class BomItem(models.Model, DataImportMixin): return Q(part__in=[part.pk for part in self.get_valid_parts_for_allocation()]) def save(self, *args, **kwargs): - + """Enforce 'clean' operation when saving a BomItem instance""" self.clean() super().save(*args, **kwargs) @@ -2686,12 +2715,14 @@ class BomItem(models.Model, DataImportMixin): raise ValidationError({'sub_part': _('Sub part must be specified')}) class Meta: + """Metaclass providing extra model definition""" verbose_name = _("BOM Item") # Prevent duplication of parent/child rows unique_together = ('part', 'sub_part') def __str__(self): + """Return a string representation of this BomItem instance""" return "{n} x {child} to make {parent}".format( parent=self.part.full_name, child=self.sub_part.full_name, @@ -2788,13 +2819,14 @@ class BomItemSubstitute(models.Model): """ class Meta: + """Metaclass providing extra model definition""" verbose_name = _("BOM Item Substitute") # Prevent duplication of substitute parts unique_together = ('part', 'bom_item') def save(self, *args, **kwargs): - + """Enforce a full_clean when saving the BomItemSubstitute model""" self.full_clean() super().save(*args, **kwargs) @@ -2814,6 +2846,7 @@ class BomItemSubstitute(models.Model): @staticmethod def get_api_url(): + """Returns the list API endpoint URL associated with this model""" return reverse('api-bom-substitute-list') bom_item = models.ForeignKey( @@ -2847,6 +2880,7 @@ class PartRelated(models.Model): verbose_name=_('Part 2'), help_text=_('Select Related Part')) def __str__(self): + """Return a string representation of this Part-Part relationship""" return f'{self.part_1} <--> {self.part_2}' def validate(self, part_1, part_2): diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 9e9f5ac7d1..4094821670 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -1,4 +1,4 @@ -"""JSON serializers for Part app.""" +"""DRF data serializers for Part app.""" import imghdr from decimal import Decimal @@ -37,10 +37,6 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment, class CategorySerializer(InvenTreeModelSerializer): """Serializer for PartCategory.""" - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - def get_starred(self, category): """Return True if the category is directly "starred" by the current user.""" return category in self.context.get('starred_categories', []) @@ -54,6 +50,7 @@ class CategorySerializer(InvenTreeModelSerializer): starred = serializers.SerializerMethodField() class Meta: + """Metaclass defining serializer fields""" model = PartCategory fields = [ 'pk', @@ -74,6 +71,7 @@ class CategoryTree(InvenTreeModelSerializer): """Serializer for PartCategory tree.""" class Meta: + """Metaclass defining serializer fields""" model = PartCategory fields = [ 'pk', @@ -86,6 +84,7 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer): """Serializer for the PartAttachment class.""" class Meta: + """Metaclass defining serializer fields""" model = PartAttachment fields = [ @@ -109,6 +108,7 @@ class PartTestTemplateSerializer(InvenTreeModelSerializer): key = serializers.CharField(read_only=True) class Meta: + """Metaclass defining serializer fields""" model = PartTestTemplate fields = [ @@ -142,6 +142,7 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): price_string = serializers.CharField(source='price', read_only=True) class Meta: + """Metaclass defining serializer fields""" model = PartSellPriceBreak fields = [ 'pk', @@ -172,6 +173,7 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer): price_string = serializers.CharField(source='price', read_only=True) class Meta: + """Metaclass defining serializer fields""" model = PartInternalPriceBreak fields = [ 'pk', @@ -206,6 +208,7 @@ class PartThumbSerializerUpdate(InvenTreeModelSerializer): image = InvenTreeAttachmentSerializerField(required=True) class Meta: + """Metaclass defining serializer fields""" model = Part fields = [ 'image', @@ -216,6 +219,7 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer): """JSON serializer for the PartParameterTemplate model.""" class Meta: + """Metaclass defining serializer fields""" model = PartParameterTemplate fields = [ 'pk', @@ -230,6 +234,7 @@ class PartParameterSerializer(InvenTreeModelSerializer): template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True) class Meta: + """Metaclass defining serializer fields""" model = PartParameter fields = [ 'pk', @@ -248,6 +253,7 @@ class PartBriefSerializer(InvenTreeModelSerializer): stock = serializers.FloatField(source='total_stock') class Meta: + """Metaclass defining serializer fields""" model = Part fields = [ 'pk', @@ -277,10 +283,14 @@ class PartSerializer(InvenTreeModelSerializer): """ def get_api_url(self): + """Return the API url associated with this serializer""" return reverse_lazy('api-part-list') def __init__(self, *args, **kwargs): - """Custom initialization method for PartSerializer, so that we can optionally pass extra fields based on the query.""" + """Custom initialization method for PartSerializer: + + - Allows us to optionally pass extra fields based on the query. + """ self.starred_parts = kwargs.pop('starred_parts', []) category_detail = kwargs.pop('category_detail', False) @@ -452,6 +462,7 @@ class PartSerializer(InvenTreeModelSerializer): ) class Meta: + """Metaclass defining serializer fields""" model = Part partial = True fields = [ @@ -503,6 +514,7 @@ class PartRelationSerializer(InvenTreeModelSerializer): part_2_detail = PartSerializer(source='part_2', read_only=True, many=False) class Meta: + """Metaclass defining serializer fields""" model = PartRelated fields = [ 'pk', @@ -520,6 +532,7 @@ class PartStarSerializer(InvenTreeModelSerializer): username = serializers.CharField(source='user.username', read_only=True) class Meta: + """Metaclass defining serializer fields""" model = PartStar fields = [ 'pk', @@ -536,6 +549,7 @@ class BomItemSubstituteSerializer(InvenTreeModelSerializer): part_detail = PartBriefSerializer(source='part', read_only=True, many=False) class Meta: + """Metaclass defining serializer fields""" model = BomItemSubstitute fields = [ 'pk', @@ -553,6 +567,7 @@ class BomItemSerializer(InvenTreeModelSerializer): quantity = InvenTreeDecimalField(required=True) def validate_quantity(self, quantity): + """Perform validation for the BomItem quantity field""" if quantity <= 0: raise serializers.ValidationError(_("Quantity must be greater than zero")) @@ -584,9 +599,11 @@ class BomItemSerializer(InvenTreeModelSerializer): available_variant_stock = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): - # part_detail and sub_part_detail serializers are only included if requested. - # This saves a bunch of database requests + """Determine if extra detail fields are to be annotated on this serializer + - part_detail and sub_part_detail serializers are only included if requested. + - This saves a bunch of database requests + """ part_detail = kwargs.pop('part_detail', False) sub_part_detail = kwargs.pop('sub_part_detail', False) include_pricing = kwargs.pop('include_pricing', False) @@ -609,6 +626,7 @@ class BomItemSerializer(InvenTreeModelSerializer): @staticmethod def setup_eager_loading(queryset): + """Prefetch against the provided queryset to speed up database access""" queryset = queryset.prefetch_related('part') queryset = queryset.prefetch_related('part__category') queryset = queryset.prefetch_related('part__stock_items') @@ -810,6 +828,7 @@ class BomItemSerializer(InvenTreeModelSerializer): return purchase_price_avg class Meta: + """Metaclass defining serializer fields""" model = BomItem fields = [ 'allow_variants', @@ -849,6 +868,7 @@ class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): category_detail = CategorySerializer(source='category', many=False, read_only=True) class Meta: + """Metaclass defining serializer fields""" model = PartCategoryParameterTemplate fields = [ 'pk', @@ -863,6 +883,7 @@ class PartCopyBOMSerializer(serializers.Serializer): """Serializer for copying a BOM from another part.""" class Meta: + """Metaclass defining serializer fields""" fields = [ 'part', 'remove_existing', @@ -929,6 +950,7 @@ class BomImportUploadSerializer(DataFileUploadSerializer): TARGET_MODEL = BomItem class Meta: + """Metaclass defining serializer fields""" fields = [ 'data_file', 'part', @@ -948,7 +970,7 @@ class BomImportUploadSerializer(DataFileUploadSerializer): ) def save(self): - + """The uploaded data file has been validated, accept the submitted data""" data = self.validated_data if data.get('clear_existing_bom', False): @@ -959,11 +981,15 @@ class BomImportUploadSerializer(DataFileUploadSerializer): class BomImportExtractSerializer(DataFileExtractSerializer): - """""" + """Serializer class for exatracting BOM data from an uploaded file. + + The parent class DataFileExtractSerializer does most of the heavy lifting here. + """ TARGET_MODEL = BomItem def validate_extracted_columns(self): + """Validate that the extracted columns are correct""" super().validate_extracted_columns() part_columns = ['part', 'part_name', 'part_ipn', 'part_id'] @@ -973,7 +999,7 @@ class BomImportExtractSerializer(DataFileExtractSerializer): raise serializers.ValidationError(_("No part column specified")) def process_row(self, row): - + """Process a single row from the loaded BOM file""" # Skip any rows which are at a lower "level" level = row.get('level', None) @@ -1050,7 +1076,10 @@ class BomImportSubmitSerializer(serializers.Serializer): items = BomItemSerializer(many=True, required=True) def validate(self, data): + """Validate the submitted BomItem data: + - At least one line (BomItem) is required + """ items = data['items'] if len(items) == 0: @@ -1061,7 +1090,11 @@ class BomImportSubmitSerializer(serializers.Serializer): return data def save(self): + """POST: Perform final save of submitted BOM data: + - By this stage each line in the BOM has been validated + - Individually 'save' (create) each BomItem line + """ data = self.validated_data items = data['items'] diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index cc824e2d61..db1443042a 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -1,3 +1,5 @@ +"""Background task definitions for the 'part' app""" + import logging from django.utils.translation import gettext_lazy as _ @@ -11,6 +13,11 @@ logger = logging.getLogger("inventree") def notify_low_stock(part: part.models.Part): + """Notify interested users that a part is 'low stock': + + - Triggered when the available stock for a given part falls be low the configured threhsold + - A notification is delivered to any users who are 'subscribed' to this part + """ name = _("Low stock notification") message = _(f'The available stock for {part.name} has fallen below the configured minimum level') context = { diff --git a/InvenTree/part/templatetags/__init__.py b/InvenTree/part/templatetags/__init__.py index e69de29bb2..ce063ef241 100644 --- a/InvenTree/part/templatetags/__init__.py +++ b/InvenTree/part/templatetags/__init__.py @@ -0,0 +1 @@ +"""Custom InvenTree template tags for HTML template rendering""" diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 2cdb78d326..e48c6f89c6 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -106,18 +106,6 @@ def str2bool(x, *args, **kwargs): return InvenTree.helpers.str2bool(x) -@register.simple_tag() -def inrange(n, *args, **kwargs): - """Return range(n) for iterating through a numeric quantity.""" - return range(n) - - -@register.simple_tag() -def multiply(x, y, *args, **kwargs): - """Multiply two numbers together.""" - return InvenTree.helpers.decimal2string(x * y) - - @register.simple_tag() def add(x, y, *args, **kwargs): """Add two numbers together.""" @@ -211,16 +199,19 @@ def inventree_version(shortstring=False, *args, **kwargs): @register.simple_tag() def inventree_is_development(*args, **kwargs): + """Returns True if this is a development version of InvenTree""" return version.isInvenTreeDevelopmentVersion() @register.simple_tag() def inventree_is_release(*args, **kwargs): + """Returns True if this is a release version of InvenTree""" return not version.isInvenTreeDevelopmentVersion() @register.simple_tag() def inventree_docs_version(*args, **kwargs): + """Returns the InvenTree documentation version""" return version.inventreeDocsVersion() @@ -367,6 +358,7 @@ def progress_bar(val, max_val, *args, **kwargs): @register.simple_tag() def get_color_theme_css(username): + """Return the cutsom theme .css file for the selected user""" user_theme_name = get_user_color_theme(username) # Build path to CSS sheet inventree_css_sheet = os.path.join('css', 'color-themes', user_theme_name + '.css') @@ -496,7 +488,7 @@ class I18nStaticNode(StaticNode): """ def render(self, context): # pragma: no cover - + """Render this node with the determined locale context.""" self.original = getattr(self, 'original', None) if not self.original: diff --git a/InvenTree/part/templatetags/status_codes.py b/InvenTree/part/templatetags/status_codes.py index 57c9fe630f..d3811c38f5 100644 --- a/InvenTree/part/templatetags/status_codes.py +++ b/InvenTree/part/templatetags/status_codes.py @@ -29,6 +29,7 @@ def stock_status_label(key, *args, **kwargs): @register.simple_tag def stock_status_text(key, *args, **kwargs): + """Render the text value of a StockItem status value""" return mark_safe(StockStatus.text(key)) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 015841f760..cade51c4fb 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1,3 +1,5 @@ +"""Unit tests for the various part API endpoints""" + from django.urls import reverse import PIL @@ -11,7 +13,8 @@ from company.models import Company from InvenTree.api_tester import InvenTreeAPITestCase from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, StockStatus) -from part.models import BomItem, BomItemSubstitute, Part, PartCategory +from part.models import (BomItem, BomItemSubstitute, Part, PartCategory, + PartRelated) from stock.models import StockItem, StockLocation @@ -40,8 +43,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase): ] def test_category_list(self): - - # List all part categories + """Test the PartCategoryList API endpoint""" url = reverse('api-part-category-list') response = self.get(url, expected_code=200) @@ -103,10 +105,6 @@ class PartOptionsAPITest(InvenTreeAPITestCase): 'part.add', ] - def setUp(self): - - super().setUp() - def test_part(self): """Test the Part API OPTIONS.""" actions = self.getActions(reverse('api-part-list'))['POST'] @@ -207,21 +205,18 @@ class PartAPITest(InvenTreeAPITestCase): 'part_category.add', ] - def setUp(self): - super().setUp() - def test_get_categories(self): """Test that we can retrieve list of part categories, with various filtering options.""" url = reverse('api-part-category-list') # Request *all* part categories - response = self.client.get(url, format='json') + response = self.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 8) # Request top-level part categories only - response = self.client.get( + response = self.get( url, { 'parent': 'null', @@ -232,7 +227,7 @@ class PartAPITest(InvenTreeAPITestCase): self.assertEqual(len(response.data), 2) # Children of PartCategory<1>, cascade - response = self.client.get( + response = self.get( url, { 'parent': 1, @@ -244,7 +239,7 @@ class PartAPITest(InvenTreeAPITestCase): self.assertEqual(len(response.data), 5) # Children of PartCategory<1>, do not cascade - response = self.client.get( + response = self.get( url, { 'parent': 1, @@ -263,7 +258,7 @@ class PartAPITest(InvenTreeAPITestCase): } url = reverse('api-part-category-list') - response = self.client.post(url, data, format='json') + response = self.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) parent = response.data['pk'] @@ -275,19 +270,20 @@ class PartAPITest(InvenTreeAPITestCase): 'description': 'A sort of animal', 'parent': parent, } - response = self.client.post(url, data, format='json') + response = self.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data['parent'], parent) self.assertEqual(response.data['name'], animal) self.assertEqual(response.data['pathstring'], 'Animals/' + animal) # There should be now 8 categories - response = self.client.get(url, format='json') + response = self.get(url, format='json') self.assertEqual(len(response.data), 12) def test_cat_detail(self): + """Test the PartCategoryDetail API endpoint""" url = reverse('api-part-category-detail', kwargs={'pk': 4}) - response = self.client.get(url, format='json') + response = self.get(url, format='json') # Test that we have retrieved the category self.assertEqual(response.data['description'], 'Integrated Circuits') @@ -298,22 +294,22 @@ class PartAPITest(InvenTreeAPITestCase): data['name'] = 'Changing category' data['parent'] = None data['description'] = 'Changing the description' - response = self.client.patch(url, data, format='json') + response = self.patch(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['description'], 'Changing the description') self.assertIsNone(response.data['parent']) - def test_get_all_parts(self): + def test_filter_parts(self): + """Test part filtering using the API""" url = reverse('api-part-list') data = {'cascade': True} - response = self.client.get(url, data, format='json') + response = self.get(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), Part.objects.count()) - def test_get_parts_by_cat(self): - url = reverse('api-part-list') + # Test filtering parts by category data = {'category': 2} - response = self.client.get(url, data, format='json') + response = self.get(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) # There should only be 2 objects in category C @@ -322,6 +318,28 @@ class PartAPITest(InvenTreeAPITestCase): for part in response.data: self.assertEqual(part['category'], 2) + def test_filter_by_related(self): + """Test that we can filter by the 'related' status""" + url = reverse('api-part-list') + + # Initially there are no relations, so this should return zero results + response = self.get(url, {'related': 1}, expected_code=200) + self.assertEqual(len(response.data), 0) + + # Add some relationships + PartRelated.objects.create( + part_1=Part.objects.get(pk=1), + part_2=Part.objects.get(pk=2), + ) + + PartRelated.objects.create( + part_2=Part.objects.get(pk=1), + part_1=Part.objects.get(pk=3) + ) + + response = self.get(url, {'related': 1}, expected_code=200) + self.assertEqual(len(response.data), 2) + def test_include_children(self): """Test the special 'include_child_categories' flag. @@ -331,31 +349,31 @@ class PartAPITest(InvenTreeAPITestCase): data = {'category': 1, 'cascade': True} # Now request to include child categories - response = self.client.get(url, data, format='json') + response = self.get(url, data, format='json') # Now there should be 5 total parts self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 3) def test_test_templates(self): - + """Test the PartTestTemplate API""" url = reverse('api-part-test-template-list') # List ALL items - response = self.client.get(url) + response = self.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 7) # Request for a particular part - response = self.client.get(url, data={'part': 10000}) + response = self.get(url, data={'part': 10000}) self.assertEqual(len(response.data), 5) - response = self.client.get(url, data={'part': 10004}) + response = self.get(url, data={'part': 10004}) self.assertEqual(len(response.data), 7) # Try to post a new object (missing description) - response = self.client.post( + response = self.post( url, data={ 'part': 10000, @@ -367,7 +385,7 @@ class PartAPITest(InvenTreeAPITestCase): self.assertEqual(response.status_code, 400) # Try to post a new object (should succeed) - response = self.client.post( + response = self.post( url, data={ 'part': 10000, @@ -381,7 +399,7 @@ class PartAPITest(InvenTreeAPITestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) # Try to post a new test with the same name (should fail) - response = self.client.post( + response = self.post( url, data={ 'part': 10004, @@ -394,7 +412,7 @@ class PartAPITest(InvenTreeAPITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # Try to post a new test against a non-trackable part (should fail) - response = self.client.post( + response = self.post( url, data={ 'part': 1, @@ -408,7 +426,7 @@ class PartAPITest(InvenTreeAPITestCase): """Return list of part thumbnails.""" url = reverse('api-part-thumbs') - response = self.client.get(url) + response = self.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -432,7 +450,7 @@ class PartAPITest(InvenTreeAPITestCase): """ url = reverse('api-part-list') - response = self.client.post(url, { + response = self.post(url, { 'name': 'all defaults', 'description': 'my test part', 'category': 1, @@ -454,7 +472,7 @@ class PartAPITest(InvenTreeAPITestCase): self.user ) - response = self.client.post(url, { + response = self.post(url, { 'name': 'all defaults', 'description': 'my test part 2', 'category': 1, @@ -464,7 +482,7 @@ class PartAPITest(InvenTreeAPITestCase): self.assertTrue(response.data['purchaseable']) # "default" values should not be used if the value is specified - response = self.client.post(url, { + response = self.post(url, { 'name': 'all defaults', 'description': 'my test part 2', 'category': 1, @@ -858,14 +876,12 @@ class PartDetailTests(InvenTreeAPITestCase): 'part_category.add', ] - def setUp(self): - super().setUp() - def test_part_operations(self): + """Test that Part instances can be adjusted via the API""" n = Part.objects.count() # Create a part - response = self.client.post( + response = self.post( reverse('api-part-list'), { 'name': 'my test api part', @@ -890,7 +906,7 @@ class PartDetailTests(InvenTreeAPITestCase): # Let's change the name of the part - response = self.client.patch(url, { + response = self.patch(url, { 'name': 'a new better name', }) @@ -908,14 +924,14 @@ class PartDetailTests(InvenTreeAPITestCase): # Now, try to set the name to the *same* value # 2021-06-22 this test is to check that the "duplicate part" checks don't do strange things - response = self.client.patch(url, { + response = self.patch(url, { 'name': 'a new better name', }) self.assertEqual(response.status_code, 200) # Try to remove the part - response = self.client.delete(url) + response = self.delete(url) # As the part is 'active' we cannot delete it self.assertEqual(response.status_code, 405) @@ -923,7 +939,7 @@ class PartDetailTests(InvenTreeAPITestCase): # So, let's make it not active response = self.patch(url, {'active': False}, expected_code=200) - response = self.client.delete(url) + response = self.delete(url) self.assertEqual(response.status_code, 204) # Part count should have reduced @@ -932,7 +948,7 @@ class PartDetailTests(InvenTreeAPITestCase): def test_duplicates(self): """Check that trying to create 'duplicate' parts results in errors.""" # Create a part - response = self.client.post(reverse('api-part-list'), { + response = self.post(reverse('api-part-list'), { 'name': 'part', 'description': 'description', 'IPN': 'IPN-123', @@ -945,7 +961,7 @@ class PartDetailTests(InvenTreeAPITestCase): n = Part.objects.count() # Check that we cannot create a duplicate in a different category - response = self.client.post(reverse('api-part-list'), { + response = self.post(reverse('api-part-list'), { 'name': 'part', 'description': 'description', 'IPN': 'IPN-123', @@ -968,7 +984,7 @@ class PartDetailTests(InvenTreeAPITestCase): self.assertEqual(Part.objects.count(), n) # But a different 'revision' *can* be created - response = self.client.post(reverse('api-part-list'), { + response = self.post(reverse('api-part-list'), { 'name': 'part', 'description': 'description', 'IPN': 'IPN-123', @@ -985,7 +1001,7 @@ class PartDetailTests(InvenTreeAPITestCase): url = reverse('api-part-detail', kwargs={'pk': pk}) # Attempt to alter the revision code - response = self.client.patch( + response = self.patch( url, { 'revision': 'A', @@ -996,7 +1012,7 @@ class PartDetailTests(InvenTreeAPITestCase): self.assertEqual(response.status_code, 400) # But we *can* change it to a unique revision code - response = self.client.patch( + response = self.patch( url, { 'revision': 'C', @@ -1010,7 +1026,7 @@ class PartDetailTests(InvenTreeAPITestCase): self.assignRole('part.add') # Create a new part - response = self.client.post( + response = self.post( reverse('api-part-list'), { 'name': 'imagine', @@ -1175,7 +1191,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase): ] def setUp(self): - + """Create test data as part of setup routine""" super().setUp() # Ensure the part "variant" tree is correctly structured @@ -1199,9 +1215,10 @@ class PartAPIAggregationTest(InvenTreeAPITestCase): StockItem.objects.create(part=self.part, quantity=400, status=StockStatus.LOST) def get_part_data(self): + """Helper function for retrieving part data""" url = reverse('api-part-list') - response = self.client.get(url, format='json') + response = self.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1397,9 +1414,6 @@ class BomItemTest(InvenTreeAPITestCase): 'part.delete', ] - def setUp(self): - super().setUp() - def test_bom_list(self): """Tests for the BomItem list endpoint.""" # How many BOM items currently exist in the database? @@ -1518,7 +1532,7 @@ class BomItemTest(InvenTreeAPITestCase): # Now try to create a BomItem which references itself data['part'] = 100 data['sub_part'] = 100 - self.client.post(url, data, expected_code=400) + self.post(url, data, expected_code=400) def test_variants(self): """Tests for BomItem use with variants.""" @@ -1781,20 +1795,16 @@ class PartParameterTest(InvenTreeAPITestCase): 'params', ] - def setUp(self): - - super().setUp() - def test_list_params(self): """Test for listing part parameters.""" url = reverse('api-part-parameter-list') - response = self.client.get(url, format='json') + response = self.get(url, format='json') self.assertEqual(len(response.data), 5) # Filter by part - response = self.client.get( + response = self.get( url, { 'part': 3, @@ -1805,7 +1815,7 @@ class PartParameterTest(InvenTreeAPITestCase): self.assertEqual(len(response.data), 3) # Filter by template - response = self.client.get( + response = self.get( url, { 'template': 1, @@ -1819,7 +1829,7 @@ class PartParameterTest(InvenTreeAPITestCase): """Test that we can create a param via the API.""" url = reverse('api-part-parameter-list') - response = self.client.post( + response = self.post( url, { 'part': '2', @@ -1830,7 +1840,7 @@ class PartParameterTest(InvenTreeAPITestCase): self.assertEqual(response.status_code, 201) - response = self.client.get(url, format='json') + response = self.get(url, format='json') self.assertEqual(len(response.data), 6) @@ -1838,7 +1848,7 @@ class PartParameterTest(InvenTreeAPITestCase): """Tests for the PartParameter detail endpoint.""" url = reverse('api-part-parameter-detail', kwargs={'pk': 5}) - response = self.client.get(url) + response = self.get(url) self.assertEqual(response.status_code, 200) @@ -1849,12 +1859,12 @@ class PartParameterTest(InvenTreeAPITestCase): self.assertEqual(data['data'], '12') # PATCH data back in - response = self.client.patch(url, {'data': '15'}, format='json') + response = self.patch(url, {'data': '15'}, format='json') self.assertEqual(response.status_code, 200) # Check that the data changed! - response = self.client.get(url, format='json') + response = self.get(url, format='json') data = response.data diff --git a/InvenTree/part/test_bom_export.py b/InvenTree/part/test_bom_export.py index 67b31a812d..d7e92d1da4 100644 --- a/InvenTree/part/test_bom_export.py +++ b/InvenTree/part/test_bom_export.py @@ -8,6 +8,7 @@ from InvenTree.helpers import InvenTreeTestCase class BomExportTest(InvenTreeTestCase): + """Class for performing unit testing of BOM export functionality""" fixtures = [ 'category', @@ -19,6 +20,7 @@ class BomExportTest(InvenTreeTestCase): roles = 'all' def setUp(self): + """Perform test setup functions""" super().setUp() self.url = reverse('bom-download', kwargs={'pk': 100}) diff --git a/InvenTree/part/test_bom_import.py b/InvenTree/part/test_bom_import.py index bce7ac4d2f..71f47510a1 100644 --- a/InvenTree/part/test_bom_import.py +++ b/InvenTree/part/test_bom_import.py @@ -18,6 +18,7 @@ class BomUploadTest(InvenTreeAPITestCase): ] def setUp(self): + """Create BOM data as part of setup routine""" super().setUp() self.part = Part.objects.create( @@ -37,7 +38,7 @@ class BomUploadTest(InvenTreeAPITestCase): ) def post_bom(self, filename, file_data, clear_existing=None, expected_code=None, content_type='text/plain'): - + """Helper function for submitting a BOM file""" bom_file = SimpleUploadedFile( filename, file_data, diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py index c17f7a845d..58c5dfb032 100644 --- a/InvenTree/part/test_bom_item.py +++ b/InvenTree/part/test_bom_item.py @@ -1,4 +1,6 @@ +"""Unit tests for the BomItem model""" + from decimal import Decimal import django.core.exceptions as django_exceptions @@ -9,6 +11,7 @@ from .models import BomItem, BomItemSubstitute, Part class BomItemTest(TestCase): + """Class for unit testing BomItem model""" fixtures = [ 'category', @@ -22,21 +25,25 @@ class BomItemTest(TestCase): ] def setUp(self): + """Create initial data""" self.bob = Part.objects.get(id=100) self.orphan = Part.objects.get(name='Orphan') self.r1 = Part.objects.get(name='R_2K2_0805') def test_str(self): + """Test the string representation of a BOMItem""" b = BomItem.objects.get(id=1) self.assertEqual(str(b), '10 x M2x4 LPHS to make BOB | Bob | A2') def test_has_bom(self): + """Test the has_bom attribute""" self.assertFalse(self.orphan.has_bom) self.assertTrue(self.bob.has_bom) self.assertEqual(self.bob.bom_count, 4) def test_in_bom(self): + """Test BOM aggregation""" parts = self.bob.getRequiredParts() self.assertIn(self.orphan, parts) @@ -44,6 +51,7 @@ class BomItemTest(TestCase): self.assertTrue(self.bob.check_if_part_in_bom(self.orphan)) def test_used_in(self): + """Test that the 'used_in_count' attribute is calculated correctly""" self.assertEqual(self.bob.used_in_count, 1) self.assertEqual(self.orphan.used_in_count, 1) @@ -116,6 +124,7 @@ class BomItemTest(TestCase): self.assertNotEqual(h1, h2) def test_pricing(self): + """Test BOM pricing""" self.bob.get_price(1) self.assertEqual( self.bob.get_bom_price_range(1, internal=True), diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index 8270341ce9..bdc8fcc8b2 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -1,3 +1,5 @@ +"""Unit tests for the PartCategory model""" + from django.core.exceptions import ValidationError from django.test import TestCase @@ -18,7 +20,7 @@ class CategoryTest(TestCase): ] def setUp(self): - # Extract some interesting categories for time-saving + """Extract some interesting categories for time-saving""" self.electronics = PartCategory.objects.get(name='Electronics') self.mechanical = PartCategory.objects.get(name='Mechanical') self.resistors = PartCategory.objects.get(name='Resistors') @@ -111,8 +113,7 @@ class CategoryTest(TestCase): self.assertEqual(len(part_parameter), 1) def test_invalid_name(self): - # Test that an illegal character is prohibited in a category name - + """Test that an illegal character is prohibited in a category name""" cat = PartCategory(name='test/with/illegal/chars', description='Test category', parent=None) with self.assertRaises(ValidationError) as err: diff --git a/InvenTree/part/test_migrations.py b/InvenTree/part/test_migrations.py index 56b6f664ef..f3562af872 100644 --- a/InvenTree/part/test_migrations.py +++ b/InvenTree/part/test_migrations.py @@ -32,7 +32,7 @@ class TestForwardMigrations(MigratorTestCase): print(p.is_template) def test_models_exist(self): - + """Test that the Part model can still be accessed at the end of schema migration""" Part = self.new_state.apps.get_model('part', 'part') self.assertEqual(Part.objects.count(), 5) @@ -42,3 +42,7 @@ class TestForwardMigrations(MigratorTestCase): part.save() part.is_template = False part.save() + + for name in ['A', 'C', 'E']: + part = Part.objects.get(name=name) + self.assertEqual(part.description, f"My part {name}") diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index 1db23f0be2..7a13bfa6ce 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -1,4 +1,4 @@ -# Tests for Part Parameters +"""Various unit tests for Part Parameters""" import django.core.exceptions as django_exceptions from django.test import TestCase, TransactionTestCase @@ -8,6 +8,7 @@ from .models import (Part, PartCategory, PartCategoryParameterTemplate, class TestParams(TestCase): + """Unit test class for testing the PartParameter model""" fixtures = [ 'location', @@ -17,7 +18,7 @@ class TestParams(TestCase): ] def test_str(self): - + """Test the str representation of the PartParameterTemplate model""" t1 = PartParameterTemplate.objects.get(pk=1) self.assertEqual(str(t1), 'Length (mm)') @@ -28,7 +29,7 @@ class TestParams(TestCase): self.assertEqual(str(c1), 'Mechanical | Length | 2.8') def test_validate(self): - + """Test validation for part templates""" n = PartParameterTemplate.objects.all().count() t1 = PartParameterTemplate(name='abcde', units='dd') @@ -44,6 +45,7 @@ class TestParams(TestCase): class TestCategoryTemplates(TransactionTestCase): + """Test class for PartCategoryParameterTemplate model""" fixtures = [ 'location', @@ -53,7 +55,7 @@ class TestCategoryTemplates(TransactionTestCase): ] def test_validate(self): - + """Test that category templates are correctly applied to Part instances""" # Category templates n = PartCategoryParameterTemplate.objects.all().count() self.assertEqual(n, 2) @@ -79,6 +81,7 @@ class TestCategoryTemplates(TransactionTestCase): 'main': True, 'parent': True, } + # Save it with category parameters part.save(**{'add_category_templates': add_category_templates}) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 0069a5e4e2..b3c0ec5aa8 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -15,8 +15,8 @@ from common.notifications import UIMessageNotification, storage from InvenTree import version from InvenTree.helpers import InvenTreeTestCase -from .models import (Part, PartCategory, PartCategoryStar, PartStar, - PartTestTemplate, rename_part_image) +from .models import (Part, PartCategory, PartCategoryStar, PartRelated, + PartStar, PartTestTemplate, rename_part_image) from .templatetags import inventree_extras @@ -24,39 +24,42 @@ class TemplateTagTest(InvenTreeTestCase): """Tests for the custom template tag code.""" def test_define(self): + """Test the 'define' template tag""" self.assertEqual(int(inventree_extras.define(3)), 3) def test_str2bool(self): + """Various test for the str2bool template tag""" self.assertEqual(int(inventree_extras.str2bool('true')), True) self.assertEqual(int(inventree_extras.str2bool('yes')), True) self.assertEqual(int(inventree_extras.str2bool('none')), False) self.assertEqual(int(inventree_extras.str2bool('off')), False) - def test_inrange(self): - self.assertEqual(inventree_extras.inrange(3), range(3)) - - def test_multiply(self): - self.assertEqual(int(inventree_extras.multiply(3, 5)), 15) - def test_add(self): + """Test that the 'add""" self.assertEqual(int(inventree_extras.add(3, 5)), 8) def test_plugins_enabled(self): + """Test the plugins_enabled tag""" self.assertEqual(inventree_extras.plugins_enabled(), True) def test_inventree_instance_name(self): + """Test the 'instance name' setting""" self.assertEqual(inventree_extras.inventree_instance_name(), 'InvenTree server') def test_inventree_base_url(self): + """Test that the base URL tag returns correctly""" self.assertEqual(inventree_extras.inventree_base_url(), '') def test_inventree_is_release(self): + """Test that the release version check functions as expected""" self.assertEqual(inventree_extras.inventree_is_release(), not version.isInvenTreeDevelopmentVersion()) def test_inventree_docs_version(self): + """Test that the documentation version template tag returns correctly""" self.assertEqual(inventree_extras.inventree_docs_version(), version.inventreeDocsVersion()) def test_hash(self): + """Test that the commit hash template tag returns correctly""" result_hash = inventree_extras.inventree_commit_hash() if settings.DOCKER: # pragma: no cover # Testing inside docker environment *may* return an empty git commit hash @@ -66,6 +69,7 @@ class TemplateTagTest(InvenTreeTestCase): self.assertGreater(len(result_hash), 5) def test_date(self): + """Test that the commit date template tag returns correctly""" d = inventree_extras.inventree_commit_date() if settings.DOCKER: # pragma: no cover # Testing inside docker environment *may* return an empty git commit hash @@ -75,26 +79,33 @@ class TemplateTagTest(InvenTreeTestCase): self.assertEqual(len(d.split('-')), 3) def test_github(self): + """Test that the github URL template tag returns correctly""" self.assertIn('github.com', inventree_extras.inventree_github_url()) def test_docs(self): + """Test that the documentation URL template tag returns correctly""" self.assertIn('inventree.readthedocs.io', inventree_extras.inventree_docs_url()) def test_keyvalue(self): + """Test keyvalue template tag""" self.assertEqual(inventree_extras.keyvalue({'a': 'a'}, 'a'), 'a') def test_mail_configured(self): + """Test that mail configuration returns False""" self.assertEqual(inventree_extras.mail_configured(), False) def test_user_settings(self): + """Test user settings""" result = inventree_extras.user_settings(self.user) self.assertEqual(len(result), len(InvenTreeUserSetting.SETTINGS)) def test_global_settings(self): + """Test global settings""" result = inventree_extras.global_settings() self.assertEqual(len(result), len(InvenTreeSetting.SETTINGS)) def test_visible_global_settings(self): + """Test that hidden global settings are actually hidden""" result = inventree_extras.visible_global_settings() n = len(result) @@ -122,6 +133,9 @@ class PartTest(TestCase): ] def setUp(self): + """Create some Part instances as part of init routine""" + super().setUp() + self.r1 = Part.objects.get(name='R_2K2_0805') self.r2 = Part.objects.get(name='R_4K7_0603') @@ -130,7 +144,7 @@ class PartTest(TestCase): Part.objects.rebuild() def test_tree(self): - # Test that the part variant tree is working properly + """Test that the part variant tree is working properly""" chair = Part.objects.get(pk=10000) self.assertEqual(chair.get_children().count(), 3) self.assertEqual(chair.get_descendant_count(), 4) @@ -142,9 +156,23 @@ class PartTest(TestCase): self.assertEqual(Part.objects.filter(tree_id=chair.tree_id).count(), 5) def test_str(self): + """Test string representation of a Part""" p = Part.objects.get(pk=100) self.assertEqual(str(p), "BOB | Bob | A2 - Can we build it?") + def test_related(self): + """Test the part relationship functionality""" + self.assertEqual(self.r1.related_count, 0) + self.assertEqual(self.r2.related_count, 0) + + PartRelated.objects.create(part_1=self.r1, part_2=self.r2) + + self.assertEqual(self.r1.related_count, 1) + self.assertEqual(self.r2.related_count, 1) + + self.assertTrue(self.r2 in self.r1.get_related_parts()) + self.assertTrue(self.r1 in self.r2.get_related_parts()) + def test_duplicate(self): """Test that we cannot create a "duplicate" Part.""" n = Part.objects.count() @@ -198,10 +226,12 @@ class PartTest(TestCase): part_2.validate_unique() def test_attributes(self): + """Test Part attributes""" self.assertEqual(self.r1.name, 'R_2K2_0805') self.assertEqual(self.r1.get_absolute_url(), '/part/3/') def test_category(self): + """Test PartCategory path""" self.assertEqual(str(self.c1.category), 'Electronics/Capacitors - Capacitors') orphan = Part.objects.get(name='Orphan') @@ -209,26 +239,29 @@ class PartTest(TestCase): self.assertEqual(orphan.category_path, '') def test_rename_img(self): + """Test that an image can be renamed""" img = rename_part_image(self.r1, 'hello.png') self.assertEqual(img, os.path.join('part_images', 'hello.png')) def test_stock(self): - # No stock of any resistors + """Test case where there is zero stock""" res = Part.objects.filter(description__contains='resistor') for r in res: self.assertEqual(r.total_stock, 0) self.assertEqual(r.available_stock, 0) def test_barcode(self): + """Test barcode format functionality""" barcode = self.r1.format_barcode(brief=False) self.assertIn('InvenTree', barcode) self.assertIn(self.r1.name, barcode) def test_copy(self): + """Test that we can 'deep copy' a Part instance""" self.r2.deep_copy(self.r1, image=True, bom=True) def test_sell_pricing(self): - # check that the sell pricebreaks were loaded + """Check that the sell pricebreaks were loaded""" self.assertTrue(self.r1.has_price_breaks) self.assertEqual(self.r1.price_breaks.count(), 2) # check that the sell pricebreaks work @@ -236,7 +269,7 @@ class PartTest(TestCase): self.assertEqual(float(self.r1.get_price(10)), 1.0) def test_internal_pricing(self): - # check that the sell pricebreaks were loaded + """Check that the sell pricebreaks were loaded""" self.assertTrue(self.r1.has_internal_price_breaks) self.assertEqual(self.r1.internal_price_breaks.count(), 2) # check that the sell pricebreaks work @@ -262,6 +295,7 @@ class PartTest(TestCase): class TestTemplateTest(TestCase): + """Unit test for the TestTemplate class""" fixtures = [ 'category', @@ -271,7 +305,7 @@ class TestTemplateTest(TestCase): ] def test_template_count(self): - + """Tests for the test template functions""" chair = Part.objects.get(pk=10000) # Tests for the top-level chair object (nothing above it!) @@ -288,8 +322,7 @@ class TestTemplateTest(TestCase): self.assertEqual(variant.getTestTemplates(required=True).count(), 5) def test_uniqueness(self): - # Test names must be unique for this part and also parts above - + """Test names must be unique for this part and also parts above""" variant = Part.objects.get(pk=10004) with self.assertRaises(ValidationError): @@ -424,6 +457,7 @@ class PartSettingsTest(InvenTreeTestCase): class PartSubscriptionTests(InvenTreeTestCase): + """Unit tests for part 'subscription'""" fixtures = [ 'location', @@ -432,6 +466,7 @@ class PartSubscriptionTests(InvenTreeTestCase): ] def setUp(self): + """Create category and part data as part of setup routine""" super().setUp() # electronics / IC / MCU @@ -531,6 +566,7 @@ class BaseNotificationIntegrationTest(InvenTreeTestCase): ] def setUp(self): + """Add an email address as part of initialization""" super().setUp() # Add Mailadress EmailAddress.objects.create(user=self.user, email='test@testing.com') @@ -568,6 +604,7 @@ class PartNotificationTest(BaseNotificationIntegrationTest): """Integration test for part notifications.""" def test_notification(self): + """Test that a notification is generated""" self._notification_run(UIMessageNotification) # There should be 1 notification message right now diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 3964fb91f5..761356a5de 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -8,6 +8,7 @@ from .models import Part class PartViewTestCase(InvenTreeTestCase): + """Base class for unit testing the various Part views""" fixtures = [ 'category', @@ -21,13 +22,12 @@ class PartViewTestCase(InvenTreeTestCase): roles = 'all' superuser = True - def setUp(self): - super().setUp() - class PartListTest(PartViewTestCase): + """Unit tests for the PartList view""" def test_part_index(self): + """Test that the PartIndex page returns successfully""" response = self.client.get(reverse('part-index')) self.assertEqual(response.status_code, 200) @@ -38,6 +38,7 @@ class PartListTest(PartViewTestCase): class PartDetailTest(PartViewTestCase): + """Unit tests for the PartDetail view""" def test_part_detail(self): """Test that we can retrieve a part detail page.""" @@ -67,6 +68,7 @@ class PartDetailTest(PartViewTestCase): pk = 1 def test_ipn_match(index_result=False, detail_result=False): + """Helper function for matching IPN detail view""" index_redirect = False detail_redirect = False @@ -117,11 +119,12 @@ class PartQRTest(PartViewTestCase): """Tests for the Part QR Code AJAX view.""" def test_html_redirect(self): - # A HTML request for a QR code should be redirected (use an AJAX request instead) + """A HTML request for a QR code should be redirected (use an AJAX request instead)""" response = self.client.get(reverse('part-qr', args=(1,))) self.assertEqual(response.status_code, 302) def test_valid_part(self): + """Test QR code response for a Part""" response = self.client.get(reverse('part-qr', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) @@ -131,6 +134,7 @@ class PartQRTest(PartViewTestCase): self.assertIn('