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 rest_framework import serializers
from rest_framework import generics
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]:
super(generics.UpdateAPIView, self).perform_update(serializer)
else:
pass
return data

View File

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

View File

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

View File

@ -50,10 +50,10 @@
},
{
sortable: true,
field: 'part',
field: 'part_name',
title: 'Part',
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 InvenTree.views import TreeSerializer
from InvenTree.serializers import DraftRUDView
class PartCategoryTree(TreeSerializer):
@ -56,7 +55,7 @@ class CategoryList(generics.ListCreateAPIView):
]
class PartDetail(DraftRUDView):
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Part.objects.all()
serializer_class = PartSerializer
@ -125,7 +124,7 @@ class PartList(generics.ListCreateAPIView):
]
class BomList(generics.ListAPIView):
class BomList(generics.ListCreateAPIView):
queryset = BomItem.objects.all()
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()
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):
queryset = SupplierPriceBreak.objects.all()
@ -189,16 +208,31 @@ cat_api_urls = [
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 = [
url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),
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'^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'^.*$', 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):
return reverse('bom-detail', kwargs={'pk': self.id})
return reverse('bom-item-detail', kwargs={'pk': self.id})
# A link to the parent part
# 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
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():
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:
verbose_name = "BOM Item"

View File

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

View File

@ -7,11 +7,16 @@
<h3>Part Suppliers</h3>
<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>
<hr>
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'>
</table>
@ -46,10 +51,10 @@
},
{
sortable: true,
field: 'supplier',
field: 'supplier_name',
title: 'Supplier',
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' %}"
});
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
{% endblock %}

View File

@ -41,14 +41,14 @@
visible: false,
},
{
field: 'part',
field: 'part_detail',
title: 'Part',
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',
},
{

View File

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

View File

@ -13,7 +13,6 @@ from .serializers import LocationSerializer
from .serializers import StockTrackingSerializer
from InvenTree.views import TreeSerializer
from InvenTree.serializers import DraftRUDView
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
@ -26,7 +25,7 @@ class StockCategoryTree(TreeSerializer):
model = StockLocation
class StockDetail(DraftRUDView):
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
"""
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">
{% csrf_token %}
{% load crispy_forms_tags %}
{% crispy form %}
</form>