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
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:
119
boxes/templates/boxes/box_detail.html
Normal file
119
boxes/templates/boxes/box_detail.html
Normal 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">← 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>
|
||||
152
boxes/tests.py
152
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, '<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/')
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user