From 1260de6bce3d133ab510c213b88280e9661fcc7a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 25 May 2026 10:37:01 +0000 Subject: [PATCH] Updates --- src/backend/InvenTree/InvenTree/helpers.py | 33 +++++++---- src/backend/InvenTree/common/admin.py | 10 ++++ .../InvenTree/common/migrations/0044_note.py | 2 + .../migrations/0045_auto_20260525_0956.py | 56 +++++++++++++++---- src/backend/InvenTree/common/models.py | 6 +- 5 files changed, 84 insertions(+), 23 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index 8f57b77bfe..2d207a114f 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -937,23 +937,25 @@ def remove_non_printable_characters(value: str, remove_newline=True) -> str: return cleaned -def clean_markdown(value: str) -> str: - """Clean a markdown string. +def get_markdownify_settings() -> dict: + """Return the settings for markdownify, or an empty dict if not defined.""" + try: + return settings.MARKDOWNIFY['default'] + except (AttributeError, KeyError): + return {} + + +def markdown_to_html(value: str) -> str: + """Convert a markdown string to HTML. This function will remove javascript and other potentially harmful content from the markdown string. """ import markdown - try: - markdownify_settings = settings.MARKDOWNIFY['default'] - except (AttributeError, KeyError): - markdownify_settings = {} - + markdownify_settings = get_markdownify_settings() extensions = markdownify_settings.get('MARKDOWN_EXTENSIONS', []) extension_configs = markdownify_settings.get('MARKDOWN_EXTENSION_CONFIGS', {}) - # Generate raw HTML from provided markdown (without sanitizing) - # Note: The 'html' output_format is required to generate self closing tags, e.g. instead of html = markdown.markdown( value or '', extensions=extensions, @@ -961,7 +963,18 @@ def clean_markdown(value: str) -> str: output_format='html', ) - # nh3 sanitizer settings + return html + + +def clean_markdown(value: str) -> str: + """Clean a markdown string. + + This function will remove javascript and other potentially harmful content from the markdown string. + """ + html = markdown_to_html(value) + + markdownify_settings = get_markdownify_settings() + whitelist_tags = markdownify_settings.get('WHITELIST_TAGS', DEFAULT_TAGS) whitelist_attrs = markdownify_settings.get('WHITELIST_ATTRS', DEFAULT_ATTRS) whitelist_styles = markdownify_settings.get('WHITELIST_STYLES', DEFAULT_CSS) diff --git a/src/backend/InvenTree/common/admin.py b/src/backend/InvenTree/common/admin.py index 5703423afc..f137581595 100644 --- a/src/backend/InvenTree/common/admin.py +++ b/src/backend/InvenTree/common/admin.py @@ -50,6 +50,16 @@ class SelectionListAdmin(admin.ModelAdmin): inlines = [SelectionListEntryInlineAdmin] +@admin.register(common.models.Note) +class NoteAdmin(admin.ModelAdmin): + """Admin interface for Note objects.""" + + list_display = ('title', 'model_type', 'model_id') + + list_filter = ('model_type',) + search_fields = ('title', 'description') + + @admin.register(common.models.Attachment) class AttachmentAdmin(admin.ModelAdmin): """Admin interface for Attachment objects.""" diff --git a/src/backend/InvenTree/common/migrations/0044_note.py b/src/backend/InvenTree/common/migrations/0044_note.py index 98f4957b81..f6338bc6b0 100644 --- a/src/backend/InvenTree/common/migrations/0044_note.py +++ b/src/backend/InvenTree/common/migrations/0044_note.py @@ -1,5 +1,6 @@ # Generated by Django 5.2.14 on 2026-05-18 14:16 +import common.validators import InvenTree.models import django.db.models.deletion from django.conf import settings @@ -73,6 +74,7 @@ class Migration(migrations.Migration): models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype", + validators=[common.validators.validate_notes_model_type] ), ), ( diff --git a/src/backend/InvenTree/common/migrations/0045_auto_20260525_0956.py b/src/backend/InvenTree/common/migrations/0045_auto_20260525_0956.py index f7e5d6ee8e..8d71b951fc 100644 --- a/src/backend/InvenTree/common/migrations/0045_auto_20260525_0956.py +++ b/src/backend/InvenTree/common/migrations/0045_auto_20260525_0956.py @@ -4,40 +4,72 @@ from tqdm import tqdm from django.db import migrations +from InvenTree.helpers import markdown_to_html + def migrate_notes(apps, schema_editor): """Migrate existing notes to the new Note model.""" + ContentType = apps.get_model("contenttypes", "ContentType") + # New target models Note = apps.get_model('common', 'Note') NotesImage = apps.get_model('common', 'NotesImage') for app, model in [ - ('build', 'Build'), - ('company', 'Company'), - ('company', 'ManufacturerPart'), - ('company', 'SupplierPart'), - ('order', 'PurchaseOrder'), - ('order', 'ReturnOrder'), - ('order', 'SalesOrder'), - ('order', 'TransferOrder'), - ('part', 'Part'), - ('stock', 'StockItem'), + ('build', 'build'), + ('company', 'company'), + ('company', 'manufacturerpart'), + ('company', 'supplierpart'), + ('order', 'purchaseorder'), + ('order', 'returnorder'), + ('order', 'salesorder'), + ('order', 'transferorder'), + ('part', 'part'), + ('stock', 'stockitem'), ]: # Find old model which contains the 'notes' field OldModel = apps.get_model(app, model) with_notes = OldModel.objects.exclude(notes__isnull=True).exclude(notes='') + content_type, _created = ContentType.objects.get_or_create(app_label=app, model=model) if not with_notes.exists(): continue progress = tqdm(total=with_notes.count(), desc=f'Migrating notes for {app}.{model}') - for obj in with_notes: - # TODO ... + initial_note_count = Note.objects.count() + expected_note_count = initial_note_count + with_notes.count() + + for instance in with_notes: + # Tasks: + # [x] - Convert markdown notes to HTML format + # [x] - Create a new Note instance (mark it as 'primary') + # [x] - Copy the content of the 'notes' field to the Note instance + # [x] - Link the Note instance to the original object (using GenericForeignKey) + # [ ] - Handle any associated images (if applicable) + + content = markdown_to_html(instance.notes) + + note = Note.objects.create( + title="Note", # We don't have a title field in the old model, so we'll just use a default value + content=content, + model_type=content_type, + model_id=instance.pk, + primary=True + ) progress.update(1) + # Find any existing NotesImage objects associated with this instance + images = NotesImage.objects.filter(model_type__iexact=model, model_id=instance.pk) + + # Point any associated images to the new Note instance + for image in images: + image.note = note + image.save() + + assert Note.objects.count() == expected_note_count, f"Expected {expected_note_count} notes, but found {Note.objects.count()} after migration." class Migration(migrations.Migration): diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index f7ee87ee0d..37fbda2752 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -3055,7 +3055,11 @@ class Note( if instance and isinstance(instance, InvenTreeNoteMixin): instance.check_note_delete(self) - model_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + model_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + validators=[common.validators.validate_notes_model_type], + ) model_id = models.PositiveIntegerField()