Initial commit: Django project with boxes app

- Set up Django 5.2.9 project structure
- Add boxes app with Box and BoxType models
- Box: alphanumeric ID (max 10 chars) with foreign key to BoxType
- BoxType: name and dimensions (width/height/length in mm)
- Configure admin interface for both models
- Add comprehensive test suite (14 tests)
This commit is contained in:
2025-12-28 13:04:37 +01:00
commit accefa2533
16 changed files with 487 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
__pycache__/
**/*.pyc
lib/
lib64
bin/
pyvenv.cfg
include/
keys/
.venv/
.idea/
*.kate-swp
node_modules/
package-lock.json
package.json
AGENT*.md
# Diagram cache directory
.env
data/db.sqlite3

0
boxes/__init__.py Normal file
View File

20
boxes/admin.py Normal file
View File

@@ -0,0 +1,20 @@
from django.contrib import admin
from .models import Box, BoxType
@admin.register(BoxType)
class BoxTypeAdmin(admin.ModelAdmin):
"""Admin configuration for BoxType model."""
list_display = ('name', 'width', 'height', 'length')
search_fields = ('name',)
@admin.register(Box)
class BoxAdmin(admin.ModelAdmin):
"""Admin configuration for Box model."""
list_display = ('id', 'box_type')
list_filter = ('box_type',)
search_fields = ('id',)

6
boxes/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BoxesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'boxes'

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.9 on 2025-12-28 12:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='BoxType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Box',
fields=[
('id', models.CharField(help_text='Alphanumeric identifier (max 10 characters)', max_length=10, primary_key=True, serialize=False)),
('box_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boxes', to='boxes.boxtype')),
],
options={
'verbose_name_plural': 'boxes',
},
),
]

View File

37
boxes/models.py Normal file
View File

@@ -0,0 +1,37 @@
from django.db import models
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

130
boxes/tests.py Normal file
View File

@@ -0,0 +1,130 @@
from django.contrib.admin.sites import AdminSite
from django.db import IntegrityError
from django.test import TestCase
from .admin import BoxAdmin, BoxTypeAdmin
from .models import Box, BoxType
class BoxTypeModelTests(TestCase):
"""Tests for the BoxType model."""
def setUp(self):
"""Set up test fixtures."""
self.box_type = BoxType.objects.create(
name='Small Box',
width=100,
height=50,
length=150
)
def test_box_type_str_returns_name(self):
"""BoxType __str__ should return the name."""
self.assertEqual(str(self.box_type), 'Small Box')
def test_box_type_creation(self):
"""BoxType should be created with correct attributes."""
self.assertEqual(self.box_type.name, 'Small Box')
self.assertEqual(self.box_type.width, 100)
self.assertEqual(self.box_type.height, 50)
self.assertEqual(self.box_type.length, 150)
def test_box_type_ordering(self):
"""BoxTypes should be ordered by name."""
BoxType.objects.create(name='Alpha Box', width=10, height=10, length=10)
BoxType.objects.create(name='Zeta Box', width=20, height=20, length=20)
box_types = list(BoxType.objects.values_list('name', flat=True))
self.assertEqual(box_types, ['Alpha Box', 'Small Box', 'Zeta Box'])
class BoxModelTests(TestCase):
"""Tests for the Box model."""
def setUp(self):
"""Set up test fixtures."""
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
)
def test_box_str_returns_id(self):
"""Box __str__ should return the box ID."""
self.assertEqual(str(self.box), 'BOX001')
def test_box_creation(self):
"""Box should be created with correct attributes."""
self.assertEqual(self.box.id, 'BOX001')
self.assertEqual(self.box.box_type, self.box_type)
def test_box_type_relationship(self):
"""Box should be accessible from BoxType via related_name."""
self.assertIn(self.box, self.box_type.boxes.all())
def test_box_id_max_length(self):
"""Box ID should accept up to 10 characters."""
box = Box.objects.create(id='ABCD123456', box_type=self.box_type)
self.assertEqual(len(box.id), 10)
def test_box_type_protect_on_delete(self):
"""Deleting a BoxType with boxes should raise IntegrityError."""
with self.assertRaises(IntegrityError):
self.box_type.delete()
def test_box_type_delete_when_no_boxes(self):
"""Deleting a BoxType without boxes should succeed."""
empty_type = BoxType.objects.create(
name='Empty Type',
width=10,
height=10,
length=10
)
empty_type_id = empty_type.id
empty_type.delete()
self.assertFalse(BoxType.objects.filter(id=empty_type_id).exists())
class BoxTypeAdminTests(TestCase):
"""Tests for the BoxType admin configuration."""
def setUp(self):
"""Set up test fixtures."""
self.site = AdminSite()
self.admin = BoxTypeAdmin(BoxType, self.site)
def test_list_display(self):
"""BoxTypeAdmin should display correct fields."""
self.assertEqual(
self.admin.list_display,
('name', 'width', 'height', 'length')
)
def test_search_fields(self):
"""BoxTypeAdmin should search by name."""
self.assertEqual(self.admin.search_fields, ('name',))
class BoxAdminTests(TestCase):
"""Tests for the Box admin configuration."""
def setUp(self):
"""Set up test fixtures."""
self.site = AdminSite()
self.admin = BoxAdmin(Box, self.site)
def test_list_display(self):
"""BoxAdmin should display correct fields."""
self.assertEqual(self.admin.list_display, ('id', 'box_type'))
def test_list_filter(self):
"""BoxAdmin should filter by box_type."""
self.assertEqual(self.admin.list_filter, ('box_type',))
def test_search_fields(self):
"""BoxAdmin should search by id."""
self.assertEqual(self.admin.search_fields, ('id',))

3
boxes/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
labhelper/__init__.py Normal file
View File

16
labhelper/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for labhelper project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'labhelper.settings')
application = get_asgi_application()

123
labhelper/settings.py Normal file
View File

@@ -0,0 +1,123 @@
"""
Django settings for labhelper project.
Generated by 'django-admin startproject' using Django 5.2.9.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-t1kymy8ugqnj$li2knhha0@gc5v8f3bge=$+gbybj2$jt28uqm'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'boxes',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'labhelper.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'labhelper.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

22
labhelper/urls.py Normal file
View File

@@ -0,0 +1,22 @@
"""
URL configuration for labhelper project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]

16
labhelper/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for labhelper project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'labhelper.settings')
application = get_wsgi_application()

22
manage.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'labhelper.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

36
requirements.txt Normal file
View File

@@ -0,0 +1,36 @@
appdirs==1.4.4
asgiref==3.8.1
blessed==1.21.0
certifi==2025.8.3
charset-normalizer==3.4.3
curtsies==0.4.3
cwcwidth==0.1.10
Django==5.2.9
django-admin-sortable2==2.2.8
django-js-asset==3.1.2
django-mptt==0.17.0
django-mptt-admin==2.8.0
django-nested-admin==4.1.1
django-nested-inline==0.4.6
django-revproxy==0.13.0
greenlet==3.2.4
gunicorn==23.0.0
idna==3.10
jedi==0.19.2
Markdown==3.8.2
packaging==25.0
parsedatetime==2.6
parso==0.8.4
pep8==1.7.1
prompt_toolkit==3.0.51
Pygments==2.19.2
python-dateutil==2.9.0.post0
python-monkey-business==1.1.0
pyxdg==0.28
requests==2.32.5
six==1.17.0
sqlparse==0.5.3
urllib3==2.6.0
wcwidth==0.2.13
bleach==6.1.0
coverage==7.6.1