diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index abb6102a6f..eb87b8f77a 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -8,6 +8,9 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from django.http import JsonResponse
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework import filters
+
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -41,6 +44,28 @@ class InfoView(AjaxView):
return JsonResponse(data)
+class AttachmentMixin:
+ """
+ Mixin for creating attachment objects,
+ and ensuring the user information is saved correctly.
+ """
+
+ permission_classes = [permissions.IsAuthenticated]
+
+ filter_backends = [
+ DjangoFilterBackend,
+ filters.OrderingFilter,
+ filters.SearchFilter,
+ ]
+
+ def perform_create(self, serializer):
+ """ Save the user information when a file is uploaded """
+
+ attachment = serializer.save()
+ attachment.user = self.request.user
+ attachment.save()
+
+
class ActionPluginView(APIView):
"""
Endpoint for running custom action plugins.
diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index d2a5c5daa6..e192e34a0f 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -7,6 +7,7 @@ from __future__ import unicode_literals
import os
from django.db import models
+from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
@@ -41,6 +42,8 @@ class InvenTreeAttachment(models.Model):
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):
"""
@@ -55,6 +58,15 @@ class InvenTreeAttachment(models.Model):
comment = models.CharField(max_length=100, help_text=_('File comment'))
+ user = models.ForeignKey(
+ User,
+ on_delete=models.SET_NULL,
+ blank=True, null=True,
+ help_text=_('User'),
+ )
+
+ upload_date = models.DateField(auto_now_add=True, null=True, blank=True)
+
@property
def basename(self):
return os.path.basename(self.attachment.name)
diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py
index cb48b4c11d..dba493baab 100644
--- a/InvenTree/order/api.py
+++ b/InvenTree/order/api.py
@@ -12,6 +12,7 @@ from rest_framework import filters
from django.conf.urls import url, include
from InvenTree.helpers import str2bool
+from InvenTree.api import AttachmentMixin
from part.models import Part
from company.models import SupplierPart
@@ -200,7 +201,7 @@ class POLineItemDetail(generics.RetrieveUpdateAPIView):
]
-class SOAttachmentList(generics.ListCreateAPIView):
+class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a SalesOrderAttachment (file upload)
"""
@@ -208,12 +209,6 @@ class SOAttachmentList(generics.ListCreateAPIView):
queryset = SalesOrderAttachment.objects.all()
serializer_class = SOAttachmentSerializer
- filter_backends = [
- DjangoFilterBackend,
- filters.OrderingFilter,
- filters.SearchFilter,
- ]
-
filter_fields = [
'order',
]
@@ -399,7 +394,7 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView):
permission_classes = [permissions.IsAuthenticated]
-class POAttachmentList(generics.ListCreateAPIView):
+class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)
"""
@@ -407,12 +402,6 @@ class POAttachmentList(generics.ListCreateAPIView):
queryset = PurchaseOrderAttachment.objects.all()
serializer_class = POAttachmentSerializer
- filter_backends = [
- DjangoFilterBackend,
- filters.OrderingFilter,
- filters.SearchFilter,
- ]
-
filter_fields = [
'order',
]
diff --git a/InvenTree/order/migrations/0033_auto_20200512_1033.py b/InvenTree/order/migrations/0033_auto_20200512_1033.py
new file mode 100644
index 0000000000..2c3abbb0d0
--- /dev/null
+++ b/InvenTree/order/migrations/0033_auto_20200512_1033.py
@@ -0,0 +1,26 @@
+# Generated by Django 3.0.5 on 2020-05-12 10:33
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('order', '0032_auto_20200427_0044'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='purchaseorderattachment',
+ name='user',
+ field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='salesorderattachment',
+ name='user',
+ field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/InvenTree/order/migrations/0034_auto_20200512_1054.py b/InvenTree/order/migrations/0034_auto_20200512_1054.py
new file mode 100644
index 0000000000..9124bb1cce
--- /dev/null
+++ b/InvenTree/order/migrations/0034_auto_20200512_1054.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.5 on 2020-05-12 10:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('order', '0033_auto_20200512_1033'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='purchaseorderattachment',
+ name='upload_date',
+ field=models.DateField(auto_now_add=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='salesorderattachment',
+ name='upload_date',
+ field=models.DateField(auto_now_add=True, null=True),
+ ),
+ ]
diff --git a/InvenTree/order/templates/order/po_attachments.html b/InvenTree/order/templates/order/po_attachments.html
index 388b7197b6..a08b618fca 100644
--- a/InvenTree/order/templates/order/po_attachments.html
+++ b/InvenTree/order/templates/order/po_attachments.html
@@ -12,39 +12,8 @@
-
+{% include "attachment_table.html" with attachments=order.attachments.all %}
-
-
-
- {% trans "File" %} |
- {% trans "Comment" %} |
- |
-
-
-
- {% for attachment in order.attachments.all %}
-
- {{ attachment.basename }} |
- {{ attachment.comment }} |
-
-
-
-
-
- |
-
- {% endfor %}
-
-
{% endblock %}
@@ -61,8 +30,10 @@ $("#new-attachment").click(function() {
$("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this);
+
+ var url = `/order/purchase-order/attachment/${button.attr('pk')}/edit/`;
- launchModalForm(button.attr('url'), {
+ launchModalForm(url, {
reload: true,
});
});
@@ -70,7 +41,11 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() {
$("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this);
- launchModalForm(button.attr('url'), {
+ var url = `/order/purchase-order/attachment/${button.attr('pk')}/delete/`;
+
+ console.log("url: " + url);
+
+ launchModalForm(url, {
reload: true,
});
});
diff --git a/InvenTree/order/templates/order/so_attachments.html b/InvenTree/order/templates/order/so_attachments.html
index ca4170beff..aff62213e5 100644
--- a/InvenTree/order/templates/order/so_attachments.html
+++ b/InvenTree/order/templates/order/so_attachments.html
@@ -8,43 +8,11 @@
{% include 'order/so_tabs.html' with tab='attachments' %}
-{% trans "Sales Order Attachments" %}
+{% trans "Sales Order Attachments" %}
-
-
-
-
-
- {% trans "File" %} |
- {% trans "Comment" %} |
- |
-
-
-
- {% for attachment in order.attachments.all %}
-
- {{ attachment.basename }} |
- {{ attachment.comment }} |
-
-
-
-
-
- |
-
- {% endfor %}
-
-
+{% include "attachment_table.html" with attachments=order.attachments.all %}
{% endblock %}
@@ -62,7 +30,9 @@ $("#new-attachment").click(function() {
$("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this);
- launchModalForm(button.attr('url'), {
+ var url = `/order/sales-order/attachment/${button.attr('pk')}/edit/`;
+
+ launchModalForm(url, {
reload: true,
});
});
@@ -70,7 +40,9 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() {
$("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this);
- launchModalForm(button.attr('url'), {
+ var url = `/order/sales-order/attachment/${button.attr('pk')}/delete/`;
+
+ launchModalForm(url, {
reload: true,
});
});
diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py
index daa83eb5ab..22104df5c7 100644
--- a/InvenTree/order/urls.py
+++ b/InvenTree/order/urls.py
@@ -42,7 +42,7 @@ purchase_order_urls = [
])),
])),
- url(r'^attachments/', include([
+ url(r'^attachment/', include([
url(r'^new/', views.PurchaseOrderAttachmentCreate.as_view(), name='po-attachment-create'),
url(r'^(?P\d+)/edit/', views.PurchaseOrderAttachmentEdit.as_view(), name='po-attachment-edit'),
url(r'^(?P\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'),
@@ -86,7 +86,7 @@ sales_order_urls = [
])),
])),
- url(r'^attachments/', include([
+ url(r'^attachment/', include([
url(r'^new/', views.SalesOrderAttachmentCreate.as_view(), name='so-attachment-create'),
url(r'^(?P\d+)/edit/', views.SalesOrderAttachmentEdit.as_view(), name='so-attachment-edit'),
url(r'^(?P\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'),
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index 02132d34cc..370c629cac 100644
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -93,6 +93,10 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView):
ajax_form_title = _("Add Purchase Order Attachment")
ajax_template_name = "modal_form.html"
+ def post_save(self, **kwargs):
+ self.object.user = self.request.user
+ self.object.save()
+
def get_data(self):
return {
"success": _("Added attachment")
@@ -133,6 +137,10 @@ class SalesOrderAttachmentCreate(AjaxCreateView):
form_class = order_forms.EditSalesOrderAttachmentForm
ajax_form_title = _('Add Sales Order Attachment')
+ def post_save(self, **kwargs):
+ self.object.user = self.request.user
+ self.object.save()
+
def get_data(self):
return {
'success': _('Added attachment')
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 8f9121885d..84ff83af0a 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -25,6 +25,7 @@ from . import serializers as part_serializers
from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull
+from InvenTree.api import AttachmentMixin
class PartCategoryTree(TreeSerializer):
@@ -106,7 +107,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = PartCategory.objects.all()
-class PartAttachmentList(generics.ListCreateAPIView):
+class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a PartAttachment (file upload).
"""
@@ -114,14 +115,6 @@ class PartAttachmentList(generics.ListCreateAPIView):
queryset = PartAttachment.objects.all()
serializer_class = part_serializers.PartAttachmentSerializer
- permission_classes = [permissions.IsAuthenticated]
-
- filter_backends = [
- DjangoFilterBackend,
- filters.OrderingFilter,
- filters.SearchFilter,
- ]
-
filter_fields = [
'part',
]
@@ -296,24 +289,17 @@ class PartList(generics.ListCreateAPIView):
else:
return Response(data)
- def create(self, request, *args, **kwargs):
- """ Override the default 'create' behaviour:
+ def perform_create(self, serializer):
+ """
We wish to save the user who created this part!
- Note: Implementation coped from DRF class CreateModelMixin
+ Note: Implementation copied from DRF class CreateModelMixin
"""
- serializer = self.get_serializer(data=request.data)
- serializer.is_valid(raise_exception=True)
-
- # Record the user who created this Part object
part = serializer.save()
- part.creation_user = request.user
+ part.creation_user = self.request.user
part.save()
- headers = self.get_success_headers(serializer.data)
- return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
-
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
diff --git a/InvenTree/part/migrations/0036_partattachment_user.py b/InvenTree/part/migrations/0036_partattachment_user.py
new file mode 100644
index 0000000000..d59bc7ffb2
--- /dev/null
+++ b/InvenTree/part/migrations/0036_partattachment_user.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.0.5 on 2020-05-12 10:33
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('part', '0035_auto_20200406_0045'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='partattachment',
+ name='user',
+ field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0037_partattachment_upload_date.py b/InvenTree/part/migrations/0037_partattachment_upload_date.py
new file mode 100644
index 0000000000..4169870178
--- /dev/null
+++ b/InvenTree/part/migrations/0037_partattachment_upload_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.5 on 2020-05-12 10:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0036_partattachment_user'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='partattachment',
+ name='upload_date',
+ field=models.DateField(auto_now_add=True, null=True),
+ ),
+ ]
diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html
index b89a40e654..965645b748 100644
--- a/InvenTree/part/templates/part/attachments.html
+++ b/InvenTree/part/templates/part/attachments.html
@@ -9,54 +9,7 @@
-
-
-
-
-
-
- {% trans "File" %} |
- {% trans "Comment" %} |
- |
-
-
-
- {% for attachment in part.attachments.all %}
-
- {{ attachment.basename }} |
- {{ attachment.comment }} |
-
-
-
-
-
- |
-
- {% endfor %}
-
-{% if part.variant_of and part.variant_of.attachments.count > 0 %}
-
-
- Attachments for template part {{ part.variant_of.full_name }}
- |
-
-{% for attachment in part.variant_of.attachments.all %}
-
- {{ attachment.basename }} |
- {{ attachment.comment }} |
- |
-
-{% endfor %}
-{% endif %}
-
+{% include "attachment_table.html" with attachments=part.attachments.all %}
{% endblock %}
@@ -73,7 +26,9 @@
$("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this);
- launchModalForm(button.attr('url'),
+ var url = `/part/attachment/${button.attr('pk')}/edit/`;
+
+ launchModalForm(url,
{
reload: true,
});
@@ -82,7 +37,9 @@
$("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this);
- launchModalForm(button.attr('url'), {
+ var url = `/part/attachment/${button.attr('pk')}/delete/`;
+
+ launchModalForm(url, {
success: function() {
location.reload();
}
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 89a285cf60..ce2fc415be 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -74,6 +74,11 @@ class PartAttachmentCreate(AjaxCreateView):
ajax_form_title = _("Add part attachment")
ajax_template_name = "modal_form.html"
+ def post_save(self):
+ """ Record the user that uploaded the attachment """
+ self.object.user = self.request.user
+ self.object.save()
+
def get_data(self):
return {
'success': _('Added attachment')
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index a56d4081d0..1c654d8b3c 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -29,6 +29,7 @@ from .serializers import StockItemAttachmentSerializer
from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull
+from InvenTree.api import AttachmentMixin
from decimal import Decimal, InvalidOperation
@@ -645,7 +646,7 @@ class StockList(generics.ListCreateAPIView):
]
-class StockAttachmentList(generics.ListCreateAPIView):
+class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a StockItemAttachment (file upload)
"""
@@ -653,12 +654,6 @@ class StockAttachmentList(generics.ListCreateAPIView):
queryset = StockItemAttachment.objects.all()
serializer_class = StockItemAttachmentSerializer
- filter_backends = [
- DjangoFilterBackend,
- filters.OrderingFilter,
- filters.SearchFilter,
- ]
-
filter_fields = [
'stock_item',
]
diff --git a/InvenTree/stock/migrations/0037_stockitemattachment_user.py b/InvenTree/stock/migrations/0037_stockitemattachment_user.py
new file mode 100644
index 0000000000..b0f87df81a
--- /dev/null
+++ b/InvenTree/stock/migrations/0037_stockitemattachment_user.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.0.5 on 2020-05-12 10:33
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('stock', '0036_stockitemattachment'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='stockitemattachment',
+ name='user',
+ field=models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/InvenTree/stock/migrations/0038_stockitemattachment_upload_date.py b/InvenTree/stock/migrations/0038_stockitemattachment_upload_date.py
new file mode 100644
index 0000000000..0347b6f0ea
--- /dev/null
+++ b/InvenTree/stock/migrations/0038_stockitemattachment_upload_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.5 on 2020-05-12 10:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0037_stockitemattachment_user'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='stockitemattachment',
+ name='upload_date',
+ field=models.DateField(auto_now_add=True, null=True),
+ ),
+ ]
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index 69cb726f73..622f93e620 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -193,6 +193,16 @@ class LocationSerializer(InvenTreeModelSerializer):
class StockItemAttachmentSerializer(InvenTreeModelSerializer):
""" Serializer for StockItemAttachment model """
+ def __init_(self, *args, **kwargs):
+ user_detail = kwargs.pop('user_detail', False)
+
+ super().__init__(*args, **kwargs)
+
+ if user_detail is not True:
+ self.fields.pop('user_detail')
+
+ user_detail = UserSerializerBrief(source='user', read_only=True)
+
class Meta:
model = StockItemAttachment
@@ -200,7 +210,9 @@ class StockItemAttachmentSerializer(InvenTreeModelSerializer):
'pk',
'stock_item',
'attachment',
- 'comment'
+ 'comment',
+ 'user',
+ 'user_detail',
]
diff --git a/InvenTree/stock/templates/stock/item_attachments.html b/InvenTree/stock/templates/stock/item_attachments.html
index 6aeff554d0..2d056afbcf 100644
--- a/InvenTree/stock/templates/stock/item_attachments.html
+++ b/InvenTree/stock/templates/stock/item_attachments.html
@@ -10,40 +10,7 @@
{% trans "Stock Item Attachments" %}
-
-
-
-
-
-
- {% trans "File" %} |
- {% trans "Comment" %} |
- |
-
-
-
- {% for attachment in item.attachments.all %}
-
- {{ attachment.basename }} |
- {{ attachment.comment }} |
-
-
-
-
-
- |
-
- {% endfor %}
-
-
+{% include "attachment_table.html" with attachments=item.attachments.all %}
{% endblock %}
@@ -60,7 +27,9 @@ $("#new-attachment").click(function() {
$("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this);
- launchModalForm(button.attr('url'),
+ var url = `/stock/item/attachment/${button.attr('pk')}/edit/`;
+
+ launchModalForm(url,
{
reload: true,
});
@@ -69,7 +38,9 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() {
$("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this);
- launchModalForm(button.attr('url'), {
+ var url = `/stock/item/attachment/${button.attr('pk')}/delete/`;
+
+ launchModalForm(url, {
success: function() {
location.reload();
}
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 19edf8666d..e616be1f35 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -160,6 +160,12 @@ class StockItemAttachmentCreate(AjaxCreateView):
ajax_form_title = _("Add Stock Item Attachment")
ajax_template_name = "modal_form.html"
+ def post_save(self, **kwargs):
+ """ Record the user that uploaded the attachment """
+
+ self.object.user = self.request.user
+ self.object.save()
+
def get_data(self):
return {
'success': _("Added attachment")
diff --git a/InvenTree/templates/attachment_table.html b/InvenTree/templates/attachment_table.html
new file mode 100644
index 0000000000..090ae566f6
--- /dev/null
+++ b/InvenTree/templates/attachment_table.html
@@ -0,0 +1,40 @@
+{% load i18n %}
+
+
+
+
+
+
+ {% trans "File" %} |
+ {% trans "Comment" %} |
+ {% trans "Uploaded" %} |
+ |
+
+
+
+ {% for attachment in attachments %}
+
+ {{ attachment.basename }} |
+ {{ attachment.comment }} |
+
+ {% if attachment.upload_date %}{{ attachment.upload_date }}{% endif %}
+ {% if attachment.user %}{{ attachment.user.username }}{% endif %}
+ |
+
+
+
+
+
+ |
+
+ {% endfor %}
+
+
\ No newline at end of file