diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 80e509173f..d9070702a7 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -21,8 +21,8 @@ from InvenTree.api import BulkDeleteMixin from InvenTree.config import CONFIG_LOOKUPS from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER from InvenTree.helpers import inheritors -from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI, - RetrieveUpdateDestroyAPI) +from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI, + RetrieveUpdateAPI, RetrieveUpdateDestroyAPI) from InvenTree.permissions import IsSuperuser from plugin.models import NotificationUserSetting from plugin.serializers import NotificationUserSettingSerializer @@ -440,6 +440,20 @@ class ConfigDetail(RetrieveAPI): return {key: value} +class NotesImageList(ListCreateAPI): + """List view for all notes images.""" + + queryset = common.models.NotesImage.objects.all() + serializer_class = common.serializers.NotesImageSerializer + permission_classes = [permissions.IsAuthenticated, ] + + def perform_create(self, serializer): + """Create (upload) a new notes image""" + image = serializer.save() + image.user = self.request.user + image.save() + + settings_api_urls = [ # User settings re_path(r'^user/', include([ @@ -473,6 +487,9 @@ common_api_urls = [ # Webhooks path('webhook//', WebhookView.as_view(), name='api-webhook'), + # Uploaded images for notes + re_path(r'^notes-image-upload/', NotesImageList.as_view(), name='api-notes-image-list'), + # Currencies re_path(r'^currency/', include([ re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'), diff --git a/InvenTree/common/migrations/0017_notesimage.py b/InvenTree/common/migrations/0017_notesimage.py new file mode 100644 index 0000000000..3ca73dcb5e --- /dev/null +++ b/InvenTree/common/migrations/0017_notesimage.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.18 on 2023-04-17 05:54 + +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), + ('common', '0016_alter_notificationentry_updated'), + ] + + operations = [ + migrations.CreateModel( + name='NotesImage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(help_text='Image file', upload_to='notes_images', verbose_name='Image')), + ('date', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index c5123a7e72..bdc0bbd717 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -2642,3 +2642,27 @@ class NewsFeedEntry(models.Model): help_text=_('Was this news item read?'), default=False ) + + +def rename_notes_image(instance, filename): + """Function for renaming uploading image file. Will store in the 'notes' directory.""" + + fname = os.path.basename(filename) + return os.path.join('notes', fname) + + +class NotesImage(models.Model): + """Model for storing uploading images for the 'notes' fields of various models. + + Simply stores the image file, for use in the 'notes' field (of any models which support markdown) + """ + + image = models.ImageField( + upload_to='notes_images', + verbose_name=_('Image'), + help_text=_('Image file'), + ) + + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + + date = models.DateTimeField(auto_now_add=True) diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 7b6998da5a..3e12b49b8d 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -5,9 +5,10 @@ from django.urls import reverse from rest_framework import serializers from common.models import (InvenTreeSetting, InvenTreeUserSetting, - NewsFeedEntry, NotificationMessage) + NewsFeedEntry, NotesImage, NotificationMessage) from InvenTree.helpers import construct_absolute_url, get_objectreference -from InvenTree.serializers import InvenTreeModelSerializer +from InvenTree.serializers import (InvenTreeAttachmentSerializerField, + InvenTreeModelSerializer) class SettingsSerializer(InvenTreeModelSerializer): @@ -230,3 +231,25 @@ class ConfigSerializer(serializers.Serializer): if not isinstance(instance, str): instance = list(instance.keys())[0] return {'key': instance, **self.instance[instance]} + + +class NotesImageSerializer(InvenTreeModelSerializer): + """Serializer for the NotesImage model.""" + + class Meta: + """Meta options for NotesImageSerializer.""" + + model = NotesImage + fields = [ + 'pk', + 'image', + 'user', + 'date', + ] + + read_only_fields = [ + 'date', + 'user', + ] + + image = InvenTreeAttachmentSerializerField(required=True) diff --git a/InvenTree/templates/js/translated/helpers.js b/InvenTree/templates/js/translated/helpers.js index 04b50d5cbd..f2858e25e7 100644 --- a/InvenTree/templates/js/translated/helpers.js +++ b/InvenTree/templates/js/translated/helpers.js @@ -380,6 +380,10 @@ function renderLink(text, url, options={}) { } +/* + * Configure an EasyMDE editor for the given element, + * allowing markdown editing of the notes field. + */ function setupNotesField(element, url, options={}) { var editable = options.editable || false; @@ -419,12 +423,25 @@ function setupNotesField(element, url, options={}) { element: document.getElementById(element), initialValue: initial, toolbar: toolbar_icons, + uploadImage: true, + imagePathAbsolute: true, + imageUploadFunction: function(imageFile, onSuccess, onError) { + // Attempt to upload the image to the InvenTree server + var form_data = new FormData(); + + form_data.append('image', imageFile); + + inventreeFormDataUpload('{% url "api-notes-image-list" %}', form_data, { + success: function(response) { + console.log("Uploading image:", response.image); + onSuccess(response.image); + }, + error: function(xhr, status, error) { + onError(error); + } + }); + }, shortcuts: [], - renderingConfig: { - markedOptions: { - sanitize: true, - } - } });