From ff8dcabb12029ebb3ea82e6b1b0620d692e21c73 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 15 Aug 2021 22:43:52 +1000 Subject: [PATCH 1/6] New custom serializer for handling attachments --- InvenTree/InvenTree/models.py | 14 ++++++++++++ InvenTree/InvenTree/serializers.py | 26 +++++++++++++++++++++++ InvenTree/part/serializers.py | 6 +++++- InvenTree/part/templates/part/detail.html | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 3213838e78..35b9c3ff61 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -55,6 +55,20 @@ class InvenTreeAttachment(models.Model): return "attachments" + def get_filename(self): + + return os.path.basename(self.attachment.name) + + def rename(self, filename): + """ + Rename this attachment with the provided filename. + + - Filename cannot be empty + - Filename must have an extension + - Filename will have random data appended if a file exists with the same name + """ + pass + def __str__(self): return os.path.basename(self.attachment.name) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index baf08e112b..a8c02a1217 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -208,6 +208,32 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return data +class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): + """ + Special case of an InvenTreeModelSerializer, which handles an "attachment" model. + + The only real addition here is that we support "renaming" of the attachment file. + """ + + # The 'filename' field must be present in the serializer + filename = serializers.CharField( + label=_('Filename'), + required=False, + source='get_filename', + ) + + def update(self, instance, validated_data): + """ + Filename can only be edited on "update" + """ + + instance = super().update(instance, validated_data) + + print(validated_data) + + return instance + + class InvenTreeAttachmentSerializerField(serializers.FileField): """ Override the DRF native FileField serializer, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index e2c8c3fa4d..c2d515cf32 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -1,6 +1,7 @@ """ JSON serializers for Part app """ + import imghdr from decimal import Decimal @@ -16,7 +17,9 @@ from djmoney.contrib.django_rest_framework import MoneyField from InvenTree.serializers import (InvenTreeAttachmentSerializerField, InvenTreeImageSerializerField, InvenTreeModelSerializer, + InvenTreeAttachmentSerializer, InvenTreeMoneySerializer) + from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from stock.models import StockItem @@ -51,7 +54,7 @@ class CategorySerializer(InvenTreeModelSerializer): ] -class PartAttachmentSerializer(InvenTreeModelSerializer): +class PartAttachmentSerializer(InvenTreeAttachmentSerializer): """ Serializer for the PartAttachment class """ @@ -65,6 +68,7 @@ class PartAttachmentSerializer(InvenTreeModelSerializer): 'pk', 'part', 'attachment', + 'filename', 'comment', 'upload_date', ] diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 846320b8e1..80e4a77d1b 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -868,6 +868,7 @@ constructForm(url, { fields: { + filename: {}, comment: {}, }, title: '{% trans "Edit Attachment" %}', From 3dcf1746e6251190abe018ebac4abdec5097e0b5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Aug 2021 10:41:02 +1000 Subject: [PATCH 2/6] Functionality for renaming attached files --- InvenTree/InvenTree/models.py | 85 +++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 35b9c3ff61..2ca179bb40 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -5,8 +5,10 @@ Generic models which provide extra functionality over base Django model types. from __future__ import unicode_literals import os +import logging from django.db import models +from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ @@ -21,6 +23,9 @@ from mptt.exceptions import InvalidMove from .validators import validate_tree_name +logger = logging.getLogger('inventree') + + def rename_attachment(instance, filename): """ Function for renaming an attachment file. @@ -55,20 +60,6 @@ class InvenTreeAttachment(models.Model): return "attachments" - def get_filename(self): - - return os.path.basename(self.attachment.name) - - def rename(self, filename): - """ - Rename this attachment with the provided filename. - - - Filename cannot be empty - - Filename must have an extension - - Filename will have random data appended if a file exists with the same name - """ - pass - def __str__(self): return os.path.basename(self.attachment.name) @@ -91,6 +82,72 @@ class InvenTreeAttachment(models.Model): def basename(self): return os.path.basename(self.attachment.name) + @basename.setter + def basename(self, fn): + """ + Function to rename the attachment file. + + - Filename cannot be empty + - Filename cannot contain illegal characters + - Filename must specify an extension + - Filename cannot match an existing file + """ + + fn = fn.strip() + + if len(fn) == 0: + raise ValidationError(_('Filename must not be empty')) + + attachment_dir = os.path.join( + settings.MEDIA_ROOT, + self.getSubdir() + ) + + old_file = os.path.join( + settings.MEDIA_ROOT, + self.attachment.name + ) + + new_file = os.path.join( + settings.MEDIA_ROOT, + self.getSubdir(), + fn + ) + + new_file = os.path.abspath(new_file) + + # Check that there are no directory tricks going on... + if not os.path.dirname(new_file) == attachment_dir: + logger.error(f"Attempted to rename attachment outside valid directory: '{new_file}'") + raise ValidationError(_("Invalid attachment directory")) + + # Ignore further checks if the filename is not actually being renamed + if new_file == old_file: + return + + forbidden = ["'", '"', "#", "@", "!", "&", "^", "<", ">", ":", ";", "/", "\\", "|", "?", "*", "%", "~", "`"] + + for c in forbidden: + if c in fn: + raise ValidationError(_(f"Filename contains illegal character '{c}'")) + + if len(fn.split('.')) < 2: + raise ValidationError(_("Filename missing extension")) + + if not os.path.exists(old_file): + logger.error(f"Trying to rename attachment '{old_file}' which does not exist") + return + + if os.path.exists(new_file): + raise ValidationError(_("Attachment with this filename already exists")) + + try: + os.rename(old_file, new_file) + self.attachment.name = os.path.join(self.getSubdir(), fn) + self.save() + except: + raise ValidationError(_("Error renaming file")) + class Meta: abstract = True From d9f29b4a702c0ddb9880a9caa607be1d4f122a8f Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Aug 2021 10:41:26 +1000 Subject: [PATCH 3/6] Updates for InvenTree serializer classes - Catch and re-throw errors correctly --- InvenTree/InvenTree/serializers.py | 31 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index a8c02a1217..b156e39167 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -167,6 +167,18 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return self.instance + def update(self, instance, validated_data): + """ + Catch any django ValidationError, and re-throw as a DRF ValidationError + """ + + try: + instance = super().update(instance, validated_data) + except (ValidationError, DjangoValidationError) as exc: + raise ValidationError(detail=serializers.as_serializer_error(exc)) + + return instance + def run_validation(self, data=empty): """ Perform serializer validation. @@ -188,7 +200,10 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): # Update instance fields for attr, value in data.items(): - setattr(instance, attr, value) + try: + setattr(instance, attr, value) + except (ValidationError, DjangoValidationError) as exc: + raise ValidationError(detail=serializers.as_serializer_error(exc)) # Run a 'full_clean' on the model. # Note that by default, DRF does *not* perform full model validation! @@ -219,20 +234,10 @@ class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): filename = serializers.CharField( label=_('Filename'), required=False, - source='get_filename', + source='basename', + allow_blank=False, ) - def update(self, instance, validated_data): - """ - Filename can only be edited on "update" - """ - - instance = super().update(instance, validated_data) - - print(validated_data) - - return instance - class InvenTreeAttachmentSerializerField(serializers.FileField): """ From f8b22bc7b7368e040dd972310094eba6b9ee6fea Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Aug 2021 10:49:31 +1000 Subject: [PATCH 4/6] Refactor BuildAttachment model --- InvenTree/build/serializers.py | 6 ++++-- InvenTree/build/templates/build/detail.html | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 5c0fced884..69e3a7aed0 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -10,7 +10,8 @@ from django.db.models import BooleanField from rest_framework import serializers -from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializerField, UserSerializerBrief +from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer +from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief from stock.serializers import StockItemSerializerBrief from stock.serializers import LocationSerializer @@ -158,7 +159,7 @@ class BuildItemSerializer(InvenTreeModelSerializer): ] -class BuildAttachmentSerializer(InvenTreeModelSerializer): +class BuildAttachmentSerializer(InvenTreeAttachmentSerializer): """ Serializer for a BuildAttachment """ @@ -172,6 +173,7 @@ class BuildAttachmentSerializer(InvenTreeModelSerializer): 'pk', 'build', 'attachment', + 'filename', 'comment', 'upload_date', ] diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index fe716b87f2..d6b59a060d 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -369,6 +369,7 @@ loadAttachmentTable( constructForm(url, { fields: { + filename: {}, comment: {}, }, onSuccess: reloadAttachmentTable, From 6141ddc3ebec840b02ac817222b249924387c896 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Aug 2021 10:53:28 +1000 Subject: [PATCH 5/6] SalesOrderAttachment and PurchaseOrderAttachment --- InvenTree/order/serializers.py | 7 +++++-- InvenTree/order/templates/order/purchase_order_detail.html | 1 + InvenTree/order/templates/order/sales_order_detail.html | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 4a95bbb166..e97d19250a 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -14,6 +14,7 @@ from rest_framework import serializers from sql_util.utils import SubqueryCount from InvenTree.serializers import InvenTreeModelSerializer +from InvenTree.serializers import InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField @@ -160,7 +161,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): ] -class POAttachmentSerializer(InvenTreeModelSerializer): +class POAttachmentSerializer(InvenTreeAttachmentSerializer): """ Serializers for the PurchaseOrderAttachment model """ @@ -174,6 +175,7 @@ class POAttachmentSerializer(InvenTreeModelSerializer): 'pk', 'order', 'attachment', + 'filename', 'comment', 'upload_date', ] @@ -381,7 +383,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): ] -class SOAttachmentSerializer(InvenTreeModelSerializer): +class SOAttachmentSerializer(InvenTreeAttachmentSerializer): """ Serializers for the SalesOrderAttachment model """ @@ -395,6 +397,7 @@ class SOAttachmentSerializer(InvenTreeModelSerializer): 'pk', 'order', 'attachment', + 'filename', 'comment', 'upload_date', ] diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index ed352d1135..586ce73f14 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -122,6 +122,7 @@ constructForm(url, { fields: { + filename: {}, comment: {}, }, onSuccess: reloadAttachmentTable, diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 277c1f4278..30799e2296 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -112,6 +112,7 @@ constructForm(url, { fields: { + filename: {}, comment: {}, }, onSuccess: reloadAttachmentTable, From 23b2b56de4c9c8e2b57f13735a085502a69555ef Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Aug 2021 10:56:00 +1000 Subject: [PATCH 6/6] StockItemAttachment --- InvenTree/stock/serializers.py | 5 +++-- InvenTree/stock/templates/stock/item.html | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 41dc959f02..e7ec2fd291 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -25,7 +25,7 @@ import common.models from company.serializers import SupplierPartSerializer from part.serializers import PartBriefSerializer from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer -from InvenTree.serializers import InvenTreeAttachmentSerializerField +from InvenTree.serializers import InvenTreeAttachmentSerializer, InvenTreeAttachmentSerializerField class LocationBriefSerializer(InvenTreeModelSerializer): @@ -253,7 +253,7 @@ class LocationSerializer(InvenTreeModelSerializer): ] -class StockItemAttachmentSerializer(InvenTreeModelSerializer): +class StockItemAttachmentSerializer(InvenTreeAttachmentSerializer): """ Serializer for StockItemAttachment model """ def __init__(self, *args, **kwargs): @@ -277,6 +277,7 @@ class StockItemAttachmentSerializer(InvenTreeModelSerializer): 'pk', 'stock_item', 'attachment', + 'filename', 'comment', 'upload_date', 'user', diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 19295d1198..0ac9c285a6 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -215,6 +215,7 @@ constructForm(url, { fields: { + filename: {}, comment: {}, }, title: '{% trans "Edit Attachment" %}',