Add new generic "notes" model

This commit is contained in:
Oliver Walters
2026-05-18 13:56:34 +00:00
parent 01bc34e08a
commit 71d51ff7b4
3 changed files with 257 additions and 2 deletions
+80 -2
View File
@@ -632,14 +632,14 @@ class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model):
return params return params
def check_parameter_delete(self, parameter): def check_parameter_delete(self, parameter) -> bool:
"""Run a check to determine if the provided parameter can be deleted. """Run a check to determine if the provided parameter can be deleted.
The default implementation always returns True, but this can be overridden in the implementing class. The default implementation always returns True, but this can be overridden in the implementing class.
""" """
return True return True
def check_parameter_save(self, parameter): def check_parameter_save(self, parameter) -> bool:
"""Run a check to determine if the provided parameter can be saved. """Run a check to determine if the provided parameter can be saved.
The default implementation always returns True, but this can be overridden in the implementing class. The default implementation always returns True, but this can be overridden in the implementing class.
@@ -647,6 +647,84 @@ class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model):
return True return True
class InvenTreeNoteMixin(InvenTreePermissionCheckMixin):
"""Provides an abstracted class for managing notes.
Links the implementing model to the common.models.Note table,
and provides multiple accessor / helper methods.
"""
class Meta:
"""Metaclass options for InvenTreeNoteMixin."""
abstract = True
# Define a reverse relation to the Note model
notes_list = GenericRelation(
'common.Note', content_type_field='model_type', object_id_field='model_id'
)
@property
def notes(self) -> QuerySet:
"""Return a queryset containing all notes for this model."""
# Check the query cache for pre-fetched parameters
if cache := getattr(self, '_prefetched_objects_cache', None):
if 'notes_list' in cache:
return cache['notes_list']
return self.notes_list.all()
def delete(self, *args, **kwargs):
"""Handle the deletion of a model instance.
Before deleting the model instance, delete any associated notes.
"""
self.notes_list.all().delete()
super().delete(*args, **kwargs)
@transaction.atomic
def copy_notes_from(self, other, **kwargs):
"""Copy all notes from another model instance.
Arguments:
other: The other model instance to copy notes from
**kwargs: Additional keyword arguments to pass to the Note constructor
"""
import common.models
notes = []
content_type = ContentType.objects.get_for_model(self.__class__)
for note in other.notes.all():
note.pk = None
note.model_id = self.pk
note.model_type = content_type
notes.append(note)
if len(notes) > 0:
common.models.Note.objects.bulk_create(notes, batch_size=250)
def get_note(self, title: str):
"""Return a Note instance for the given note title."""
return self.notes_list.filter(title=title).first()
def check_note_delete(self, note) -> bool:
"""Run a check to determine if the provided note can be deleted.
The default implementation always returns True, but this can be overridden in the implementing class.
"""
return True
def check_note_save(self, note) -> bool:
"""Run a check to determine if the provided note can be saved.
The default implementation always returns True, but this can be overridden in the implementing class.
"""
return True
class InvenTreeAttachmentMixin(InvenTreePermissionCheckMixin): class InvenTreeAttachmentMixin(InvenTreePermissionCheckMixin):
"""Provides an abstracted class for managing file attachments. """Provides an abstracted class for managing file attachments.
@@ -0,0 +1,95 @@
# Generated by Django 5.2.14 on 2026-05-18 13:55
import InvenTree.models
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("common", "0041_auto_20251203_1244"),
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Note",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"metadata",
models.JSONField(
blank=True,
help_text="JSON metadata field, for use by external plugins",
null=True,
verbose_name="Plugin Metadata",
),
),
(
"updated",
models.DateTimeField(
blank=True,
default=None,
help_text="Timestamp of last update",
null=True,
verbose_name="Updated",
),
),
("model_id", models.PositiveIntegerField()),
(
"title",
models.CharField(
help_text="Note title", max_length=100, verbose_name="Title"
),
),
(
"description",
models.CharField(
blank=True,
help_text="Optional description field",
max_length=250,
verbose_name="Description",
),
),
(
"model_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
(
"updated_by",
models.ForeignKey(
blank=True,
help_text="User who last updated this object",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated",
to=settings.AUTH_USER_MODEL,
verbose_name="Update By",
),
),
],
options={
"verbose_name": "Note",
"verbose_name_plural": "Notes",
},
bases=(
InvenTree.models.ContentTypeMixin,
InvenTree.models.PluginValidationMixin,
models.Model,
),
),
]
+82
View File
@@ -2906,6 +2906,88 @@ class Parameter(
return self.template.description return self.template.description
class Note(
UpdatedUserMixin, InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
):
"""Class which represents a note assigned to a particular model instance.
Attributes:
model_type: The type of model to which this note is linked
model_id: The ID of the model to which this note is linked
user: The user who created the note
title: The title of the note
description: A description of the note (optional)
content: The content of the note
created: Date/time that the note was created
"""
class Meta:
"""Meta options for Note model."""
verbose_name = _('Note')
verbose_name_plural = _('Notes')
@staticmethod
def get_api_url() -> str:
"""Return the API URL associated with the Parameter model."""
return reverse('api-note-list')
def save(self, *args, **kwargs):
"""Perform custom save checks before saving a Note instance."""
self.check_save()
super().save(*args, **kwargs)
def delete(self):
"""Perform custom delete checks before deleting a Parameter instance."""
self.check_delete()
super().delete()
def clean(self):
"""Clean / validate the note before saving to the database."""
# TODO: Implement this
def check_save(self):
"""Check if this note can be saved."""
from InvenTree.models import InvenTreeNoteMixin
try:
instance = self.content_object
except InvenTree.models.InvenTreeModel.DoesNotExist:
return
if instance and isinstance(instance, InvenTreeNoteMixin):
instance.check_note_save(self)
def check_delete(self):
"""Check if this note can be deleted."""
from InvenTree.models import InvenTreeNoteMixin
try:
instance = self.content_object
except InvenTree.models.InvenTreeModel.DoesNotExist:
return
if instance and isinstance(instance, InvenTreeNoteMixin):
instance.check_note_delete(self)
model_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
model_id = models.PositiveIntegerField()
content_object = GenericForeignKey('model_type', 'model_id')
title = models.CharField(
max_length=100, verbose_name=_('Title'), help_text=_('Note title')
)
description = models.CharField(
max_length=250,
blank=True,
verbose_name=_('Description'),
help_text=_('Optional description field'),
)
class BarcodeScanResult(InvenTree.models.InvenTreeModel): class BarcodeScanResult(InvenTree.models.InvenTreeModel):
"""Model for storing barcode scans results.""" """Model for storing barcode scans results."""