mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-20 05:46:34 +00:00
Merge pull request #1716 from SchrodingersGat/drf-api-forms
[WIP] API Forms
This commit is contained in:
@ -127,7 +127,10 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a single PartCategory object """
|
||||
"""
|
||||
API endpoint for detail view of a single PartCategory object
|
||||
"""
|
||||
|
||||
serializer_class = part_serializers.CategorySerializer
|
||||
queryset = PartCategory.objects.all()
|
||||
|
||||
@ -229,6 +232,24 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for PartAttachment model
|
||||
"""
|
||||
|
||||
queryset = PartAttachment.objects.all()
|
||||
serializer_class = part_serializers.PartAttachmentSerializer
|
||||
|
||||
|
||||
class PartTestTemplateDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail endpoint for PartTestTemplate model
|
||||
"""
|
||||
|
||||
queryset = PartTestTemplate.objects.all()
|
||||
serializer_class = part_serializers.PartTestTemplateSerializer
|
||||
|
||||
|
||||
class PartTestTemplateList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for listing (and creating) a PartTestTemplate.
|
||||
@ -337,8 +358,9 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
# By default, include 'category_detail' information in the detail view
|
||||
try:
|
||||
kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', False))
|
||||
kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', True))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@ -1032,11 +1054,13 @@ part_api_urls = [
|
||||
|
||||
# Base URL for PartTestTemplate API endpoints
|
||||
url(r'^test-template/', include([
|
||||
url(r'^(?P<pk>\d+)/', PartTestTemplateDetail.as_view(), name='api-part-test-template-detail'),
|
||||
url(r'^$', PartTestTemplateList.as_view(), name='api-part-test-template-list'),
|
||||
])),
|
||||
|
||||
# Base URL for PartAttachment API endpoints
|
||||
url(r'^attachment/', include([
|
||||
url(r'^(?P<pk>\d+)/', PartAttachmentDetail.as_view(), name='api-part-attachment-detail'),
|
||||
url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'),
|
||||
])),
|
||||
|
||||
|
@ -5,21 +5,21 @@ Django Forms for interacting with Part objects
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mptt.fields import TreeNodeChoiceField
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.helpers import GetExportFormats
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
|
||||
from mptt.fields import TreeNodeChoiceField
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import common.models
|
||||
|
||||
from .models import Part, PartCategory, PartAttachment, PartRelated
|
||||
from .models import Part, PartCategory, PartRelated
|
||||
from .models import BomItem
|
||||
from .models import PartParameterTemplate, PartParameter
|
||||
from .models import PartCategoryParameterTemplate
|
||||
from .models import PartTestTemplate
|
||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||
|
||||
|
||||
@ -65,22 +65,6 @@ class PartImageForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class EditPartTestTemplateForm(HelperForm):
|
||||
""" Class for creating / editing a PartTestTemplate object """
|
||||
|
||||
class Meta:
|
||||
model = PartTestTemplate
|
||||
|
||||
fields = [
|
||||
'part',
|
||||
'test_name',
|
||||
'description',
|
||||
'required',
|
||||
'requires_value',
|
||||
'requires_attachment',
|
||||
]
|
||||
|
||||
|
||||
class BomExportForm(forms.Form):
|
||||
""" Simple form to let user set BOM export options,
|
||||
before exporting a BOM (bill of materials) file.
|
||||
@ -185,18 +169,6 @@ class CreatePartRelatedForm(HelperForm):
|
||||
}
|
||||
|
||||
|
||||
class EditPartAttachmentForm(HelperForm):
|
||||
""" Form for editing a PartAttachment object """
|
||||
|
||||
class Meta:
|
||||
model = PartAttachment
|
||||
fields = [
|
||||
'part',
|
||||
'attachment',
|
||||
'comment'
|
||||
]
|
||||
|
||||
|
||||
class SetPartCategoryForm(forms.Form):
|
||||
""" Form for setting the category of multiple Part objects """
|
||||
|
||||
|
@ -75,6 +75,10 @@ class PartCategory(InvenTreeTree):
|
||||
|
||||
default_keywords = models.CharField(null=True, blank=True, max_length=250, verbose_name=_('Default keywords'), help_text=_('Default keywords for parts in this category'))
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-category-list')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('category-detail', kwargs={'pk': self.id})
|
||||
|
||||
@ -329,6 +333,11 @@ class Part(MPTTModel):
|
||||
# For legacy reasons the 'variant_of' field is used to indicate the MPTT parent
|
||||
parent_attr = 'variant_of'
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
|
||||
return reverse('api-part-list')
|
||||
|
||||
def get_context_data(self, request, **kwargs):
|
||||
"""
|
||||
Return some useful context data about this part for template rendering
|
||||
@ -1968,6 +1977,10 @@ class PartAttachment(InvenTreeAttachment):
|
||||
Model for storing file attachments against a Part object
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
return os.path.join("part_files", str(self.part.id))
|
||||
|
||||
@ -1979,6 +1992,10 @@ class PartSellPriceBreak(common.models.PriceBreak):
|
||||
"""
|
||||
Represents a price break for selling this part
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-sale-price-list')
|
||||
|
||||
part = models.ForeignKey(
|
||||
Part, on_delete=models.CASCADE,
|
||||
@ -1996,6 +2013,10 @@ class PartInternalPriceBreak(common.models.PriceBreak):
|
||||
Represents a price break for internally selling this part
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-internal-price-list')
|
||||
|
||||
part = models.ForeignKey(
|
||||
Part, on_delete=models.CASCADE,
|
||||
related_name='internalpricebreaks',
|
||||
@ -2040,6 +2061,10 @@ class PartTestTemplate(models.Model):
|
||||
run on the model (refer to the validate_unique function).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-test-template-list')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
self.clean()
|
||||
@ -2138,6 +2163,10 @@ class PartParameterTemplate(models.Model):
|
||||
units: The units of the Parameter [string]
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-param-template-list')
|
||||
|
||||
def __str__(self):
|
||||
s = str(self.name)
|
||||
if self.units:
|
||||
@ -2175,6 +2204,10 @@ class PartParameter(models.Model):
|
||||
data: The data (value) of the Parameter [string]
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-param-list')
|
||||
|
||||
def __str__(self):
|
||||
# String representation of a PartParameter (used in the admin interface)
|
||||
return "{part} : {param} = {data}{units}".format(
|
||||
@ -2266,6 +2299,10 @@ class BomItem(models.Model):
|
||||
allow_variants: Stock for part variants can be substituted for this BomItem
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-bom-list')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
self.clean()
|
||||
|
@ -4,6 +4,7 @@ JSON serializers for Part app
|
||||
import imghdr
|
||||
from decimal import Decimal
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Coalesce
|
||||
@ -38,6 +39,7 @@ class CategorySerializer(InvenTreeModelSerializer):
|
||||
'name',
|
||||
'description',
|
||||
'default_location',
|
||||
'default_keywords',
|
||||
'pathstring',
|
||||
'url',
|
||||
'parent',
|
||||
@ -59,7 +61,12 @@ class PartAttachmentSerializer(InvenTreeModelSerializer):
|
||||
'pk',
|
||||
'part',
|
||||
'attachment',
|
||||
'comment'
|
||||
'comment',
|
||||
'upload_date',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'upload_date',
|
||||
]
|
||||
|
||||
|
||||
@ -187,6 +194,9 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
Used when displaying all details of a single component.
|
||||
"""
|
||||
|
||||
def get_api_url(self):
|
||||
return reverse_lazy('api-part-list')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Custom initialization method for PartSerializer,
|
||||
@ -326,9 +336,10 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
'category',
|
||||
'category_detail',
|
||||
'component',
|
||||
'description',
|
||||
'default_location',
|
||||
'default_expiry',
|
||||
'default_location',
|
||||
'default_supplier',
|
||||
'description',
|
||||
'full_name',
|
||||
'image',
|
||||
'in_stock',
|
||||
|
@ -19,51 +19,68 @@
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadAttachmentTable(
|
||||
'{% url "api-part-attachment-list" %}',
|
||||
{
|
||||
filters: {
|
||||
part: {{ part.pk }},
|
||||
},
|
||||
onEdit: function(pk) {
|
||||
var url = `/api/part/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
fields: {
|
||||
comment: {},
|
||||
},
|
||||
title: '{% trans "Edit Attachment" %}',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
});
|
||||
},
|
||||
onDelete: function(pk) {
|
||||
var url = `/api/part/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
method: 'DELETE',
|
||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
||||
title: '{% trans "Delete Attachment" %}',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
enableDragAndDrop(
|
||||
'#attachment-dropzone',
|
||||
"{% url 'part-attachment-create' %}",
|
||||
'{% url "api-part-attachment-list" %}',
|
||||
{
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
label: 'attachment',
|
||||
success: function(data, status, xhr) {
|
||||
location.reload();
|
||||
reloadAttachmentTable();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$("#new-attachment").click(function() {
|
||||
launchModalForm("{% url 'part-attachment-create' %}?part={{ part.id }}",
|
||||
|
||||
constructForm(
|
||||
'{% url "api-part-attachment-list" %}',
|
||||
{
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$("#attachment-table").on('click', '.attachment-edit-button', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = `/part/attachment/${button.attr('pk')}/edit/`;
|
||||
|
||||
launchModalForm(url,
|
||||
{
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$("#attachment-table").on('click', '.attachment-delete-button', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = `/part/attachment/${button.attr('pk')}/delete/`;
|
||||
|
||||
launchModalForm(url, {
|
||||
success: function() {
|
||||
location.reload();
|
||||
method: 'POST',
|
||||
fields: {
|
||||
attachment: {},
|
||||
comment: {},
|
||||
part: {
|
||||
value: {{ part.pk }},
|
||||
hidden: true,
|
||||
}
|
||||
},
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Add Attachment" %}',
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#attachment-table").inventreeTable({
|
||||
)
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,16 +0,0 @@
|
||||
{% extends "modal_delete_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
{% trans "Are you sure you want to delete this BOM item?" %}
|
||||
<br>
|
||||
{% trans "Deleting this entry will remove the BOM row from the following part" %}:
|
||||
|
||||
<ul class='list-group'>
|
||||
<li class='list-group-item'>
|
||||
<b>{{ item.part.full_name }}</b> - <i>{{ item.part.description }}</i>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
@ -124,7 +124,7 @@
|
||||
|
||||
// Wait for *all* the requests to complete
|
||||
$.when.apply($, requests).then(function() {
|
||||
$('#bom-table').bootstrapTable('refresh');
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -268,13 +268,25 @@
|
||||
|
||||
{% if category %}
|
||||
$("#cat-edit").click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'category-edit' category.id %}",
|
||||
|
||||
constructForm(
|
||||
'{% url "api-part-category-detail" category.pk %}',
|
||||
{
|
||||
fields: {
|
||||
name: {},
|
||||
description: {},
|
||||
parent: {
|
||||
help_text: '{% trans "Select parent category" %}',
|
||||
},
|
||||
default_location: {},
|
||||
default_keywords: {
|
||||
icon: 'fa-key',
|
||||
}
|
||||
},
|
||||
title: '{% trans "Edit Part Category" %}',
|
||||
reload: true
|
||||
},
|
||||
}
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
{% if category.parent %}
|
||||
|
@ -51,7 +51,6 @@
|
||||
field: 'manufacturer',
|
||||
label: '{% trans "New Manufacturer" %}',
|
||||
title: '{% trans "Create new manufacturer" %}',
|
||||
url: "{% url 'manufacturer-create' %}",
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -61,17 +60,10 @@
|
||||
|
||||
var selections = $("#manufacturer-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.pk);
|
||||
});
|
||||
|
||||
launchModalForm("{% url 'manufacturer-part-delete' %}", {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
reload: true,
|
||||
deleteManufacturerParts(selections, {
|
||||
onSuccess: function() {
|
||||
$("#manufacturer-table").bootstrapTable("refresh");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -81,7 +73,7 @@
|
||||
{
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
part_detail: false,
|
||||
part_detail: true,
|
||||
manufacturer_detail: true,
|
||||
},
|
||||
}
|
||||
|
@ -237,6 +237,16 @@
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
function reloadImage(data) {
|
||||
// If image / thumbnail data present, live update
|
||||
if (data.image) {
|
||||
$('#part-image').attr('src', data.image);
|
||||
} else {
|
||||
// Otherwise, reload the page
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
enableDragAndDrop(
|
||||
'#part-thumb',
|
||||
"{% url 'api-part-detail' part.id %}",
|
||||
@ -244,14 +254,7 @@
|
||||
label: 'image',
|
||||
method: 'PATCH',
|
||||
success: function(data, status, xhr) {
|
||||
|
||||
// If image / thumbnail data present, live update
|
||||
if (data.image) {
|
||||
$('#part-image').attr('src', data.image);
|
||||
} else {
|
||||
// Otherwise, reload the page
|
||||
location.reload();
|
||||
}
|
||||
reloadImage(data);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -293,11 +296,20 @@
|
||||
});
|
||||
|
||||
$("#part-image-upload").click(function() {
|
||||
launchModalForm("{% url 'part-image-upload' part.id %}",
|
||||
|
||||
constructForm(
|
||||
'{% url "api-part-detail" part.pk %}',
|
||||
{
|
||||
reload: true
|
||||
method: 'PATCH',
|
||||
fields: {
|
||||
image: {},
|
||||
},
|
||||
title: '{% trans "Upload Image" %}',
|
||||
onSuccess: function(data) {
|
||||
reloadImage(data);
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@ -357,6 +369,75 @@
|
||||
});
|
||||
|
||||
$("#part-edit").click(function() {
|
||||
|
||||
constructForm('{% url "api-part-detail" part.id %}', {
|
||||
focus: 'name',
|
||||
fields: {
|
||||
category: {
|
||||
secondary: {
|
||||
label: '{% trans "New Category" %}',
|
||||
title: '{% trans "Create New Part Category" %}',
|
||||
api_url: '{% url "api-part-category-list" %}',
|
||||
method: 'POST',
|
||||
fields: {
|
||||
name: {},
|
||||
description: {},
|
||||
parent: {
|
||||
secondary: {
|
||||
title: '{% trans "New Parent" %}',
|
||||
api_url: '{% url "api-part-category-list" %}',
|
||||
method: 'POST',
|
||||
fields: {
|
||||
name: {},
|
||||
description: {},
|
||||
parent: {},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
name: {
|
||||
placeholder: 'part name',
|
||||
},
|
||||
IPN: {},
|
||||
description: {},
|
||||
revision: {},
|
||||
keywords: {
|
||||
icon: 'fa-key',
|
||||
},
|
||||
variant_of: {},
|
||||
link: {
|
||||
icon: 'fa-link',
|
||||
},
|
||||
default_location: {
|
||||
secondary: {
|
||||
label: '{% trans "New Location" %}',
|
||||
title: '{% trans "Create new stock location" %}',
|
||||
|
||||
},
|
||||
},
|
||||
default_supplier: {
|
||||
filters: {
|
||||
part: {{ part.pk }},
|
||||
part_detail: true,
|
||||
manufacturer_detail: true,
|
||||
supplier_detail: true,
|
||||
},
|
||||
secondary: {
|
||||
label: '{% trans "New Supplier Part" %}',
|
||||
title: '{% trans "Create new supplier part" %}',
|
||||
}
|
||||
},
|
||||
units: {},
|
||||
minimum_stock: {},
|
||||
},
|
||||
title: '{% trans "Edit Part" %}',
|
||||
reload: true,
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
launchModalForm(
|
||||
"{% url 'part-edit' part.id %}",
|
||||
{
|
||||
|
@ -44,34 +44,52 @@ function reloadTable() {
|
||||
}
|
||||
|
||||
$("#add-test-template").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'part-test-template-create' %}",
|
||||
{
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
|
||||
constructForm('{% url "api-part-test-template-list" %}', {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
test_name: {},
|
||||
description: {},
|
||||
required: {},
|
||||
requires_value: {},
|
||||
requires_attachment: {},
|
||||
part: {
|
||||
value: {{ part.pk }},
|
||||
hidden: true,
|
||||
}
|
||||
},
|
||||
title: '{% trans "Add Test Result Template" %}',
|
||||
onSuccess: reloadTable
|
||||
});
|
||||
});
|
||||
|
||||
$("#test-template-table").on('click', '.button-test-edit', function() {
|
||||
var button = $(this);
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
var url = `/part/test-template/${button.attr('pk')}/edit/`;
|
||||
var url = `/api/part/test-template/${pk}/`;
|
||||
|
||||
launchModalForm(url, {
|
||||
success: reloadTable,
|
||||
constructForm(url, {
|
||||
fields: {
|
||||
test_name: {},
|
||||
description: {},
|
||||
required: {},
|
||||
requires_value: {},
|
||||
requires_attachment: {},
|
||||
},
|
||||
title: '{% trans "Edit Test Result Template" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
$("#test-template-table").on('click', '.button-test-delete', function() {
|
||||
var button = $(this);
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
var url = `/part/test-template/${button.attr('pk')}/delete/`;
|
||||
var url = `/api/part/test-template/${pk}/`;
|
||||
|
||||
launchModalForm(url, {
|
||||
success: reloadTable,
|
||||
constructForm(url, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Test Result Template" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -49,13 +49,11 @@
|
||||
field: 'supplier',
|
||||
label: '{% trans "New Supplier" %}',
|
||||
title: '{% trans "Create new supplier" %}',
|
||||
url: "{% url 'supplier-create' %}"
|
||||
},
|
||||
{
|
||||
field: 'manufacturer',
|
||||
label: '{% trans "New Manufacturer" %}',
|
||||
title: '{% trans "Create new manufacturer" %}',
|
||||
url: "{% url 'manufacturer-create' %}",
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -16,6 +16,106 @@ from company.models import Company
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
|
||||
class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Tests for the various OPTIONS endpoints in the /part/ API
|
||||
|
||||
Ensure that the required field details are provided!
|
||||
"""
|
||||
|
||||
roles = [
|
||||
'part.add',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
def test_part(self):
|
||||
"""
|
||||
Test the Part API OPTIONS
|
||||
"""
|
||||
|
||||
actions = self.getActions(reverse('api-part-list'))['POST']
|
||||
|
||||
# Check that a bunch o' fields are contained
|
||||
for f in ['assembly', 'component', 'description', 'image', 'IPN']:
|
||||
self.assertTrue(f in actions.keys())
|
||||
|
||||
# Active is a 'boolean' field
|
||||
active = actions['active']
|
||||
|
||||
self.assertTrue(active['default'])
|
||||
self.assertEqual(active['help_text'], 'Is this part active?')
|
||||
self.assertEqual(active['type'], 'boolean')
|
||||
self.assertEqual(active['read_only'], False)
|
||||
|
||||
# String field
|
||||
ipn = actions['IPN']
|
||||
self.assertEqual(ipn['type'], 'string')
|
||||
self.assertFalse(ipn['required'])
|
||||
self.assertEqual(ipn['max_length'], 100)
|
||||
self.assertEqual(ipn['help_text'], 'Internal Part Number')
|
||||
|
||||
# Related field
|
||||
category = actions['category']
|
||||
|
||||
self.assertEqual(category['type'], 'related field')
|
||||
self.assertTrue(category['required'])
|
||||
self.assertFalse(category['read_only'])
|
||||
self.assertEqual(category['label'], 'Category')
|
||||
self.assertEqual(category['model'], 'partcategory')
|
||||
self.assertEqual(category['api_url'], reverse('api-part-category-list'))
|
||||
self.assertEqual(category['help_text'], 'Part category')
|
||||
|
||||
def test_category(self):
|
||||
"""
|
||||
Test the PartCategory API OPTIONS endpoint
|
||||
"""
|
||||
|
||||
actions = self.getActions(reverse('api-part-category-list'))
|
||||
|
||||
# actions should *not* contain 'POST' as we do not have the correct role
|
||||
self.assertFalse('POST' in actions)
|
||||
|
||||
self.assignRole('part_category.add')
|
||||
|
||||
actions = self.getActions(reverse('api-part-category-list'))['POST']
|
||||
|
||||
name = actions['name']
|
||||
|
||||
self.assertTrue(name['required'])
|
||||
self.assertEqual(name['label'], 'Name')
|
||||
|
||||
loc = actions['default_location']
|
||||
self.assertEqual(loc['api_url'], reverse('api-location-list'))
|
||||
|
||||
def test_bom_item(self):
|
||||
"""
|
||||
Test the BomItem API OPTIONS endpoint
|
||||
"""
|
||||
|
||||
actions = self.getActions(reverse('api-bom-list'))['POST']
|
||||
|
||||
inherited = actions['inherited']
|
||||
|
||||
self.assertEqual(inherited['type'], 'boolean')
|
||||
|
||||
# 'part' reference
|
||||
part = actions['part']
|
||||
|
||||
self.assertTrue(part['required'])
|
||||
self.assertFalse(part['read_only'])
|
||||
self.assertTrue(part['filters']['assembly'])
|
||||
|
||||
# 'sub_part' reference
|
||||
sub_part = actions['sub_part']
|
||||
|
||||
self.assertTrue(sub_part['required'])
|
||||
self.assertEqual(sub_part['type'], 'related field')
|
||||
self.assertTrue(sub_part['filters']['component'])
|
||||
|
||||
|
||||
class PartAPITest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Series of tests for the Part DRF API
|
||||
|
@ -232,29 +232,6 @@ class PartRelatedTests(PartViewTestCase):
|
||||
self.assertEqual(n, 1)
|
||||
|
||||
|
||||
class PartAttachmentTests(PartViewTestCase):
|
||||
|
||||
def test_valid_create(self):
|
||||
""" test creation of an attachment for a valid part """
|
||||
|
||||
response = self.client.get(reverse('part-attachment-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# TODO - Create a new attachment using this view
|
||||
|
||||
def test_invalid_create(self):
|
||||
""" test creation of an attachment for an invalid part """
|
||||
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def test_edit(self):
|
||||
""" test editing an attachment """
|
||||
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
class PartQRTest(PartViewTestCase):
|
||||
""" Tests for the Part QR Code AJAX view """
|
||||
|
||||
@ -294,11 +271,6 @@ class CategoryTest(PartViewTestCase):
|
||||
# Form should still return OK
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_edit(self):
|
||||
""" Retrieve the part category editing form """
|
||||
response = self.client.get(reverse('category-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_set_category(self):
|
||||
""" Test that the "SetCategory" view works """
|
||||
|
||||
|
@ -17,12 +17,6 @@ part_related_urls = [
|
||||
url(r'^(?P<pk>\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'),
|
||||
]
|
||||
|
||||
part_attachment_urls = [
|
||||
url(r'^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'),
|
||||
]
|
||||
|
||||
sale_price_break_urls = [
|
||||
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/', views.PartSalePriceBreakEdit.as_view(), name='sale-price-break-edit'),
|
||||
@ -79,7 +73,6 @@ part_detail_urls = [
|
||||
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
|
||||
|
||||
# Normal thumbnail with form
|
||||
url(r'^thumbnail/?', views.PartImageUpload.as_view(), name='part-image-upload'),
|
||||
url(r'^thumb-select/?', views.PartImageSelect.as_view(), name='part-image-select'),
|
||||
url(r'^thumb-download/', views.PartImageDownloadFromURL.as_view(), name='part-image-download'),
|
||||
|
||||
@ -103,7 +96,6 @@ category_urls = [
|
||||
|
||||
# Category detail views
|
||||
url(r'(?P<pk>\d+)/', include([
|
||||
url(r'^edit/', views.CategoryEdit.as_view(), name='category-edit'),
|
||||
url(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
url(r'^parameters/', include(category_parameter_urls)),
|
||||
|
||||
@ -117,7 +109,6 @@ category_urls = [
|
||||
|
||||
part_bom_urls = [
|
||||
url(r'^edit/?', views.BomItemEdit.as_view(), name='bom-item-edit'),
|
||||
url('^delete/?', views.BomItemDelete.as_view(), name='bom-item-delete'),
|
||||
]
|
||||
|
||||
# URL list for part web interface
|
||||
@ -148,22 +139,12 @@ part_urls = [
|
||||
# Part related
|
||||
url(r'^related-parts/', include(part_related_urls)),
|
||||
|
||||
# Part attachments
|
||||
url(r'^attachment/', include(part_attachment_urls)),
|
||||
|
||||
# Part price breaks
|
||||
url(r'^sale-price/', include(sale_price_break_urls)),
|
||||
|
||||
# Part internal price breaks
|
||||
url(r'^internal-price/', include(internal_price_break_urls)),
|
||||
|
||||
# Part test templates
|
||||
url(r'^test-template/', include([
|
||||
url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/', views.PartTestTemplateEdit.as_view(), name='part-test-template-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/', views.PartTestTemplateDelete.as_view(), name='part-test-template-delete'),
|
||||
])),
|
||||
|
||||
# Part parameters
|
||||
url(r'^parameter/', include(part_parameter_urls)),
|
||||
|
||||
|
@ -31,12 +31,11 @@ import io
|
||||
from rapidfuzz import fuzz
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from .models import PartCategory, Part, PartAttachment, PartRelated
|
||||
from .models import PartCategory, Part, PartRelated
|
||||
from .models import PartParameterTemplate, PartParameter
|
||||
from .models import PartCategoryParameterTemplate
|
||||
from .models import BomItem
|
||||
from .models import match_part_names
|
||||
from .models import PartTestTemplate
|
||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
@ -155,146 +154,6 @@ class PartRelatedDelete(AjaxDeleteView):
|
||||
role_required = 'part.change'
|
||||
|
||||
|
||||
class PartAttachmentCreate(AjaxCreateView):
|
||||
""" View for creating a new PartAttachment object
|
||||
|
||||
- The view only makes sense if a Part object is passed to it
|
||||
"""
|
||||
model = PartAttachment
|
||||
form_class = part_forms.EditPartAttachmentForm
|
||||
ajax_form_title = _("Add part attachment")
|
||||
ajax_template_name = "modal_form.html"
|
||||
|
||||
def save(self, form, **kwargs):
|
||||
"""
|
||||
Record the user that uploaded this attachment
|
||||
"""
|
||||
|
||||
attachment = form.save(commit=False)
|
||||
attachment.user = self.request.user
|
||||
attachment.save()
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'success': _('Added attachment')
|
||||
}
|
||||
|
||||
def get_initial(self):
|
||||
""" Get initial data for new PartAttachment object.
|
||||
|
||||
- Client should have requested this form with a parent part in mind
|
||||
- e.g. ?part=<pk>
|
||||
"""
|
||||
|
||||
initials = super(AjaxCreateView, self).get_initial()
|
||||
|
||||
# TODO - If the proper part was not sent, return an error message
|
||||
try:
|
||||
initials['part'] = Part.objects.get(id=self.request.GET.get('part', None))
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
def get_form(self):
|
||||
""" Create a form to upload a new PartAttachment
|
||||
|
||||
- Hide the 'part' field
|
||||
"""
|
||||
|
||||
form = super(AjaxCreateView, self).get_form()
|
||||
|
||||
form.fields['part'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class PartAttachmentEdit(AjaxUpdateView):
|
||||
""" View for editing a PartAttachment object """
|
||||
|
||||
model = PartAttachment
|
||||
form_class = part_forms.EditPartAttachmentForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = _('Edit attachment')
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'success': _('Part attachment updated')
|
||||
}
|
||||
|
||||
def get_form(self):
|
||||
form = super(AjaxUpdateView, self).get_form()
|
||||
|
||||
form.fields['part'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class PartAttachmentDelete(AjaxDeleteView):
|
||||
""" View for deleting a PartAttachment """
|
||||
|
||||
model = PartAttachment
|
||||
ajax_form_title = _("Delete Part Attachment")
|
||||
ajax_template_name = "attachment_delete.html"
|
||||
context_object_name = "attachment"
|
||||
|
||||
role_required = 'part.change'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'danger': _('Deleted part attachment')
|
||||
}
|
||||
|
||||
|
||||
class PartTestTemplateCreate(AjaxCreateView):
|
||||
""" View for creating a PartTestTemplate """
|
||||
|
||||
model = PartTestTemplate
|
||||
form_class = part_forms.EditPartTestTemplateForm
|
||||
ajax_form_title = _("Create Test Template")
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
try:
|
||||
part_id = self.request.GET.get('part', None)
|
||||
initials['part'] = Part.objects.get(pk=part_id)
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
form.fields['part'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class PartTestTemplateEdit(AjaxUpdateView):
|
||||
""" View for editing a PartTestTemplate """
|
||||
|
||||
model = PartTestTemplate
|
||||
form_class = part_forms.EditPartTestTemplateForm
|
||||
ajax_form_title = _("Edit Test Template")
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
form.fields['part'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class PartTestTemplateDelete(AjaxDeleteView):
|
||||
""" View for deleting a PartTestTemplate """
|
||||
|
||||
model = PartTestTemplate
|
||||
ajax_form_title = _("Delete Test Template")
|
||||
|
||||
|
||||
class PartSetCategory(AjaxUpdateView):
|
||||
""" View for settings the part category for multiple parts at once """
|
||||
|
||||
@ -1219,21 +1078,6 @@ class PartImageDownloadFromURL(AjaxUpdateView):
|
||||
)
|
||||
|
||||
|
||||
class PartImageUpload(AjaxUpdateView):
|
||||
""" View for uploading a new Part image """
|
||||
|
||||
model = Part
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = _('Upload Part Image')
|
||||
|
||||
form_class = part_forms.PartImageForm
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'success': _('Updated part image'),
|
||||
}
|
||||
|
||||
|
||||
class PartImageSelect(AjaxUpdateView):
|
||||
""" View for selecting Part image from existing images. """
|
||||
|
||||
@ -2934,17 +2778,10 @@ class BomItemEdit(AjaxUpdateView):
|
||||
return form
|
||||
|
||||
|
||||
class BomItemDelete(AjaxDeleteView):
|
||||
""" Delete view for removing BomItem """
|
||||
|
||||
model = BomItem
|
||||
ajax_template_name = 'part/bom-delete.html'
|
||||
context_object_name = 'item'
|
||||
ajax_form_title = _('Confim BOM item deletion')
|
||||
|
||||
|
||||
class PartSalePriceBreakCreate(AjaxCreateView):
|
||||
""" View for creating a sale price break for a part """
|
||||
"""
|
||||
View for creating a sale price break for a part
|
||||
"""
|
||||
|
||||
model = PartSellPriceBreak
|
||||
form_class = part_forms.EditPartSalePriceBreakForm
|
||||
|
Reference in New Issue
Block a user