diff --git a/src/backend/InvenTree/InvenTree/models.py b/src/backend/InvenTree/InvenTree/models.py index ce5be5e25c..e5cc92e18d 100644 --- a/src/backend/InvenTree/InvenTree/models.py +++ b/src/backend/InvenTree/InvenTree/models.py @@ -646,7 +646,7 @@ class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model): return True -class InvenTreeNoteMixin(InvenTreePermissionCheckMixin): +class InvenTreeNoteMixin(InvenTreePermissionCheckMixin, models.Model): """Provides an abstracted class for managing notes. Links the implementing model to the common.models.Note table, diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index cd23c032a9..bb9cf5a8dc 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -3022,6 +3022,8 @@ class Note( model_type=self.model_type, model_id=self.model_id ).exclude(pk=self.pk).update(primary=False) + self.cleanup_images() + def delete(self): """Perform custom delete checks before deleting a Parameter instance.""" self.check_delete() @@ -3120,6 +3122,18 @@ class Note( if instance and isinstance(instance, InvenTreeNoteMixin): instance.check_note_delete(self) + def cleanup_images(self): + """Remove any images which are no longer referenced in the note content.""" + for image in self.images.all(): + if image.image and image.image.url not in self.content: + image.delete() + + def render_html(self) -> str: + """Return the content of the note rendered as HTML.""" + from django.utils.safestring import mark_safe + + return mark_safe(self.content) + model_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) model_id = models.PositiveIntegerField() diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index 330aa1980f..f62abeeb82 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -465,6 +465,61 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa ) +@register.simple_tag() +def note_instance( + instance: Model, title: Optional[str] = None +) -> Optional[common.models.Note]: + """Return a Note object for the given instance and note name. + + Arguments: + instance: A Model object + title: The title of the note to retrieve (case insensitive) + + Returns: + A Note object, or None if not found + + Note: If the 'title' argument is not provided, the first Note object associated with the instance will be returned (if any). + """ + if not instance: + raise ValueError('notes tag requires a valid Model instance') + + if not hasattr(instance, 'notes'): + raise TypeError("notes tag requires a Model with a 'notes' attribute") + + notes = instance.notes + + if title: + # First try with exact match + if note := notes.filter(title=title).first(): + return note + + # Next, try with case-insensitive match + if note := notes.filter(title__iexact=title).first(): + return note + + # If no title is provided, or if no matching note is found, return the first note (if any) + return notes.order_by('-primary').first() + + +@register.simple_tag() +def note(instance: Model, title: Optional[str] = None) -> str: + """Return the HTML content of a Note object for the given instance and note name. + + Arguments: + instance: A Model object + title: The title of the note to retrieve (case insensitive) + + Returns: + The HTML content of the Note, or an empty string if not found + + Note: If the 'title' argument is not provided, the first Note object associated with the instance will be returned (if any). + """ + if note := note_instance(instance, title): + return note.render_html() + + return '' + + @register.simple_tag() def parameter( instance: Model, parameter_name: str