import os from django.db import models from django.utils.text import slugify def thing_picture_upload_path(instance, filename): """Generate a custom path for thing pictures in format: -.""" extension = os.path.splitext(filename)[1] safe_name = slugify(instance.name) if instance.pk: return f'things/{instance.pk}-{safe_name}{extension}' else: return f'things/temp-{safe_name}{extension}' class BoxType(models.Model): """A type of storage box with specific dimensions.""" name = models.CharField(max_length=255) width = models.PositiveIntegerField(help_text='Width in millimeters') height = models.PositiveIntegerField(help_text='Height in millimeters') length = models.PositiveIntegerField(help_text='Length in millimeters') class Meta: ordering = ['name'] def __str__(self): return self.name class Box(models.Model): """A storage box in the lab.""" id = models.CharField( max_length=10, primary_key=True, help_text='Alphanumeric identifier (max 10 characters)' ) box_type = models.ForeignKey( BoxType, on_delete=models.PROTECT, related_name='boxes' ) class Meta: verbose_name_plural = 'boxes' def __str__(self): return self.id class Facet(models.Model): """A category of tags (e.g., Priority, Category, Status).""" class Cardinality(models.TextChoices): SINGLE = 'single', 'Single (0..1)' MULTIPLE = 'multiple', 'Multiple (0..n)' name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) color = models.CharField( max_length=7, default='#667eea', help_text='Hex color code (e.g., #667eea)' ) cardinality = models.CharField( max_length=10, choices=Cardinality.choices, default=Cardinality.MULTIPLE, help_text='Can a thing have multiple tags of this facet?' ) class Meta: ordering = ['name'] def __str__(self): return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs) class Tag(models.Model): """A tag value for a specific facet.""" facet = models.ForeignKey( Facet, on_delete=models.CASCADE, related_name='tags' ) name = models.CharField( max_length=100, help_text='Tag description (e.g., "High", "Electronics")' ) class Meta: ordering = ['facet', 'name'] unique_together = [['facet', 'name']] def __str__(self): return f'{self.facet.name}:{self.name}' class Thing(models.Model): """An item stored in a box.""" name = models.CharField(max_length=255) box = models.ForeignKey( Box, on_delete=models.PROTECT, related_name='things' ) description = models.TextField(blank=True) picture = models.ImageField(upload_to=thing_picture_upload_path, blank=True) tags = models.ManyToManyField( Tag, blank=True, related_name='things' ) class Meta: ordering = ['name'] def save(self, *args, **kwargs): """Override save to rename picture file after instance gets a pk.""" if self.picture and not self.pk: picture = self.picture super().save(*args, **kwargs) new_path = thing_picture_upload_path(self, picture.name) if picture.name != new_path: try: old_path = self.picture.path if os.path.exists(old_path): new_full_path = os.path.join(os.path.dirname(old_path), os.path.basename(new_path)) os.rename(old_path, new_full_path) self.picture.name = new_path super().save(update_fields=['picture']) except (AttributeError, FileNotFoundError): pass else: super().save(*args, **kwargs) def __str__(self): return self.name def thing_file_upload_path(instance, filename): """Generate a custom path for thing files in format: things/files//""" return f'things/files/{instance.thing.id}/{filename}' class ThingFile(models.Model): """A file attachment for a Thing.""" thing = models.ForeignKey( Thing, on_delete=models.CASCADE, related_name='files' ) file = models.FileField(upload_to=thing_file_upload_path) title = models.CharField(max_length=255, help_text='Descriptive name for the file') uploaded_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-uploaded_at'] def __str__(self): return f'{self.thing.name} - {self.title}' def filename(self): """Return the original filename.""" return os.path.basename(self.file.name) class ThingLink(models.Model): """A hyperlink for a Thing.""" thing = models.ForeignKey( Thing, on_delete=models.CASCADE, related_name='links' ) url = models.URLField(max_length=2048) title = models.CharField(max_length=255, help_text='Descriptive title for the link') uploaded_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-uploaded_at'] def __str__(self): return f'{self.thing.name} - {self.title}'