Add box detail page with thumbnails and view tests
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/labhelper) (push) Successful in 2m26s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/labhelper-data-loader) (push) Successful in 23s
SonarQube Scan / SonarQube Trigger (push) Failing after 19s

This commit is contained in:
2025-12-28 21:07:50 +01:00
parent 23ede15938
commit ac11b6fa51
8 changed files with 289 additions and 4 deletions

View File

@@ -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

View File

@@ -0,0 +1,119 @@
{% load thumbnail %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Box {{ box.id }} - LabHelper</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
}
.box-info {
background: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #4a90a4;
color: white;
font-weight: 600;
}
tr:hover {
background-color: #f8f9fa;
}
.thumbnail {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 4px;
}
.no-image {
width: 200px;
height: 200px;
background-color: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
color: #999;
border-radius: 4px;
}
.back-link {
margin-bottom: 20px;
display: inline-block;
}
.empty-message {
background: white;
padding: 40px;
text-align: center;
border-radius: 8px;
color: #666;
}
</style>
</head>
<body>
<a href="/" class="back-link">&larr; Back to Home</a>
<h1>Box {{ box.id }}</h1>
<div class="box-info">
<strong>Type:</strong> {{ box.box_type.name }}
({{ box.box_type.width }} x {{ box.box_type.height }} x {{ box.box_type.length }} mm)
</div>
{% if things %}
<table>
<thead>
<tr>
<th>Picture</th>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for thing in things %}
<tr>
<td>
{% if thing.picture %}
{% thumbnail thing.picture "200x200" crop="center" as thumb %}
<img src="{{ thumb.url }}" alt="{{ thing.name }}" class="thumbnail">
{% endthumbnail %}
{% else %}
<div class="no-image">No image</div>
{% endif %}
</td>
<td>{{ thing.name }}</td>
<td>{{ thing.thing_type.name }}</td>
<td>{{ thing.description|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-message">
This box is empty.
</div>
{% endif %}
</body>
</html>

View File

@@ -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, '<img')
# Clean up
thing.picture.delete()
def test_box_detail_url_name(self):
"""Box detail URL should be reversible by name."""
url = reverse('box_detail', kwargs={'box_id': 'BOX001'})
self.assertEqual(url, '/box/BOX001/')

View File

@@ -1,7 +1,20 @@
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from .models import Box
def index(request):
"""Simple index page."""
html = '<h1>LabHelper</h1><p><a href="/admin/">Admin</a></p>'
return HttpResponse(html)
def box_detail(request, box_id):
"""Display contents of a box."""
box = get_object_or_404(Box, pk=box_id)
things = box.things.select_related('thing_type').all()
return render(request, 'boxes/box_detail.html', {
'box': box,
'things': things,
})

Binary file not shown.

View File

@@ -39,6 +39,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django_mptt_admin',
'sorl.thumbnail',
'boxes',
]

View File

@@ -19,10 +19,11 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path
from boxes.views import index
from boxes.views import box_detail, index
urlpatterns = [
path('', index, name='index'),
path('box/<str:box_id>/', box_detail, name='box_detail'),
path('admin/', admin.site.urls),
]

View File

@@ -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