From 2f3e29ab13c138f96722014dde3a20c1504710d3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 08:52:25 +1000 Subject: [PATCH 1/9] more api fixes --- InvenTree/project/views.py | 4 +++- InvenTree/stock/urls.py | 3 ++- InvenTree/stock/views.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index bb4d415139..cbae1e88ce 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -7,6 +7,8 @@ from .serializers import ProjectPartSerializer class ProjectDetail(generics.RetrieveUpdateAPIView): + """ Project details + """ queryset = Project.objects.all() serializer_class = ProjectSerializer @@ -25,7 +27,7 @@ class NewProjectCategory(generics.CreateAPIView): """ serializer_class = ProjectCategoryDetailSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - + class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index a9dc1bc09e..c7a27fe560 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -10,5 +10,6 @@ urlpatterns = [ url(r'^location/(?P[0-9]+)$', views.LocationDetail.as_view()), # 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()) ] diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 5f76cbebf1..8100abdb92 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -5,7 +5,7 @@ from .models import StockLocation, StockItem from .serializers import StockItemSerializer, LocationDetailSerializer -class PartStockDetail(generics.ListAPIView): +class PartStockDetail(generics.ListCreateAPIView): """ Return a list of all stockitems for a given part """ From 56ba30eb5257ff7b64b5dc273002ab16df49a9ac Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 09:29:58 +1000 Subject: [PATCH 2/9] Added ability to delete project --- InvenTree/project/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index cbae1e88ce..f7a5359d7e 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -6,7 +6,7 @@ from .serializers import ProjectCategoryDetailSerializer from .serializers import ProjectPartSerializer -class ProjectDetail(generics.RetrieveUpdateAPIView): +class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): """ Project details """ @@ -27,7 +27,7 @@ class NewProjectCategory(generics.CreateAPIView): """ serializer_class = ProjectCategoryDetailSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - + class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): From 7acae4ea478e1d9efd2571fdb3a9a08535965068 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 10:54:07 +1000 Subject: [PATCH 3/9] ProjectPart API fix When a new ProjectPart is added, the Project link is automatically set --- InvenTree/project/views.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index f7a5359d7e..af9992cf11 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -16,6 +16,8 @@ class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): class ProjectList(generics.ListCreateAPIView): + """ List all projects + """ queryset = Project.objects.all() serializer_class = ProjectSerializer @@ -30,6 +32,8 @@ class NewProjectCategory(generics.CreateAPIView): class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): + """ Project details + """ queryset = ProjectCategory.objects.all() serializer_class = ProjectCategoryDetailSerializer @@ -37,6 +41,9 @@ class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): 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) serializer_class = ProjectCategoryDetailSerializer @@ -44,6 +51,8 @@ class ProjectCategoryList(generics.ListCreateAPIView): class ProjectPartsList(generics.ListCreateAPIView): + """ List all parts associated with a particular project + """ serializer_class = ProjectPartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) @@ -51,3 +60,8 @@ class ProjectPartsList(generics.ListCreateAPIView): def get_queryset(self): project_id = self.kwargs['pk'] return ProjectPart.objects.filter(project=project_id) + + def create(self, request, *args, **kwargs): + # Ensure project link is set correctly + request.data['project'] = self.kwargs['pk'] + return super(ProjectPartsList, self).create(request, *args, **kwargs) From 04524d38ab00d951451c8d84069ab56769494b21 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 10:54:18 +1000 Subject: [PATCH 4/9] Prevent duplicate ProjectParts --- InvenTree/project/models.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index bf2bd42368..0b063af036 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -41,12 +41,38 @@ class Project(models.Model): 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): """ 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 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) project = models.ForeignKey(Project, on_delete=models.CASCADE) quantity = models.PositiveIntegerField(default=1) From b13f135d8e8ac2b7ed8a3a2485c8e42665b1f395 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 11:16:49 +1000 Subject: [PATCH 5/9] Project API improvements - Added DetailView for ProjectPart - Improved URL structure --- InvenTree/project/urls.py | 14 ++++++++++++-- InvenTree/project/views.py | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py index 1dd0fba554..4fc57c753f 100644 --- a/InvenTree/project/urls.py +++ b/InvenTree/project/urls.py @@ -9,9 +9,17 @@ from . import views projectdetailpatterns = [ # Single project detail url(r'^$', views.ProjectDetail.as_view()), +] + +projectpartpatterns = [ + # Detail of a single project part + url(r'^(?P[0-9]+)/$', views.ProjectPartDetail.as_view()), # Parts associated with a project - url(r'^parts/$', views.ProjectPartsList.as_view()), + url(r'^\?[^/]*/$', views.ProjectPartsList.as_view()), + + # All project parts + url(r'^$', views.ProjectPartsList.as_view()), ] projectcategorypatterns = [ @@ -23,7 +31,6 @@ projectcategorypatterns = [ # Create a new category url(r'^new/$', views.NewProjectCategory.as_view()) - ] urlpatterns = [ @@ -34,6 +41,9 @@ urlpatterns = [ # List of all projects url(r'^$', views.ProjectList.as_view()), + # Project parts + url(r'^parts/', include(projectpartpatterns)), + # Project categories url(r'^category/', include(projectcategorypatterns)), ] diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index af9992cf11..7c3518ee33 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -58,10 +58,23 @@ class ProjectPartsList(generics.ListCreateAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) def get_queryset(self): - project_id = self.kwargs['pk'] - return ProjectPart.objects.filter(project=project_id) + project_id = self.request.query_params.get('project', None) + + 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 - request.data['project'] = self.kwargs['pk'] + request.data['project'] = self.request.query_params.get('project', None) 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,) From 817a67cd413be9c552386d9a9559aef5790c1a0d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 11:46:05 +1000 Subject: [PATCH 6/9] Fix for project API If project is not specified in URL, take value from JSON --- InvenTree/project/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index 7c3518ee33..923fff27fe 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -67,7 +67,9 @@ class ProjectPartsList(generics.ListCreateAPIView): def create(self, request, *args, **kwargs): # Ensure project link is set correctly - request.data['project'] = self.request.query_params.get('project', None) + 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) From 39a6bcdf3e896c2ccbf3d1345acf572736971e6e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 11:46:18 +1000 Subject: [PATCH 7/9] Improved Part API --- InvenTree/part/models.py | 37 +++++++++++++++------------- InvenTree/part/serializers.py | 12 +++++++++- InvenTree/part/urls.py | 34 ++++++++++++++++++-------- InvenTree/part/views.py | 45 +++++++++++++++++++++++++++++++---- 4 files changed, 96 insertions(+), 32 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index c479037743..16f136287e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -95,13 +95,8 @@ class PartParameterTemplate(models.Model): A PartParameterTemplate can be optionally associated with a PartCategory """ name = models.CharField(max_length=20) - description = models.CharField(max_length=100, 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 PARAM_NUMERIC = 10 PARAM_TEXT = 20 @@ -143,10 +138,31 @@ class CategoryParameterLink(models.Model): 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): """ PartParameter is associated with a single part """ + objects = PartParameterManager() + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') template = models.ForeignKey(PartParameterTemplate) @@ -155,17 +171,6 @@ class PartParameter(models.Model): min_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): return "{name} : {val}{units}".format( name=self.template.name, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 774815ac1a..06ad724b50 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import Part, PartCategory, PartParameter +from .models import Part, PartCategory, PartParameter, PartParameterTemplate class PartParameterSerializer(serializers.ModelSerializer): @@ -58,3 +58,13 @@ class PartCategoryDetailSerializer(serializers.ModelSerializer): 'path', 'children', 'parts') + + +class PartTemplateSerializer(serializers.ModelSerializer): + + class Meta: + model = PartParameterTemplate + fields = ('pk', + 'name', + 'units', + 'format') diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 01db39c149..4fe5a369a4 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -16,16 +16,24 @@ categorypatterns = [ url(r'^$', views.PartCategoryList.as_view()) ] -""" URL patterns associated with a particular part: -/part/ -> Detail view of a given part -/part//parameters -> List parameters associated with a part -""" -partdetailpatterns = [ - # Single part detail - url(r'^$', views.PartDetail.as_view()), +partparampatterns = [ + # Detail of a single part parameter + url(r'^(?P[0-9]+)/$', views.PartParamDetail.as_view()), + + # Parameters associated with a particular part + url(r'^\?[^/]*/$', views.PartParamList.as_view()), + + # All part parameters + url(r'^$', views.PartParamList.as_view()), +] + +parttemplatepatterns = [ + # Detail of a single part field template + url(r'^(?P[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: @@ -36,11 +44,17 @@ partdetailpatterns = [ """ urlpatterns = [ # Individual part - url(r'^(?P[0-9]+)/', include(partdetailpatterns)), + url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), # Part categories url(r'^category/', views.PartCategoryList.as_view()), + # Part parameters + url(r'^parameters/', include(partparampatterns)), + + # Part templates + url(r'^templates/', include(parttemplatepatterns)), + # List of all parts url(r'^$', views.PartList.as_view()) ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 55d5255789..289f38eb43 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,9 +1,10 @@ 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 PartCategoryDetailSerializer from .serializers import PartParameterSerializer +from .serializers import PartTemplateSerializer class PartDetail(generics.RetrieveUpdateDestroyAPIView): @@ -14,13 +15,33 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class PartParameters(generics.ListCreateAPIView): +class PartParamList(generics.ListCreateAPIView): """ Return all parameters associated with a particular part """ def get_queryset(self): - part_id = self.kwargs['pk'] - return PartParameter.objects.filter(part=part_id) + part_id = self.request.query_params.get('part', None) + if part_id: + return PartParameter.objects.filter(part=part_id) + else: + return PartParameter.objects.all() + + serializer_class = PartParameterSerializer + 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,) @@ -32,7 +53,7 @@ class PartList(generics.ListCreateAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class PartCategoryDetail(generics.RetrieveUpdateAPIView): +class PartCategoryDetail(generics.RetrieveUpdateDestroyAPIView): """ Return information on a single PartCategory """ queryset = PartCategory.objects.all() @@ -47,3 +68,17 @@ class PartCategoryList(generics.ListCreateAPIView): queryset = PartCategory.objects.filter(parent=None) serializer_class = PartCategoryDetailSerializer 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,) From 6f9bf45e22fd5bfaf5acdb125fb2409ac9d0cf5c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 12:11:42 +1000 Subject: [PATCH 8/9] Added django_filter app --- InvenTree/InvenTree/settings.py | 1 + requirements/base.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index ab2205a296..975af8c20b 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -32,6 +32,7 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ + 'django_filters', 'rest_framework', # Core django modules diff --git a/requirements/base.txt b/requirements/base.txt index 1a4283d80d..0c17b62bfe 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,2 +1,3 @@ Django==1.11 djangorestframework==3.6.2 +django_filter==1.0.2 From 2299cd07008ed5f9b78268db3c90ff8fed552b8e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Apr 2017 12:14:50 +1000 Subject: [PATCH 9/9] Further API improvements --- InvenTree/part/models.py | 1 + InvenTree/part/urls.py | 4 ++-- InvenTree/part/views.py | 30 ++++++++++++++++++++++++++++-- InvenTree/project/urls.py | 7 ++----- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 16f136287e..5afa01a293 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -157,6 +157,7 @@ class PartParameterManager(models.Manager): return super(PartParameterManager, self).create(*args, **kwargs) + class PartParameter(models.Model): """ PartParameter is associated with a single part """ diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 4fe5a369a4..b351ce3eda 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -55,6 +55,6 @@ urlpatterns = [ # Part templates url(r'^templates/', include(parttemplatepatterns)), - # List of all parts - url(r'^$', views.PartList.as_view()) + # List parts with optional filters + url(r'^\?*[^/]*/?$', views.PartList.as_view()), ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 289f38eb43..c4646b6a6e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,3 +1,5 @@ +# import django_filters + from rest_framework import generics, permissions from .models import PartCategory, Part, PartParameter, PartParameterTemplate @@ -46,9 +48,33 @@ class PartParamDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class PartList(generics.ListCreateAPIView): +""" +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): + """ 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 permission_classes = (permissions.IsAuthenticatedOrReadOnly,) diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py index 4fc57c753f..27c7ab2f0b 100644 --- a/InvenTree/project/urls.py +++ b/InvenTree/project/urls.py @@ -15,11 +15,8 @@ projectpartpatterns = [ # Detail of a single project part url(r'^(?P[0-9]+)/$', views.ProjectPartDetail.as_view()), - # Parts associated with a project - url(r'^\?[^/]*/$', views.ProjectPartsList.as_view()), - - # All project parts - url(r'^$', views.ProjectPartsList.as_view()), + # List project parts, with optional filters + url(r'^\?*[^/]*/?$', views.ProjectPartsList.as_view()), ] projectcategorypatterns = [