mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-02 02:01:20 +00:00
Updates
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user