diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 4eb8dd8daa..e8430694d0 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -173,3 +173,25 @@ class InvenTreeTree(models.Model): """ return self.path + + +def FilterChildren(queryset, parent): + """ Filter a queryset, limit to only objects that are a child of the given parent + Filter is passed in the URL string, e.g. '/?parent=123' + To accommodate for items without a parent, top-level items can be specified as: + none / false / null / top / 0 + """ + + if not parent: + return queryset + elif isinstance(parent, str) and parent.lower() in ['none', 'false', 'null', 'top', '0']: + return queryset.filter(parent=None) + else: + try: + parent_id = int(parent) + if parent_id == 0: + return queryset.filter(parent=None) + else: + return queryset.filter(parent=parent_id) + except: + return queryset diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 06ad724b50..7e5cfd3aaa 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -43,21 +43,13 @@ class PartCategoryBriefSerializer(serializers.ModelSerializer): class PartCategoryDetailSerializer(serializers.ModelSerializer): - # List of parts in this category - parts = PartSerializer(many=True, read_only=True) - - # List of child categories under this one - children = PartCategoryBriefSerializer(many=True, read_only=True) - class Meta: model = PartCategory fields = ('pk', 'name', 'description', 'parent', - 'path', - 'children', - 'parts') + 'path') class PartTemplateSerializer(serializers.ModelSerializer): diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index b351ce3eda..bb7595a271 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -10,30 +10,26 @@ from . import views categorypatterns = [ # Part category detail - url(r'^category/(?P[0-9]+)/$', views.PartCategoryDetail.as_view()), + url(r'^(?P[0-9]+)/?$', views.PartCategoryDetail.as_view()), # List of top-level categories - url(r'^$', views.PartCategoryList.as_view()) + url(r'^\?*[^/]*/?$', views.PartCategoryList.as_view()) ] partparampatterns = [ # Detail of a single part parameter - url(r'^(?P[0-9]+)/$', views.PartParamDetail.as_view()), + 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()), + url(r'^\?*[^/]*/?$', views.PartParamList.as_view()), ] parttemplatepatterns = [ # Detail of a single part field template - url(r'^(?P[0-9]+)/$', views.PartTemplateDetail.as_view()), + url(r'^(?P[0-9]+)/?$', views.PartTemplateDetail.as_view()), # List all part field templates url(r'^$', views.PartTemplateList.as_view()) - ] """ Top-level URL patterns for the Part app: @@ -44,16 +40,16 @@ parttemplatepatterns = [ """ urlpatterns = [ # Individual part - url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), + url(r'^(?P[0-9]+)/?$', views.PartDetail.as_view()), # Part categories - url(r'^category/', views.PartCategoryList.as_view()), + url(r'^category/?', include(categorypatterns)), # Part parameters - url(r'^parameters/', include(partparampatterns)), + url(r'^parameters/?', include(partparampatterns)), # Part templates - url(r'^templates/', include(parttemplatepatterns)), + url(r'^templates/?', include(parttemplatepatterns)), # List parts with optional filters url(r'^\?*[^/]*/?$', views.PartList.as_view()), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index c4646b6a6e..711df899f8 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2,6 +2,7 @@ from rest_framework import generics, permissions +from InvenTree.models import FilterChildren from .models import PartCategory, Part, PartParameter, PartParameterTemplate from .serializers import PartSerializer from .serializers import PartCategoryDetailSerializer @@ -68,8 +69,10 @@ class PartList(generics.ListCreateAPIView): def get_queryset(self): parts = Part.objects.all() + params = self.request.query_params + + cat_id = params.get('category', None) - cat_id = self.request.query_params.get('category', None) if cat_id: parts = parts.filter(category=cat_id) @@ -91,6 +94,16 @@ class PartCategoryList(generics.ListCreateAPIView): """ Return a list of all top-level part categories. Categories are considered "top-level" if they do not have a parent """ + + def get_queryset(self): + params = self.request.query_params + + categories = PartCategory.objects.all() + + categories = FilterChildren(categories, params.get('parent', None)) + + return categories + queryset = PartCategory.objects.filter(parent=None) serializer_class = PartCategoryDetailSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py index dd9c9cb784..00c70fed18 100644 --- a/InvenTree/project/serializers.py +++ b/InvenTree/project/serializers.py @@ -35,16 +35,10 @@ class ProjectCategoryBriefSerializer(serializers.ModelSerializer): class ProjectCategoryDetailSerializer(serializers.ModelSerializer): - projects = ProjectSerializer(many=True, read_only=True) - - children = ProjectCategoryBriefSerializer(many=True, read_only=True) - class Meta: model = ProjectCategory fields = ('pk', 'name', 'description', 'parent', - 'path', - 'children', - 'projects') + 'path') diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py index 27c7ab2f0b..d0f50bac56 100644 --- a/InvenTree/project/urls.py +++ b/InvenTree/project/urls.py @@ -13,34 +13,30 @@ projectdetailpatterns = [ projectpartpatterns = [ # Detail of a single project part - url(r'^(?P[0-9]+)/$', views.ProjectPartDetail.as_view()), + url(r'^(?P[0-9]+)/?$', views.ProjectPartDetail.as_view()), # List project parts, with optional filters url(r'^\?*[^/]*/?$', views.ProjectPartsList.as_view()), ] projectcategorypatterns = [ - # List of top-level project categories - url(r'^$', views.ProjectCategoryList.as_view()), - # Detail of a single project category - url(r'^(?P[0-9]+)/$', views.ProjectCategoryDetail.as_view()), + url(r'^(?P[0-9]+)/?$', views.ProjectCategoryDetail.as_view()), - # Create a new category - url(r'^new/$', views.NewProjectCategory.as_view()) + # List of project categories, with filters + url(r'^\?*[^/]*/?$', views.ProjectCategoryList.as_view()), ] urlpatterns = [ - # Individual project URL - url(r'^(?P[0-9]+)/', include(projectdetailpatterns)), + url(r'^(?P[0-9]+)/?', include(projectdetailpatterns)), # List of all projects url(r'^$', views.ProjectList.as_view()), # Project parts - url(r'^parts/', include(projectpartpatterns)), + url(r'^parts/?', include(projectpartpatterns)), # Project categories - url(r'^category/', include(projectcategorypatterns)), + url(r'^category/?', include(projectcategorypatterns)), ] diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index 923fff27fe..716cd19a79 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -1,5 +1,6 @@ from rest_framework import generics, permissions +from InvenTree.models import FilterChildren from .models import ProjectCategory, Project, ProjectPart from .serializers import ProjectSerializer from .serializers import ProjectCategoryDetailSerializer @@ -16,21 +17,24 @@ class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): class ProjectList(generics.ListCreateAPIView): - """ List all projects + """ List projects """ - queryset = Project.objects.all() + def get_queryset(self): + projects = Project.objects.all() + params = self.request.query_params + + cat_id = params.get('category', None) + + if cat_id: + projects = projects.filter(category=cat_id) + + return projects + serializer_class = ProjectSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class NewProjectCategory(generics.CreateAPIView): - """ Create a new Project Category - """ - serializer_class = ProjectCategoryDetailSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): """ Project details """ @@ -41,29 +45,42 @@ class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): class ProjectCategoryList(generics.ListCreateAPIView): - """ Top-level project categories. - Projects are considered top-level if they do not have a parent + """ List project categories """ - queryset = ProjectCategory.objects.filter(parent=None) + def get_queryset(self): + params = self.request.query_params + + categories = ProjectCategory.objects.all() + + categories = FilterChildren(categories, params.get('parent', None)) + + return categories + serializer_class = ProjectCategoryDetailSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) class ProjectPartsList(generics.ListCreateAPIView): - """ List all parts associated with a particular project + """ List project parts """ serializer_class = ProjectPartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) def get_queryset(self): - project_id = self.request.query_params.get('project', None) + parts = ProjectPart.objects.all() + params = self.request.query_params + project_id = params.get('project', None) if project_id: - return ProjectPart.objects.filter(project=project_id) - else: - return ProjectPart.objects.all() + parts = parts.filter(project=project_id) + + part_id = params.get('part', None) + if part_id: + parts = parts.filter(part=part_id) + + return parts def create(self, request, *args, **kwargs): # Ensure project link is set correctly diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 0e06115d39..a542edf150 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -35,18 +35,10 @@ class LocationDetailSerializer(serializers.ModelSerializer): """ Detailed information about a stock location """ - # List of all stock items in this location - items = StockItemSerializer(many=True, read_only=True) - - # List of all child locations under this one - children = LocationBriefSerializer(many=True, read_only=True) - class Meta: model = StockLocation fields = ('pk', 'name', 'description', 'parent', - 'path', - 'children', - 'items') + 'path') diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index c7a27fe560..b3496e5768 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -1,15 +1,20 @@ -from django.conf.urls import url +from django.conf.urls import url, include from . import views -urlpatterns = [ - # List all stock quantities for a given part - url(r'^part/(?P[0-9]+)$', views.PartStockDetail.as_view()), +locpatterns = [ + url(r'^(?P[0-9]+)/?$', views.LocationDetail.as_view()), - # List all stock items in a given location - url(r'^location/(?P[0-9]+)$', views.LocationDetail.as_view()), - - # List all top-level locations - url(r'^location/$', views.LocationList.as_view()), - url(r'^$', views.LocationList.as_view()) + url(r'^\?*[^/]*/?$', views.LocationList.as_view()) +] + +urlpatterns = [ + # Stock location urls + url(r'^location/?', include(locpatterns)), + + # Detail for a single stock item + url(r'^(?P[0-9]+)$', views.StockDetail.as_view()), + + # List all stock items, with optional filters + url(r'^\?*[^/]*/?$', views.StockList.as_view()), ] diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 8100abdb92..96ffd8633c 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1,33 +1,80 @@ -from rest_framework import generics +from rest_framework import generics, permissions +import django_filters +from InvenTree.models import FilterChildren from .models import StockLocation, StockItem - from .serializers import StockItemSerializer, LocationDetailSerializer -class PartStockDetail(generics.ListCreateAPIView): - """ Return a list of all stockitems for a given part - """ +class StockDetail(generics.RetrieveUpdateDestroyAPIView): + + queryset = StockItem.objects.all() + serializer_class = StockItemSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class StockFilter(django_filters.rest_framework.FilterSet): + min_stock = django_filters.NumberFilter(name='quantity', lookup_expr='gte') + max_stock = django_filters.NumberFilter(name='quantity', lookup_expr='lte') + + class Meta: + model = StockItem + fields = ['quantity'] + + +class StockList(generics.ListCreateAPIView): serializer_class = StockItemSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) + filter_class = StockFilter def get_queryset(self): - part_id = self.kwargs['part'] - return StockItem.objects.filter(part=part_id) + items = StockItem.objects.all() + + # Specify a particular part + part_id = self.request.query_params.get('part', None) + if part_id: + items = items.filter(part=part_id) + + # Specify a particular location + loc_id = self.request.query_params.get('location', None) + + if loc_id: + items = items.filter(location=loc_id) + + return items + + def create(self, request, *args, **kwargs): + # If the PART parameter is passed in the URL, use that + part_id = self.request.query_params.get('part', None) + if part_id: + request.data['part'] = part_id + return super(StockList, self).create(request, *args, **kwargs) -class LocationDetail(generics.RetrieveAPIView): +class LocationDetail(generics.RetrieveUpdateDestroyAPIView): """ Return information on a specific stock location """ queryset = StockLocation.objects.all() serializer_class = LocationDetailSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class LocationList(generics.ListAPIView): +class LocationList(generics.ListCreateAPIView): """ Return a list of top-level locations Locations are considered "top-level" if they do not have a parent """ - queryset = StockLocation.objects.filter(parent=None) + def get_queryset(self): + params = self.request.query_params + + locations = StockLocation.objects.all() + + locations = FilterChildren(locations, params.get('parent', None)) + + return locations + serializer_class = LocationDetailSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,)