Merge pull request #1962 from SchrodingersGat/attachment-edit

Attachment edit
This commit is contained in:
Oliver 2021-08-16 11:14:14 +10:00 committed by GitHub
commit fa163b8866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 125 additions and 8 deletions

View File

@ -5,8 +5,10 @@ Generic models which provide extra functionality over base Django model types.
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import logging
from django.db import models from django.db import models
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -21,6 +23,9 @@ from mptt.exceptions import InvalidMove
from .validators import validate_tree_name from .validators import validate_tree_name
logger = logging.getLogger('inventree')
def rename_attachment(instance, filename): def rename_attachment(instance, filename):
""" """
Function for renaming an attachment file. Function for renaming an attachment file.
@ -77,6 +82,72 @@ class InvenTreeAttachment(models.Model):
def basename(self): def basename(self):
return os.path.basename(self.attachment.name) 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: class Meta:
abstract = True abstract = True

View File

@ -167,6 +167,18 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
return self.instance 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): def run_validation(self, data=empty):
""" """
Perform serializer validation. Perform serializer validation.
@ -188,7 +200,10 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
# Update instance fields # Update instance fields
for attr, value in data.items(): 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. # Run a 'full_clean' on the model.
# Note that by default, DRF does *not* perform full model validation! # Note that by default, DRF does *not* perform full model validation!
@ -208,6 +223,22 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
return data 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): class InvenTreeAttachmentSerializerField(serializers.FileField):
""" """
Override the DRF native FileField serializer, Override the DRF native FileField serializer,

View File

@ -10,7 +10,8 @@ from django.db.models import BooleanField
from rest_framework import serializers 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 StockItemSerializerBrief
from stock.serializers import LocationSerializer from stock.serializers import LocationSerializer
@ -158,7 +159,7 @@ class BuildItemSerializer(InvenTreeModelSerializer):
] ]
class BuildAttachmentSerializer(InvenTreeModelSerializer): class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """
Serializer for a BuildAttachment Serializer for a BuildAttachment
""" """
@ -172,6 +173,7 @@ class BuildAttachmentSerializer(InvenTreeModelSerializer):
'pk', 'pk',
'build', 'build',
'attachment', 'attachment',
'filename',
'comment', 'comment',
'upload_date', 'upload_date',
] ]

View File

@ -369,6 +369,7 @@ loadAttachmentTable(
constructForm(url, { constructForm(url, {
fields: { fields: {
filename: {},
comment: {}, comment: {},
}, },
onSuccess: reloadAttachmentTable, onSuccess: reloadAttachmentTable,

View File

@ -14,6 +14,7 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount from sql_util.utils import SubqueryCount
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField from InvenTree.serializers import InvenTreeAttachmentSerializerField
@ -160,7 +161,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
] ]
class POAttachmentSerializer(InvenTreeModelSerializer): class POAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """
Serializers for the PurchaseOrderAttachment model Serializers for the PurchaseOrderAttachment model
""" """
@ -174,6 +175,7 @@ class POAttachmentSerializer(InvenTreeModelSerializer):
'pk', 'pk',
'order', 'order',
'attachment', 'attachment',
'filename',
'comment', 'comment',
'upload_date', 'upload_date',
] ]
@ -381,7 +383,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
] ]
class SOAttachmentSerializer(InvenTreeModelSerializer): class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """
Serializers for the SalesOrderAttachment model Serializers for the SalesOrderAttachment model
""" """
@ -395,6 +397,7 @@ class SOAttachmentSerializer(InvenTreeModelSerializer):
'pk', 'pk',
'order', 'order',
'attachment', 'attachment',
'filename',
'comment', 'comment',
'upload_date', 'upload_date',
] ]

View File

@ -122,6 +122,7 @@
constructForm(url, { constructForm(url, {
fields: { fields: {
filename: {},
comment: {}, comment: {},
}, },
onSuccess: reloadAttachmentTable, onSuccess: reloadAttachmentTable,

View File

@ -112,6 +112,7 @@
constructForm(url, { constructForm(url, {
fields: { fields: {
filename: {},
comment: {}, comment: {},
}, },
onSuccess: reloadAttachmentTable, onSuccess: reloadAttachmentTable,

View File

@ -1,6 +1,7 @@
""" """
JSON serializers for Part app JSON serializers for Part app
""" """
import imghdr import imghdr
from decimal import Decimal from decimal import Decimal
@ -16,7 +17,9 @@ from djmoney.contrib.django_rest_framework import MoneyField
from InvenTree.serializers import (InvenTreeAttachmentSerializerField, from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
InvenTreeImageSerializerField, InvenTreeImageSerializerField,
InvenTreeModelSerializer, InvenTreeModelSerializer,
InvenTreeAttachmentSerializer,
InvenTreeMoneySerializer) InvenTreeMoneySerializer)
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
from stock.models import StockItem from stock.models import StockItem
@ -51,7 +54,7 @@ class CategorySerializer(InvenTreeModelSerializer):
] ]
class PartAttachmentSerializer(InvenTreeModelSerializer): class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """
Serializer for the PartAttachment class Serializer for the PartAttachment class
""" """
@ -65,6 +68,7 @@ class PartAttachmentSerializer(InvenTreeModelSerializer):
'pk', 'pk',
'part', 'part',
'attachment', 'attachment',
'filename',
'comment', 'comment',
'upload_date', 'upload_date',
] ]

View File

@ -868,6 +868,7 @@
constructForm(url, { constructForm(url, {
fields: { fields: {
filename: {},
comment: {}, comment: {},
}, },
title: '{% trans "Edit Attachment" %}', title: '{% trans "Edit Attachment" %}',

View File

@ -25,7 +25,7 @@ import common.models
from company.serializers import SupplierPartSerializer from company.serializers import SupplierPartSerializer
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField from InvenTree.serializers import InvenTreeAttachmentSerializer, InvenTreeAttachmentSerializerField
class LocationBriefSerializer(InvenTreeModelSerializer): class LocationBriefSerializer(InvenTreeModelSerializer):
@ -253,7 +253,7 @@ class LocationSerializer(InvenTreeModelSerializer):
] ]
class StockItemAttachmentSerializer(InvenTreeModelSerializer): class StockItemAttachmentSerializer(InvenTreeAttachmentSerializer):
""" Serializer for StockItemAttachment model """ """ Serializer for StockItemAttachment model """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -277,6 +277,7 @@ class StockItemAttachmentSerializer(InvenTreeModelSerializer):
'pk', 'pk',
'stock_item', 'stock_item',
'attachment', 'attachment',
'filename',
'comment', 'comment',
'upload_date', 'upload_date',
'user', 'user',

View File

@ -215,6 +215,7 @@
constructForm(url, { constructForm(url, {
fields: { fields: {
filename: {},
comment: {}, comment: {},
}, },
title: '{% trans "Edit Attachment" %}', title: '{% trans "Edit Attachment" %}',