diff --git a/argocd/deployment.yaml b/argocd/deployment.yaml
index d4b7b71..b4bf478 100644
--- a/argocd/deployment.yaml
+++ b/argocd/deployment.yaml
@@ -18,14 +18,14 @@ spec:
fsGroupChangePolicy: "OnRootMismatch"
initContainers:
- name: loader
- image: git.baumann.gr/adebaumann/labhelper-data-loader:0.004
+ image: git.baumann.gr/adebaumann/labhelper-data-loader:0.005
command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: web
- image: git.baumann.gr/adebaumann/labhelper:0.010
+ image: git.baumann.gr/adebaumann/labhelper:0.011
imagePullPolicy: Always
ports:
- containerPort: 8000
diff --git a/boxes/templates/boxes/box_detail.html b/boxes/templates/boxes/box_detail.html
new file mode 100644
index 0000000..68d3a87
--- /dev/null
+++ b/boxes/templates/boxes/box_detail.html
@@ -0,0 +1,119 @@
+{% load thumbnail %}
+
+
+
+
+
+ Box {{ box.id }} - LabHelper
+
+
+
+ ← Back to Home
+
+ Box {{ box.id }}
+
+
+ Type: {{ box.box_type.name }}
+ ({{ box.box_type.width }} x {{ box.box_type.height }} x {{ box.box_type.length }} mm)
+
+
+ {% if things %}
+
+
+
+ | Picture |
+ Name |
+ Type |
+ Description |
+
+
+
+ {% for thing in things %}
+
+
+ {% if thing.picture %}
+ {% thumbnail thing.picture "200x200" crop="center" as thumb %}
+
+ {% endthumbnail %}
+ {% else %}
+ No image
+ {% endif %}
+ |
+ {{ thing.name }} |
+ {{ thing.thing_type.name }} |
+ {{ thing.description|default:"-" }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
+ This box is empty.
+
+ {% endif %}
+
+
diff --git a/boxes/tests.py b/boxes/tests.py
index 35a6207..8433342 100644
--- a/boxes/tests.py
+++ b/boxes/tests.py
@@ -1,7 +1,8 @@
from django.contrib.admin.sites import AdminSite
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import IntegrityError
-from django.test import TestCase
+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
@@ -337,3 +338,152 @@ class ThingAdminTests(TestCase):
def test_search_fields(self):
"""ThingAdmin should search by name and description."""
self.assertEqual(self.admin.search_fields, ('name', 'description'))
+
+
+class IndexViewTests(TestCase):
+ """Tests for the index view."""
+
+ def setUp(self):
+ """Set up test client."""
+ self.client = Client()
+
+ def test_index_returns_200(self):
+ """Index page should return 200 status."""
+ response = self.client.get('/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_index_contains_labhelper(self):
+ """Index page should contain LabHelper title."""
+ response = self.client.get('/')
+ self.assertContains(response, 'LabHelper')
+
+ def test_index_contains_admin_link(self):
+ """Index page should contain link to admin."""
+ response = self.client.get('/')
+ self.assertContains(response, '/admin/')
+
+
+class BoxDetailViewTests(TestCase):
+ """Tests for the box detail view."""
+
+ def setUp(self):
+ """Set up test fixtures."""
+ self.client = Client()
+ 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')
+
+ def test_box_detail_returns_200(self):
+ """Box detail page should return 200 status."""
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_box_detail_returns_404_for_invalid_box(self):
+ """Box detail page should return 404 for non-existent box."""
+ response = self.client.get('/box/INVALID/')
+ self.assertEqual(response.status_code, 404)
+
+ def test_box_detail_shows_box_id(self):
+ """Box detail page should show box ID."""
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertContains(response, 'BOX001')
+
+ def test_box_detail_shows_box_type(self):
+ """Box detail page should show box type name."""
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertContains(response, 'Standard Box')
+
+ def test_box_detail_shows_dimensions(self):
+ """Box detail page should show box dimensions."""
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertContains(response, '200')
+ self.assertContains(response, '100')
+ self.assertContains(response, '300')
+
+ def test_box_detail_shows_empty_message(self):
+ """Box detail page should show empty message when no things."""
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertContains(response, 'This box is empty')
+
+ def test_box_detail_shows_thing(self):
+ """Box detail page should show things in the box."""
+ Thing.objects.create(
+ name='Arduino Uno',
+ thing_type=self.thing_type,
+ box=self.box,
+ description='A microcontroller board'
+ )
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertContains(response, 'Arduino Uno')
+ self.assertContains(response, 'Electronics')
+ self.assertContains(response, 'A microcontroller board')
+
+ def test_box_detail_shows_no_image_placeholder(self):
+ """Box detail page should show placeholder for things without images."""
+ Thing.objects.create(
+ name='Test Item',
+ thing_type=self.thing_type,
+ box=self.box
+ )
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertContains(response, 'No image')
+
+ def test_box_detail_shows_multiple_things(self):
+ """Box detail page should show multiple things."""
+ Thing.objects.create(
+ name='Item One',
+ thing_type=self.thing_type,
+ box=self.box
+ )
+ Thing.objects.create(
+ name='Item Two',
+ thing_type=self.thing_type,
+ box=self.box
+ )
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertContains(response, 'Item One')
+ self.assertContains(response, 'Item Two')
+
+ def test_box_detail_uses_correct_template(self):
+ """Box detail page should use the correct template."""
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertTemplateUsed(response, 'boxes/box_detail.html')
+
+ def test_box_detail_with_image(self):
+ """Box detail page should show thumbnail for things with images."""
+ # Create a simple 1x1 pixel PNG
+ image_data = (
+ b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01'
+ b'\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00'
+ b'\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00'
+ b'\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82'
+ )
+ image = SimpleUploadedFile(
+ name='test.png',
+ content=image_data,
+ content_type='image/png'
+ )
+ thing = Thing.objects.create(
+ name='Item With Image',
+ thing_type=self.thing_type,
+ box=self.box,
+ picture=image
+ )
+ response = self.client.get(f'/box/{self.box.id}/')
+ self.assertContains(response, 'Item With Image')
+ self.assertContains(response, '
/', box_detail, name='box_detail'),
path('admin/', admin.site.urls),
]
diff --git a/requirements.txt b/requirements.txt
index 70dc548..6572697 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -33,5 +33,6 @@ sqlparse==0.5.3
urllib3==2.6.0
wcwidth==0.2.13
Pillow==11.1.0
+sorl-thumbnail==12.11.0
bleach==6.1.0
coverage==7.6.1