mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add new global setting to control auto-upload of test reports (#3137)
* Add new global setting to control auto-upload of test reports * Adds callback to attach a copy of the test report when printing * Fix for all attachment API endpoints - The AttachmentMixin must come first! - User was not being set, as the custom 'perform_create' function was never called * Remove duplicated UserSerializer * display uploading user in attachment table * Add unit test to check the test report is automatically uploaded
This commit is contained in:
parent
2b1d8f5b79
commit
a066fcc909
@ -60,29 +60,6 @@ class InvenTreeMoneySerializer(MoneyField):
|
||||
return amount
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for User - provides all fields."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = User
|
||||
fields = 'all'
|
||||
|
||||
|
||||
class UserSerializerBrief(serializers.ModelSerializer):
|
||||
"""Serializer for User - provides limited information."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = User
|
||||
fields = [
|
||||
'pk',
|
||||
'username',
|
||||
]
|
||||
|
||||
|
||||
class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
"""Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
|
||||
|
||||
@ -218,6 +195,21 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class UserSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for a User."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines serializer fields."""
|
||||
model = User
|
||||
fields = [
|
||||
'pk',
|
||||
'username',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email'
|
||||
]
|
||||
|
||||
|
||||
class ReferenceIndexingSerializerMixin():
|
||||
"""This serializer mixin ensures the the reference is not to big / small for the BigIntegerField."""
|
||||
|
||||
@ -239,9 +231,7 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||
|
||||
/media/foo/bar.jpg
|
||||
|
||||
Why? You can't handle the why!
|
||||
|
||||
Actually, if the server process is serving the data at 127.0.0.1,
|
||||
If the server process is serving the data at 127.0.0.1,
|
||||
but a proxy service (e.g. nginx) is then providing DNS lookup to the outside world,
|
||||
then an attachment which prefixes the "address" of the internal server
|
||||
will not be accessible from the outside world.
|
||||
@ -261,6 +251,8 @@ class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
|
||||
The only real addition here is that we support "renaming" of the attachment file.
|
||||
"""
|
||||
|
||||
user_detail = UserSerializer(source='user', read_only=True, many=False)
|
||||
|
||||
attachment = InvenTreeAttachmentSerializerField(
|
||||
required=False,
|
||||
allow_null=False,
|
||||
|
@ -413,7 +413,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
|
||||
class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
class BuildAttachmentList(AttachmentMixin, generics.ListCreateAPIView):
|
||||
"""API endpoint for listing (and creating) BuildOrderAttachment objects."""
|
||||
|
||||
queryset = BuildOrderAttachment.objects.all()
|
||||
@ -428,7 +428,7 @@ class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
class BuildAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Detail endpoint for a BuildOrderAttachment object."""
|
||||
|
||||
queryset = BuildOrderAttachment.objects.all()
|
||||
|
@ -11,7 +11,7 @@ from rest_framework import serializers
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
|
||||
from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin
|
||||
from InvenTree.serializers import ReferenceIndexingSerializerMixin, UserSerializer
|
||||
|
||||
import InvenTree.helpers
|
||||
from InvenTree.helpers import extract_serial_numbers
|
||||
@ -40,7 +40,7 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer
|
||||
|
||||
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
issued_by_detail = UserSerializerBrief(source='issued_by', read_only=True)
|
||||
issued_by_detail = UserSerializer(source='issued_by', read_only=True)
|
||||
|
||||
responsible_detail = OwnerSerializer(source='responsible', read_only=True)
|
||||
|
||||
@ -860,6 +860,8 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'filename',
|
||||
'comment',
|
||||
'upload_date',
|
||||
'user',
|
||||
'user_detail',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
|
@ -965,12 +965,19 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
},
|
||||
|
||||
'REPORT_ENABLE_TEST_REPORT': {
|
||||
'name': _('Test Reports'),
|
||||
'name': _('Enable Test Reports'),
|
||||
'description': _('Enable generation of test reports'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'REPORT_ATTACH_TEST_REPORT': {
|
||||
'name': _('Attach Test Reports'),
|
||||
'description': _('When printing a Test Report, attach a copy of the Test Report to the associated Stock Item'),
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'STOCK_BATCH_CODE_TEMPLATE': {
|
||||
'name': _('Batch Code Template'),
|
||||
'description': _('Template for generating default batch codes for stock items'),
|
||||
|
@ -158,6 +158,8 @@ class ManufacturerPartAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'link',
|
||||
'comment',
|
||||
'upload_date',
|
||||
'user',
|
||||
'user_detail',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
|
@ -527,7 +527,7 @@ class PurchaseOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = serializers.PurchaseOrderExtraLineSerializer
|
||||
|
||||
|
||||
class SalesOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
class SalesOrderAttachmentList(AttachmentMixin, generics.ListCreateAPIView):
|
||||
"""API endpoint for listing (and creating) a SalesOrderAttachment (file upload)"""
|
||||
|
||||
queryset = models.SalesOrderAttachment.objects.all()
|
||||
@ -542,7 +542,7 @@ class SalesOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
class SalesOrderAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Detail endpoint for SalesOrderAttachment."""
|
||||
|
||||
queryset = models.SalesOrderAttachment.objects.all()
|
||||
@ -1056,7 +1056,7 @@ class SalesOrderShipmentComplete(generics.CreateAPIView):
|
||||
return ctx
|
||||
|
||||
|
||||
class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
class PurchaseOrderAttachmentList(AttachmentMixin, generics.ListCreateAPIView):
|
||||
"""API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)"""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
@ -1071,7 +1071,7 @@ class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class PurchaseOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
class PurchaseOrderAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Detail endpoint for a PurchaseOrderAttachment."""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
|
@ -630,6 +630,8 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'filename',
|
||||
'comment',
|
||||
'upload_date',
|
||||
'user',
|
||||
'user_detail',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@ -1348,6 +1350,8 @@ class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'link',
|
||||
'comment',
|
||||
'upload_date',
|
||||
'user',
|
||||
'user_detail',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
|
@ -302,7 +302,7 @@ class PartInternalPriceList(generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
|
||||
class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
class PartAttachmentList(AttachmentMixin, generics.ListCreateAPIView):
|
||||
"""API endpoint for listing (and creating) a PartAttachment (file upload)."""
|
||||
|
||||
queryset = PartAttachment.objects.all()
|
||||
@ -317,7 +317,7 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
class PartAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Detail endpoint for PartAttachment model."""
|
||||
|
||||
queryset = PartAttachment.objects.all()
|
||||
|
@ -94,6 +94,8 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'link',
|
||||
'comment',
|
||||
'upload_date',
|
||||
'user',
|
||||
'user_detail',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""API functionality for the 'report' app"""
|
||||
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.http import HttpResponse
|
||||
from django.template.exceptions import TemplateDoesNotExist
|
||||
from django.urls import include, path, re_path
|
||||
@ -15,7 +16,7 @@ import common.models
|
||||
import InvenTree.helpers
|
||||
import order.models
|
||||
import part.models
|
||||
from stock.models import StockItem
|
||||
from stock.models import StockItem, StockItemAttachment
|
||||
|
||||
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
|
||||
SalesOrderReport, TestReport)
|
||||
@ -158,6 +159,18 @@ class PartReportMixin:
|
||||
class ReportPrintMixin:
|
||||
"""Mixin for printing reports."""
|
||||
|
||||
def report_callback(self, object, report, request):
|
||||
"""Callback function for each object/report combination.
|
||||
|
||||
Allows functionality to be performed before returning the consolidated PDF
|
||||
|
||||
Arguments:
|
||||
object: The model instance to be printed
|
||||
report: The individual PDF file object
|
||||
request: The request instance associated with this print call
|
||||
"""
|
||||
...
|
||||
|
||||
def print(self, request, items_to_print):
|
||||
"""Print this report template against a number of pre-validated items."""
|
||||
if len(items_to_print) == 0:
|
||||
@ -182,12 +195,16 @@ class ReportPrintMixin:
|
||||
report.object_to_print = item
|
||||
|
||||
report_name = report.generate_filename(request)
|
||||
output = report.render(request)
|
||||
|
||||
# Run report callback for each generated report
|
||||
self.report_callback(item, output, request)
|
||||
|
||||
try:
|
||||
if debug_mode:
|
||||
outputs.append(report.render_as_string(request))
|
||||
else:
|
||||
outputs.append(report.render(request))
|
||||
outputs.append(output)
|
||||
except TemplateDoesNotExist as e:
|
||||
template = str(e)
|
||||
if not template:
|
||||
@ -326,6 +343,22 @@ class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin, R
|
||||
queryset = TestReport.objects.all()
|
||||
serializer_class = TestReportSerializer
|
||||
|
||||
def report_callback(self, item, report, request):
|
||||
"""Callback to (optionally) save a copy of the generated report"""
|
||||
|
||||
if common.models.InvenTreeSetting.get_setting('REPORT_ATTACH_TEST_REPORT'):
|
||||
|
||||
# Construct a PDF file object
|
||||
pdf = report.get_document().write_pdf()
|
||||
pdf_content = ContentFile(pdf, "test_report.pdf")
|
||||
|
||||
StockItemAttachment.objects.create(
|
||||
attachment=pdf_content,
|
||||
stock_item=item,
|
||||
user=request.user,
|
||||
comment=_("Test report")
|
||||
)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Check if valid stock item(s) have been provided."""
|
||||
items = self.get_items()
|
||||
|
@ -9,9 +9,9 @@ from django.urls import reverse
|
||||
|
||||
import report.models as report_models
|
||||
from build.models import Build
|
||||
from common.models import InvenTreeUserSetting
|
||||
from common.models import InvenTreeSetting, InvenTreeUserSetting
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
from stock.models import StockItem
|
||||
from stock.models import StockItem, StockItemAttachment
|
||||
|
||||
|
||||
class ReportTest(InvenTreeAPITestCase):
|
||||
@ -141,15 +141,28 @@ class TestReportTest(ReportTest):
|
||||
# Now print with a valid StockItem
|
||||
item = StockItem.objects.first()
|
||||
|
||||
response = self.get(url, {'item': item.pk})
|
||||
response = self.get(url, {'item': item.pk}, expected_code=200)
|
||||
|
||||
# Response should be a StreamingHttpResponse (PDF file)
|
||||
self.assertEqual(type(response), StreamingHttpResponse)
|
||||
|
||||
headers = response.headers
|
||||
|
||||
self.assertEqual(headers['Content-Type'], 'application/pdf')
|
||||
|
||||
# By default, this should *not* have created an attachment against this stockitem
|
||||
self.assertFalse(StockItemAttachment.objects.filter(stock_item=item).exists())
|
||||
|
||||
# Change the setting, now the test report should be attached automatically
|
||||
InvenTreeSetting.set_setting('REPORT_ATTACH_TEST_REPORT', True, None)
|
||||
|
||||
response = self.get(url, {'item': item.pk}, expected_code=200)
|
||||
headers = response.headers
|
||||
self.assertEqual(headers['Content-Type'], 'application/pdf')
|
||||
|
||||
# Check that a report has been uploaded
|
||||
attachment = StockItemAttachment.objects.filter(stock_item=item).first()
|
||||
self.assertIsNotNone(attachment)
|
||||
|
||||
|
||||
class BuildReportTest(ReportTest):
|
||||
"""Unit test class for the BuildReport model"""
|
||||
|
@ -1043,7 +1043,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
|
||||
class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
class StockAttachmentList(AttachmentMixin, generics.ListCreateAPIView):
|
||||
"""API endpoint for listing (and creating) a StockItemAttachment (file upload)."""
|
||||
|
||||
queryset = StockItemAttachment.objects.all()
|
||||
@ -1060,7 +1060,7 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
class StockAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Detail endpoint for StockItemAttachment."""
|
||||
|
||||
queryset = StockItemAttachment.objects.all()
|
||||
|
@ -20,9 +20,9 @@ def delete_scheduled(apps, schema_editor):
|
||||
|
||||
items = StockItem.objects.filter(scheduled_for_deletion=True)
|
||||
|
||||
logger.info(f"Removing {items.count()} stock items scheduled for deletion")
|
||||
|
||||
items.delete()
|
||||
if items.count() > 0:
|
||||
logger.info(f"Removing {items.count()} stock items scheduled for deletion")
|
||||
items.delete()
|
||||
|
||||
Task = apps.get_model('django_q', 'schedule')
|
||||
|
||||
|
@ -549,19 +549,6 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
|
||||
"""Serializer for StockItemAttachment model."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Add detail fields."""
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user_detail is not True:
|
||||
self.fields.pop('user_detail')
|
||||
|
||||
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
|
||||
|
||||
# TODO: Record the uploading user when creating or updating an attachment!
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
@ -589,7 +576,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
|
||||
class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for the StockItemTestResult model."""
|
||||
|
||||
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
|
||||
user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True)
|
||||
|
||||
key = serializers.CharField(read_only=True)
|
||||
|
||||
@ -650,7 +637,7 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
item_detail = StockItemSerializerBrief(source='item', many=False, read_only=True)
|
||||
|
||||
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', many=False, read_only=True)
|
||||
user_detail = InvenTree.serializers.UserSerializer(source='user', many=False, read_only=True)
|
||||
|
||||
deltas = serializers.JSONField(read_only=True)
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
{% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" icon="fa-print" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" icon="fa-laptop-code" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" icon="fa-vial" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="REPORT_ATTACH_TEST_REPORT" icon="fa-file-upload" %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@ -244,8 +244,14 @@ function loadAttachmentTable(url, options) {
|
||||
{
|
||||
field: 'upload_date',
|
||||
title: '{% trans "Upload Date" %}',
|
||||
formatter: function(value) {
|
||||
return renderDate(value);
|
||||
formatter: function(value, row) {
|
||||
var html = renderDate(value);
|
||||
|
||||
if (row.user_detail) {
|
||||
html += `<span class='badge bg-dark rounded-pill float-right'>${row.user_detail.username}</div>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -10,8 +10,9 @@ from rest_framework.authtoken.models import Token
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from InvenTree.serializers import UserSerializer
|
||||
from users.models import Owner, RuleSet, check_user_role
|
||||
from users.serializers import OwnerSerializer, UserSerializer
|
||||
from users.serializers import OwnerSerializer
|
||||
|
||||
|
||||
class OwnerList(generics.ListAPIView):
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""DRF API serializers for the 'users' app"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
@ -9,19 +8,6 @@ from InvenTree.serializers import InvenTreeModelSerializer
|
||||
from .models import Owner
|
||||
|
||||
|
||||
class UserSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for a User."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines serializer fields."""
|
||||
model = User
|
||||
fields = ('pk',
|
||||
'username',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',)
|
||||
|
||||
|
||||
class OwnerSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for an "Owner" (either a "user" or a "group")"""
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user