2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00

Docstring checks in QC checks (#3089)

* Add pre-commit to the stack

* exclude static

* Add locales to excludes

* fix style errors

* rename pipeline steps

* also wait on precommit

* make template matching simpler

* Use the same code for python setup everywhere

* use step and cache for python setup

* move regular settings up into general envs

* just use full update

* Use invoke instead of static references

* make setup actions more similar

* use python3

* refactor names to be similar

* fix runner version

* fix references

* remove incidential change

* use matrix for os

* Github can't do this right now

* ignore docstyle errors

* Add seperate docstring test

* update flake call

* do not fail on docstring

* refactor setup into workflow

* update reference

* switch to action

* resturcture

* add bash statements

* remove os from cache

* update input checks

* make code cleaner

* fix boolean

* no relative paths

* install wheel by python

* switch to install

* revert back to simple wheel

* refactor import export tests

* move setup keys back to not disturbe tests

* remove docstyle till that is fixed

* update references

* continue on error

* add docstring test

* use relativ action references

* Change step / job docstrings

* update to merge

* reformat comments 1

* fix docstrings 2

* fix docstrings 3

* fix docstrings 4

* fix docstrings 5

* fix docstrings 6

* fix docstrings 7

* fix docstrings 8

* fix docstirns 9

* fix docstrings 10

* docstring adjustments

* update the remaining docstrings

* small docstring changes

* fix function name

* update support files for docstrings

* Add missing args to docstrings

* Remove outdated function

* Add docstrings for the 'build' app

* Make API code cleaner

* add more docstrings for plugin app

* Remove dead code for plugin settings
No idea what that was even intended for

* ignore __init__ files for docstrings

* More docstrings

* Update docstrings for the 'part' directory

* Fixes for related_part functionality

* Fix removed stuff from merge 99676ee

* make more consistent

* Show statistics for docstrings

* add more docstrings

* move specific register statements to make them clearer to understant

* More docstrings for common

* and more docstrings

* and more

* simpler call

* docstrings for notifications

* docstrings for common/tests

* Add docs for common/models

* Revert "move specific register statements to make them clearer to understant"

This reverts commit ca96654622.

* use typing here

* Revert "Make API code cleaner"

This reverts commit 24fb68bd3e.

* docstring updates for the 'users' app

* Add generic Meta info to simple Meta classes

* remove unneeded unique_together statements

* More simple metas

* Remove unnecessary format specifier

* Remove extra json format specifiers

* Add docstrings for the 'plugin' app

* Docstrings for the 'label' app

* Add missing docstrings for the 'report' app

* Fix build test regression

* Fix top-level files

* docstrings for InvenTree/InvenTree

* reduce unneeded code

* add docstrings

* and more docstrings

* more docstrings

* more docstrings for stock

* more docstrings

* docstrings for order/views

* Docstrings for various files in the 'order' app

* Docstrings for order/test_api.py

* Docstrings for order/serializers.py

* Docstrings for order/admin.py

* More docstrings for the order app

* Add docstrings for the 'company' app

* Add unit tests for rebuilding the reference fields

* Prune out some more dead code

* remove more dead code

Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
Matthias Mair
2022-06-01 17:37:39 +02:00
committed by GitHub
parent 66a6915213
commit 0c97a50e47
223 changed files with 4416 additions and 6980 deletions

View File

@ -1,9 +1 @@
"""
The Part module is responsible for Part management.
It includes models for:
- PartCategory
- Part
- BomItem
"""
"""The Part module is responsible for Part management."""

View File

@ -1,3 +1,5 @@
"""Admin class definitions for the 'part' app"""
from django.contrib import admin
import import_export.widgets as widgets
@ -11,7 +13,7 @@ from stock.models import StockLocation
class PartResource(ModelResource):
""" Class for managing Part data import/export """
"""Class for managing Part data import/export."""
# ForeignKey fields
category = Field(attribute='category', widget=widgets.ForeignKeyWidget(models.PartCategory))
@ -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
@ -49,8 +52,7 @@ class PartResource(ModelResource):
]
def get_queryset(self):
""" Prefetch related data for quicker access """
"""Prefetch related data for quicker access."""
query = super().get_queryset()
query = query.prefetch_related(
'category',
@ -62,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
@ -82,7 +93,7 @@ class PartAdmin(ImportExportModelAdmin):
class PartCategoryResource(ModelResource):
""" Class for managing PartCategory data import/export """
"""Class for managing PartCategory data import/export."""
parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(models.PartCategory))
@ -91,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
@ -103,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)
@ -111,6 +124,7 @@ class PartCategoryResource(ModelResource):
class PartCategoryAdmin(ImportExportModelAdmin):
"""Admin class for the PartCategory model"""
resource_class = PartCategoryResource
@ -122,35 +136,21 @@ class PartCategoryAdmin(ImportExportModelAdmin):
class PartRelatedAdmin(admin.ModelAdmin):
"""
Class to manage PartRelated objects
"""
"""Class to manage PartRelated objects."""
autocomplete_fields = ('part_1', 'part_2')
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')
@ -158,7 +158,7 @@ class PartTestTemplateAdmin(admin.ModelAdmin):
class BomItemResource(ModelResource):
""" Class for managing BomItem data import/export """
"""Class for managing BomItem data import/export."""
level = Field(attribute='level', readonly=True)
@ -189,25 +189,18 @@ class BomItemResource(ModelResource):
sub_assembly = Field(attribute='sub_part__assembly', readonly=True)
def dehydrate_quantity(self, item):
"""
Special consideration for the 'quantity' field on data export.
We do not want a spreadsheet full of "1.0000" (we'd rather "1")
"""Special consideration for the 'quantity' field on data export. We do not want a spreadsheet full of "1.0000" (we'd rather "1")
Ref: https://django-import-export.readthedocs.io/en/latest/getting_started.html#advanced-data-manipulation-on-export
"""
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):
"""
If we are exporting for the purposes of generating
a 'bom-import' template, there are some fields which
we are not interested in.
"""
"""If we are exporting for the purposes of generating a 'bom-import' template, there are some fields which we are not interested in."""
fields = super().get_fields(**kwargs)
# If we are not generating an "import" template,
@ -239,6 +232,7 @@ class BomItemResource(ModelResource):
return fields
class Meta:
"""Metaclass definition"""
model = models.BomItem
skip_unchanged = True
report_skipped = False
@ -253,6 +247,7 @@ class BomItemResource(ModelResource):
class BomItemAdmin(ImportExportModelAdmin):
"""Admin class for the BomItem model"""
resource_class = BomItemResource
@ -264,13 +259,15 @@ class BomItemAdmin(ImportExportModelAdmin):
class ParameterTemplateAdmin(ImportExportModelAdmin):
"""Admin class for the PartParameterTemplate model"""
list_display = ('name', 'units')
search_fields = ('name', 'units')
class ParameterResource(ModelResource):
""" Class for managing PartParameter data import/export """
"""Class for managing PartParameter data import/export."""
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(models.Part))
@ -281,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
@ -288,6 +286,7 @@ class ParameterResource(ModelResource):
class ParameterAdmin(ImportExportModelAdmin):
"""Admin class for the PartParameter model"""
resource_class = ParameterResource
@ -297,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',)
@ -323,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)

View File

@ -1,6 +1,4 @@
"""
Provides a JSON API for the Part app
"""
"""Provides a JSON API for the Part app."""
import datetime
from decimal import Decimal, InvalidOperation
@ -41,7 +39,7 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
class CategoryList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartCategory objects.
"""API endpoint for accessing a list of PartCategory objects.
- GET: Return a list of PartCategory objects
- POST: Create a new PartCategory object
@ -51,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:
@ -63,11 +61,10 @@ class CategoryList(generics.ListCreateAPIView):
return ctx
def filter_queryset(self, queryset):
"""
Custom filtering:
"""Custom filtering:
- Allow filtering by "null" parent to retrieve top-level part categories
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@ -158,15 +155,13 @@ class CategoryList(generics.ListCreateAPIView):
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for detail view of a single PartCategory object
"""
"""API endpoint for detail view of a single PartCategory object."""
serializer_class = part_serializers.CategorySerializer
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:
@ -178,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))
@ -190,16 +185,17 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
class CategoryMetadata(generics.RetrieveUpdateAPIView):
"""API endpoint for viewing / updating PartCategory metadata"""
"""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()
class CategoryParameterList(generics.ListAPIView):
""" API endpoint for accessing a list of PartCategoryParameterTemplate objects.
"""API endpoint for accessing a list of PartCategoryParameterTemplate objects.
- GET: Return a list of PartCategoryParameterTemplate objects
"""
@ -208,13 +204,12 @@ class CategoryParameterList(generics.ListAPIView):
serializer_class = part_serializers.CategoryParameterTemplateSerializer
def get_queryset(self):
"""
Custom filtering:
"""Custom filtering:
- Allow filtering by "null" parent to retrieve all categories parameter templates
- Allow filtering by category
- Allow traversing all parent categories
"""
queryset = super().get_queryset()
params = self.request.query_params
@ -241,9 +236,7 @@ class CategoryParameterList(generics.ListAPIView):
class CategoryTree(generics.ListAPIView):
"""
API endpoint for accessing a list of PartCategory objects ready for rendering a tree.
"""
"""API endpoint for accessing a list of PartCategory objects ready for rendering a tree."""
queryset = PartCategory.objects.all()
serializer_class = part_serializers.CategoryTree
@ -258,18 +251,14 @@ class CategoryTree(generics.ListAPIView):
class PartSalePriceDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Detail endpoint for PartSellPriceBreak model
"""
"""Detail endpoint for PartSellPriceBreak model."""
queryset = PartSellPriceBreak.objects.all()
serializer_class = part_serializers.PartSalePriceSerializer
class PartSalePriceList(generics.ListCreateAPIView):
"""
API endpoint for list view of PartSalePriceBreak model
"""
"""API endpoint for list view of PartSalePriceBreak model."""
queryset = PartSellPriceBreak.objects.all()
serializer_class = part_serializers.PartSalePriceSerializer
@ -284,18 +273,14 @@ class PartSalePriceList(generics.ListCreateAPIView):
class PartInternalPriceDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Detail endpoint for PartInternalPriceBreak model
"""
"""Detail endpoint for PartInternalPriceBreak model."""
queryset = PartInternalPriceBreak.objects.all()
serializer_class = part_serializers.PartInternalPriceSerializer
class PartInternalPriceList(generics.ListCreateAPIView):
"""
API endpoint for list view of PartInternalPriceBreak model
"""
"""API endpoint for list view of PartInternalPriceBreak model."""
queryset = PartInternalPriceBreak.objects.all()
serializer_class = part_serializers.PartInternalPriceSerializer
@ -311,9 +296,7 @@ class PartInternalPriceList(generics.ListCreateAPIView):
class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a PartAttachment (file upload).
"""
"""API endpoint for listing (and creating) a PartAttachment (file upload)."""
queryset = PartAttachment.objects.all()
serializer_class = part_serializers.PartAttachmentSerializer
@ -328,38 +311,30 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
"""
Detail endpoint for PartAttachment model
"""
"""Detail endpoint for PartAttachment model."""
queryset = PartAttachment.objects.all()
serializer_class = part_serializers.PartAttachmentSerializer
class PartTestTemplateDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Detail endpoint for PartTestTemplate model
"""
"""Detail endpoint for PartTestTemplate model."""
queryset = PartTestTemplate.objects.all()
serializer_class = part_serializers.PartTestTemplateSerializer
class PartTestTemplateList(generics.ListCreateAPIView):
"""
API endpoint for listing (and creating) a PartTestTemplate.
"""
"""API endpoint for listing (and creating) a PartTestTemplate."""
queryset = PartTestTemplate.objects.all()
serializer_class = part_serializers.PartTestTemplateSerializer
def filter_queryset(self, queryset):
"""
Filter the test list queryset.
"""Filter the test list queryset.
If filtering by 'part', we include results for any parts "above" the specified part.
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@ -390,15 +365,13 @@ class PartTestTemplateList(generics.ListCreateAPIView):
class PartThumbs(generics.ListAPIView):
"""
API endpoint for retrieving information on available Part thumbnails
"""
"""API endpoint for retrieving information on available Part thumbnails."""
queryset = Part.objects.all()
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
@ -407,11 +380,10 @@ class PartThumbs(generics.ListAPIView):
return queryset
def list(self, request, *args, **kwargs):
"""
Serialize the available Part images.
"""Serialize the available Part images.
- Images may be used for multiple parts!
"""
queryset = self.filter_queryset(self.get_queryset())
# Return the most popular parts first
@ -436,7 +408,7 @@ class PartThumbs(generics.ListAPIView):
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
""" API endpoint for updating Part thumbnails"""
"""API endpoint for updating Part thumbnails."""
queryset = Part.objects.all()
serializer_class = part_serializers.PartThumbSerializerUpdate
@ -447,8 +419,7 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
class PartScheduling(generics.RetrieveAPIView):
"""
API endpoint for delivering "scheduling" information about a given part via the API.
"""API endpoint for delivering "scheduling" information about a given part via the API.
Returns a chronologically ordered list about future "scheduled" events,
concerning stock levels for the part:
@ -462,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()
@ -470,13 +441,12 @@ class PartScheduling(generics.RetrieveAPIView):
schedule = []
def add_schedule_entry(date, quantity, title, label, url):
"""
Check if a scheduled entry should be added:
"""Check if a scheduled entry should be added:
- date must be non-null
- date cannot be in the "past"
- quantity must not be zero
"""
if date and date >= today and quantity != 0:
schedule.append({
'date': date,
@ -583,25 +553,22 @@ class PartScheduling(generics.RetrieveAPIView):
class PartMetadata(generics.RetrieveUpdateAPIView):
"""
API endpoint for viewing / updating Part metadata
"""
"""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()
class PartSerialNumberDetail(generics.RetrieveAPIView):
"""
API endpoint for returning extra serial number information about a particular part
"""
"""API endpoint for returning extra serial number information about a particular part."""
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
@ -621,15 +588,13 @@ class PartSerialNumberDetail(generics.RetrieveAPIView):
class PartCopyBOM(generics.CreateAPIView):
"""
API endpoint for duplicating a BOM
"""
"""API endpoint for duplicating a BOM."""
queryset = Part.objects.all()
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:
@ -641,13 +606,13 @@ class PartCopyBOM(generics.CreateAPIView):
class PartValidateBOM(generics.RetrieveUpdateAPIView):
"""
API endpoint for 'validating' the BOM for a given Part
"""
"""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',
@ -667,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'))
@ -675,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)
@ -691,7 +657,7 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView):
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single Part object """
"""API endpoint for detail view of a single Part object."""
queryset = Part.objects.all()
serializer_class = part_serializers.PartSerializer
@ -699,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)
@ -706,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))
@ -726,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:
@ -734,16 +705,14 @@ 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):
"""
Custom update functionality for Part instance.
"""Custom update functionality for Part instance.
- If the 'starred' field is provided, update the 'starred' status against current user
"""
if 'starred' in request.data:
starred = str2bool(request.data.get('starred', False))
@ -755,8 +724,8 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
class PartFilter(rest_filters.FilterSet):
"""
Custom filters for the PartList endpoint.
"""Custom filters for the PartList endpoint.
Uses the django_filters extension framework
"""
@ -764,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:
@ -791,10 +760,7 @@ class PartFilter(rest_filters.FilterSet):
low_stock = rest_filters.BooleanFilter(label='Low stock', method='filter_low_stock')
def filter_low_stock(self, queryset, name, value):
"""
Filter by "low stock" status
"""
"""Filter by "low stock" status."""
value = str2bool(value)
if value:
@ -812,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:
@ -826,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:
@ -854,8 +820,7 @@ class PartFilter(rest_filters.FilterSet):
class PartList(APIDownloadMixin, generics.ListCreateAPIView):
"""
API endpoint for accessing a list of Part objects
"""API endpoint for accessing a list of Part objects.
- GET: Return list of objects
- POST: Create a new Part object
@ -882,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()
@ -904,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)
@ -912,14 +878,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
return DownloadFile(filedata, filename)
def list(self, request, *args, **kwargs):
"""
Overide the 'list' method, as the PartCategory objects are
very expensive to serialize!
"""Overide the 'list' method, as the PartCategory objects are very expensive to serialize!
So we will serialize them first, and keep them in memory,
so that they do not have to be serialized multiple times...
So we will serialize them first, and keep them in memory, so that they do not have to be serialized multiple times...
"""
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
@ -980,12 +942,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
@transaction.atomic
def create(self, request, *args, **kwargs):
"""
We wish to save the user who created this part!
"""We wish to save the user who created this part!
Note: Implementation copied from DRF class CreateModelMixin
"""
# TODO: Unit tests for this function!
serializer = self.get_serializer(data=request.data)
@ -1128,18 +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)
@ -1392,15 +1348,13 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
class PartRelatedList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of PartRelated objects
"""
"""API endpoint for accessing a list of PartRelated objects."""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
def filter_queryset(self, queryset):
"""Custom queryset filtering"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@ -1421,16 +1375,14 @@ class PartRelatedList(generics.ListCreateAPIView):
class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for accessing detail view of a PartRelated object
"""
"""API endpoint for accessing detail view of a PartRelated object."""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
class PartParameterTemplateList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameterTemplate objects.
"""API endpoint for accessing a list of PartParameterTemplate objects.
- GET: Return list of PartParameterTemplate objects
- POST: Create a new PartParameterTemplate object
@ -1454,10 +1406,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
]
def filter_queryset(self, queryset):
"""
Custom filtering for the PartParameterTemplate API
"""
"""Custom filtering for the PartParameterTemplate API."""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@ -1493,7 +1442,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
class PartParameterList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameter objects
"""API endpoint for accessing a list of PartParameter objects.
- GET: Return list of PartParameter objects
- POST: Create a new PartParameter object
@ -1513,18 +1462,14 @@ class PartParameterList(generics.ListCreateAPIView):
class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for detail view of a single PartParameter object
"""
"""API endpoint for detail view of a single PartParameter object."""
queryset = PartParameter.objects.all()
serializer_class = part_serializers.PartParameterSerializer
class BomFilter(rest_filters.FilterSet):
"""
Custom filters for the BOM list
"""
"""Custom filters for the BOM list."""
# Boolean filters for BOM item
optional = rest_filters.BooleanFilter(label='BOM line is optional')
@ -1542,8 +1487,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)
@ -1565,8 +1509,7 @@ class BomFilter(rest_filters.FilterSet):
class BomList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of BomItem objects.
"""API endpoint for accessing a list of BomItem objects.
- GET: Return list of BomItem objects
- POST: Create a new BomItem object
@ -1577,6 +1520,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())
@ -1602,6 +1546,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:
@ -1626,7 +1577,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)
@ -1635,7 +1586,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
@ -1716,18 +1667,13 @@ class BomList(generics.ListCreateAPIView):
return queryset
def include_pricing(self):
"""
Determine if pricing information should be included in the response
"""
"""Determine if pricing information should be included in the response."""
pricing_default = InvenTreeSetting.get_setting('PART_SHOW_PRICE_IN_BOM')
return str2bool(self.request.query_params.get('include_pricing', pricing_default))
def annotate_pricing(self, queryset):
"""
Add part pricing information to the queryset
"""
"""Add part pricing information to the queryset."""
# Annotate with purchase prices
queryset = queryset.annotate(
purchase_price_min=Min('sub_part__stock_items__purchase_price'),
@ -1742,8 +1688,7 @@ class BomList(generics.ListCreateAPIView):
).values('pk', 'sub_part', 'purchase_price', 'purchase_price_currency')
def convert_price(price, currency, decimal_places=4):
""" Convert price field, returns Money field """
"""Convert price field, returns Money field."""
price_adjusted = None
# Get default currency from settings
@ -1796,8 +1741,7 @@ class BomList(generics.ListCreateAPIView):
class BomImportUpload(generics.CreateAPIView):
"""
API endpoint for uploading a complete Bill of Materials.
"""API endpoint for uploading a complete Bill of Materials.
It is assumed that the BOM has been extracted from a file using the BomExtract endpoint.
"""
@ -1806,10 +1750,7 @@ class BomImportUpload(generics.CreateAPIView):
serializer_class = part_serializers.BomImportUploadSerializer
def create(self, request, *args, **kwargs):
"""
Custom create function to return the extracted data
"""
"""Custom create function to return the extracted data."""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
@ -1821,31 +1762,27 @@ class BomImportUpload(generics.CreateAPIView):
class BomImportExtract(generics.CreateAPIView):
"""
API endpoint for extracting BOM data from a BOM file.
"""
"""API endpoint for extracting BOM data from a BOM file."""
queryset = Part.objects.none()
serializer_class = part_serializers.BomImportExtractSerializer
class BomImportSubmit(generics.CreateAPIView):
"""
API endpoint for submitting BOM data from a BOM file
"""
"""API endpoint for submitting BOM data from a BOM file."""
queryset = BomItem.objects.none()
serializer_class = part_serializers.BomImportSubmitSerializer
class BomDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single BomItem object """
"""API endpoint for detail view of a single BomItem object."""
queryset = BomItem.objects.all()
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)
@ -1855,19 +1792,17 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
class BomItemValidate(generics.UpdateAPIView):
""" API endpoint for validating a BomItem """
"""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()
serializer_class = BomItemValidationSerializer
def update(self, request, *args, **kwargs):
""" Perform update request """
"""Perform update request."""
partial = kwargs.pop('partial', False)
valid = request.data.get('valid', False)
@ -1884,9 +1819,7 @@ class BomItemValidate(generics.UpdateAPIView):
class BomItemSubstituteList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of BomItemSubstitute objects
"""
"""API endpoint for accessing a list of BomItemSubstitute objects."""
serializer_class = part_serializers.BomItemSubstituteSerializer
queryset = BomItemSubstitute.objects.all()
@ -1904,9 +1837,7 @@ class BomItemSubstituteList(generics.ListCreateAPIView):
class BomItemSubstituteDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for detail view of a single BomItemSubstitute object
"""
"""API endpoint for detail view of a single BomItemSubstitute object."""
queryset = BomItemSubstitute.objects.all()
serializer_class = part_serializers.BomItemSubstituteSerializer

View File

@ -1,3 +1,5 @@
"""part app specification"""
import logging
from django.apps import AppConfig
@ -9,24 +11,19 @@ logger = logging.getLogger("inventree")
class PartConfig(AppConfig):
"""Config class for the 'part' app"""
name = 'part'
def ready(self):
"""
This function is called whenever the Part app is loaded.
"""
"""This function is called whenever the Part app is loaded."""
if canAppAccessDatabase():
self.update_trackable_status()
def update_trackable_status(self):
"""
Check for any instances where a trackable part is used in the BOM
for a non-trackable part.
"""Check for any instances where a trackable part is used in the BOM for a non-trackable part.
In such a case, force the top-level part to be trackable too.
"""
from .models import BomItem
try:

View File

@ -1,5 +1,5 @@
"""
Functionality for Bill of Material (BOM) management.
"""Functionality for Bill of Material (BOM) management.
Primarily BOM upload tools.
"""
@ -11,18 +11,16 @@ from company.models import ManufacturerPart, SupplierPart
from InvenTree.helpers import DownloadFile, GetExportFormats, normalize
from .admin import BomItemResource
from .models import BomItem
from .models import BomItem, Part
def IsValidBOMFormat(fmt):
""" Test if a file format specifier is in the valid list of BOM file formats """
"""Test if a file format specifier is in the valid list of BOM file formats."""
return fmt.strip().lower() in GetExportFormats()
def MakeBomTemplate(fmt):
""" Generate a Bill of Materials upload template file (for user download) """
"""Generate a Bill of Materials upload template file (for user download)."""
fmt = fmt.strip().lower()
if not IsValidBOMFormat(fmt):
@ -44,14 +42,22 @@ def MakeBomTemplate(fmt):
return DownloadFile(data, filename)
def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False, manufacturer_data=False):
""" Export a BOM (Bill of Materials) for a given part.
def ExportBom(part: Part, fmt='csv', cascade: bool = False, max_levels: int = None, parameter_data=False, stock_data=False, supplier_data=False, manufacturer_data=False):
"""Export a BOM (Bill of Materials) for a given part.
Args:
fmt: File format (default = 'csv')
cascade: If True, multi-level BOM output is supported. Otherwise, a flat top-level-only BOM is exported.
"""
part (Part): Part for which the BOM should be exported
fmt (str, optional): file format. Defaults to 'csv'.
cascade (bool, optional): If True, multi-level BOM output is supported. Otherwise, a flat top-level-only BOM is exported.. Defaults to False.
max_levels (int, optional): Levels of items that should be included. None for np sublevels. Defaults to None.
parameter_data (bool, optional): Additonal data that should be added. Defaults to False.
stock_data (bool, optional): Additonal data that should be added. Defaults to False.
supplier_data (bool, optional): Additonal data that should be added. Defaults to False.
manufacturer_data (bool, optional): Additonal data that should be added. Defaults to False.
Returns:
StreamingHttpResponse: Response that can be passed to the endpoint
"""
if not IsValidBOMFormat(fmt):
fmt = 'csv'
@ -92,9 +98,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
pass
if parameter_data:
"""
If requested, add extra columns for each PartParameter associated with each line item
"""
"""If requested, add extra columns for each PartParameter associated with each line item."""
parameter_cols = {}
@ -117,9 +121,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
add_columns_to_dataset(parameter_cols_ordered, len(bom_items))
if stock_data:
"""
If requested, add extra columns for stock data associated with each line item
"""
"""If requested, add extra columns for stock data associated with each line item."""
stock_headers = [
_('Default Location'),
@ -172,9 +174,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
add_columns_to_dataset(stock_cols, len(bom_items))
if manufacturer_data or supplier_data:
"""
If requested, add extra columns for each SupplierPart and ManufacturerPart associated with each line item
"""
"""If requested, add extra columns for each SupplierPart and ManufacturerPart associated with each line item."""
# Keep track of the supplier parts we have already exported
supplier_parts_used = set()

View File

@ -1,13 +1,10 @@
"""
Django Forms for interacting with Part objects
"""
"""Django Forms for interacting with Part objects."""
from django import forms
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
@ -18,24 +15,8 @@ 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
"""
"""Form for downloading an image from a URL."""
url = forms.URLField(
label=_('URL'),
@ -44,6 +25,7 @@ class PartImageDownloadForm(HelperForm):
)
class Meta:
"""Metaclass defines fields for this form"""
model = Part
fields = [
'url',
@ -51,11 +33,10 @@ class PartImageDownloadForm(HelperForm):
class BomMatchItemForm(MatchItemForm):
""" Override MatchItemForm fields """
"""Override MatchItemForm fields."""
def get_special_field(self, col_guess, row, file_manager):
""" Set special fields """
"""Set special fields."""
# set quantity field
if 'quantity' in col_guess.lower():
return forms.CharField(
@ -74,15 +55,16 @@ class BomMatchItemForm(MatchItemForm):
class SetPartCategoryForm(forms.Form):
""" Form for setting the category of multiple Part objects """
"""Form for setting the category of multiple Part objects."""
part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
class EditPartParameterTemplateForm(HelperForm):
""" Form for editing a PartParameterTemplate object """
"""Form for editing a PartParameterTemplate object."""
class Meta:
"""Metaclass defines fields for this form"""
model = PartParameterTemplate
fields = [
'name',
@ -91,7 +73,7 @@ class EditPartParameterTemplateForm(HelperForm):
class EditCategoryParameterTemplateForm(HelperForm):
""" Form for editing a PartCategoryParameterTemplate object """
"""Form for editing a PartCategoryParameterTemplate object."""
add_to_same_level_categories = forms.BooleanField(required=False,
initial=False,
@ -102,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',
@ -113,7 +96,7 @@ class EditCategoryParameterTemplateForm(HelperForm):
class PartPriceForm(forms.Form):
""" Simple form for viewing part pricing information """
"""Simple form for viewing part pricing information."""
quantity = forms.IntegerField(
required=True,
@ -123,6 +106,7 @@ class PartPriceForm(forms.Form):
)
class Meta:
"""Metaclass defines fields for this form"""
model = Part
fields = [
'quantity',
@ -130,13 +114,12 @@ class PartPriceForm(forms.Form):
class EditPartSalePriceBreakForm(HelperForm):
"""
Form for creating / editing a sale price for a part
"""
"""Form for creating / editing a sale price for a part."""
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
class Meta:
"""Metaclass defines fields for this form"""
model = PartSellPriceBreak
fields = [
'part',
@ -146,13 +129,12 @@ class EditPartSalePriceBreakForm(HelperForm):
class EditPartInternalPriceBreakForm(HelperForm):
"""
Form for creating / editing a internal price for a part
"""
"""Form for creating / editing a internal price for a part."""
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
class Meta:
"""Metaclass defines fields for this form"""
model = PartInternalPriceBreak
fields = [
'part',

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,4 @@
"""
JSON serializers for Part app
"""
"""DRF data serializers for Part app."""
import imghdr
from decimal import Decimal
@ -37,17 +35,10 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
class CategorySerializer(InvenTreeModelSerializer):
""" Serializer for PartCategory """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
"""Serializer for PartCategory."""
def get_starred(self, category):
"""
Return True if the category is directly "starred" by the current user
"""
"""Return True if the category is directly "starred" by the current user."""
return category in self.context.get('starred_categories', [])
url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -59,6 +50,7 @@ class CategorySerializer(InvenTreeModelSerializer):
starred = serializers.SerializerMethodField()
class Meta:
"""Metaclass defining serializer fields"""
model = PartCategory
fields = [
'pk',
@ -76,11 +68,10 @@ class CategorySerializer(InvenTreeModelSerializer):
class CategoryTree(InvenTreeModelSerializer):
"""
Serializer for PartCategory tree
"""
"""Serializer for PartCategory tree."""
class Meta:
"""Metaclass defining serializer fields"""
model = PartCategory
fields = [
'pk',
@ -90,11 +81,10 @@ class CategoryTree(InvenTreeModelSerializer):
class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
"""
Serializer for the PartAttachment class
"""
"""Serializer for the PartAttachment class."""
class Meta:
"""Metaclass defining serializer fields"""
model = PartAttachment
fields = [
@ -113,13 +103,12 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
class PartTestTemplateSerializer(InvenTreeModelSerializer):
"""
Serializer for the PartTestTemplate class
"""
"""Serializer for the PartTestTemplate class."""
key = serializers.CharField(read_only=True)
class Meta:
"""Metaclass defining serializer fields"""
model = PartTestTemplate
fields = [
@ -135,9 +124,7 @@ class PartTestTemplateSerializer(InvenTreeModelSerializer):
class PartSalePriceSerializer(InvenTreeModelSerializer):
"""
Serializer for sale prices for Part model.
"""
"""Serializer for sale prices for Part model."""
quantity = InvenTreeDecimalField()
@ -155,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',
@ -167,9 +155,7 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
class PartInternalPriceSerializer(InvenTreeModelSerializer):
"""
Serializer for internal prices for Part model.
"""
"""Serializer for internal prices for Part model."""
quantity = InvenTreeDecimalField()
@ -187,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',
@ -199,8 +186,8 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
class PartThumbSerializer(serializers.Serializer):
"""
Serializer for the 'image' field of the Part model.
"""Serializer for the 'image' field of the Part model.
Used to serve and display existing Part images.
"""
@ -209,12 +196,10 @@ class PartThumbSerializer(serializers.Serializer):
class PartThumbSerializerUpdate(InvenTreeModelSerializer):
""" Serializer for updating Part thumbnail """
"""Serializer for updating Part thumbnail."""
def validate_image(self, value):
"""
Check that file is an image.
"""
"""Check that file is an image."""
validate = imghdr.what(value)
if not validate:
raise serializers.ValidationError("File is not an image")
@ -223,6 +208,7 @@ class PartThumbSerializerUpdate(InvenTreeModelSerializer):
image = InvenTreeAttachmentSerializerField(required=True)
class Meta:
"""Metaclass defining serializer fields"""
model = Part
fields = [
'image',
@ -230,9 +216,10 @@ class PartThumbSerializerUpdate(InvenTreeModelSerializer):
class PartParameterTemplateSerializer(InvenTreeModelSerializer):
""" JSON serializer for the PartParameterTemplate model """
"""JSON serializer for the PartParameterTemplate model."""
class Meta:
"""Metaclass defining serializer fields"""
model = PartParameterTemplate
fields = [
'pk',
@ -242,11 +229,12 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
class PartParameterSerializer(InvenTreeModelSerializer):
""" JSON serializers for the PartParameter model """
"""JSON serializers for the PartParameter model."""
template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True)
class Meta:
"""Metaclass defining serializer fields"""
model = PartParameter
fields = [
'pk',
@ -258,13 +246,14 @@ class PartParameterSerializer(InvenTreeModelSerializer):
class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """
"""Serializer for Part (brief detail)"""
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
stock = serializers.FloatField(source='total_stock')
class Meta:
"""Metaclass defining serializer fields"""
model = Part
fields = [
'pk',
@ -288,19 +277,20 @@ class PartBriefSerializer(InvenTreeModelSerializer):
class PartSerializer(InvenTreeModelSerializer):
""" Serializer for complete detail information of a part.
"""Serializer for complete detail information of a part.
Used when displaying all details of a single component.
"""
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)
@ -317,12 +307,10 @@ class PartSerializer(InvenTreeModelSerializer):
@staticmethod
def annotate_queryset(queryset):
"""
Add some extra annotations to the queryset,
performing database queries as efficiently as possible,
to reduce database trips.
"""
"""Add some extra annotations to the queryset.
Performing database queries as efficiently as possible, to reduce database trips.
"""
# Annotate with the total 'in stock' quantity
queryset = queryset.annotate(
in_stock=Coalesce(
@ -444,10 +432,7 @@ class PartSerializer(InvenTreeModelSerializer):
return queryset
def get_starred(self, part):
"""
Return "true" if the part is starred by the current user.
"""
"""Return "true" if the part is starred by the current user."""
return part in self.starred_parts
# Extra detail for the category
@ -477,6 +462,7 @@ class PartSerializer(InvenTreeModelSerializer):
)
class Meta:
"""Metaclass defining serializer fields"""
model = Part
partial = True
fields = [
@ -522,14 +508,13 @@ class PartSerializer(InvenTreeModelSerializer):
class PartRelationSerializer(InvenTreeModelSerializer):
"""
Serializer for a PartRelated model
"""
"""Serializer for a PartRelated model."""
part_1_detail = PartSerializer(source='part_1', read_only=True, many=False)
part_2_detail = PartSerializer(source='part_2', read_only=True, many=False)
class Meta:
"""Metaclass defining serializer fields"""
model = PartRelated
fields = [
'pk',
@ -541,12 +526,13 @@ class PartRelationSerializer(InvenTreeModelSerializer):
class PartStarSerializer(InvenTreeModelSerializer):
""" Serializer for a PartStar object """
"""Serializer for a PartStar object."""
partname = serializers.CharField(source='part.full_name', read_only=True)
username = serializers.CharField(source='user.username', read_only=True)
class Meta:
"""Metaclass defining serializer fields"""
model = PartStar
fields = [
'pk',
@ -558,13 +544,12 @@ class PartStarSerializer(InvenTreeModelSerializer):
class BomItemSubstituteSerializer(InvenTreeModelSerializer):
"""
Serializer for the BomItemSubstitute class
"""
"""Serializer for the BomItemSubstitute class."""
part_detail = PartBriefSerializer(source='part', read_only=True, many=False)
class Meta:
"""Metaclass defining serializer fields"""
model = BomItemSubstitute
fields = [
'pk',
@ -575,15 +560,14 @@ class BomItemSubstituteSerializer(InvenTreeModelSerializer):
class BomItemSerializer(InvenTreeModelSerializer):
"""
Serializer for BomItem object
"""
"""Serializer for BomItem object."""
price_range = serializers.CharField(read_only=True)
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"))
@ -615,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)
@ -640,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')
@ -663,18 +650,15 @@ class BomItemSerializer(InvenTreeModelSerializer):
@staticmethod
def annotate_queryset(queryset):
"""
Annotate the BomItem queryset with extra information:
"""Annotate the BomItem queryset with extra information:
Annotations:
available_stock: The amount of stock available for the sub_part Part object
"""
"""
Construct an "available stock" quantity:
available_stock = total_stock - build_order_allocations - sales_order_allocations
"""
build_order_filter = Q(build__status__in=BuildStatus.ACTIVE_CODES)
sales_order_filter = Q(
line__order__status__in=SalesOrderStatus.OPEN,
@ -799,8 +783,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
return queryset
def get_purchase_price_range(self, obj):
""" Return purchase price range """
"""Return purchase price range."""
try:
purchase_price_min = obj.purchase_price_min
except AttributeError:
@ -830,8 +813,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
return purchase_price_range
def get_purchase_price_avg(self, obj):
""" Return purchase price average """
"""Return purchase price average."""
try:
purchase_price_avg = obj.purchase_price_avg
except AttributeError:
@ -846,6 +828,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
return purchase_price_avg
class Meta:
"""Metaclass defining serializer fields"""
model = BomItem
fields = [
'allow_variants',
@ -877,7 +860,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
""" Serializer for PartCategoryParameterTemplate """
"""Serializer for PartCategoryParameterTemplate."""
parameter_template = PartParameterTemplateSerializer(many=False,
read_only=True)
@ -885,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',
@ -896,11 +880,10 @@ class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
class PartCopyBOMSerializer(serializers.Serializer):
"""
Serializer for copying a BOM from another part
"""
"""Serializer for copying a BOM from another part."""
class Meta:
"""Metaclass defining serializer fields"""
fields = [
'part',
'remove_existing',
@ -919,10 +902,7 @@ class PartCopyBOMSerializer(serializers.Serializer):
)
def validate_part(self, part):
"""
Check that a 'valid' part was selected
"""
"""Check that a 'valid' part was selected."""
return part
remove_existing = serializers.BooleanField(
@ -950,10 +930,7 @@ class PartCopyBOMSerializer(serializers.Serializer):
)
def save(self):
"""
Actually duplicate the BOM
"""
"""Actually duplicate the BOM."""
base_part = self.context['part']
data = self.validated_data
@ -968,13 +945,12 @@ class PartCopyBOMSerializer(serializers.Serializer):
class BomImportUploadSerializer(DataFileUploadSerializer):
"""
Serializer for uploading a file and extracting data from it.
"""
"""Serializer for uploading a file and extracting data from it."""
TARGET_MODEL = BomItem
class Meta:
"""Metaclass defining serializer fields"""
fields = [
'data_file',
'part',
@ -994,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):
@ -1005,12 +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']
@ -1020,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)
@ -1089,8 +1068,7 @@ class BomImportExtractSerializer(DataFileExtractSerializer):
class BomImportSubmitSerializer(serializers.Serializer):
"""
Serializer for uploading a BOM against a specified part.
"""Serializer for uploading a BOM against a specified part.
A "BOM" is a set of BomItem objects which are to be validated together as a set
"""
@ -1098,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:
@ -1109,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']

View File

@ -1,61 +1,38 @@
"""
User-configurable settings for the Part app
"""
"""User-configurable settings for the Part app."""
from common.models import InvenTreeSetting
def part_assembly_default():
"""
Returns the default value for the 'assembly' field of a Part object
"""
"""Returns the default value for the 'assembly' field of a Part object."""
return InvenTreeSetting.get_setting('PART_ASSEMBLY')
def part_template_default():
"""
Returns the default value for the 'is_template' field of a Part object
"""
"""Returns the default value for the 'is_template' field of a Part object."""
return InvenTreeSetting.get_setting('PART_TEMPLATE')
def part_virtual_default():
"""
Returns the default value for the 'is_virtual' field of Part object
"""
"""Returns the default value for the 'is_virtual' field of Part object."""
return InvenTreeSetting.get_setting('PART_VIRTUAL')
def part_component_default():
"""
Returns the default value for the 'component' field of a Part object
"""
"""Returns the default value for the 'component' field of a Part object."""
return InvenTreeSetting.get_setting('PART_COMPONENT')
def part_purchaseable_default():
"""
Returns the default value for the 'purchasable' field for a Part object
"""
"""Returns the default value for the 'purchasable' field for a Part object."""
return InvenTreeSetting.get_setting('PART_PURCHASEABLE')
def part_salable_default():
"""
Returns the default value for the 'salable' field for a Part object
"""
"""Returns the default value for the 'salable' field for a Part object."""
return InvenTreeSetting.get_setting('PART_SALABLE')
def part_trackable_default():
"""
Returns the default value for the 'trackable' field for a Part object
"""
"""Returns the default value for the 'trackable' field for a Part object."""
return InvenTreeSetting.get_setting('PART_TRACKABLE')

View File

@ -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 = {
@ -33,12 +40,10 @@ def notify_low_stock(part: part.models.Part):
def notify_low_stock_if_required(part: part.models.Part):
"""
Check if the stock quantity has fallen below the minimum threshold of part.
"""Check if the stock quantity has fallen below the minimum threshold of part.
If true, notify the users who have subscribed to the part
"""
# Run "up" the tree, to allow notification for "parent" parts
parts = part.get_ancestors(include_self=True, ascending=True)

View File

@ -0,0 +1 @@
"""Custom InvenTree template tags for HTML template rendering"""

View File

@ -1,9 +1,4 @@
# -*- coding: utf-8 -*-
"""
This module provides template tags for extra functionality,
over and above the built-in Django tags.
"""
"""This module provides template tags for extra functionality, over and above the built-in Django tags."""
import logging
import os
@ -33,26 +28,22 @@ logger = logging.getLogger('inventree')
@register.simple_tag()
def define(value, *args, **kwargs):
"""
Shortcut function to overcome the shortcomings of the django templating language
"""Shortcut function to overcome the shortcomings of the django templating language.
Use as follows: {% define "hello_world" as hello %}
Ref: https://stackoverflow.com/questions/1070398/how-to-set-a-value-of-a-variable-inside-a-template-code
"""
return value
@register.simple_tag(takes_context=True)
def render_date(context, date_object):
"""
Renders a date according to the preference of the provided user
"""Renders a date according to the preference of the provided user.
Note that the user preference is stored using the formatting adopted by moment.js,
which differs from the python formatting!
"""
if date_object is None:
return None
@ -105,59 +96,43 @@ def render_date(context, date_object):
@register.simple_tag()
def decimal(x, *args, **kwargs):
""" Simplified rendering of a decimal number """
"""Simplified rendering of a decimal number."""
return InvenTree.helpers.decimal2string(x)
@register.simple_tag()
def str2bool(x, *args, **kwargs):
""" Convert a string to a boolean value """
"""Convert a string to a boolean value."""
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 """
"""Add two numbers together."""
return x + y
@register.simple_tag()
def to_list(*args):
""" Return the input arguments as list """
"""Return the input arguments as list."""
return args
@register.simple_tag()
def part_allocation_count(build, part, *args, **kwargs):
""" Return the total number of <part> allocated to <build> """
"""Return the total number of <part> allocated to <build>"""
return InvenTree.helpers.decimal2string(build.getAllocatedQuantity(part))
@register.simple_tag()
def inventree_in_debug_mode(*args, **kwargs):
""" Return True if the server is running in DEBUG mode """
"""Return True if the server is running in DEBUG mode."""
return djangosettings.DEBUG
@register.simple_tag()
def inventree_show_about(user, *args, **kwargs):
""" Return True if the about modal should be shown """
"""Return True if the about modal should be shown."""
if InvenTreeSetting.get_setting('INVENTREE_RESTRICT_ABOUT') and not user.is_superuser:
return False
return True
@ -165,22 +140,19 @@ def inventree_show_about(user, *args, **kwargs):
@register.simple_tag()
def inventree_docker_mode(*args, **kwargs):
""" Return True if the server is running as a Docker image """
"""Return True if the server is running as a Docker image."""
return djangosettings.DOCKER
@register.simple_tag()
def plugins_enabled(*args, **kwargs):
""" Return True if plugins are enabled for the server instance """
"""Return True if plugins are enabled for the server instance."""
return djangosettings.PLUGINS_ENABLED
@register.simple_tag()
def inventree_db_engine(*args, **kwargs):
""" Return the InvenTree database backend e.g. 'postgresql' """
"""Return the InvenTree database backend e.g. 'postgresql'."""
db = djangosettings.DATABASES['default']
engine = db.get('ENGINE', _('Unknown database'))
@ -192,33 +164,31 @@ def inventree_db_engine(*args, **kwargs):
@register.simple_tag()
def inventree_instance_name(*args, **kwargs):
""" Return the InstanceName associated with the current database """
"""Return the InstanceName associated with the current database."""
return version.inventreeInstanceName()
@register.simple_tag()
def inventree_title(*args, **kwargs):
""" Return the title for the current instance - respecting the settings """
"""Return the title for the current instance - respecting the settings"""
return version.inventreeInstanceTitle()
@register.simple_tag()
def inventree_base_url(*args, **kwargs):
""" Return the INVENTREE_BASE_URL setting """
"""Return the INVENTREE_BASE_URL setting."""
return InvenTreeSetting.get_setting('INVENTREE_BASE_URL')
@register.simple_tag()
def python_version(*args, **kwargs):
"""
Return the current python version
"""
"""Return the current python version."""
return sys.version.split(' ')[0]
@register.simple_tag()
def inventree_version(shortstring=False, *args, **kwargs):
""" Return InvenTree version string """
"""Return InvenTree version string."""
if shortstring:
return _("{title} v{version}".format(
title=version.inventreeInstanceTitle(),
@ -229,53 +199,55 @@ 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()
@register.simple_tag()
def inventree_api_version(*args, **kwargs):
""" Return InvenTree API version """
"""Return InvenTree API version."""
return version.inventreeApiVersion()
@register.simple_tag()
def django_version(*args, **kwargs):
""" Return Django version string """
"""Return Django version string."""
return version.inventreeDjangoVersion()
@register.simple_tag()
def inventree_commit_hash(*args, **kwargs):
""" Return InvenTree git commit hash string """
"""Return InvenTree git commit hash string."""
return version.inventreeCommitHash()
@register.simple_tag()
def inventree_commit_date(*args, **kwargs):
""" Return InvenTree git commit date string """
"""Return InvenTree git commit date string."""
return version.inventreeCommitDate()
@register.simple_tag()
def inventree_github_url(*args, **kwargs):
""" Return URL for InvenTree github site """
"""Return URL for InvenTree github site."""
return "https://github.com/InvenTree/InvenTree/"
@register.simple_tag()
def inventree_docs_url(*args, **kwargs):
""" Return URL for InvenTree documenation site """
"""Return URL for InvenTree documenation site."""
tag = version.inventreeDocsVersion()
return f"https://inventree.readthedocs.io/en/{tag}"
@ -283,24 +255,23 @@ def inventree_docs_url(*args, **kwargs):
@register.simple_tag()
def inventree_credits_url(*args, **kwargs):
""" Return URL for InvenTree credits site """
"""Return URL for InvenTree credits site."""
return "https://inventree.readthedocs.io/en/latest/credits/"
@register.simple_tag()
def default_currency(*args, **kwargs):
""" Returns the default currency code """
"""Returns the default currency code."""
return currency_code_default()
@register.simple_tag()
def setting_object(key, *args, **kwargs):
"""
Return a setting object speciifed by the given key
"""Return a setting object speciifed by the given key.
(Or return None if the setting does not exist)
if a user-setting was requested return that
"""
if 'plugin' in kwargs:
# Note, 'plugin' is an instance of an InvenTreePlugin class
@ -319,10 +290,7 @@ def setting_object(key, *args, **kwargs):
@register.simple_tag()
def settings_value(key, *args, **kwargs):
"""
Return a settings value specified by the given key
"""
"""Return a settings value specified by the given key."""
if 'user' in kwargs:
if not kwargs['user'] or (kwargs['user'] and kwargs['user'].is_authenticated is False):
return InvenTreeUserSetting.get_setting(key)
@ -333,37 +301,25 @@ def settings_value(key, *args, **kwargs):
@register.simple_tag()
def user_settings(user, *args, **kwargs):
"""
Return all USER settings as a key:value dict
"""
"""Return all USER settings as a key:value dict."""
return InvenTreeUserSetting.allValues(user=user)
@register.simple_tag()
def global_settings(*args, **kwargs):
"""
Return all GLOBAL InvenTree settings as a key:value dict
"""
"""Return all GLOBAL InvenTree settings as a key:value dict."""
return InvenTreeSetting.allValues()
@register.simple_tag()
def visible_global_settings(*args, **kwargs):
"""
Return any global settings which are not marked as 'hidden'
"""
"""Return any global settings which are not marked as 'hidden'."""
return InvenTreeSetting.allValues(exclude_hidden=True)
@register.simple_tag()
def progress_bar(val, max_val, *args, **kwargs):
"""
Render a progress bar element
"""
"""Render a progress bar element."""
item_id = kwargs.get('id', 'progress-bar')
val = InvenTree.helpers.normalize(val)
@ -402,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')
@ -414,7 +371,7 @@ def get_color_theme_css(username):
@register.simple_tag()
def get_user_color_theme(username):
""" Get current user color theme """
"""Get current user color theme."""
try:
user_theme = ColorTheme.objects.filter(user=username).get()
user_theme_name = user_theme.name
@ -428,10 +385,7 @@ def get_user_color_theme(username):
@register.simple_tag()
def get_available_themes(*args, **kwargs):
"""
Return the available theme choices
"""
"""Return the available theme choices."""
themes = []
for key, name in ColorTheme.get_color_themes_choices():
@ -445,13 +399,11 @@ def get_available_themes(*args, **kwargs):
@register.simple_tag()
def primitive_to_javascript(primitive):
"""
Convert a python primitive to a javascript primitive.
"""Convert a python primitive to a javascript primitive.
e.g. True -> true
'hello' -> '"hello"'
"""
if type(primitive) is bool:
return str(primitive).lower()
@ -465,10 +417,9 @@ def primitive_to_javascript(primitive):
@register.filter
def keyvalue(dict, key):
"""
access to key of supplied dict
"""Access to key of supplied dict.
usage:
Usage:
{% mydict|keyvalue:mykey %}
"""
return dict.get(key)
@ -476,10 +427,9 @@ def keyvalue(dict, key):
@register.simple_tag()
def call_method(obj, method_name, *args):
"""
enables calling model methods / functions from templates with arguments
"""Enables calling model methods / functions from templates with arguments.
usage:
Usage:
{% call_method model_object 'fnc_name' argument1 %}
"""
method = getattr(obj, method_name)
@ -488,8 +438,7 @@ def call_method(obj, method_name, *args):
@register.simple_tag()
def authorized_owners(group):
""" Return authorized owners """
"""Return authorized owners."""
owners = []
try:
@ -507,41 +456,39 @@ def authorized_owners(group):
@register.simple_tag()
def object_link(url_name, pk, ref):
""" Return highlighted link to object """
"""Return highlighted link to object."""
ref_url = reverse(url_name, kwargs={'pk': pk})
return mark_safe('<b><a href="{}">{}</a></b>'.format(ref_url, ref))
@register.simple_tag()
def mail_configured():
""" Return if mail is configured """
"""Return if mail is configured."""
return bool(settings.EMAIL_HOST)
@register.simple_tag()
def inventree_customize(reference, *args, **kwargs):
""" Return customization values for the user interface """
"""Return customization values for the user interface."""
return djangosettings.CUSTOMIZE.get(reference, '')
@register.simple_tag()
def inventree_logo(*args, **kwargs):
""" Return the path to the logo-file """
"""Return the path to the logo-file."""
if settings.CUSTOM_LOGO:
return default_storage.url(settings.CUSTOM_LOGO)
return static('img/inventree.png')
class I18nStaticNode(StaticNode):
"""
custom StaticNode
replaces a variable named *lng* in the path with the current language
"""
def render(self, context): # pragma: no cover
"""Custom StaticNode.
Replaces a variable named *lng* in the path with the current language
"""
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:
@ -561,17 +508,16 @@ if settings.DEBUG:
@register.simple_tag()
def i18n_static(url_name):
""" simple tag to enable {% url %} functionality instead of {% static %} """
"""Simple tag to enable {% url %} functionality instead of {% static %}"""
return reverse(url_name)
else: # pragma: no cover
@register.tag('i18n_static')
def do_i18n_static(parser, token):
"""
Overrides normal static, adds language - lookup for prerenderd files #1485
"""Overrides normal static, adds language - lookup for prerenderd files #1485
usage (like static):
Usage (like static):
{% i18n_static path [as varname] %}
"""
bits = token.split_contents()

View File

@ -1,6 +1,4 @@
"""
Provide templates for the various model status codes.
"""
"""Provide templates for the various model status codes."""
from django import template
from django.utils.safestring import mark_safe
@ -13,28 +11,29 @@ register = template.Library()
@register.simple_tag
def purchase_order_status_label(key, *args, **kwargs):
""" Render a PurchaseOrder status label """
"""Render a PurchaseOrder status label."""
return mark_safe(PurchaseOrderStatus.render(key, large=kwargs.get('large', False)))
@register.simple_tag
def sales_order_status_label(key, *args, **kwargs):
""" Render a SalesOrder status label """
"""Render a SalesOrder status label."""
return mark_safe(SalesOrderStatus.render(key, large=kwargs.get('large', False)))
@register.simple_tag
def stock_status_label(key, *args, **kwargs):
""" Render a StockItem status label """
"""Render a StockItem status label."""
return mark_safe(StockStatus.render(key, large=kwargs.get('large', False)))
@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))
@register.simple_tag
def build_status_label(key, *args, **kwargs):
""" Render a Build status label """
"""Render a Build status label."""
return mark_safe(BuildStatus.render(key, large=kwargs.get('large', False)))

View File

@ -1,3 +1,5 @@
"""Unit tests for the various part API endpoints"""
from django.urls import reverse
import PIL
@ -11,12 +13,13 @@ 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
class PartCategoryAPITest(InvenTreeAPITestCase):
"""Unit tests for the PartCategory API"""
"""Unit tests for the PartCategory API."""
fixtures = [
'category',
@ -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)
@ -73,8 +75,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
self.assertEqual(len(response.data), 5)
def test_category_metadata(self):
"""Test metadata endpoint for the PartCategory"""
"""Test metadata endpoint for the PartCategory."""
cat = PartCategory.objects.get(pk=1)
cat.metadata = {
@ -95,8 +96,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
class PartOptionsAPITest(InvenTreeAPITestCase):
"""
Tests for the various OPTIONS endpoints in the /part/ API
"""Tests for the various OPTIONS endpoints in the /part/ API.
Ensure that the required field details are provided!
"""
@ -105,15 +105,8 @@ class PartOptionsAPITest(InvenTreeAPITestCase):
'part.add',
]
def setUp(self):
super().setUp()
def test_part(self):
"""
Test the Part API OPTIONS
"""
"""Test the Part API OPTIONS."""
actions = self.getActions(reverse('api-part-list'))['POST']
# Check that a bunch o' fields are contained
@ -147,10 +140,7 @@ class PartOptionsAPITest(InvenTreeAPITestCase):
self.assertEqual(category['help_text'], 'Part category')
def test_category(self):
"""
Test the PartCategory API OPTIONS endpoint
"""
"""Test the PartCategory API OPTIONS endpoint."""
actions = self.getActions(reverse('api-part-category-list'))
# actions should *not* contain 'POST' as we do not have the correct role
@ -169,10 +159,7 @@ class PartOptionsAPITest(InvenTreeAPITestCase):
self.assertEqual(loc['api_url'], reverse('api-location-list'))
def test_bom_item(self):
"""
Test the BomItem API OPTIONS endpoint
"""
"""Test the BomItem API OPTIONS endpoint."""
actions = self.getActions(reverse('api-bom-list'))['POST']
inherited = actions['inherited']
@ -195,8 +182,8 @@ class PartOptionsAPITest(InvenTreeAPITestCase):
class PartAPITest(InvenTreeAPITestCase):
"""
Series of tests for the Part DRF API
"""Series of tests for the Part DRF API.
- Tests for Part API
- Tests for PartCategory API
"""
@ -218,67 +205,57 @@ 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.
"""
"""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)
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',
},
format='json'
}
)
self.assertEqual(len(response.data), 2)
# Children of PartCategory<1>, cascade
response = self.client.get(
response = self.get(
url,
{
'parent': 1,
'cascade': 'true',
},
format='json',
}
)
self.assertEqual(len(response.data), 5)
# Children of PartCategory<1>, do not cascade
response = self.client.get(
response = self.get(
url,
{
'parent': 1,
'cascade': 'false',
},
format='json',
}
)
self.assertEqual(len(response.data), 3)
def test_add_categories(self):
""" Check that we can add categories """
"""Check that we can add categories."""
data = {
'name': 'Animals',
'description': 'All animals go here'
}
url = reverse('api-part-category-list')
response = self.client.post(url, data, format='json')
response = self.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
parent = response.data['pk']
@ -290,19 +267,20 @@ class PartAPITest(InvenTreeAPITestCase):
'description': 'A sort of animal',
'parent': parent,
}
response = self.client.post(url, data, format='json')
response = self.post(url, data)
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)
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)
# Test that we have retrieved the category
self.assertEqual(response.data['description'], 'Integrated Circuits')
@ -313,22 +291,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)
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)
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)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# There should only be 2 objects in category C
@ -337,39 +315,62 @@ 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
"""Test the special 'include_child_categories' flag.
If provided, parts are provided for ANY child category (recursive)
"""
url = reverse('api-part-list')
data = {'category': 1, 'cascade': True}
# Now request to include child categories
response = self.client.get(url, data, format='json')
response = self.get(url, data)
# 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,
@ -381,34 +382,32 @@ 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,
'test_name': 'New Test',
'required': True,
'description': 'a test description'
},
format='json',
}
)
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,
'test_name': " newtest",
'description': 'dafsdf',
},
format='json',
}
)
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,
@ -419,21 +418,15 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_get_thumbs(self):
"""
Return list of part thumbnails
"""
"""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)
def test_paginate(self):
"""
Test pagination of the Part list API
"""
"""Test pagination of the Part list API."""
for n in [1, 5, 10]:
response = self.get(reverse('api-part-list'), {'limit': n})
@ -445,16 +438,14 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(len(data['results']), n)
def test_default_values(self):
"""
Tests for 'default' values:
"""Tests for 'default' values:
Ensure that unspecified fields revert to "default" values
(as specified in the model field definition)
"""
url = reverse('api-part-list')
response = self.client.post(url, {
response = self.post(url, {
'name': 'all defaults',
'description': 'my test part',
'category': 1,
@ -476,7 +467,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,
@ -486,7 +477,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,
@ -498,10 +489,7 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertFalse(response.data['purchaseable'])
def test_initial_stock(self):
"""
Tests for initial stock quantity creation
"""
"""Tests for initial stock quantity creation."""
url = reverse('api-part-list')
# Track how many parts exist at the start of this test
@ -555,10 +543,7 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(new_part.total_stock, 12345)
def test_initial_supplier_data(self):
"""
Tests for initial creation of supplier / manufacturer data
"""
"""Tests for initial creation of supplier / manufacturer data."""
url = reverse('api-part-list')
n = Part.objects.count()
@ -620,10 +605,7 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(new_part.manufacturer_parts.count(), 1)
def test_strange_chars(self):
"""
Test that non-standard ASCII chars are accepted
"""
"""Test that non-standard ASCII chars are accepted."""
url = reverse('api-part-list')
name = "Kaltgerätestecker"
@ -641,15 +623,13 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(response.data['description'], description)
def test_template_filters(self):
"""
Unit tests for API filters related to template parts:
"""Unit tests for API filters related to template parts:
- variant_of : Return children of specified part
- ancestor : Return descendants of specified part
Uses the 'chair template' part (pk=10000)
"""
# Rebuild the MPTT structure before running these tests
Part.objects.rebuild()
@ -732,11 +712,7 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(len(response.data), 101)
def test_variant_stock(self):
"""
Unit tests for the 'variant_stock' annotation,
which provides a stock count for *variant* parts
"""
"""Unit tests for the 'variant_stock' annotation, which provides a stock count for *variant* parts."""
# Ensure the MPTT structure is in a known state before running tests
Part.objects.rebuild()
@ -820,8 +796,7 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(response.data['variant_stock'], 500)
def test_part_download(self):
"""Test download of part data via the API"""
"""Test download of part data via the API."""
url = reverse('api-part-list')
required_cols = [
@ -873,9 +848,7 @@ class PartAPITest(InvenTreeAPITestCase):
class PartDetailTests(InvenTreeAPITestCase):
"""
Test that we can create / edit / delete Part objects via the API
"""
"""Test that we can create / edit / delete Part objects via the API."""
fixtures = [
'category',
@ -898,14 +871,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',
@ -930,7 +901,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',
})
@ -948,14 +919,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)
@ -963,19 +934,16 @@ 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
self.assertEqual(Part.objects.count(), n)
def test_duplicates(self):
"""
Check that trying to create 'duplicate' parts results in errors
"""
"""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',
@ -988,7 +956,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',
@ -1011,7 +979,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',
@ -1028,18 +996,17 @@ 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',
},
format='json',
}
)
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',
@ -1049,14 +1016,11 @@ class PartDetailTests(InvenTreeAPITestCase):
self.assertEqual(response.status_code, 200)
def test_image_upload(self):
"""
Test that we can upload an image to the part API
"""
"""Test that we can upload an image to the part API."""
self.assignRole('part.add')
# Create a new part
response = self.client.post(
response = self.post(
reverse('api-part-list'),
{
'name': 'imagine',
@ -1120,10 +1084,7 @@ class PartDetailTests(InvenTreeAPITestCase):
self.assertIsNotNone(p.image)
def test_details(self):
"""
Test that the required details are available
"""
"""Test that the required details are available."""
p = Part.objects.get(pk=1)
url = reverse('api-part-detail', kwargs={'pk': 1})
@ -1152,10 +1113,7 @@ class PartDetailTests(InvenTreeAPITestCase):
self.assertEqual(data['unallocated_stock'], 9000)
def test_part_metadata(self):
"""
Tests for the part metadata endpoint
"""
"""Tests for the part metadata endpoint."""
url = reverse('api-part-metadata', kwargs={'pk': 1})
part = Part.objects.get(pk=1)
@ -1206,9 +1164,7 @@ class PartDetailTests(InvenTreeAPITestCase):
class PartAPIAggregationTest(InvenTreeAPITestCase):
"""
Tests to ensure that the various aggregation annotations are working correctly...
"""
"""Tests to ensure that the various aggregation annotations are working correctly..."""
fixtures = [
'category',
@ -1229,7 +1185,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
@ -1253,9 +1209,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)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -1267,10 +1224,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
self.assertTrue(False) # pragma: no cover
def test_stock_quantity(self):
"""
Simple test for the stock quantity
"""
"""Simple test for the stock quantity."""
data = self.get_part_data()
self.assertEqual(data['in_stock'], 600)
@ -1290,11 +1244,10 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
self.assertEqual(data['stock_item_count'], 105)
def test_allocation_annotations(self):
"""
Tests for query annotations which add allocation information.
"""Tests for query annotations which add allocation information.
Ref: https://github.com/inventree/InvenTree/pull/2797
"""
# We are looking at Part ID 100 ("Bob")
url = reverse('api-part-detail', kwargs={'pk': 100})
@ -1438,9 +1391,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
class BomItemTest(InvenTreeAPITestCase):
"""
Unit tests for the BomItem API
"""
"""Unit tests for the BomItem API."""
fixtures = [
'category',
@ -1457,14 +1408,8 @@ class BomItemTest(InvenTreeAPITestCase):
'part.delete',
]
def setUp(self):
super().setUp()
def test_bom_list(self):
"""
Tests for the BomItem list endpoint
"""
"""Tests for the BomItem list endpoint."""
# How many BOM items currently exist in the database?
n = BomItem.objects.count()
@ -1529,10 +1474,7 @@ class BomItemTest(InvenTreeAPITestCase):
self.assertTrue(key in el)
def test_get_bom_detail(self):
"""
Get the detail view for a single BomItem object
"""
"""Get the detail view for a single BomItem object."""
url = reverse('api-bom-item-detail', kwargs={'pk': 3})
response = self.get(url, expected_code=200)
@ -1570,10 +1512,7 @@ class BomItemTest(InvenTreeAPITestCase):
self.assertEqual(response.data['note'], 'Added a note')
def test_add_bom_item(self):
"""
Test that we can create a new BomItem via the API
"""
"""Test that we can create a new BomItem via the API."""
url = reverse('api-bom-list')
data = {
@ -1587,13 +1526,10 @@ 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
"""
"""Tests for BomItem use with variants."""
stock_url = reverse('api-stock-list')
# BOM item we are interested in
@ -1675,10 +1611,7 @@ class BomItemTest(InvenTreeAPITestCase):
self.assertEqual(len(response.data), 2)
def test_substitutes(self):
"""
Tests for BomItem substitutes
"""
"""Tests for BomItem substitutes."""
url = reverse('api-bom-substitute-list')
stock_url = reverse('api-stock-list')
@ -1760,10 +1693,7 @@ class BomItemTest(InvenTreeAPITestCase):
self.assertEqual(data['available_stock'], 9000)
def test_bom_item_uses(self):
"""
Tests for the 'uses' field
"""
"""Tests for the 'uses' field."""
url = reverse('api-bom-list')
# Test that the direct 'sub_part' association works
@ -1813,10 +1743,7 @@ class BomItemTest(InvenTreeAPITestCase):
self.assertEqual(len(response.data), i)
def test_bom_variant_stock(self):
"""
Test for 'available_variant_stock' annotation
"""
"""Test for 'available_variant_stock' annotation."""
Part.objects.rebuild()
# BOM item we are interested in
@ -1852,10 +1779,7 @@ class BomItemTest(InvenTreeAPITestCase):
class PartParameterTest(InvenTreeAPITestCase):
"""
Tests for the ParParameter API
"""
"""Tests for the ParParameter API."""
superuser = True
fixtures = [
@ -1865,51 +1789,39 @@ class PartParameterTest(InvenTreeAPITestCase):
'params',
]
def setUp(self):
super().setUp()
def test_list_params(self):
"""
Test for listing part parameters
"""
"""Test for listing part parameters."""
url = reverse('api-part-parameter-list')
response = self.client.get(url, format='json')
response = self.get(url)
self.assertEqual(len(response.data), 5)
# Filter by part
response = self.client.get(
response = self.get(
url,
{
'part': 3,
},
format='json'
}
)
self.assertEqual(len(response.data), 3)
# Filter by template
response = self.client.get(
response = self.get(
url,
{
'template': 1,
},
format='json',
}
)
self.assertEqual(len(response.data), 3)
def test_create_param(self):
"""
Test that we can create a param via the API
"""
"""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',
@ -1920,18 +1832,15 @@ class PartParameterTest(InvenTreeAPITestCase):
self.assertEqual(response.status_code, 201)
response = self.client.get(url, format='json')
response = self.get(url)
self.assertEqual(len(response.data), 6)
def test_param_detail(self):
"""
Tests for the PartParameter detail endpoint
"""
"""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)
@ -1942,12 +1851,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'})
self.assertEqual(response.status_code, 200)
# Check that the data changed!
response = self.client.get(url, format='json')
response = self.get(url)
data = response.data

View File

@ -1,6 +1,4 @@
"""
Unit testing for BOM export functionality
"""
"""Unit testing for BOM export functionality."""
import csv
@ -10,6 +8,7 @@ from InvenTree.helpers import InvenTreeTestCase
class BomExportTest(InvenTreeTestCase):
"""Class for performing unit testing of BOM export functionality"""
fixtures = [
'category',
@ -21,15 +20,13 @@ class BomExportTest(InvenTreeTestCase):
roles = 'all'
def setUp(self):
"""Perform test setup functions"""
super().setUp()
self.url = reverse('bom-download', kwargs={'pk': 100})
def test_bom_template(self):
"""
Test that the BOM template can be downloaded from the server
"""
"""Test that the BOM template can be downloaded from the server."""
url = reverse('bom-upload-template')
# Download an XLS template
@ -78,10 +75,7 @@ class BomExportTest(InvenTreeTestCase):
self.assertTrue(header in headers)
def test_export_csv(self):
"""
Test BOM download in CSV format
"""
"""Test BOM download in CSV format."""
params = {
'format': 'csv',
'cascade': True,
@ -142,10 +136,7 @@ class BomExportTest(InvenTreeTestCase):
self.assertTrue(header in expected)
def test_export_xls(self):
"""
Test BOM download in XLS format
"""
"""Test BOM download in XLS format."""
params = {
'format': 'xls',
'cascade': True,
@ -163,10 +154,7 @@ class BomExportTest(InvenTreeTestCase):
self.assertEqual(content, 'attachment; filename="BOB | Bob | A2_BOM.xls"')
def test_export_xlsx(self):
"""
Test BOM download in XLSX format
"""
"""Test BOM download in XLSX format."""
params = {
'format': 'xlsx',
'cascade': True,
@ -181,10 +169,7 @@ class BomExportTest(InvenTreeTestCase):
self.assertEqual(response.status_code, 200)
def test_export_json(self):
"""
Test BOM download in JSON format
"""
"""Test BOM download in JSON format."""
params = {
'format': 'json',
'cascade': True,

View File

@ -1,6 +1,4 @@
"""
Unit testing for BOM upload / import functionality
"""
"""Unit testing for BOM upload / import functionality."""
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
@ -12,9 +10,7 @@ from part.models import Part
class BomUploadTest(InvenTreeAPITestCase):
"""
Test BOM file upload API endpoint
"""
"""Test BOM file upload API endpoint."""
roles = [
'part.add',
@ -22,6 +18,7 @@ class BomUploadTest(InvenTreeAPITestCase):
]
def setUp(self):
"""Create BOM data as part of setup routine"""
super().setUp()
self.part = Part.objects.create(
@ -41,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,
@ -63,10 +60,7 @@ class BomUploadTest(InvenTreeAPITestCase):
return response
def test_missing_file(self):
"""
POST without a file
"""
"""POST without a file."""
response = self.post(
reverse('api-bom-import-upload'),
data={},
@ -76,10 +70,7 @@ class BomUploadTest(InvenTreeAPITestCase):
self.assertIn('No file was submitted', str(response.data['data_file']))
def test_unsupported_file(self):
"""
POST with an unsupported file type
"""
"""POST with an unsupported file type."""
response = self.post_bom(
'sample.txt',
b'hello world',
@ -89,10 +80,7 @@ class BomUploadTest(InvenTreeAPITestCase):
self.assertIn('Unsupported file type', str(response.data['data_file']))
def test_broken_file(self):
"""
Test upload with broken (corrupted) files
"""
"""Test upload with broken (corrupted) files."""
response = self.post_bom(
'sample.csv',
b'',
@ -111,10 +99,7 @@ class BomUploadTest(InvenTreeAPITestCase):
self.assertIn('Unsupported format, or corrupt file', str(response.data['data_file']))
def test_missing_rows(self):
"""
Test upload of an invalid file (without data rows)
"""
"""Test upload of an invalid file (without data rows)"""
dataset = tablib.Dataset()
dataset.headers = [
@ -142,10 +127,7 @@ class BomUploadTest(InvenTreeAPITestCase):
self.assertIn('No data rows found in file', str(response.data))
def test_missing_columns(self):
"""
Upload extracted data, but with missing columns
"""
"""Upload extracted data, but with missing columns."""
url = reverse('api-bom-import-extract')
rows = [
@ -195,10 +177,7 @@ class BomUploadTest(InvenTreeAPITestCase):
)
def test_invalid_data(self):
"""
Upload data which contains errors
"""
"""Upload data which contains errors."""
dataset = tablib.Dataset()
# Only these headers are strictly necessary
@ -241,10 +220,7 @@ class BomUploadTest(InvenTreeAPITestCase):
self.assertEqual(rows[5]['data']['errors']['part'], 'Part is not designated as a component')
def test_part_guess(self):
"""
Test part 'guessing' when PK values are not supplied
"""
"""Test part 'guessing' when PK values are not supplied."""
dataset = tablib.Dataset()
# Should be able to 'guess' the part from the name
@ -304,10 +280,7 @@ class BomUploadTest(InvenTreeAPITestCase):
self.assertEqual(rows[idx]['data']['part'], components[idx].pk)
def test_levels(self):
"""
Test that multi-level BOMs are correctly handled during upload
"""
"""Test that multi-level BOMs are correctly handled during upload."""
url = reverse('api-bom-import-extract')
dataset = tablib.Dataset()

View File

@ -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,22 +51,19 @@ 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)
def test_self_reference(self):
""" Test that we get an appropriate error when we create a BomItem which points to itself """
"""Test that we get an appropriate error when we create a BomItem which points to itself."""
with self.assertRaises(django_exceptions.ValidationError):
# A validation error should be raised here
item = BomItem.objects.create(part=self.bob, sub_part=self.bob, quantity=7)
item.clean() # pragma: no cover
def test_integer_quantity(self):
"""
Test integer validation for BomItem
"""
"""Test integer validation for BomItem."""
p = Part.objects.create(name="test", description="d", component=True, trackable=True)
# Creation of a BOMItem with a non-integer quantity of a trackable Part should fail
@ -70,8 +74,7 @@ class BomItemTest(TestCase):
BomItem.objects.create(part=self.bob, sub_part=p, quantity=21)
def test_overage(self):
""" Test that BOM line overages are calculated correctly """
"""Test that BOM line overages are calculated correctly."""
item = BomItem.objects.get(part=100, sub_part=50)
q = 300
@ -106,8 +109,7 @@ class BomItemTest(TestCase):
self.assertEqual(n, 3150)
def test_item_hash(self):
""" Test BOM item hash encoding """
"""Test BOM item hash encoding."""
item = BomItem.objects.get(part=100, sub_part=50)
h1 = item.get_item_hash()
@ -122,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),
@ -135,10 +138,7 @@ class BomItemTest(TestCase):
)
def test_substitutes(self):
"""
Tests for BOM item substitutes
"""
"""Tests for BOM item substitutes."""
# We will make some subtitute parts for the "orphan" part
bom_item = BomItem.objects.get(
part=self.bob,

View File

@ -1,3 +1,5 @@
"""Unit tests for the PartCategory model"""
from django.core.exceptions import ValidationError
from django.test import TestCase
@ -5,8 +7,7 @@ from .models import Part, PartCategory, PartParameter, PartParameterTemplate
class CategoryTest(TestCase):
"""
Tests to ensure that the relational category tree functions correctly.
"""Tests to ensure that the relational category tree functions correctly.
Loads the following test fixtures:
- category.yaml
@ -19,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')
@ -29,9 +30,7 @@ class CategoryTest(TestCase):
self.transceivers = PartCategory.objects.get(name='Transceivers')
def test_parents(self):
""" Test that the parent fields are properly set,
based on the test fixtures """
"""Test that the parent fields are properly set, based on the test fixtures."""
self.assertEqual(self.resistors.parent, self.electronics)
self.assertEqual(self.capacitors.parent, self.electronics)
self.assertEqual(self.electronics.parent, None)
@ -39,8 +38,7 @@ class CategoryTest(TestCase):
self.assertEqual(self.fasteners.parent, self.mechanical)
def test_children_count(self):
""" Test that categories have the correct number of children """
"""Test that categories have the correct number of children."""
self.assertTrue(self.electronics.has_children)
self.assertTrue(self.mechanical.has_children)
@ -48,8 +46,7 @@ class CategoryTest(TestCase):
self.assertEqual(len(self.mechanical.children.all()), 1)
def test_unique_childs(self):
""" Test the 'unique_children' functionality """
"""Test the 'unique_children' functionality."""
childs = [item.pk for item in self.electronics.getUniqueChildren()]
self.assertIn(self.transceivers.id, childs)
@ -58,8 +55,7 @@ class CategoryTest(TestCase):
self.assertNotIn(self.fasteners.id, childs)
def test_unique_parents(self):
""" Test the 'unique_parents' functionality """
"""Test the 'unique_parents' functionality."""
parents = [item.pk for item in self.transceivers.getUniqueParents()]
self.assertIn(self.electronics.id, parents)
@ -67,22 +63,16 @@ class CategoryTest(TestCase):
self.assertNotIn(self.fasteners.id, parents)
def test_path_string(self):
""" Test that the category path string works correctly """
"""Test that the category path string works correctly."""
self.assertEqual(str(self.resistors), 'Electronics/Resistors - Resistors')
self.assertEqual(str(self.transceivers.pathstring), 'Electronics/IC/Transceivers')
def test_url(self):
""" Test that the PartCategory URL works """
"""Test that the PartCategory URL works."""
self.assertEqual(self.capacitors.get_absolute_url(), '/part/category/3/')
def test_part_count(self):
""" Test that the Category part count works """
self.assertTrue(self.resistors.has_parts)
self.assertTrue(self.fasteners.has_parts)
self.assertFalse(self.transceivers.has_parts)
"""Test that the Category part count works."""
self.assertEqual(self.fasteners.partcount(), 2)
self.assertEqual(self.capacitors.partcount(), 1)
@ -96,8 +86,7 @@ class CategoryTest(TestCase):
self.assertEqual(self.electronics.item_count, self.electronics.partcount())
def test_parameters(self):
""" Test that the Category parameters are correctly fetched """
"""Test that the Category parameters are correctly fetched."""
# Check number of SQL queries to iterate other parameters
with self.assertNumQueries(7):
# Prefetch: 3 queries (parts, parameters and parameters_template)
@ -121,8 +110,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:
@ -135,8 +123,7 @@ class CategoryTest(TestCase):
cat.save()
def test_delete(self):
""" Test that category deletion moves the children properly """
"""Test that category deletion moves the children properly."""
# Delete the 'IC' category and 'Transceiver' should move to be under 'Electronics'
self.assertEqual(self.transceivers.parent, self.ic)
self.assertEqual(self.ic.parent, self.electronics)
@ -156,8 +143,7 @@ class CategoryTest(TestCase):
self.assertEqual(f.category, self.mechanical)
def test_default_locations(self):
""" Test traversal for default locations """
"""Test traversal for default locations."""
self.assertEqual(str(self.fasteners.default_location), 'Office/Drawer_1 - In my desk')
# Any part under electronics should default to 'Home'
@ -174,12 +160,11 @@ class CategoryTest(TestCase):
self.assertIsNone(w.get_default_location())
def test_category_tree(self):
"""
Unit tests for the part category tree structure (MPTT)
"""Unit tests for the part category tree structure (MPTT)
Ensure that the MPTT structure is rebuilt correctly,
and the correct ancestor tree is observed.
"""
# Clear out any existing parts
Part.objects.all().delete()

View File

@ -1,6 +1,4 @@
"""
Unit tests for the part model database migrations
"""
"""Unit tests for the part model database migrations."""
from django_test_migrations.contrib.unittest_case import MigratorTestCase
@ -8,18 +6,13 @@ from InvenTree import helpers
class TestForwardMigrations(MigratorTestCase):
"""
Test entire schema migration sequence for the part app
"""
"""Test entire schema migration sequence for the part app."""
migrate_from = ('part', helpers.getOldestMigrationFile('part'))
migrate_to = ('part', helpers.getNewestMigrationFile('part'))
def prepare(self):
"""
Create initial data
"""
"""Create initial data."""
Part = self.old_state.apps.get_model('part', 'part')
Part.objects.create(name='A', description='My part A')
@ -39,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)
@ -49,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}")

View File

@ -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})

View File

@ -1,4 +1,4 @@
# Tests for the Part model
"""Tests for the Part model."""
import os
@ -21,42 +21,45 @@ from .templatetags import inventree_extras
class TemplateTagTest(InvenTreeTestCase):
""" Tests for the custom template tag code """
"""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)
@ -112,7 +123,7 @@ class TemplateTagTest(InvenTreeTestCase):
class PartTest(TestCase):
""" Tests for the Part model """
"""Tests for the Part model."""
fixtures = [
'category',
@ -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,14 +156,12 @@ 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_duplicate(self):
"""
Test that we cannot create a "duplicate" Part
"""
"""Test that we cannot create a "duplicate" Part."""
n = Part.objects.count()
cat = PartCategory.objects.get(pk=1)
@ -201,10 +213,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')
@ -212,26 +226,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
@ -239,7 +256,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
@ -247,8 +264,7 @@ class PartTest(TestCase):
self.assertEqual(float(self.r1.get_internal_price(10)), 0.5)
def test_metadata(self):
"""Unit tests for the Part metadata field"""
"""Unit tests for the Part metadata field."""
p = Part.objects.get(pk=1)
self.assertIsNone(p.metadata)
@ -266,6 +282,7 @@ class PartTest(TestCase):
class TestTemplateTest(TestCase):
"""Unit test for the TestTemplate class"""
fixtures = [
'category',
@ -275,7 +292,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!)
@ -292,8 +309,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):
@ -324,17 +340,13 @@ class TestTemplateTest(TestCase):
class PartSettingsTest(InvenTreeTestCase):
"""
Tests to ensure that the user-configurable default values work as expected.
"""Tests to ensure that the user-configurable default values work as expected.
Some fields for the Part model can have default values specified by the user.
"""
def make_part(self):
"""
Helper function to create a simple part
"""
"""Helper function to create a simple part."""
part = Part.objects.create(
name='Test Part',
description='I am but a humble test part',
@ -344,20 +356,14 @@ class PartSettingsTest(InvenTreeTestCase):
return part
def test_defaults(self):
"""
Test that the default values for the part settings are correct
"""
"""Test that the default values for the part settings are correct."""
self.assertTrue(part.settings.part_component_default())
self.assertTrue(part.settings.part_purchaseable_default())
self.assertFalse(part.settings.part_salable_default())
self.assertFalse(part.settings.part_trackable_default())
def test_initial(self):
"""
Test the 'initial' default values (no default values have been set)
"""
"""Test the 'initial' default values (no default values have been set)"""
part = self.make_part()
self.assertTrue(part.component)
@ -366,10 +372,7 @@ class PartSettingsTest(InvenTreeTestCase):
self.assertFalse(part.trackable)
def test_custom(self):
"""
Update some of the part values and re-test
"""
"""Update some of the part values and re-test."""
for val in [True, False]:
InvenTreeSetting.set_setting('PART_COMPONENT', val, self.user)
InvenTreeSetting.set_setting('PART_PURCHASEABLE', val, self.user)
@ -395,10 +398,7 @@ class PartSettingsTest(InvenTreeTestCase):
Part.objects.filter(pk=part.pk).delete()
def test_duplicate_ipn(self):
"""
Test the setting which controls duplicate IPN values
"""
"""Test the setting which controls duplicate IPN values."""
# Create a part
Part.objects.create(name='Hello', description='A thing', IPN='IPN123', revision='A')
@ -444,6 +444,7 @@ class PartSettingsTest(InvenTreeTestCase):
class PartSubscriptionTests(InvenTreeTestCase):
"""Unit tests for part 'subscription'"""
fixtures = [
'location',
@ -452,6 +453,7 @@ class PartSubscriptionTests(InvenTreeTestCase):
]
def setUp(self):
"""Create category and part data as part of setup routine"""
super().setUp()
# electronics / IC / MCU
@ -465,10 +467,7 @@ class PartSubscriptionTests(InvenTreeTestCase):
)
def test_part_subcription(self):
"""
Test basic subscription against a part
"""
"""Test basic subscription against a part."""
# First check that the user is *not* subscribed to the part
self.assertFalse(self.part.is_starred_by(self.user))
@ -485,10 +484,7 @@ class PartSubscriptionTests(InvenTreeTestCase):
self.assertFalse(self.part.is_starred_by(self.user))
def test_variant_subscription(self):
"""
Test subscription against a parent part
"""
"""Test subscription against a parent part."""
# Construct a sub-part to star against
sub_part = Part.objects.create(
name='sub_part',
@ -505,10 +501,7 @@ class PartSubscriptionTests(InvenTreeTestCase):
self.assertTrue(sub_part.is_starred_by(self.user))
def test_category_subscription(self):
"""
Test subscription against a PartCategory
"""
"""Test subscription against a PartCategory."""
self.assertEqual(PartCategoryStar.objects.count(), 0)
self.assertFalse(self.part.is_starred_by(self.user))
@ -533,10 +526,7 @@ class PartSubscriptionTests(InvenTreeTestCase):
self.assertFalse(self.part.is_starred_by(self.user))
def test_parent_category_subscription(self):
"""
Check that a parent category can be subscribed to
"""
"""Check that a parent category can be subscribed to."""
# Top-level "electronics" category
cat = PartCategory.objects.get(pk=1)
@ -553,7 +543,7 @@ class PartSubscriptionTests(InvenTreeTestCase):
class BaseNotificationIntegrationTest(InvenTreeTestCase):
""" Integration test for notifications """
"""Integration test for notifications."""
fixtures = [
'location',
@ -563,6 +553,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')
@ -571,8 +562,8 @@ class BaseNotificationIntegrationTest(InvenTreeTestCase):
self.part = Part.objects.get(name='R_2K2_0805')
def _notification_run(self, run_class=None):
"""
Run a notification test suit through.
"""Run a notification test suit through.
If you only want to test one class pass it to run_class
"""
# reload notification methods
@ -597,9 +588,10 @@ class BaseNotificationIntegrationTest(InvenTreeTestCase):
class PartNotificationTest(BaseNotificationIntegrationTest):
""" Integration test for part notifications """
"""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

View File

@ -1,4 +1,4 @@
""" Unit tests for Part Views (see views.py) """
"""Unit tests for Part Views (see views.py)"""
from django.urls import reverse
@ -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,10 +38,10 @@ 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 """
"""Test that we can retrieve a part detail page."""
pk = 1
response = self.client.get(reverse('part-detail', args=(pk,)))
@ -58,8 +58,8 @@ class PartDetailTest(PartViewTestCase):
self.assertEqual(response.context['category'], part.category)
def test_part_detail_from_ipn(self):
"""
Test that we can retrieve a part detail page from part IPN:
"""Test that we can retrieve a part detail page from part IPN:
- if no part with matching IPN -> return part index
- if unique IPN match -> return part detail page
- if multiple IPN matches -> return part index
@ -68,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
@ -108,22 +109,22 @@ class PartDetailTest(PartViewTestCase):
test_ipn_match(index_result=True, detail_result=False)
def test_bom_download(self):
""" Test downloading a BOM for a valid part """
"""Test downloading a BOM for a valid part."""
response = self.client.get(reverse('bom-download', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertIn('streaming_content', dir(response))
class PartQRTest(PartViewTestCase):
""" Tests for the Part QR Code AJAX view """
"""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)
@ -133,17 +134,17 @@ class PartQRTest(PartViewTestCase):
self.assertIn('<img src=', data)
def test_invalid_part(self):
"""Test response for an invalid Part ID value"""
response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
class CategoryTest(PartViewTestCase):
""" Tests for PartCategory related views """
"""Tests for PartCategory related views."""
def test_set_category(self):
""" Test that the "SetCategory" view works """
"""Test that the "SetCategory" view works."""
url = reverse('part-set-category')
response = self.client.get(url, {'parts[]': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')

View File

@ -1,11 +1,9 @@
"""
URL lookup for Part app. Provides URL endpoints for:
"""URL lookup for Part app. Provides URL endpoints for:
- Display / Create / Edit / Delete PartCategory
- Display / Create / Edit / Delete Part
- Create / Edit / Delete PartAttachment
- Display / Create / Edit / Delete SupplierPart
"""
from django.urls import include, re_path

View File

@ -1,6 +1,4 @@
"""
Django views for interacting with Part app
"""
"""Django views for interacting with Part app."""
import io
import os
@ -43,18 +41,23 @@ from .models import (Part, PartCategory, PartCategoryParameterTemplate,
class PartIndex(InvenTreeRoleMixin, ListView):
""" View for displaying list of Part objects
"""
"""View for displaying list of Part objects."""
model = Part
template_name = 'part/category.html'
context_object_name = 'parts'
def get_queryset(self):
"""Custom queryset lookup to prefetch related fields"""
return Part.objects.all().select_related('category')
def get_context_data(self, **kwargs):
"""Returns custom context data for the PartIndex view:
- children: Number of child categories
- category_count: Number of child categories
- part_count: Number of parts contained
"""
context = super().get_context_data(**kwargs).copy()
# View top-level categories
@ -68,7 +71,7 @@ class PartIndex(InvenTreeRoleMixin, ListView):
class PartSetCategory(AjaxUpdateView):
""" View for settings the part category for multiple parts at once """
"""View for settings the part category for multiple parts at once."""
ajax_template_name = 'part/set_category.html'
ajax_form_title = _('Set Part Category')
@ -80,8 +83,7 @@ class PartSetCategory(AjaxUpdateView):
parts = []
def get(self, request, *args, **kwargs):
""" Respond to a GET request to this view """
"""Respond to a GET request to this view."""
self.request = request
if 'parts[]' in request.GET:
@ -92,8 +94,7 @@ class PartSetCategory(AjaxUpdateView):
return self.renderJsonResponse(request, form=self.get_form(), context=self.get_context_data())
def post(self, request, *args, **kwargs):
""" Respond to a POST request to this view """
"""Respond to a POST request to this view."""
self.parts = []
for item in request.POST:
@ -125,17 +126,15 @@ class PartSetCategory(AjaxUpdateView):
}
if valid:
self.set_category()
with transaction.atomic():
for part in self.parts:
part.category = self.category
part.save()
return self.renderJsonResponse(request, data=data, form=self.get_form(), context=self.get_context_data())
@transaction.atomic
def set_category(self):
for part in self.parts:
part.set_category(self.category)
def get_context_data(self):
""" Return context data for rendering in the form """
"""Return context data for rendering in the form."""
ctx = {}
ctx['parts'] = self.parts
@ -146,10 +145,11 @@ class PartSetCategory(AjaxUpdateView):
class PartImport(FileManagementFormView):
''' Part: Upload file, match to fields and import parts(using multi-Step form) '''
"""Part: Upload file, match to fields and import parts(using multi-Step form)"""
permission_required = 'part.add'
class PartFileManager(FileManager):
"""Import field definitions"""
REQUIRED_HEADERS = [
'Name',
'Description',
@ -226,7 +226,7 @@ class PartImport(FileManagementFormView):
file_manager_class = PartFileManager
def get_field_selection(self):
""" Fill the form fields for step 3 """
"""Fill the form fields for step 3."""
# fetch available elements
self.allowed_items = {}
self.matches = {}
@ -269,7 +269,7 @@ class PartImport(FileManagementFormView):
row[idx.lower()] = data
def done(self, form_list, **kwargs):
""" Create items """
"""Create items."""
items = self.get_clean_items()
import_done = 0
@ -343,6 +343,7 @@ class PartImport(FileManagementFormView):
class PartImportAjax(FileManagementAjaxView, PartImport):
"""Multi-step form wizard for importing Part data"""
ajax_form_steps_template = [
'part/import_wizard/ajax_part_upload.html',
'part/import_wizard/ajax_match_fields.html',
@ -350,12 +351,12 @@ class PartImportAjax(FileManagementAjaxView, PartImport):
]
def validate(self, obj, form, **kwargs):
"""Validation is performed based on the current form step"""
return PartImport.validate(self, self.steps.current, form, **kwargs)
class PartDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
""" Detail view for Part object
"""
"""Detail view for Part object."""
context_object_name = 'part'
queryset = Part.objects.all().select_related('category')
@ -364,9 +365,7 @@ class PartDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
# Add in some extra context information based on query params
def get_context_data(self, **kwargs):
"""
Provide extra context data to template
"""
"""Provide extra context data to template."""
context = super().get_context_data(**kwargs)
part = self.get_object()
@ -389,14 +388,15 @@ class PartDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
return context
def get_quantity(self):
""" Return set quantity in decimal format """
"""Return set quantity in decimal format."""
return Decimal(self.request.POST.get('quantity', 1))
def get_part(self):
"""Return the Part instance associated with this view"""
return self.get_object()
def get_pricing(self, quantity=1, currency=None):
""" returns context with pricing information """
"""Returns context with pricing information."""
ctx = PartPricing.get_pricing(self, quantity, currency)
part = self.get_part()
default_currency = inventree_settings.currency_code_default()
@ -503,10 +503,11 @@ class PartDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
return ctx
def get_initials(self):
""" returns initials for form """
"""Returns initials for form."""
return {'quantity': self.get_quantity()}
def post(self, request, *args, **kwargs):
"""POST action performs as a GET action"""
self.object = self.get_object()
kwargs['object'] = self.object
ctx = self.get_context_data(**kwargs)
@ -514,11 +515,13 @@ class PartDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
class PartDetailFromIPN(PartDetail):
"""Part detail view using the IPN (internal part number) of the Part as the lookup field"""
slug_field = 'IPN'
slug_url_kwarg = 'slug'
def get_object(self):
""" Return Part object which IPN field matches the slug value """
"""Return Part object which IPN field matches the slug value."""
queryset = self.get_queryset()
# Get slug
slug = self.kwargs.get(self.slug_url_kwarg)
@ -541,7 +544,7 @@ class PartDetailFromIPN(PartDetail):
return None
def get(self, request, *args, **kwargs):
""" Attempt to match slug to a Part, else redirect to PartIndex view """
"""Attempt to match slug to a Part, else redirect to PartIndex view."""
self.object = self.get_object()
if not self.object:
@ -551,15 +554,14 @@ class PartDetailFromIPN(PartDetail):
class PartQRCode(QRCodeView):
""" View for displaying a QR code for a Part object """
"""View for displaying a QR code for a Part object."""
ajax_form_title = _("Part QR Code")
role_required = 'part.view'
def get_qr_data(self):
""" Generate QR code data for the Part """
"""Generate QR code data for the Part."""
try:
part = Part.objects.get(id=self.pk)
return part.format_barcode()
@ -568,9 +570,7 @@ class PartQRCode(QRCodeView):
class PartImageDownloadFromURL(AjaxUpdateView):
"""
View for downloading an image from a provided URL
"""
"""View for downloading an image from a provided URL."""
model = Part
@ -579,12 +579,10 @@ class PartImageDownloadFromURL(AjaxUpdateView):
ajax_form_title = _('Download Image')
def validate(self, part, form):
"""
Validate that the image data are correct.
"""Validate that the image data are correct.
- Try to download the image!
"""
# First ensure that the normal validation routines pass
if not form.is_valid():
return
@ -628,10 +626,7 @@ class PartImageDownloadFromURL(AjaxUpdateView):
return
def save(self, part, form, **kwargs):
"""
Save the downloaded image to the part
"""
"""Save the downloaded image to the part."""
fmt = self.image.format
if not fmt:
@ -651,7 +646,7 @@ class PartImageDownloadFromURL(AjaxUpdateView):
class PartImageSelect(AjaxUpdateView):
""" View for selecting Part image from existing images. """
"""View for selecting Part image from existing images."""
model = Part
ajax_template_name = 'part/select_image.html'
@ -662,7 +657,7 @@ class PartImageSelect(AjaxUpdateView):
]
def post(self, request, *args, **kwargs):
"""Perform POST action to assign selected image to the Part instance"""
part = self.get_object()
form = self.get_form()
@ -690,7 +685,7 @@ class PartImageSelect(AjaxUpdateView):
class BomUpload(InvenTreeRoleMixin, DetailView):
""" View for uploading a BOM file, and handling BOM data importing. """
"""View for uploading a BOM file, and handling BOM data importing."""
context_object_name = 'part'
queryset = Part.objects.all()
@ -698,21 +693,21 @@ class BomUpload(InvenTreeRoleMixin, DetailView):
class BomUploadTemplate(AjaxView):
"""
Provide a BOM upload template file for download.
"""Provide a BOM upload template file for download.
- Generates a template file in the provided format e.g. ?format=csv
"""
def get(self, request, *args, **kwargs):
"""Perform a GET request to download the 'BOM upload' template"""
export_format = request.GET.get('format', 'csv')
return MakeBomTemplate(export_format)
class BomDownload(AjaxView):
"""
Provide raw download of a BOM file.
"""Provide raw download of a BOM file.
- File format should be passed as a query param e.g. ?format=csv
"""
@ -721,7 +716,7 @@ class BomDownload(AjaxView):
model = Part
def get(self, request, *args, **kwargs):
"""Perform GET request to download BOM data"""
part = get_object_or_404(Part, pk=self.kwargs['pk'])
export_format = request.GET.get('format', 'csv')
@ -762,13 +757,14 @@ class BomDownload(AjaxView):
)
def get_data(self):
"""Return a cutsom message"""
return {
'info': 'Exported BOM'
}
class PartDelete(AjaxDeleteView):
""" View to delete a Part object """
"""View to delete a Part object."""
model = Part
ajax_template_name = 'part/partial_delete.html'
@ -778,13 +774,14 @@ class PartDelete(AjaxDeleteView):
success_url = '/part/'
def get_data(self):
"""Returns custom message once the part deletion has been performed"""
return {
'danger': _('Part was deleted'),
}
class PartPricing(AjaxView):
""" View for inspecting part pricing information """
"""View for inspecting part pricing information."""
model = Part
ajax_template_name = "part/part_pricing.html"
@ -794,17 +791,18 @@ class PartPricing(AjaxView):
role_required = ['sales_order.view', 'part.view']
def get_quantity(self):
""" Return set quantity in decimal format """
"""Return set quantity in decimal format."""
return Decimal(self.request.POST.get('quantity', 1))
def get_part(self):
"""Return the Part instance associated with this view"""
try:
return Part.objects.get(id=self.kwargs['pk'])
except Part.DoesNotExist:
return None
def get_pricing(self, quantity=1, currency=None):
""" returns context with pricing information """
"""Returns context with pricing information."""
if quantity <= 0:
quantity = 1
@ -898,16 +896,18 @@ class PartPricing(AjaxView):
return ctx
def get_initials(self):
""" returns initials for form """
"""Returns initials for form."""
return {'quantity': self.get_quantity()}
def get(self, request, *args, **kwargs):
"""Perform custom GET action for this view"""
init = self.get_initials()
qty = self.get_quantity()
return self.renderJsonResponse(request, self.form_class(initial=init), context=self.get_pricing(qty))
def post(self, request, *args, **kwargs):
"""Perform custom POST action for this view"""
currency = None
quantity = self.get_quantity()
@ -931,9 +931,7 @@ class PartPricing(AjaxView):
class PartParameterTemplateCreate(AjaxCreateView):
"""
View for creating a new PartParameterTemplate
"""
"""View for creating a new PartParameterTemplate."""
model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm
@ -941,9 +939,7 @@ class PartParameterTemplateCreate(AjaxCreateView):
class PartParameterTemplateEdit(AjaxUpdateView):
"""
View for editing a PartParameterTemplate
"""
"""View for editing a PartParameterTemplate."""
model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm
@ -951,14 +947,14 @@ class PartParameterTemplateEdit(AjaxUpdateView):
class PartParameterTemplateDelete(AjaxDeleteView):
""" View for deleting an existing PartParameterTemplate """
"""View for deleting an existing PartParameterTemplate."""
model = PartParameterTemplate
ajax_form_title = _("Delete Part Parameter Template")
class CategoryDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
""" Detail view for PartCategory """
"""Detail view for PartCategory."""
model = PartCategory
context_object_name = 'category'
@ -966,7 +962,12 @@ class CategoryDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
template_name = 'part/category.html'
def get_context_data(self, **kwargs):
"""Returns custom context data for the CategoryDetail view:
- part_count: Number of parts in this category
- starred_directly: True if this category is starred directly by the requesting user
- starred: True if this category is starred by the requesting user
"""
context = super().get_context_data(**kwargs).copy()
try:
@ -980,20 +981,22 @@ class CategoryDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
if category:
# Insert "starred" information
context['starred'] = category.is_starred_by(self.request.user)
context['starred_directly'] = context['starred'] and category.is_starred_by(
self.request.user,
include_parents=False,
)
if context['starred_directly']:
# Save a database lookup - if 'starred_directly' is True, we know 'starred' is also
context['starred'] = True
else:
context['starred'] = category.is_starred_by(self.request.user)
return context
class CategoryDelete(AjaxDeleteView):
"""
Delete view to delete a PartCategory
"""
"""Delete view to delete a PartCategory."""
model = PartCategory
ajax_template_name = 'part/category_delete.html'
ajax_form_title = _('Delete Part Category')
@ -1001,20 +1004,21 @@ class CategoryDelete(AjaxDeleteView):
success_url = '/part/'
def get_data(self):
"""Return custom context data when the category is deleted"""
return {
'danger': _('Part category was deleted'),
}
class CategoryParameterTemplateCreate(AjaxCreateView):
""" View for creating a new PartCategoryParameterTemplate """
"""View for creating a new PartCategoryParameterTemplate."""
model = PartCategoryParameterTemplate
form_class = part_forms.EditCategoryParameterTemplateForm
ajax_form_title = _('Create Category Parameter Template')
def get_initial(self):
""" Get initial data for Category """
"""Get initial data for Category."""
initials = super().get_initial()
category_id = self.kwargs.get('pk', None)
@ -1028,11 +1032,11 @@ class CategoryParameterTemplateCreate(AjaxCreateView):
return initials
def get_form(self):
""" Create a form to upload a new CategoryParameterTemplate
"""Create a form to upload a new CategoryParameterTemplate.
- Hide the 'category' field (parent part)
- Display parameter templates which are not yet related
"""
form = super().get_form()
form.fields['category'].widget = HiddenInput()
@ -1062,14 +1066,13 @@ class CategoryParameterTemplateCreate(AjaxCreateView):
return form
def post(self, request, *args, **kwargs):
""" Capture the POST request
"""Capture the POST request.
- If the add_to_all_categories object is set, link parameter template to
all categories
- If the add_to_same_level_categories object is set, link parameter template to
same level categories
"""
form = self.get_form()
valid = form.is_valid()
@ -1108,13 +1111,18 @@ class CategoryParameterTemplateCreate(AjaxCreateView):
class CategoryParameterTemplateEdit(AjaxUpdateView):
""" View for editing a PartCategoryParameterTemplate """
"""View for editing a PartCategoryParameterTemplate."""
model = PartCategoryParameterTemplate
form_class = part_forms.EditCategoryParameterTemplateForm
ajax_form_title = _('Edit Category Parameter Template')
def get_object(self):
"""Returns the PartCategoryParameterTemplate associated with this view
- First, attempt lookup based on supplied 'pid' kwarg
- Else, attempt lookup based on supplied 'pk' kwarg
"""
try:
self.object = self.model.objects.get(pk=self.kwargs['pid'])
except:
@ -1123,11 +1131,11 @@ class CategoryParameterTemplateEdit(AjaxUpdateView):
return self.object
def get_form(self):
""" Create a form to upload a new CategoryParameterTemplate
"""Create a form to upload a new CategoryParameterTemplate.
- Hide the 'category' field (parent part)
- Display parameter templates which are not yet related
"""
form = super().get_form()
form.fields['category'].widget = HiddenInput()
@ -1165,12 +1173,17 @@ class CategoryParameterTemplateEdit(AjaxUpdateView):
class CategoryParameterTemplateDelete(AjaxDeleteView):
""" View for deleting an existing PartCategoryParameterTemplate """
"""View for deleting an existing PartCategoryParameterTemplate."""
model = PartCategoryParameterTemplate
ajax_form_title = _("Delete Category Parameter Template")
def get_object(self):
"""Returns the PartCategoryParameterTemplate associated with this view
- First, attempt lookup based on supplied 'pid' kwarg
- Else, attempt lookup based on supplied 'pk' kwarg
"""
try:
self.object = self.model.objects.get(pk=self.kwargs['pid'])
except: