Merge branch 'inventree:master' into matmair/issue2279

This commit is contained in:
Matthias Mair 2021-11-28 17:26:15 +01:00 committed by GitHub
commit e34de8204a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 319 additions and 292 deletions

View File

@ -21,7 +21,8 @@ from django.dispatch import receiver
from mptt.models import MPTTModel, TreeForeignKey
from mptt.exceptions import InvalidMove
from .validators import validate_tree_name
from InvenTree.fields import InvenTreeURLField
from InvenTree.validators import validate_tree_name
logger = logging.getLogger('inventree')
@ -89,12 +90,15 @@ class ReferenceIndexingMixin(models.Model):
class InvenTreeAttachment(models.Model):
""" Provides an abstracted class for managing file attachments.
An attachment can be either an uploaded file, or an external URL
Attributes:
attachment: File
comment: String descriptor for the attachment
user: User associated with file upload
upload_date: Date the file was uploaded
"""
def getSubdir(self):
"""
Return the subdirectory under which attachments should be stored.
@ -103,11 +107,32 @@ class InvenTreeAttachment(models.Model):
return "attachments"
def save(self, *args, **kwargs):
# Either 'attachment' or 'link' must be specified!
if not self.attachment and not self.link:
raise ValidationError({
'attachment': _('Missing file'),
'link': _('Missing external link'),
})
super().save(*args, **kwargs)
def __str__(self):
if self.attachment is not None:
return os.path.basename(self.attachment.name)
else:
return str(self.link)
attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'),
help_text=_('Select file to attach'))
help_text=_('Select file to attach'),
blank=True, null=True
)
link = InvenTreeURLField(
blank=True, null=True,
verbose_name=_('Link'),
help_text=_('Link to external URL')
)
comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment'))
@ -123,7 +148,10 @@ class InvenTreeAttachment(models.Model):
@property
def basename(self):
if self.attachment:
return os.path.basename(self.attachment.name)
else:
return None
@basename.setter
def basename(self, fn):

View File

@ -239,22 +239,6 @@ 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,
@ -284,6 +268,27 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
return os.path.join(str(settings.MEDIA_URL), str(value))
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.
"""
attachment = InvenTreeAttachmentSerializerField(
required=False,
allow_null=False,
)
# The 'filename' field must be present in the serializer
filename = serializers.CharField(
label=_('Filename'),
required=False,
source='basename',
allow_blank=False,
)
class InvenTreeImageSerializerField(serializers.ImageField):
"""
Custom image serializer.

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0032_auto_20211014_0632'),
]
operations = [
migrations.AddField(
model_name='buildorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -16,7 +16,7 @@ from rest_framework import serializers
from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
from InvenTree.serializers import UserSerializerBrief
import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField
@ -516,8 +516,6 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializer for a BuildAttachment
"""
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = BuildOrderAttachment
@ -525,6 +523,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
'pk',
'build',
'attachment',
'link',
'filename',
'comment',
'upload_date',

View File

@ -431,54 +431,18 @@ enableDragAndDrop(
}
);
// Callback for creating a new attachment
$('#new-attachment').click(function() {
constructForm('{% url "api-build-attachment-list" %}', {
loadAttachmentTable('{% url "api-build-attachment-list" %}', {
filters: {
build: {{ build.pk }},
},
fields: {
attachment: {},
comment: {},
build: {
value: {{ build.pk }},
hidden: true,
}
},
method: 'POST',
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Attachment" %}',
});
}
});
loadAttachmentTable(
'{% url "api-build-attachment-list" %}',
{
filters: {
build: {{ build.pk }},
},
onEdit: function(pk) {
var url = `/api/build/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/build/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$('#edit-notes').click(function() {
constructForm('{% url "api-build-detail" build.pk %}', {
fields: {

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0052_auto_20211014_0631'),
]
operations = [
migrations.AddField(
model_name='purchaseorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AddField(
model_name='salesorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='purchaseorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
migrations.AlterField(
model_name='salesorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -24,7 +24,6 @@ from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField
from InvenTree.status_codes import StockStatus
from part.serializers import PartBriefSerializer
@ -377,8 +376,6 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializers for the PurchaseOrderAttachment model
"""
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = PurchaseOrderAttachment
@ -386,6 +383,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
'pk',
'order',
'attachment',
'link',
'filename',
'comment',
'upload_date',
@ -597,8 +595,6 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializers for the SalesOrderAttachment model
"""
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = SalesOrderAttachment
@ -607,6 +603,7 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
'order',
'attachment',
'filename',
'link',
'comment',
'upload_date',
]

View File

@ -124,51 +124,16 @@
}
);
loadAttachmentTable(
'{% url "api-po-attachment-list" %}',
{
loadAttachmentTable('{% url "api-po-attachment-list" %}', {
filters: {
order: {{ order.pk }},
},
onEdit: function(pk) {
var url = `/api/order/po/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/order/po/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$("#new-attachment").click(function() {
constructForm('{% url "api-po-attachment-list" %}', {
method: 'POST',
fields: {
attachment: {},
comment: {},
order: {
value: {{ order.pk }},
hidden: true,
},
},
reload: true,
title: '{% trans "Add Attachment" %}',
});
}
}
});
loadStockTable($("#stock-table"), {

View File

@ -110,55 +110,21 @@
},
label: 'attachment',
success: function(data, status, xhr) {
location.reload();
reloadAttachmentTable();
}
}
);
loadAttachmentTable(
'{% url "api-so-attachment-list" %}',
{
loadAttachmentTable('{% url "api-so-attachment-list" %}', {
filters: {
order: {{ order.pk }},
},
onEdit: function(pk) {
var url = `/api/order/so/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/order/so/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$("#new-attachment").click(function() {
constructForm('{% url "api-so-attachment-list" %}', {
method: 'POST',
fields: {
attachment: {},
comment: {},
order: {
value: {{ order.pk }},
hidden: true
}
hidden: true,
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Attachment" %}'
});
}
});
loadBuildTable($("#builds-table"), {

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0074_partcategorystar'),
]
operations = [
migrations.AddField(
model_name='partattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='partattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -75,8 +75,6 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializer for the PartAttachment class
"""
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = PartAttachment
@ -85,6 +83,7 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
'part',
'attachment',
'filename',
'link',
'comment',
'upload_date',
]

View File

@ -999,36 +999,17 @@
});
onPanelLoad("part-attachments", function() {
loadAttachmentTable(
'{% url "api-part-attachment-list" %}',
{
loadAttachmentTable('{% url "api-part-attachment-list" %}', {
filters: {
part: {{ part.pk }},
},
onEdit: function(pk) {
var url = `/api/part/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
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,
});
part: {
value: {{ part.pk }},
hidden: true
}
}
);
});
enableDragAndDrop(
'#attachment-dropzone',
@ -1043,26 +1024,6 @@
}
}
);
$("#new-attachment").click(function() {
constructForm(
'{% url "api-part-attachment-list" %}',
{
method: 'POST',
fields: {
attachment: {},
comment: {},
part: {
value: {{ part.pk }},
hidden: true,
}
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Attachment" %}',
}
)
});
});

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0069_auto_20211109_2347'),
]
operations = [
migrations.AddField(
model_name='stockitemattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='stockitemattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -420,8 +420,6 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True)
# TODO: Record the uploading user when creating or updating an attachment!
class Meta:
@ -432,6 +430,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
'stock_item',
'attachment',
'filename',
'link',
'comment',
'upload_date',
'user',

View File

@ -221,55 +221,16 @@
}
);
loadAttachmentTable(
'{% url "api-stock-attachment-list" %}',
{
loadAttachmentTable('{% url "api-stock-attachment-list" %}', {
filters: {
stock_item: {{ item.pk }},
},
onEdit: function(pk) {
var url = `/api/stock/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
comment: {},
},
title: '{% trans "Edit Attachment" %}',
onSuccess: reloadAttachmentTable
});
},
onDelete: function(pk) {
var url = `/api/stock/attachment/${pk}/`;
constructForm(url, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$("#new-attachment").click(function() {
constructForm(
'{% url "api-stock-attachment-list" %}',
{
method: 'POST',
fields: {
attachment: {},
comment: {},
stock_item: {
value: {{ item.pk }},
hidden: true,
},
},
reload: true,
title: '{% trans "Add Attachment" %}',
}
);
}
});
loadStockTestResultsTable(

View File

@ -1,5 +1,8 @@
{% load i18n %}
<button type='button' class='btn btn-outline-success' id='new-attachment-link'>
<span class='fas fa-link'></span> {% trans "Add Link" %}
</button>
<button type='button' class='btn btn-success' id='new-attachment'>
<span class='fas fa-plus-circle'></span> {% trans "Add Attachment" %}
</button>

View File

@ -6,10 +6,57 @@
*/
/* exported
addAttachmentButtonCallbacks,
loadAttachmentTable,
reloadAttachmentTable,
*/
/*
* Add callbacks to buttons for creating new attachments.
*
* Note: Attachments can also be external links!
*/
function addAttachmentButtonCallbacks(url, fields={}) {
// Callback for 'new attachment' button
$('#new-attachment').click(function() {
var file_fields = {
attachment: {},
comment: {},
};
Object.assign(file_fields, fields);
constructForm(url, {
fields: file_fields,
method: 'POST',
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Attachment" %}',
});
});
// Callback for 'new link' button
$('#new-attachment-link').click(function() {
var link_fields = {
link: {},
comment: {},
};
Object.assign(link_fields, fields);
constructForm(url, {
fields: link_fields,
method: 'POST',
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Link" %}',
});
});
}
function reloadAttachmentTable() {
$('#attachment-table').bootstrapTable('refresh');
@ -20,6 +67,8 @@ function loadAttachmentTable(url, options) {
var table = options.table || '#attachment-table';
addAttachmentButtonCallbacks(url, options.fields || {});
$(table).inventreeTable({
url: url,
name: options.name || 'attachments',
@ -34,26 +83,41 @@ function loadAttachmentTable(url, options) {
$(table).find('.button-attachment-edit').click(function() {
var pk = $(this).attr('pk');
if (options.onEdit) {
options.onEdit(pk);
constructForm(`${url}${pk}/`, {
fields: {
link: {},
comment: {},
},
processResults: function(data, fields, opts) {
// Remove the "link" field if the attachment is a file!
if (data.attachment) {
delete opts.fields.link;
}
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
});
// Add callback for 'delete' button
$(table).find('.button-attachment-delete').click(function() {
var pk = $(this).attr('pk');
if (options.onDelete) {
options.onDelete(pk);
}
constructForm(`${url}${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
});
},
columns: [
{
field: 'attachment',
title: '{% trans "File" %}',
formatter: function(value) {
title: '{% trans "Attachment" %}',
formatter: function(value, row) {
if (row.attachment) {
var icon = 'fa-file-alt';
var fn = value.toLowerCase();
@ -84,6 +148,12 @@ function loadAttachmentTable(url, options) {
var html = `<span class='fas ${icon}'></span> ${filename}`;
return renderLink(html, value);
} else if (row.link) {
var html = `<span class='fas fa-link'></span> ${row.link}`;
return renderLink(html, row.link);
} else {
return '-';
}
}
},
{