This commit is contained in:
Oliver Walters
2026-05-25 10:37:01 +00:00
parent d305ec2dd3
commit 1260de6bce
5 changed files with 84 additions and 23 deletions
+23 -10
View File
@@ -937,23 +937,25 @@ def remove_non_printable_characters(value: str, remove_newline=True) -> str:
return cleaned return cleaned
def clean_markdown(value: str) -> str: def get_markdownify_settings() -> dict:
"""Clean a markdown string. """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. This function will remove javascript and other potentially harmful content from the markdown string.
""" """
import markdown import markdown
try: markdownify_settings = get_markdownify_settings()
markdownify_settings = settings.MARKDOWNIFY['default']
except (AttributeError, KeyError):
markdownify_settings = {}
extensions = markdownify_settings.get('MARKDOWN_EXTENSIONS', []) extensions = markdownify_settings.get('MARKDOWN_EXTENSIONS', [])
extension_configs = markdownify_settings.get('MARKDOWN_EXTENSION_CONFIGS', {}) 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. <tag> instead of <tag />
html = markdown.markdown( html = markdown.markdown(
value or '', value or '',
extensions=extensions, extensions=extensions,
@@ -961,7 +963,18 @@ def clean_markdown(value: str) -> str:
output_format='html', 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_tags = markdownify_settings.get('WHITELIST_TAGS', DEFAULT_TAGS)
whitelist_attrs = markdownify_settings.get('WHITELIST_ATTRS', DEFAULT_ATTRS) whitelist_attrs = markdownify_settings.get('WHITELIST_ATTRS', DEFAULT_ATTRS)
whitelist_styles = markdownify_settings.get('WHITELIST_STYLES', DEFAULT_CSS) whitelist_styles = markdownify_settings.get('WHITELIST_STYLES', DEFAULT_CSS)
+10
View File
@@ -50,6 +50,16 @@ class SelectionListAdmin(admin.ModelAdmin):
inlines = [SelectionListEntryInlineAdmin] 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) @admin.register(common.models.Attachment)
class AttachmentAdmin(admin.ModelAdmin): class AttachmentAdmin(admin.ModelAdmin):
"""Admin interface for Attachment objects.""" """Admin interface for Attachment objects."""
@@ -1,5 +1,6 @@
# Generated by Django 5.2.14 on 2026-05-18 14:16 # Generated by Django 5.2.14 on 2026-05-18 14:16
import common.validators
import InvenTree.models import InvenTree.models
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@@ -73,6 +74,7 @@ class Migration(migrations.Migration):
models.ForeignKey( models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype", to="contenttypes.contenttype",
validators=[common.validators.validate_notes_model_type]
), ),
), ),
( (
@@ -4,40 +4,72 @@ from tqdm import tqdm
from django.db import migrations from django.db import migrations
from InvenTree.helpers import markdown_to_html
def migrate_notes(apps, schema_editor): def migrate_notes(apps, schema_editor):
"""Migrate existing notes to the new Note model.""" """Migrate existing notes to the new Note model."""
ContentType = apps.get_model("contenttypes", "ContentType")
# New target models # New target models
Note = apps.get_model('common', 'Note') Note = apps.get_model('common', 'Note')
NotesImage = apps.get_model('common', 'NotesImage') NotesImage = apps.get_model('common', 'NotesImage')
for app, model in [ for app, model in [
('build', 'Build'), ('build', 'build'),
('company', 'Company'), ('company', 'company'),
('company', 'ManufacturerPart'), ('company', 'manufacturerpart'),
('company', 'SupplierPart'), ('company', 'supplierpart'),
('order', 'PurchaseOrder'), ('order', 'purchaseorder'),
('order', 'ReturnOrder'), ('order', 'returnorder'),
('order', 'SalesOrder'), ('order', 'salesorder'),
('order', 'TransferOrder'), ('order', 'transferorder'),
('part', 'Part'), ('part', 'part'),
('stock', 'StockItem'), ('stock', 'stockitem'),
]: ]:
# Find old model which contains the 'notes' field # Find old model which contains the 'notes' field
OldModel = apps.get_model(app, model) OldModel = apps.get_model(app, model)
with_notes = OldModel.objects.exclude(notes__isnull=True).exclude(notes='') 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(): if not with_notes.exists():
continue continue
progress = tqdm(total=with_notes.count(), desc=f'Migrating notes for {app}.{model}') progress = tqdm(total=with_notes.count(), desc=f'Migrating notes for {app}.{model}')
for obj in with_notes: initial_note_count = Note.objects.count()
# TODO ... 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) 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): class Migration(migrations.Migration):
+5 -1
View File
@@ -3055,7 +3055,11 @@ class Note(
if instance and isinstance(instance, InvenTreeNoteMixin): if instance and isinstance(instance, InvenTreeNoteMixin):
instance.check_note_delete(self) 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() model_id = models.PositiveIntegerField()