Add "template" field to Note model
This commit is contained in:
parent
f5ca46e61b
commit
cf43ff37a9
|
|
@ -54,10 +54,9 @@ class SelectionListAdmin(admin.ModelAdmin):
|
|||
class NoteAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for Note objects."""
|
||||
|
||||
list_display = ('title', 'model_type', 'model_id')
|
||||
|
||||
list_filter = ('model_type',)
|
||||
search_fields = ('title', 'description')
|
||||
list_display = ('title', 'template', 'model_type', 'model_id', 'primary')
|
||||
list_filter = ('template', 'model_type')
|
||||
search_fields = ('title', 'description', 'content')
|
||||
|
||||
|
||||
@admin.register(common.models.Attachment)
|
||||
|
|
|
|||
|
|
@ -902,14 +902,16 @@ class NoteFilter(FilterSet):
|
|||
"""Metaclass options for the filterset."""
|
||||
|
||||
model = common.models.Note
|
||||
fields = ['model_type', 'model_id', 'updated_by']
|
||||
fields = ['model_type', 'model_id', 'updated_by', 'template']
|
||||
|
||||
template = rest_filters.BooleanFilter(label='Template')
|
||||
|
||||
model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')
|
||||
|
||||
def filter_model_type(self, queryset, name, value):
|
||||
"""Filter queryset to include only Parameters of the given model type."""
|
||||
"""Filter queryset by model type, allowing null for global templates."""
|
||||
return common.filters.filter_content_type(
|
||||
queryset, 'model_type', value, allow_null=False
|
||||
queryset, 'model_type', value, allow_null=True
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -932,9 +934,16 @@ class NoteList(NoteMixin, ListCreateAPI):
|
|||
filterset_class = NoteFilter
|
||||
|
||||
ordering = '-primary'
|
||||
ordering_fields = ['model_id', 'model_type', 'user', 'creation', 'primary']
|
||||
search_fields = ['content', 'model_id', 'model_type', 'user__username']
|
||||
unique_create_fields = ['model_type', 'model_id']
|
||||
ordering_fields = [
|
||||
'model_id',
|
||||
'model_type',
|
||||
'updated_by',
|
||||
'updated',
|
||||
'primary',
|
||||
'template',
|
||||
'title',
|
||||
]
|
||||
search_fields = ['title', 'description', 'content']
|
||||
|
||||
|
||||
class NoteDetail(NoteMixin, RetrieveUpdateDestroyAPI):
|
||||
|
|
|
|||
|
|
@ -3003,7 +3003,7 @@ class Note(
|
|||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['model_type', 'model_id'],
|
||||
condition=models.Q(primary=True),
|
||||
condition=models.Q(primary=True, template=False),
|
||||
name='unique_primary_note_per_model',
|
||||
)
|
||||
]
|
||||
|
|
@ -3018,30 +3018,45 @@ class Note(
|
|||
"""Perform custom save checks before saving a Note instance."""
|
||||
self.check_save()
|
||||
|
||||
# Lock sibling notes to serialize concurrent primary-flag updates
|
||||
siblings = (
|
||||
Note.objects
|
||||
.select_for_update()
|
||||
.filter(model_type=self.model_type, model_id=self.model_id)
|
||||
.exclude(pk=self.pk)
|
||||
)
|
||||
if not self.template:
|
||||
# Lock sibling notes to serialize concurrent primary-flag updates
|
||||
siblings = (
|
||||
Note.objects
|
||||
.select_for_update()
|
||||
.filter(
|
||||
model_type=self.model_type, model_id=self.model_id, template=False
|
||||
)
|
||||
.exclude(pk=self.pk)
|
||||
)
|
||||
|
||||
# If this is the *only* note for this model instance, then set it as the primary note
|
||||
if not siblings.exists():
|
||||
self.primary = True
|
||||
# If this is the *only* note for this model instance, set it as primary
|
||||
if not siblings.exists():
|
||||
self.primary = True
|
||||
|
||||
self.clean()
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Once this note is saved, mark other notes as non-primary
|
||||
if self.primary:
|
||||
siblings.update(primary=False)
|
||||
# Mark other notes as non-primary
|
||||
if self.primary:
|
||||
siblings.update(primary=False)
|
||||
else:
|
||||
# Templates skip primary-flag logic entirely
|
||||
self.primary = False
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
self.cleanup_images()
|
||||
|
||||
def clean(self):
|
||||
"""Clean / validate the note before saving to the database."""
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
if not self.template:
|
||||
if not self.model_type:
|
||||
raise ValidationError({'model_type': _('This field is required.')})
|
||||
if self.model_id is None:
|
||||
raise ValidationError({'model_id': _('This field is required.')})
|
||||
|
||||
if self.content:
|
||||
attrs = copy.deepcopy(nh3.ALLOWED_ATTRIBUTES)
|
||||
|
||||
|
|
@ -3113,6 +3128,9 @@ class Note(
|
|||
"""Check if this note can be saved."""
|
||||
from InvenTree.models import InvenTreeNoteMixin
|
||||
|
||||
if self.template or not self.model_type:
|
||||
return
|
||||
|
||||
try:
|
||||
instance = self.content_object
|
||||
except InvenTree.models.InvenTreeModel.DoesNotExist:
|
||||
|
|
@ -3125,6 +3143,9 @@ class Note(
|
|||
"""Check if this note can be deleted."""
|
||||
from InvenTree.models import InvenTreeNoteMixin
|
||||
|
||||
if self.template or not self.model_type:
|
||||
return
|
||||
|
||||
try:
|
||||
instance = self.content_object
|
||||
except InvenTree.models.InvenTreeModel.DoesNotExist:
|
||||
|
|
@ -3144,9 +3165,29 @@ class Note(
|
|||
if image.image and image.image.url not in self.content:
|
||||
image.delete()
|
||||
|
||||
model_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
template = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Template'),
|
||||
help_text=_(
|
||||
'Is this note a template (not linked to a specific model instance)?'
|
||||
),
|
||||
)
|
||||
|
||||
model_id = models.PositiveIntegerField()
|
||||
model_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
'Target model type for this note (null = applies to all model types)'
|
||||
),
|
||||
)
|
||||
|
||||
model_id = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_('Target model instance ID for this note (null for templates)'),
|
||||
)
|
||||
|
||||
content_object = GenericForeignKey('model_type', 'model_id')
|
||||
|
||||
|
|
|
|||
|
|
@ -856,6 +856,7 @@ class NoteSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
|||
model = common_models.Note
|
||||
fields = [
|
||||
'pk',
|
||||
'template',
|
||||
'model_type',
|
||||
'model_id',
|
||||
'primary',
|
||||
|
|
@ -868,17 +869,48 @@ class NoteSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
|||
|
||||
read_only_fields = ['updated', 'updated_by']
|
||||
|
||||
def validate(self, data):
|
||||
"""Validate note data — templates need no model_id; regular notes require both."""
|
||||
data = super().validate(data)
|
||||
|
||||
is_template = data.get('template', getattr(self.instance, 'template', False))
|
||||
|
||||
if not is_template:
|
||||
model_type = data.get('model_type') or getattr(
|
||||
self.instance, 'model_type', None
|
||||
)
|
||||
model_id = data.get('model_id') or getattr(self.instance, 'model_id', None)
|
||||
|
||||
if not model_type:
|
||||
raise serializers.ValidationError({
|
||||
'model_type': _('This field is required.')
|
||||
})
|
||||
if model_id is None:
|
||||
raise serializers.ValidationError({
|
||||
'model_id': _('This field is required.')
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""Save the Note instance."""
|
||||
from users.permissions import check_user_permission
|
||||
|
||||
model_type = self.validated_data.get('model_type', None)
|
||||
|
||||
if model_type is None and self.instance:
|
||||
model_type = self.instance.model_type
|
||||
|
||||
# Ensure that the user has permission to modify notes for the specified model
|
||||
user = self.context.get('request').user
|
||||
is_template = self.validated_data.get(
|
||||
'template', getattr(self.instance, 'template', False)
|
||||
)
|
||||
|
||||
if is_template:
|
||||
if not user.is_staff:
|
||||
raise PermissionDenied(
|
||||
_('Only staff users can create or edit note templates')
|
||||
)
|
||||
return super().save(updated_by=user, **kwargs)
|
||||
|
||||
model_type = self.validated_data.get('model_type') or (
|
||||
self.instance and self.instance.model_type
|
||||
)
|
||||
|
||||
target_model_class = model_type.model_class()
|
||||
|
||||
|
|
@ -902,8 +934,9 @@ class NoteSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
|||
mixin_class=InvenTreeNoteMixin,
|
||||
choices=common.validators.note_model_options,
|
||||
label=_('Model Type'),
|
||||
default='',
|
||||
allow_null=False,
|
||||
default=None,
|
||||
allow_null=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
updated_by_detail = OptionalField(
|
||||
|
|
|
|||
Loading…
Reference in New Issue