194 lines
5.4 KiB
Python
194 lines
5.4 KiB
Python
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: <id>-<name>.<extension>"""
|
|
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/<thing_id>/<filename>"""
|
|
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}'
|