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

View File

@ -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. <tag> instead of <tag />
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)

View File

@ -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."""

View File

@ -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]
),
),
(

View File

@ -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):

View File

@ -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()