import os from django.db import models from django.utils.text import slugify from mptt.models import MPTTModel, TreeForeignKey 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 ThingType(MPTTModel): """A hierarchical type/category for things stored in boxes.""" name = models.CharField(max_length=255) parent = TreeForeignKey( 'self', on_delete=models.CASCADE, null=True, blank=True, related_name='children' ) class MPTTMeta: order_insertion_by = ['name'] def __str__(self): return self.name class Thing(models.Model): """An item stored in a box.""" name = models.CharField(max_length=255) thing_type = models.ForeignKey( ThingType, on_delete=models.PROTECT, related_name='things' ) 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) 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}'