Merge pull request #156 from SchrodingersGat/api-cleanup

Api cleanup
This commit is contained in:
Oliver 2019-04-26 23:40:10 +10:00 committed by GitHub
commit 040f719b68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 116 additions and 44 deletions

View File

@ -2,7 +2,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from rest_framework import generics
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -24,13 +23,18 @@ class UserSerializerBrief(serializers.ModelSerializer):
] ]
class DraftRUDView(generics.RetrieveAPIView, generics.UpdateAPIView, generics.DestroyAPIView): class InvenTreeModelSerializer(serializers.ModelSerializer):
"""
Inherits the standard Django ModelSerializer class,
but also ensures that the underlying model class data are checked on validation.
"""
def perform_update(self, serializer): def validate(self, data):
# Run any native validation checks first (may throw an ValidationError)
data = super(serializers.ModelSerializer, self).validate(data)
ctx_data = serializer._context['request'].data # Now ensure the underlying model is correct
instance = self.Meta.model(**data)
instance.clean()
if ctx_data.get('_is_final', False) in [True, u'true', u'True', 1]: return data
super(generics.UpdateAPIView, self).perform_update(serializer)
else:
pass

View File

@ -11,7 +11,7 @@ from stock.urls import stock_urls
from build.urls import build_urls from build.urls import build_urls
from part.api import part_api_urls from part.api import part_api_urls, bom_api_urls
from company.api import company_api_urls from company.api import company_api_urls
from stock.api import stock_api_urls from stock.api import stock_api_urls
from build.api import build_api_urls from build.api import build_api_urls
@ -30,6 +30,7 @@ admin.site.site_header = "InvenTree Admin"
apipatterns = [ apipatterns = [
url(r'^part/', include(part_api_urls)), url(r'^part/', include(part_api_urls)),
url(r'^bom/', include(bom_api_urls)),
url(r'^company/', include(company_api_urls)), url(r'^company/', include(company_api_urls)),
url(r'^stock/', include(stock_api_urls)), url(r'^stock/', include(stock_api_urls)),
url(r'^build/', include(build_api_urls)), url(r'^build/', include(build_api_urls)),

View File

@ -43,7 +43,19 @@ class CompanyList(generics.ListCreateAPIView):
ordering = 'name' ordering = 'name'
class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Company.objects.all()
serializer_class = CompanySerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
company_api_urls = [ company_api_urls = [
url(r'^(?P<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),
url(r'^.*$', CompanyList.as_view(), name='api-company-list'), url(r'^.*$', CompanyList.as_view(), name='api-company-list'),
] ]

View File

@ -50,10 +50,10 @@
}, },
{ {
sortable: true, sortable: true,
field: 'part', field: 'part_name',
title: 'Part', title: 'Part',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
return renderLink(value.name, value.url + 'suppliers/'); return renderLink(value, '/part/' + row.part + '/suppliers/');
} }
}, },
{ {

View File

@ -17,7 +17,6 @@ from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
from .serializers import CategorySerializer from .serializers import CategorySerializer
from InvenTree.views import TreeSerializer from InvenTree.views import TreeSerializer
from InvenTree.serializers import DraftRUDView
class PartCategoryTree(TreeSerializer): class PartCategoryTree(TreeSerializer):
@ -56,7 +55,7 @@ class CategoryList(generics.ListCreateAPIView):
] ]
class PartDetail(DraftRUDView): class PartDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Part.objects.all() queryset = Part.objects.all()
serializer_class = PartSerializer serializer_class = PartSerializer
@ -125,7 +124,7 @@ class PartList(generics.ListCreateAPIView):
] ]
class BomList(generics.ListAPIView): class BomList(generics.ListCreateAPIView):
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
serializer_class = BomItemSerializer serializer_class = BomItemSerializer
@ -146,7 +145,17 @@ class BomList(generics.ListAPIView):
] ]
class SupplierPartList(generics.ListAPIView): class BomDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = BomItem.objects.all()
serializer_class = BomItemSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
class SupplierPartList(generics.ListCreateAPIView):
queryset = SupplierPart.objects.all() queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer serializer_class = SupplierPartSerializer
@ -167,6 +176,16 @@ class SupplierPartList(generics.ListAPIView):
] ]
class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
read_only_fields = [
]
class SupplierPriceBreakList(generics.ListCreateAPIView): class SupplierPriceBreakList(generics.ListCreateAPIView):
queryset = SupplierPriceBreak.objects.all() queryset = SupplierPriceBreak.objects.all()
@ -189,16 +208,31 @@ cat_api_urls = [
url(r'^$', CategoryList.as_view(), name='api-part-category-list'), url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
] ]
supplier_part_api_urls = [
url(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
# Catch anything else
url(r'^.*$', SupplierPartList.as_view(), name='api-part-supplier-list'),
]
part_api_urls = [ part_api_urls = [
url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'), url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),
url(r'^category/', include(cat_api_urls)), url(r'^category/', include(cat_api_urls)),
url(r'^supplier/', include(supplier_part_api_urls)),
url(r'^price-break/?', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'), url(r'^price-break/?', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'),
url(r'^supplier/?', SupplierPartList.as_view(), name='api-part-supplier-list'),
url(r'^bom/?', BomList.as_view(), name='api-bom-list'),
url(r'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'), url(r'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'),
url(r'^.*$', PartList.as_view(), name='api-part-list'), url(r'^.*$', PartList.as_view(), name='api-part-list'),
] ]
bom_api_urls = [
# BOM Item Detail
url('^(?P<pk>\d+)/?', BomDetail.as_view(), name='api-bom-detail'),
# Catch-all
url(r'^.*$', BomList.as_view(), name='api-bom-list'),
]

View File

@ -337,7 +337,7 @@ class BomItem(models.Model):
""" """
def get_absolute_url(self): def get_absolute_url(self):
return reverse('bom-detail', kwargs={'pk': self.id}) return reverse('bom-item-detail', kwargs={'pk': self.id})
# A link to the parent part # A link to the parent part
# Each part will get a reverse lookup field 'bom_items' # Each part will get a reverse lookup field 'bom_items'
@ -359,11 +359,12 @@ class BomItem(models.Model):
# A part cannot refer to itself in its BOM # A part cannot refer to itself in its BOM
if self.part == self.sub_part: if self.part == self.sub_part:
raise ValidationError(_('A part cannot contain itself as a BOM item')) raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')})
# Test for simple recursion
for item in self.sub_part.bom_items.all(): for item in self.sub_part.bom_items.all():
if self.part == item.sub_part: if self.part == item.sub_part:
raise ValidationError(_("Part '{p1}' is used in BOM for '{p2}' (recursive)".format(p1=str(self.part), p2=str(self.sub_part)))) raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)".format(p1=str(self.part), p2=str(self.sub_part)))})
class Meta: class Meta:
verbose_name = "BOM Item" verbose_name = "BOM Item"

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
from .models import Part, PartCategory, BomItem from .models import Part, PartCategory, BomItem
from .models import SupplierPart, SupplierPriceBreak from .models import SupplierPart, SupplierPriceBreak
from company.serializers import CompanyBriefSerializer from InvenTree.serializers import InvenTreeModelSerializer
class CategorySerializer(serializers.ModelSerializer): class CategorySerializer(serializers.ModelSerializer):
@ -67,20 +67,22 @@ class PartSerializer(serializers.ModelSerializer):
] ]
class BomItemSerializer(serializers.ModelSerializer): class BomItemSerializer(InvenTreeModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True) # url = serializers.CharField(source='get_absolute_url', read_only=True)
part = PartBriefSerializer(many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
sub_part = PartBriefSerializer(many=False, read_only=True) sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
class Meta: class Meta:
model = BomItem model = BomItem
fields = [ fields = [
'pk', 'pk',
'url', # 'url',
'part', 'part',
'part_detail',
'sub_part', 'sub_part',
'sub_part_detail',
'quantity', 'quantity',
'note', 'note',
] ]
@ -90,8 +92,9 @@ class SupplierPartSerializer(serializers.ModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
part = PartBriefSerializer(many=False, read_only=True) part_name = serializers.CharField(source='part.name', read_only=True)
supplier = CompanyBriefSerializer(many=False, read_only=True)
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
class Meta: class Meta:
model = SupplierPart model = SupplierPart
@ -99,7 +102,9 @@ class SupplierPartSerializer(serializers.ModelSerializer):
'pk', 'pk',
'url', 'url',
'part', 'part',
'part_name',
'supplier', 'supplier',
'supplier_name',
'SKU', 'SKU',
'manufacturer', 'manufacturer',
'MPN', 'MPN',

View File

@ -7,11 +7,16 @@
<h3>Part Suppliers</h3> <h3>Part Suppliers</h3>
<div id='button-toolbar'> <div id='button-toolbar'>
<button class="btn btn-success float-right" id='supplier-create'>New Supplier Part</button> <button class="btn btn-success" id='supplier-create'>New Supplier Part</button>
<div id='opt-dropdown' class="dropdown" style='float: right;'>
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
<span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href='#' id='supplier-part-delete' title='Delete supplier parts'>Delete</a></li>
</ul>
</div>
</div> </div>
<hr>
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'> <table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'>
</table> </table>
@ -46,10 +51,10 @@
}, },
{ {
sortable: true, sortable: true,
field: 'supplier', field: 'supplier_name',
title: 'Supplier', title: 'Supplier',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
return renderLink(value.name, value.url); return renderLink(value, '/company/' + row.supplier + '/');
} }
}, },
{ {
@ -74,4 +79,6 @@
url: "{% url 'api-part-supplier-list' %}" url: "{% url 'api-part-supplier-list' %}"
}); });
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
{% endblock %} {% endblock %}

View File

@ -41,14 +41,14 @@
visible: false, visible: false,
}, },
{ {
field: 'part', field: 'part_detail',
title: 'Part', title: 'Part',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
return renderLink(value.name, value.url); return renderLink(value.name, value.url + 'bom/');
} }
}, },
{ {
field: 'part.description', field: 'part_detail.description',
title: 'Description', title: 'Description',
}, },
{ {

View File

@ -87,7 +87,7 @@ function loadBomTable(table, options) {
// Part column // Part column
cols.push( cols.push(
{ {
field: 'sub_part', field: 'sub_part_detail',
title: 'Part', title: 'Part',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
@ -99,7 +99,7 @@ function loadBomTable(table, options) {
// Part description // Part description
cols.push( cols.push(
{ {
field: 'sub_part.description', field: 'sub_part_detail.description',
title: 'Description', title: 'Description',
} }
); );
@ -127,8 +127,8 @@ function loadBomTable(table, options) {
if (options.editable) { if (options.editable) {
cols.push({ cols.push({
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var bEdit = "<button class='btn btn-success bom-edit-button btn-sm' type='button' url='" + row.url + "edit'>Edit</button>"; var bEdit = "<button class='btn btn-success bom-edit-button btn-sm' type='button' url='/part/bom/" + row.pk + "/edit'>Edit</button>";
var bDelt = "<button class='btn btn-danger bom-delete-button btn-sm' type='button' url='" + row.url + "delete'>Delete</button>"; var bDelt = "<button class='btn btn-danger bom-delete-button btn-sm' type='button' url='/part/bom/" + row.pk + "/delete'>Delete</button>";
return "<div class='btn-group'>" + bEdit + bDelt + "</div>"; return "<div class='btn-group'>" + bEdit + bDelt + "</div>";
} }
@ -137,14 +137,14 @@ function loadBomTable(table, options) {
else { else {
cols.push( cols.push(
{ {
field: 'sub_part.available_stock', field: 'sub_part_detail.available_stock',
title: 'Available', title: 'Available',
searchable: false, searchable: false,
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var text = ""; var text = "";
if (row.quantity < row.sub_part.available_stock) if (row.quantity < row.sub_part_detail.available_stock)
{ {
text = "<span class='label label-success'>" + value + "</span>"; text = "<span class='label label-success'>" + value + "</span>";
} }

View File

@ -13,7 +13,6 @@ from .serializers import LocationSerializer
from .serializers import StockTrackingSerializer from .serializers import StockTrackingSerializer
from InvenTree.views import TreeSerializer from InvenTree.views import TreeSerializer
from InvenTree.serializers import DraftRUDView
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.views import APIView from rest_framework.views import APIView
@ -26,7 +25,7 @@ class StockCategoryTree(TreeSerializer):
model = StockLocation model = StockLocation
class StockDetail(DraftRUDView): class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
get: get:

View File

@ -1,5 +1,14 @@
{% if form.non_field_errors %}
<div class='alert alert-danger' role='alert' style='display: block;'>
<b>Error Submitting Form:</b>
{{ form.non_field_errors }}
</div>
{% endif %}
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data"> <form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% crispy form %} {% crispy form %}
</form> </form>