2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 20:16:44 +00:00

Merge pull request #35 from SchrodingersGat/master

API improvements
This commit is contained in:
Oliver 2017-04-14 12:15:22 +10:00 committed by GitHub
commit 92f4d47f74
11 changed files with 201 additions and 43 deletions

View File

@ -32,6 +32,7 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django_filters',
'rest_framework', 'rest_framework',
# Core django modules # Core django modules

View File

@ -95,13 +95,8 @@ class PartParameterTemplate(models.Model):
A PartParameterTemplate can be optionally associated with a PartCategory A PartParameterTemplate can be optionally associated with a PartCategory
""" """
name = models.CharField(max_length=20) name = models.CharField(max_length=20)
description = models.CharField(max_length=100, blank=True)
units = models.CharField(max_length=10, blank=True) units = models.CharField(max_length=10, blank=True)
default_value = models.CharField(max_length=50, blank=True)
default_min = models.CharField(max_length=50, blank=True)
default_max = models.CharField(max_length=50, blank=True)
# Parameter format # Parameter format
PARAM_NUMERIC = 10 PARAM_NUMERIC = 10
PARAM_TEXT = 20 PARAM_TEXT = 20
@ -143,10 +138,32 @@ class CategoryParameterLink(models.Model):
verbose_name_plural = "Category Parameters" verbose_name_plural = "Category Parameters"
class PartParameterManager(models.Manager):
""" Manager for handling PartParameter objects
"""
def create(self, *args, **kwargs):
""" Prevent creation of duplicate PartParameter
"""
part_id = kwargs['part']
template_id = kwargs['template']
try:
params = self.filter(part=part_id, template=template_id)
return params[0]
except:
pass
return super(PartParameterManager, self).create(*args, **kwargs)
class PartParameter(models.Model): class PartParameter(models.Model):
""" PartParameter is associated with a single part """ PartParameter is associated with a single part
""" """
objects = PartParameterManager()
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters')
template = models.ForeignKey(PartParameterTemplate) template = models.ForeignKey(PartParameterTemplate)
@ -155,17 +172,6 @@ class PartParameter(models.Model):
min_value = models.CharField(max_length=50, blank=True) min_value = models.CharField(max_length=50, blank=True)
max_value = models.CharField(max_length=50, blank=True) max_value = models.CharField(max_length=50, blank=True)
# Prevent multiple parameters of the same template
# from being added to the same part
def save(self, *args, **kwargs):
params = PartParameter.objects.filter(part=self.part, template=self.template)
if len(params) > 1:
return
if len(params) == 1 and params[0].id != self.id:
return
super(PartParameter, self).save(*args, **kwargs)
def __str__(self): def __str__(self):
return "{name} : {val}{units}".format( return "{name} : {val}{units}".format(
name=self.template.name, name=self.template.name,

View File

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Part, PartCategory, PartParameter from .models import Part, PartCategory, PartParameter, PartParameterTemplate
class PartParameterSerializer(serializers.ModelSerializer): class PartParameterSerializer(serializers.ModelSerializer):
@ -58,3 +58,13 @@ class PartCategoryDetailSerializer(serializers.ModelSerializer):
'path', 'path',
'children', 'children',
'parts') 'parts')
class PartTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = PartParameterTemplate
fields = ('pk',
'name',
'units',
'format')

View File

@ -16,16 +16,24 @@ categorypatterns = [
url(r'^$', views.PartCategoryList.as_view()) url(r'^$', views.PartCategoryList.as_view())
] ]
""" URL patterns associated with a particular part: partparampatterns = [
/part/<pk> -> Detail view of a given part # Detail of a single part parameter
/part/<pk>/parameters -> List parameters associated with a part url(r'^(?P<pk>[0-9]+)/$', views.PartParamDetail.as_view()),
"""
partdetailpatterns = [ # Parameters associated with a particular part
# Single part detail url(r'^\?[^/]*/$', views.PartParamList.as_view()),
url(r'^$', views.PartDetail.as_view()),
# All part parameters
url(r'^$', views.PartParamList.as_view()),
]
parttemplatepatterns = [
# Detail of a single part field template
url(r'^(?P<pk>[0-9]+)/$', views.PartTemplateDetail.as_view()),
# List all part field templates
url(r'^$', views.PartTemplateList.as_view())
# View part parameters
url(r'parameters/$', views.PartParameters.as_view())
] ]
""" Top-level URL patterns for the Part app: """ Top-level URL patterns for the Part app:
@ -36,11 +44,17 @@ partdetailpatterns = [
""" """
urlpatterns = [ urlpatterns = [
# Individual part # Individual part
url(r'^(?P<pk>[0-9]+)/', include(partdetailpatterns)), url(r'^(?P<pk>[0-9]+)/$', views.PartDetail.as_view()),
# Part categories # Part categories
url(r'^category/', views.PartCategoryList.as_view()), url(r'^category/', views.PartCategoryList.as_view()),
# List of all parts # Part parameters
url(r'^$', views.PartList.as_view()) url(r'^parameters/', include(partparampatterns)),
# Part templates
url(r'^templates/', include(parttemplatepatterns)),
# List parts with optional filters
url(r'^\?*[^/]*/?$', views.PartList.as_view()),
] ]

View File

@ -1,9 +1,12 @@
# import django_filters
from rest_framework import generics, permissions from rest_framework import generics, permissions
from .models import PartCategory, Part, PartParameter from .models import PartCategory, Part, PartParameter, PartParameterTemplate
from .serializers import PartSerializer from .serializers import PartSerializer
from .serializers import PartCategoryDetailSerializer from .serializers import PartCategoryDetailSerializer
from .serializers import PartParameterSerializer from .serializers import PartParameterSerializer
from .serializers import PartTemplateSerializer
class PartDetail(generics.RetrieveUpdateDestroyAPIView): class PartDetail(generics.RetrieveUpdateDestroyAPIView):
@ -14,25 +17,69 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class PartParameters(generics.ListCreateAPIView): class PartParamList(generics.ListCreateAPIView):
""" Return all parameters associated with a particular part """ Return all parameters associated with a particular part
""" """
def get_queryset(self): def get_queryset(self):
part_id = self.kwargs['pk'] part_id = self.request.query_params.get('part', None)
return PartParameter.objects.filter(part=part_id)
if part_id:
return PartParameter.objects.filter(part=part_id)
else:
return PartParameter.objects.all()
serializer_class = PartParameterSerializer serializer_class = PartParameterSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def create(self, request, *args, **kwargs):
# Ensure part link is set correctly
part_id = self.request.query_params.get('part', None)
if part_id:
request.data['part'] = part_id
return super(PartParamList, self).create(request, *args, **kwargs)
class PartParamDetail(generics.RetrieveUpdateDestroyAPIView):
""" Detail view of a single PartParameter
"""
queryset = PartParameter.objects.all()
serializer_class = PartParameterSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
"""
class PartFilter(django_filters.rest_framework.FilterSet):
min_stock = django_filters.NumberFilter(name="stock", lookup_expr="gte")
max_stock = django_filters.NumberFilter(name="stock", lookup_expr="lte")
class Meta:
model = Part
fields = ['stock']
"""
class PartList(generics.ListCreateAPIView): class PartList(generics.ListCreateAPIView):
""" Display a list of parts, with optional filters
Filters are specified in the url, e.g.
/part/?category=127
/part/?min_stock=100
"""
def get_queryset(self):
parts = Part.objects.all()
cat_id = self.request.query_params.get('category', None)
if cat_id:
parts = parts.filter(category=cat_id)
return parts
queryset = Part.objects.all()
serializer_class = PartSerializer serializer_class = PartSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class PartCategoryDetail(generics.RetrieveUpdateAPIView): class PartCategoryDetail(generics.RetrieveUpdateDestroyAPIView):
""" Return information on a single PartCategory """ Return information on a single PartCategory
""" """
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()
@ -47,3 +94,17 @@ class PartCategoryList(generics.ListCreateAPIView):
queryset = PartCategory.objects.filter(parent=None) queryset = PartCategory.objects.filter(parent=None)
serializer_class = PartCategoryDetailSerializer serializer_class = PartCategoryDetailSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class PartTemplateDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = PartParameterTemplate.objects.all()
serializer_class = PartTemplateSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class PartTemplateList(generics.ListCreateAPIView):
queryset = PartParameterTemplate.objects.all()
serializer_class = PartTemplateSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

View File

@ -41,12 +41,38 @@ class Project(models.Model):
return self.projectpart_set.all() return self.projectpart_set.all()
class ProjectPartManager(models.Manager):
""" Manager for handling ProjectParts
"""
def create(self, *args, **kwargs):
""" Test for validity of new ProjectPart before actually creating it.
If a ProjectPart already exists that references the same:
a) Part
b) Project
then return THAT project instead.
"""
project_id = kwargs['project']
part_id = kwargs['part']
try:
project_parts = self.filter(project=project_id, part=part_id)
return project_parts[0]
except:
pass
return super(ProjectPartManager, self).create(*args, **kwargs)
class ProjectPart(models.Model): class ProjectPart(models.Model):
""" A project part associates a single part with a project """ A project part associates a single part with a project
The quantity of parts required for a single-run of that project is stored. The quantity of parts required for a single-run of that project is stored.
The overage is the number of extra parts that are generally used for a single run. The overage is the number of extra parts that are generally used for a single run.
""" """
objects = ProjectPartManager()
part = models.ForeignKey(Part, on_delete=models.CASCADE) part = models.ForeignKey(Part, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1) quantity = models.PositiveIntegerField(default=1)

View File

@ -9,9 +9,14 @@ from . import views
projectdetailpatterns = [ projectdetailpatterns = [
# Single project detail # Single project detail
url(r'^$', views.ProjectDetail.as_view()), url(r'^$', views.ProjectDetail.as_view()),
]
# Parts associated with a project projectpartpatterns = [
url(r'^parts/$', views.ProjectPartsList.as_view()), # Detail of a single project part
url(r'^(?P<pk>[0-9]+)/$', views.ProjectPartDetail.as_view()),
# List project parts, with optional filters
url(r'^\?*[^/]*/?$', views.ProjectPartsList.as_view()),
] ]
projectcategorypatterns = [ projectcategorypatterns = [
@ -23,7 +28,6 @@ projectcategorypatterns = [
# Create a new category # Create a new category
url(r'^new/$', views.NewProjectCategory.as_view()) url(r'^new/$', views.NewProjectCategory.as_view())
] ]
urlpatterns = [ urlpatterns = [
@ -34,6 +38,9 @@ urlpatterns = [
# List of all projects # List of all projects
url(r'^$', views.ProjectList.as_view()), url(r'^$', views.ProjectList.as_view()),
# Project parts
url(r'^parts/', include(projectpartpatterns)),
# Project categories # Project categories
url(r'^category/', include(projectcategorypatterns)), url(r'^category/', include(projectcategorypatterns)),
] ]

View File

@ -6,7 +6,9 @@ from .serializers import ProjectCategoryDetailSerializer
from .serializers import ProjectPartSerializer from .serializers import ProjectPartSerializer
class ProjectDetail(generics.RetrieveUpdateAPIView): class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
""" Project details
"""
queryset = Project.objects.all() queryset = Project.objects.all()
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
@ -14,6 +16,8 @@ class ProjectDetail(generics.RetrieveUpdateAPIView):
class ProjectList(generics.ListCreateAPIView): class ProjectList(generics.ListCreateAPIView):
""" List all projects
"""
queryset = Project.objects.all() queryset = Project.objects.all()
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
@ -28,6 +32,8 @@ class NewProjectCategory(generics.CreateAPIView):
class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): class ProjectCategoryDetail(generics.RetrieveUpdateAPIView):
""" Project details
"""
queryset = ProjectCategory.objects.all() queryset = ProjectCategory.objects.all()
serializer_class = ProjectCategoryDetailSerializer serializer_class = ProjectCategoryDetailSerializer
@ -35,6 +41,9 @@ class ProjectCategoryDetail(generics.RetrieveUpdateAPIView):
class ProjectCategoryList(generics.ListCreateAPIView): class ProjectCategoryList(generics.ListCreateAPIView):
""" Top-level project categories.
Projects are considered top-level if they do not have a parent
"""
queryset = ProjectCategory.objects.filter(parent=None) queryset = ProjectCategory.objects.filter(parent=None)
serializer_class = ProjectCategoryDetailSerializer serializer_class = ProjectCategoryDetailSerializer
@ -42,10 +51,32 @@ class ProjectCategoryList(generics.ListCreateAPIView):
class ProjectPartsList(generics.ListCreateAPIView): class ProjectPartsList(generics.ListCreateAPIView):
""" List all parts associated with a particular project
"""
serializer_class = ProjectPartSerializer serializer_class = ProjectPartSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get_queryset(self): def get_queryset(self):
project_id = self.kwargs['pk'] project_id = self.request.query_params.get('project', None)
return ProjectPart.objects.filter(project=project_id)
if project_id:
return ProjectPart.objects.filter(project=project_id)
else:
return ProjectPart.objects.all()
def create(self, request, *args, **kwargs):
# Ensure project link is set correctly
prj_id = self.request.query_params.get('project', None)
if prj_id:
request.data['project'] = prj_id
return super(ProjectPartsList, self).create(request, *args, **kwargs)
class ProjectPartDetail(generics.RetrieveUpdateDestroyAPIView):
""" Detail for a single project part
"""
queryset = ProjectPart.objects.all()
serializer_class = ProjectPartSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

View File

@ -10,5 +10,6 @@ urlpatterns = [
url(r'^location/(?P<pk>[0-9]+)$', views.LocationDetail.as_view()), url(r'^location/(?P<pk>[0-9]+)$', views.LocationDetail.as_view()),
# List all top-level locations # List all top-level locations
url(r'^location/$', views.LocationList.as_view()) url(r'^location/$', views.LocationList.as_view()),
url(r'^$', views.LocationList.as_view())
] ]

View File

@ -5,7 +5,7 @@ from .models import StockLocation, StockItem
from .serializers import StockItemSerializer, LocationDetailSerializer from .serializers import StockItemSerializer, LocationDetailSerializer
class PartStockDetail(generics.ListAPIView): class PartStockDetail(generics.ListCreateAPIView):
""" Return a list of all stockitems for a given part """ Return a list of all stockitems for a given part
""" """

View File

@ -1,2 +1,3 @@
Django==1.11 Django==1.11
djangorestframework==3.6.2 djangorestframework==3.6.2
django_filter==1.0.2