Added arbitrary files and links to boxes

This commit is contained in:
2026-01-01 14:50:07 +01:00
parent acde0cb2f8
commit a4f9274da4
8 changed files with 673 additions and 15 deletions

View File

@@ -1,7 +1,7 @@
from django.contrib import admin
from django_mptt_admin.admin import DjangoMpttAdmin
from .models import Box, BoxType, Thing, ThingType
from .models import Box, BoxType, Thing, ThingFile, ThingLink, ThingType
@admin.register(BoxType)
@@ -35,3 +35,32 @@ class ThingAdmin(admin.ModelAdmin):
list_display = ('name', 'thing_type', 'box')
list_filter = ('thing_type', 'box')
search_fields = ('name', 'description')
class ThingFileInline(admin.TabularInline):
"""Inline admin for Thing files."""
model = ThingFile
extra = 1
fields = ('title', 'file')
class ThingLinkInline(admin.TabularInline):
"""Inline admin for Thing links."""
model = ThingLink
extra = 1
fields = ('title', 'url')
class ThingAdminWithFiles(admin.ModelAdmin):
"""Admin configuration for Thing model with files and links."""
list_display = ('name', 'thing_type', 'box')
list_filter = ('thing_type', 'box')
search_fields = ('name', 'description')
inlines = [ThingFileInline, ThingLinkInline]
admin.site.unregister(Thing)
admin.register(Thing, ThingAdminWithFiles)

View File

@@ -1,6 +1,6 @@
from django import forms
from .models import Box, BoxType, Thing
from .models import Box, BoxType, Thing, ThingFile, ThingLink
class ThingForm(forms.ModelForm):
@@ -24,6 +24,30 @@ class ThingPictureForm(forms.ModelForm):
fields = ('picture',)
class ThingFileForm(forms.ModelForm):
"""Form for adding a file to a Thing."""
class Meta:
model = ThingFile
fields = ('title', 'file')
widgets = {
'title': forms.TextInput(attrs={'style': 'width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;'}),
'file': forms.FileInput(attrs={'style': 'width: 100%;'}),
}
class ThingLinkForm(forms.ModelForm):
"""Form for adding a link to a Thing."""
class Meta:
model = ThingLink
fields = ('title', 'url')
widgets = {
'title': forms.TextInput(attrs={'style': 'width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;'}),
'url': forms.URLInput(attrs={'style': 'width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;'}),
}
class BoxTypeForm(forms.ModelForm):
"""Form for adding/editing a BoxType."""

View File

@@ -0,0 +1,79 @@
import os
from django.conf import settings
from django.core.management.base import BaseCommand
from boxes.models import ThingFile
class Command(BaseCommand):
help = 'Clean up orphaned files from deleted things'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
dest='dry_run',
help='Show what would be deleted without actually deleting',
)
def handle(self, *args, **options):
dry_run = options.get('dry_run', False)
if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN - No files will be deleted'))
self.stdout.write('Finding orphaned files...')
media_root = settings.MEDIA_ROOT
things_files_root = os.path.join(media_root, 'things', 'files')
if not os.path.exists(things_files_root):
self.stdout.write(self.style.WARNING('No things/files directory found'))
return
valid_paths = set()
for thing_file in ThingFile.objects.all():
if thing_file.file:
file_path = thing_file.file.path
if os.path.exists(file_path):
valid_paths.add(os.path.relpath(file_path, things_files_root))
self.stdout.write(f'Found {len(valid_paths)} valid files in database')
deleted_count = 0
empty_dirs_removed = 0
for root, dirs, files in os.walk(things_files_root, topdown=False):
for filename in files:
file_path = os.path.join(root, filename)
relative_path = os.path.relpath(file_path, things_files_root)
if relative_path not in valid_paths:
deleted_count += 1
if dry_run:
self.stdout.write(f'Would delete: {file_path}')
else:
try:
os.remove(file_path)
self.stdout.write(f'Deleted: {file_path}')
except OSError as e:
self.stdout.write(self.style.ERROR(f'Failed to delete {file_path}: {e}'))
for dirname in dirs:
dir_path = os.path.join(root, dirname)
if not os.listdir(dir_path):
if dry_run:
self.stdout.write(f'Would remove empty directory: {dir_path}')
else:
try:
os.rmdir(dir_path)
self.stdout.write(f'Removed empty directory: {dir_path}')
empty_dirs_removed += 1
except OSError as e:
self.stdout.write(self.style.ERROR(f'Failed to remove {dir_path}: {e}'))
if dry_run:
self.stdout.write(self.style.WARNING(f'\nDry run complete. Would delete {deleted_count} files'))
self.stdout.write(f'Would remove {empty_dirs_removed} empty directories')
else:
self.stdout.write(self.style.SUCCESS(f'\nCleanup complete! Deleted {deleted_count} orphaned files'))
self.stdout.write(f'Removed {empty_dirs_removed} empty directories')

View File

@@ -0,0 +1,41 @@
# Generated by Django 5.2.9 on 2026-01-01 13:15
import boxes.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boxes', '0004_alter_thing_picture'),
]
operations = [
migrations.CreateModel(
name='ThingFile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to=boxes.models.thing_file_upload_path)),
('title', models.CharField(help_text='Descriptive name for the file', max_length=255)),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('thing', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='boxes.thing')),
],
options={
'ordering': ['-uploaded_at'],
},
),
migrations.CreateModel(
name='ThingLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField(max_length=2048)),
('title', models.CharField(help_text='Descriptive title for the link', max_length=255)),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('thing', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='boxes.thing')),
],
options={
'ordering': ['-uploaded_at'],
},
),
]

View File

@@ -110,3 +110,50 @@ class Thing(models.Model):
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}'

View File

@@ -38,7 +38,7 @@
<label class="btn" style="cursor: pointer; display: inline-flex; align-items: center; gap: 8px;">
<i class="fas fa-camera"></i>
<span>{% if thing.picture %}Change picture{% else %}Add picture{% endif %}</span>
<input type="file" name="picture" accept="image/*" style="display: none;" onchange="this.form.submit();">
<input type="file" id="picture_upload" name="picture" accept="image/*" style="display: none;" onchange="this.form.submit();">
</label>
{% if thing.picture %}
<button type="submit" name="action" value="delete_picture" class="btn" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border: none;" onclick="return confirm('Are you sure you want to delete this picture?');">
@@ -80,10 +80,114 @@
</div>
</div>
{% endif %}
{% if thing.files.all %}
<div class="detail-row" style="margin-bottom: 25px;">
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
<i class="fas fa-file-alt"></i> Files
</div>
<div style="display: flex; flex-direction: column; gap: 10px;">
{% for file in thing.files.all %}
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-paperclip" style="color: #667eea; font-size: 16px;"></i>
<a href="{{ file.file.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ file.title }}</a>
<span style="color: #999; font-size: 12px;">({{ file.filename }})</span>
</div>
<form method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this file?');">
{% csrf_token %}
<input type="hidden" name="action" value="delete_file">
<input type="hidden" name="file_id" value="{{ file.id }}">
<button type="submit" style="background: none; border: none; cursor: pointer; color: #e74c3c; padding: 5px; transition: all 0.3s;" onmouseover="this.style.color='#c0392b'" onmouseout="this.style.color='#e74c3c'">
<i class="fas fa-times" style="font-size: 14px;"></i>
</button>
</form>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if thing.links.all %}
<div class="detail-row" style="margin-bottom: 25px;">
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
<i class="fas fa-link"></i> Links
</div>
<div style="display: flex; flex-direction: column; gap: 10px;">
{% for link in thing.links.all %}
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-external-link-alt" style="color: #667eea; font-size: 16px;"></i>
<a href="{{ link.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ link.title }}</a>
</div>
<form method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this link?');">
{% csrf_token %}
<input type="hidden" name="action" value="delete_link">
<input type="hidden" name="link_id" value="{{ link.id }}">
<button type="submit" style="background: none; border: none; cursor: pointer; color: #e74c3c; padding: 5px; transition: all 0.3s;" onmouseover="this.style.color='#c0392b'" onmouseout="this.style.color='#e74c3c'">
<i class="fas fa-times" style="font-size: 14px;"></i>
</button>
</form>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
<div class="section">
<h2 style="color: #667eea; font-size: 20px; font-weight: 700; margin-top: 0; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<i class="fas fa-plus-circle"></i> Add Attachments
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap: 30px;">
<div>
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
<i class="fas fa-file-upload"></i> Upload File
</h3>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="action" value="add_file">
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="file_title" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Title</label>
<input type="text" id="file_title" name="title" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<div>
<label for="file_upload" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">File</label>
<input type="file" id="file_upload" name="file" style="width: 100%; padding: 10px 15px; border: 2px dashed #e0e0e0; border-radius: 8px; font-size: 14px; background: #f8f9fa;">
</div>
<button type="submit" class="btn">
<i class="fas fa-upload"></i> Upload File
</button>
</div>
</form>
</div>
<div>
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
<i class="fas fa-link"></i> Add Link
</h3>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="add_link">
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="link_title" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Title</label>
<input type="text" id="link_title" name="title" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<div>
<label for="link_url" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">URL</label>
<input type="url" id="link_url" name="url" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<button type="submit" class="btn">
<i class="fas fa-plus"></i> Add Link
</button>
</div>
</form>
</div>
</div>
<form method="post" class="section">
{% csrf_token %}
<input type="hidden" name="action" value="move">
@@ -118,14 +222,5 @@ $('#new_box').on('focus', function() {
$(this).css('border-color', '#e0e0e0');
$(this).css('box-shadow', 'none');
});
$('input[type="file"]').on('change', function() {
var fileName = $(this).val().split('\\').pop();
if (fileName) {
var form = $(this).closest('form');
form.append('<input type="hidden" name="action" value="upload_picture">');
form[0].submit();
}
});
</script>
{% endblock %}

View File

@@ -6,7 +6,7 @@ from django.test import Client, TestCase
from django.urls import reverse
from .admin import BoxAdmin, BoxTypeAdmin, ThingAdmin, ThingTypeAdmin
from .models import Box, BoxType, Thing, ThingType
from .models import Box, BoxType, Thing, ThingFile, ThingLink, ThingType
class AuthTestCase(TestCase):
@@ -1310,3 +1310,303 @@ class ThingPictureUploadTests(AuthTestCase):
self.assertRedirects(response, f'/thing/{self.thing.id}/')
self.thing.refresh_from_db()
self.assertFalse(self.thing.picture.name)
class ThingFileModelTests(AuthTestCase):
"""Tests for the ThingFile model."""
def setUp(self):
"""Set up test fixtures."""
super().setUp()
self.box_type = BoxType.objects.create(
name='Standard Box',
width=200,
height=100,
length=300
)
self.box = Box.objects.create(
id='BOX001',
box_type=self.box_type
)
self.thing_type = ThingType.objects.create(name='Electronics')
self.thing = Thing.objects.create(
name='Arduino Uno',
thing_type=self.thing_type,
box=self.box
)
def test_thing_file_creation(self):
"""ThingFile should be created with correct attributes."""
thing_file = ThingFile.objects.create(
thing=self.thing,
title='Datasheet',
file='datasheets/test.pdf'
)
self.assertEqual(thing_file.thing, self.thing)
self.assertEqual(thing_file.title, 'Datasheet')
self.assertEqual(thing_file.file.name, 'datasheets/test.pdf')
def test_thing_file_str(self):
"""ThingFile __str__ should return thing name and title."""
thing_file = ThingFile.objects.create(
thing=self.thing,
title='Manual',
file='manuals/test.pdf'
)
expected = f'{self.thing.name} - Manual'
self.assertEqual(str(thing_file), expected)
def test_thing_file_ordering(self):
"""ThingFiles should be ordered by uploaded_at descending."""
file1 = ThingFile.objects.create(
thing=self.thing,
title='First File',
file='test1.pdf'
)
file2 = ThingFile.objects.create(
thing=self.thing,
title='Second File',
file='test2.pdf'
)
files = list(ThingFile.objects.all())
self.assertEqual(files[0], file2)
self.assertEqual(files[1], file1)
def test_thing_file_deletion_on_thing_delete(self):
"""Deleting a Thing should delete its files."""
thing_file = ThingFile.objects.create(
thing=self.thing,
title='Test File',
file='test.pdf'
)
file_id = thing_file.id
self.thing.delete()
self.assertFalse(ThingFile.objects.filter(id=file_id).exists())
class ThingLinkModelTests(AuthTestCase):
"""Tests for the ThingLink model."""
def setUp(self):
"""Set up test fixtures."""
super().setUp()
self.box_type = BoxType.objects.create(
name='Standard Box',
width=200,
height=100,
length=300
)
self.box = Box.objects.create(
id='BOX001',
box_type=self.box_type
)
self.thing_type = ThingType.objects.create(name='Electronics')
self.thing = Thing.objects.create(
name='Arduino Uno',
thing_type=self.thing_type,
box=self.box
)
def test_thing_link_creation(self):
"""ThingLink should be created with correct attributes."""
thing_link = ThingLink.objects.create(
thing=self.thing,
title='Manufacturer Website',
url='https://www.arduino.cc'
)
self.assertEqual(thing_link.thing, self.thing)
self.assertEqual(thing_link.title, 'Manufacturer Website')
self.assertEqual(thing_link.url, 'https://www.arduino.cc')
def test_thing_link_str(self):
"""ThingLink __str__ should return thing name and title."""
thing_link = ThingLink.objects.create(
thing=self.thing,
title='Documentation',
url='https://docs.arduino.cc'
)
expected = f'{self.thing.name} - Documentation'
self.assertEqual(str(thing_link), expected)
def test_thing_link_ordering(self):
"""ThingLinks should be ordered by uploaded_at descending."""
link1 = ThingLink.objects.create(
thing=self.thing,
title='First Link',
url='https://example1.com'
)
link2 = ThingLink.objects.create(
thing=self.thing,
title='Second Link',
url='https://example2.com'
)
links = list(ThingLink.objects.all())
self.assertEqual(links[0], link2)
self.assertEqual(links[1], link1)
def test_thing_link_deletion_on_thing_delete(self):
"""Deleting a Thing should delete its links."""
thing_link = ThingLink.objects.create(
thing=self.thing,
title='Test Link',
url='https://example.com'
)
link_id = thing_link.id
self.thing.delete()
self.assertFalse(ThingLink.objects.filter(id=link_id).exists())
class ThingFileAndLinkCRUDTests(AuthTestCase):
"""Tests for Thing file and link CRUD operations."""
def setUp(self):
"""Set up test fixtures."""
super().setUp()
self.box_type = BoxType.objects.create(
name='Standard Box',
width=200,
height=100,
length=300
)
self.box = Box.objects.create(
id='BOX001',
box_type=self.box_type
)
self.thing_type = ThingType.objects.create(name='Electronics')
self.thing = Thing.objects.create(
name='Arduino Uno',
thing_type=self.thing_type,
box=self.box
)
def test_add_file_to_thing(self):
"""Adding a file should create ThingFile."""
file_content = b'Sample PDF content'
uploaded_file = SimpleUploadedFile(
name='datasheet.pdf',
content=file_content,
content_type='application/pdf'
)
response = self.client.post(
f'/thing/{self.thing.id}/',
{'action': 'add_file', 'title': 'Datasheet', 'file': uploaded_file}
)
self.assertRedirects(response, f'/thing/{self.thing.id}/')
self.assertEqual(self.thing.files.count(), 1)
self.assertEqual(self.thing.files.first().title, 'Datasheet')
def test_add_link_to_thing(self):
"""Adding a link should create ThingLink."""
response = self.client.post(
f'/thing/{self.thing.id}/',
{
'action': 'add_link',
'title': 'Manufacturer',
'url': 'https://www.arduino.cc'
}
)
self.assertRedirects(response, f'/thing/{self.thing.id}/')
self.assertEqual(self.thing.links.count(), 1)
self.assertEqual(self.thing.links.first().title, 'Manufacturer')
self.assertEqual(self.thing.links.first().url, 'https://www.arduino.cc')
def test_delete_file_from_thing(self):
"""Deleting a file should remove it from database."""
thing_file = ThingFile.objects.create(
thing=self.thing,
title='Test File',
file='test.pdf'
)
file_id = thing_file.id
response = self.client.post(
f'/thing/{self.thing.id}/',
{'action': 'delete_file', 'file_id': str(file_id)}
)
self.assertRedirects(response, f'/thing/{self.thing.id}/')
self.assertFalse(ThingFile.objects.filter(id=file_id).exists())
def test_delete_link_from_thing(self):
"""Deleting a link should remove it from database."""
thing_link = ThingLink.objects.create(
thing=self.thing,
title='Test Link',
url='https://example.com'
)
link_id = thing_link.id
response = self.client.post(
f'/thing/{self.thing.id}/',
{'action': 'delete_link', 'link_id': str(link_id)}
)
self.assertRedirects(response, f'/thing/{self.thing.id}/')
self.assertFalse(ThingLink.objects.filter(id=link_id).exists())
def test_cannot_delete_file_from_other_thing(self):
"""Cannot delete file from another thing."""
other_thing = Thing.objects.create(
name='Other Item',
thing_type=self.thing_type,
box=self.box
)
thing_file = ThingFile.objects.create(
thing=other_thing,
title='Other File',
file='other.pdf'
)
file_id = thing_file.id
response = self.client.post(
f'/thing/{self.thing.id}/',
{'action': 'delete_file', 'file_id': str(file_id)}
)
self.assertRedirects(response, f'/thing/{self.thing.id}/')
self.assertTrue(ThingFile.objects.filter(id=file_id).exists())
def test_cannot_delete_link_from_other_thing(self):
"""Cannot delete link from another thing."""
other_thing = Thing.objects.create(
name='Other Item',
thing_type=self.thing_type,
box=self.box
)
thing_link = ThingLink.objects.create(
thing=other_thing,
title='Other Link',
url='https://other.com'
)
link_id = thing_link.id
response = self.client.post(
f'/thing/{self.thing.id}/',
{'action': 'delete_link', 'link_id': str(link_id)}
)
self.assertRedirects(response, f'/thing/{self.thing.id}/')
self.assertTrue(ThingLink.objects.filter(id=link_id).exists())
def test_thing_detail_shows_files_section(self):
"""Thing detail page should show files when they exist."""
ThingFile.objects.create(
thing=self.thing,
title='Datasheet',
file='test.pdf'
)
response = self.client.get(f'/thing/{self.thing.id}/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Files')
self.assertContains(response, 'Datasheet')
def test_thing_detail_shows_links_section(self):
"""Thing detail page should show links when they exist."""
ThingLink.objects.create(
thing=self.thing,
title='Documentation',
url='https://docs.example.com'
)
response = self.client.get(f'/thing/{self.thing.id}/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Links')
self.assertContains(response, 'Documentation')
def test_thing_detail_shows_upload_forms(self):
"""Thing detail page should show upload forms."""
response = self.client.get(f'/thing/{self.thing.id}/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Upload File')
self.assertContains(response, 'Add Link')

View File

@@ -6,10 +6,12 @@ from django.shortcuts import get_object_or_404, redirect, render
from .forms import (
BoxForm,
BoxTypeForm,
ThingFileForm,
ThingFormSet,
ThingLinkForm,
ThingPictureForm,
)
from .models import Box, BoxType, Thing, ThingType
from .models import Box, BoxType, Thing, ThingFile, ThingLink, ThingType
@login_required
@@ -46,12 +48,14 @@ def box_detail(request, box_id):
def thing_detail(request, thing_id):
"""Display details of a thing."""
thing = get_object_or_404(
Thing.objects.select_related('thing_type', 'box', 'box__box_type'),
Thing.objects.select_related('thing_type', 'box', 'box__box_type').prefetch_related('files', 'links'),
pk=thing_id
)
boxes = Box.objects.select_related('box_type').all().order_by('id')
picture_form = ThingPictureForm(instance=thing)
file_form = ThingFileForm()
link_form = ThingLinkForm()
if request.method == 'POST':
action = request.POST.get('action')
@@ -77,10 +81,49 @@ def thing_detail(request, thing_id):
thing.save()
return redirect('thing_detail', thing_id=thing.id)
elif action == 'add_file':
file_form = ThingFileForm(request.POST, request.FILES)
if file_form.is_valid():
thing_file = file_form.save(commit=False)
thing_file.thing = thing
thing_file.save()
return redirect('thing_detail', thing_id=thing.id)
elif action == 'add_link':
link_form = ThingLinkForm(request.POST)
if link_form.is_valid():
thing_link = link_form.save(commit=False)
thing_link.thing = thing
thing_link.save()
return redirect('thing_detail', thing_id=thing.id)
elif action == 'delete_file':
file_id = request.POST.get('file_id')
if file_id:
try:
thing_file = ThingFile.objects.get(pk=file_id, thing=thing)
thing_file.file.delete()
thing_file.delete()
except ThingFile.DoesNotExist:
pass
return redirect('thing_detail', thing_id=thing.id)
elif action == 'delete_link':
link_id = request.POST.get('link_id')
if link_id:
try:
thing_link = ThingLink.objects.get(pk=link_id, thing=thing)
thing_link.delete()
except ThingLink.DoesNotExist:
pass
return redirect('thing_detail', thing_id=thing.id)
return render(request, 'boxes/thing_detail.html', {
'thing': thing,
'boxes': boxes,
'picture_form': picture_form,
'file_form': file_form,
'link_form': link_form,
})