Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PUI] Notes editor #7284

Merged
merged 42 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8cb1c9c
Install mdxeditor
SchrodingersGat May 16, 2024
512ce50
Setup basic toolbar
SchrodingersGat May 16, 2024
4c3eff1
Merge branch 'master' into notes-editor
SchrodingersGat May 21, 2024
ff6f938
Refactoring
SchrodingersGat May 21, 2024
6103e1c
Add placeholder for image upload
SchrodingersGat May 21, 2024
29c2530
Merge remote-tracking branch 'origin/master' into notes-editor
SchrodingersGat May 22, 2024
b1723ba
Merge remote-tracking branch 'origin/master' into notes-editor
SchrodingersGat May 22, 2024
100cdae
Add fields to link uploaded notes to model instances
SchrodingersGat May 22, 2024
4e7e8ef
Add custom delete method for InvenTreeNotesMixin
SchrodingersGat May 22, 2024
15666b0
Refactor CUI notes editor
SchrodingersGat May 22, 2024
4ffad3d
Enable image uplaod for PUI editor
SchrodingersGat May 22, 2024
913a87e
Update <NotesEditor> component
SchrodingersGat May 22, 2024
7ed192e
Fix import
SchrodingersGat May 22, 2024
746ae65
Add button to save notes
SchrodingersGat May 22, 2024
a55f016
Prepend the host name to relative image URLs
SchrodingersGat May 22, 2024
325d7bf
Disable image resize
SchrodingersGat May 23, 2024
3b69991
Add notifications
SchrodingersGat May 23, 2024
8e69da9
Merge branch 'master' into notes-editor
SchrodingersGat May 23, 2024
67e423d
Add playwright tests
SchrodingersGat May 23, 2024
7a9e5f9
Enable "read-only" mode for notes
SchrodingersGat May 23, 2024
d18c529
Typo fix
SchrodingersGat May 23, 2024
d4bdc70
Styling updates to the editor
SchrodingersGat May 23, 2024
d00d943
Merge branch 'master' of github.com:inventree/InvenTree into notes-ed…
SchrodingersGat May 24, 2024
5f75375
Merge branch 'master' into notes-editor
SchrodingersGat May 27, 2024
1cfe3d7
Merge branch 'master' into notes-editor
SchrodingersGat May 27, 2024
2e2eaba
Merge remote-tracking branch 'origin/master' into notes-editor
SchrodingersGat May 28, 2024
bca8341
Update yarn.lock
SchrodingersGat May 28, 2024
b316af8
Bump API version
SchrodingersGat May 28, 2024
fd0bdfb
Update migration
SchrodingersGat May 28, 2024
116233b
Remove duplicated value
SchrodingersGat May 28, 2024
67517f5
Merge branch 'master' into notes-editor
SchrodingersGat May 30, 2024
29f66cd
Merge branch 'master' into notes-editor
SchrodingersGat Jun 3, 2024
2d58b75
Merge branch 'master' into notes-editor
SchrodingersGat Jun 4, 2024
ba0a660
Improve toggling between edit mode
SchrodingersGat Jun 4, 2024
026e8ce
Fix migration
SchrodingersGat Jun 4, 2024
138d50d
Fix migration
SchrodingersGat Jun 4, 2024
86dc48b
Unit test updates
SchrodingersGat Jun 4, 2024
43a11d0
Remove extraneous key prop
SchrodingersGat Jun 4, 2024
7446722
fix api version
SchrodingersGat Jun 4, 2024
44d08f6
Add custom serializer mixin for 'notes' field
SchrodingersGat Jun 4, 2024
4ae2c6c
Update to NotesEditor
SchrodingersGat Jun 4, 2024
9e483ab
Add unit test
SchrodingersGat Jun 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 204
INVENTREE_API_VERSION = 205

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""

INVENTREE_API_TEXT = """
v205 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7284
- Added model_type and model_id fields to the "NotesImage" serializer

v204 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7393
- Fixes previous API update which resulted in inconsistent ordering of currency codes

Expand Down
24 changes: 24 additions & 0 deletions src/backend/InvenTree/InvenTree/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,30 @@

abstract = True

def delete(self):
"""Custom delete method for InvenTreeNotesMixin.

- Before deleting the object, check if there are any uploaded images associated with it.
- If so, delete the notes first
"""
from common.models import NotesImage

images = NotesImage.objects.filter(
model_type=self.__class__.__name__.lower(), model_id=self.pk
)

if images.exists():
logger.info(

Check warning on line 1047 in src/backend/InvenTree/InvenTree/models.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/InvenTree/models.py#L1047

Added line #L1047 was not covered by tests
'Deleting %s uploaded images associated with %s <%s>',
images.count(),
self.__class__.__name__,
self.pk,
)

images.delete()

Check warning on line 1054 in src/backend/InvenTree/InvenTree/models.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/InvenTree/models.py#L1054

Added line #L1054 was not covered by tests

super().delete()

notes = InvenTree.fields.InvenTreeNotesField(
verbose_name=_('Notes'), help_text=_('Markdown notes (optional)')
)
Expand Down
18 changes: 18 additions & 0 deletions src/backend/InvenTree/InvenTree/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.fields import empty
from rest_framework.mixins import ListModelMixin
from rest_framework.serializers import DecimalField
from rest_framework.utils import model_meta
from taggit.serializers import TaggitSerializer
Expand Down Expand Up @@ -842,6 +843,23 @@ def save(self):
pass


class NotesFieldMixin:
"""Serializer mixin for handling 'notes' fields.

The 'notes' field will be hidden in a LIST serializer,
but available in a DETAIL serializer.
"""

def __init__(self, *args, **kwargs):
"""Remove 'notes' field from list views."""
super().__init__(*args, **kwargs)

if hasattr(self, 'context'):
if view := self.context.get('view', None):
if issubclass(view.__class__, ListModelMixin):
self.fields.pop('notes', None)


class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
"""Mixin class which allows downloading an 'image' from a remote URL.

Expand Down
4 changes: 2 additions & 2 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from InvenTree.serializers import UserSerializer

import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeDecimalField, NotesFieldMixin
from stock.status_codes import StockStatus

from stock.generators import generate_batch_code
Expand All @@ -33,7 +33,7 @@
from .models import Build, BuildLine, BuildItem, BuildOrderAttachment


class BuildSerializer(InvenTreeModelSerializer):
class BuildSerializer(NotesFieldMixin, InvenTreeModelSerializer):
"""Serializes a Build object."""

class Meta:
Expand Down
2 changes: 2 additions & 0 deletions src/backend/InvenTree/build/templates/build/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@ <h4>{% trans "Build Notes" %}</h4>
'build-notes',
'{% url "api-build-detail" build.pk %}',
{
model_type: 'build',
model_id: {{ build.pk }},
{% if roles.build.change %}
editable: true,
{% else %}
Expand Down
4 changes: 4 additions & 0 deletions src/backend/InvenTree/common/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ class NotesImageList(ListCreateAPI):
serializer_class = common.serializers.NotesImageSerializer
permission_classes = [permissions.IsAuthenticated]

filter_backends = SEARCH_ORDER_FILTER

search_fields = ['user', 'model_type', 'model_id']

def perform_create(self, serializer):
"""Create (upload) a new notes image."""
image = serializer.save()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@
setting = InvenTreeSetting.objects.filter(key=key).first()

if setting:
print(f"Updating existing setting for currency codes")
print(f"- Updating existing setting for currency codes")

Check warning on line 55 in src/backend/InvenTree/common/migrations/0023_auto_20240602_1332.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/common/migrations/0023_auto_20240602_1332.py#L55

Added line #L55 was not covered by tests
setting.value = value
setting.save()
else:
print(f"Creating new setting for currency codes")
print(f"- Creating new setting for currency codes")

Check warning on line 59 in src/backend/InvenTree/common/migrations/0023_auto_20240602_1332.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/common/migrations/0023_auto_20240602_1332.py#L59

Added line #L59 was not covered by tests
setting = InvenTreeSetting(key=key, value=value)
setting.save()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.12 on 2024-05-22 12:27

import common.validators
import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('common', '0023_auto_20240602_1332'),
]

operations = [
migrations.AddField(
model_name='notesimage',
name='model_id',
field=models.IntegerField(blank=True, default=None, help_text='Target model ID for this image', null=True),
),
migrations.AddField(
model_name='notesimage',
name='model_type',
field=models.CharField(blank=True, null=True, help_text='Target model type for this image', max_length=100, validators=[common.validators.validate_notes_model_type]),
),
]
19 changes: 16 additions & 3 deletions src/backend/InvenTree/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import json
import logging
import os
import re
import uuid
from datetime import timedelta, timezone
from enum import Enum
Expand All @@ -35,7 +34,6 @@

from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import convert_money
from djmoney.settings import CURRENCY_CHOICES
from rest_framework.exceptions import PermissionDenied

import build.validators
Expand Down Expand Up @@ -2955,7 +2953,7 @@ def rename_notes_image(instance, filename):
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)
Simply stores the image file, for use in the 'notes' field (of any models which support markdown).
"""

image = models.ImageField(
Expand All @@ -2966,6 +2964,21 @@ class NotesImage(models.Model):

date = models.DateTimeField(auto_now_add=True)

model_type = models.CharField(
max_length=100,
blank=True,
null=True,
validators=[common.validators.validate_notes_model_type],
help_text=_('Target model type for this image'),
)

model_id = models.IntegerField(
help_text=_('Target model ID for this image'),
blank=True,
null=True,
default=None,
)


class CustomUnit(models.Model):
"""Model for storing custom physical unit definitions.
Expand Down
2 changes: 1 addition & 1 deletion src/backend/InvenTree/common/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ class Meta:
"""Meta options for NotesImageSerializer."""

model = common_models.NotesImage
fields = ['pk', 'image', 'user', 'date']
fields = ['pk', 'image', 'user', 'date', 'model_type', 'model_id']

read_only_fields = ['date', 'user']

Expand Down
25 changes: 25 additions & 0 deletions src/backend/InvenTree/common/validators.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
"""Validation helpers for common models."""

import re

from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

import InvenTree.helpers_model


def validate_notes_model_type(value):
"""Ensure that the provided model type is valid.

The provided value must map to a model which implements the 'InvenTreeNotesMixin'.
"""
import InvenTree.models

Check warning on line 16 in src/backend/InvenTree/common/validators.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/common/validators.py#L16

Added line #L16 was not covered by tests

if not value:

Check warning on line 18 in src/backend/InvenTree/common/validators.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/common/validators.py#L18

Added line #L18 was not covered by tests
# Empty values are allowed
return

Check warning on line 20 in src/backend/InvenTree/common/validators.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/common/validators.py#L20

Added line #L20 was not covered by tests

model_types = list(

Check warning on line 22 in src/backend/InvenTree/common/validators.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/common/validators.py#L22

Added line #L22 was not covered by tests
InvenTree.helpers_model.getModelsWithMixin(InvenTree.models.InvenTreeNotesMixin)
)

model_names = [model.__name__.lower() for model in model_types]

Check warning on line 26 in src/backend/InvenTree/common/validators.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/common/validators.py#L26

Added line #L26 was not covered by tests

if value.lower() not in model_names:
raise ValidationError(f"Invalid model type '{value}'")

Check warning on line 29 in src/backend/InvenTree/common/validators.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/common/validators.py#L28-L29

Added lines #L28 - L29 were not covered by tests


def validate_decimal_places_min(value):
"""Validator for PRICING_DECIMAL_PLACES_MIN setting."""
Expand Down
3 changes: 2 additions & 1 deletion src/backend/InvenTree/company/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
InvenTreeModelSerializer,
InvenTreeMoneySerializer,
InvenTreeTagModelSerializer,
NotesFieldMixin,
RemoteImageMixin,
)
from part.serializers import PartBriefSerializer
Expand Down Expand Up @@ -102,7 +103,7 @@ class Meta:
]


class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
class CompanySerializer(NotesFieldMixin, RemoteImageMixin, InvenTreeModelSerializer):
"""Serializer for Company object (full detail)."""

class Meta:
Expand Down
2 changes: 2 additions & 0 deletions src/backend/InvenTree/company/templates/company/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ <h4>{% trans "Attachments" %}</h4>
'{% url "api-company-detail" company.pk %}',
{
editable: true,
model_type: "company",
model_id: {{ company.pk }},
}
);
});
Expand Down
9 changes: 5 additions & 4 deletions src/backend/InvenTree/order/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
InvenTreeDecimalField,
InvenTreeModelSerializer,
InvenTreeMoneySerializer,
NotesFieldMixin,
)
from order.status_codes import (
PurchaseOrderStatusGroups,
Expand Down Expand Up @@ -198,7 +199,7 @@ class AbstractExtraLineMeta:


class PurchaseOrderSerializer(
TotalPriceMixin, AbstractOrderSerializer, InvenTreeModelSerializer
NotesFieldMixin, TotalPriceMixin, AbstractOrderSerializer, InvenTreeModelSerializer
):
"""Serializer for a PurchaseOrder object."""

Expand Down Expand Up @@ -768,7 +769,7 @@ class Meta:


class SalesOrderSerializer(
TotalPriceMixin, AbstractOrderSerializer, InvenTreeModelSerializer
NotesFieldMixin, TotalPriceMixin, AbstractOrderSerializer, InvenTreeModelSerializer
):
"""Serializer for the SalesOrder model class."""

Expand Down Expand Up @@ -1075,7 +1076,7 @@ def annotate_queryset(queryset):
)


class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
class SalesOrderShipmentSerializer(NotesFieldMixin, InvenTreeModelSerializer):
"""Serializer for the SalesOrderShipment class."""

class Meta:
Expand Down Expand Up @@ -1536,7 +1537,7 @@ class Meta:


class ReturnOrderSerializer(
AbstractOrderSerializer, TotalPriceMixin, InvenTreeModelSerializer
NotesFieldMixin, AbstractOrderSerializer, TotalPriceMixin, InvenTreeModelSerializer
):
"""Serializer for the ReturnOrder model class."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ <h4>{% trans "Order Notes" %}</h4>
'order-notes',
'{% url "api-po-detail" order.pk %}',
{
model_type: "purchaseorder",
model_id: {{ order.pk }},
{% if roles.purchase_order.change %}
editable: true,
{% else %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ <h4>{% trans "Order Notes" %}</h4>
'order-notes',
'{% url "api-return-order-detail" order.pk %}',
{
model_type: 'returnorder',
model_id: {{ order.pk }},
{% if roles.purchase_order.change %}
editable: true,
{% else %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ <h4>{% trans "Order Notes" %}</h4>
'order-notes',
'{% url "api-so-detail" order.pk %}',
{
model_type: "salesorder",
model_id: {{ order.pk }},
{% if roles.purchase_order.change %}
editable: true,
{% else %}
Expand Down
1 change: 0 additions & 1 deletion src/backend/InvenTree/part/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,7 +1179,6 @@ class PartMixin:
queryset = Part.objects.all()

starred_parts = None

is_create = False

def get_queryset(self, *args, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions src/backend/InvenTree/part/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ def validate(self, data):


class PartSerializer(
InvenTree.serializers.NotesFieldMixin,
InvenTree.serializers.RemoteImageMixin,
InvenTree.serializers.InvenTreeTagModelSerializer,
):
Expand Down
2 changes: 2 additions & 0 deletions src/backend/InvenTree/part/templates/part/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ <h4>{% trans "Part Manufacturers" %}</h4>
'part-notes',
'{% url "api-part-detail" part.pk %}',
{
model_type: "part",
model_id: {{ part.pk }},
editable: {% js_bool roles.part.change %},
}
);
Expand Down
17 changes: 17 additions & 0 deletions src/backend/InvenTree/part/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,23 @@ def test_date_filters(self):
date = datetime.fromisoformat(item['creation_date'])
self.assertGreaterEqual(date, date_compare)

def test_part_notes(self):
"""Test the 'notes' field."""
# First test the 'LIST' endpoint - no notes information provided
url = reverse('api-part-list')

response = self.get(url, {'limit': 1}, expected_code=200)
data = response.data['results'][0]

self.assertNotIn('notes', data)

# Second, test the 'DETAIL' endpoint - notes information provided
url = reverse('api-part-detail', kwargs={'pk': data['pk']})

response = self.get(url, expected_code=200)

self.assertIn('notes', response.data)


class PartCreationTests(PartAPITestBase):
"""Tests for creating new Part instances via the API."""
Expand Down
Loading
Loading