diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 3213838e78..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. @@ -77,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 diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index baf08e112b..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! @@ -208,6 +223,22 @@ 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='basename', + allow_blank=False, + ) + + class InvenTreeAttachmentSerializerField(serializers.FileField): """ Override the DRF native FileField serializer, 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, 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, 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" %}', 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" %}',