Ongoing development of "Things"
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 2m52s
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 25s
SonarQube Scan / SonarQube Trigger (push) Failing after 20s
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 2m52s
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 25s
SonarQube Scan / SonarQube Trigger (push) Failing after 20s
This commit is contained in:
@@ -18,14 +18,14 @@ spec:
|
|||||||
fsGroupChangePolicy: "OnRootMismatch"
|
fsGroupChangePolicy: "OnRootMismatch"
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: loader
|
- name: loader
|
||||||
image: git.baumann.gr/adebaumann/labhelper-data-loader:0.003
|
image: git.baumann.gr/adebaumann/labhelper-data-loader:0.004
|
||||||
command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
|
command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: git.baumann.gr/adebaumann/labhelper:0.009
|
image: git.baumann.gr/adebaumann/labhelper:0.010
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.contrib import admin
|
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, ThingType
|
||||||
|
|
||||||
@@ -21,10 +22,9 @@ class BoxAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
@admin.register(ThingType)
|
@admin.register(ThingType)
|
||||||
class ThingTypeAdmin(admin.ModelAdmin):
|
class ThingTypeAdmin(DjangoMpttAdmin):
|
||||||
"""Admin configuration for ThingType model."""
|
"""Admin configuration for ThingType model."""
|
||||||
|
|
||||||
list_display = ('name',)
|
|
||||||
search_fields = ('name',)
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
68
boxes/migrations/0003_convert_thingtype_to_mptt.py
Normal file
68
boxes/migrations/0003_convert_thingtype_to_mptt.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Generated by Django 5.2.9 on 2025-12-28 19:39
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_tree(apps, schema_editor):
|
||||||
|
"""Rebuild MPTT tree after adding fields."""
|
||||||
|
ThingType = apps.get_model('boxes', 'ThingType')
|
||||||
|
# Import the actual model to use rebuild
|
||||||
|
from boxes.models import ThingType as RealThingType
|
||||||
|
RealThingType.objects.rebuild()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('boxes', '0002_thingtype_thing'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='thingtype',
|
||||||
|
name='parent',
|
||||||
|
field=mptt.fields.TreeForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='children',
|
||||||
|
to='boxes.thingtype'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='thingtype',
|
||||||
|
name='level',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='thingtype',
|
||||||
|
name='lft',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='thingtype',
|
||||||
|
name='rght',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='thingtype',
|
||||||
|
name='tree_id',
|
||||||
|
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='thingtype',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='thingtype',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=255),
|
||||||
|
),
|
||||||
|
migrations.RunPython(rebuild_tree, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
|
|
||||||
class BoxType(models.Model):
|
class BoxType(models.Model):
|
||||||
@@ -37,13 +38,20 @@ class Box(models.Model):
|
|||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
|
|
||||||
class ThingType(models.Model):
|
class ThingType(MPTTModel):
|
||||||
"""A type/category for things stored in boxes."""
|
"""A hierarchical type/category for things stored in boxes."""
|
||||||
|
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255)
|
||||||
|
parent = TreeForeignKey(
|
||||||
|
'self',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='children'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class MPTTMeta:
|
||||||
ordering = ['name']
|
order_insertion_by = ['name']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
213
boxes/tests.py
213
boxes/tests.py
@@ -1,9 +1,10 @@
|
|||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .admin import BoxAdmin, BoxTypeAdmin
|
from .admin import BoxAdmin, BoxTypeAdmin, ThingAdmin, ThingTypeAdmin
|
||||||
from .models import Box, BoxType
|
from .models import Box, BoxType, Thing, ThingType
|
||||||
|
|
||||||
|
|
||||||
class BoxTypeModelTests(TestCase):
|
class BoxTypeModelTests(TestCase):
|
||||||
@@ -128,3 +129,211 @@ class BoxAdminTests(TestCase):
|
|||||||
def test_search_fields(self):
|
def test_search_fields(self):
|
||||||
"""BoxAdmin should search by id."""
|
"""BoxAdmin should search by id."""
|
||||||
self.assertEqual(self.admin.search_fields, ('id',))
|
self.assertEqual(self.admin.search_fields, ('id',))
|
||||||
|
|
||||||
|
|
||||||
|
class ThingTypeModelTests(TestCase):
|
||||||
|
"""Tests for the ThingType model."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.thing_type = ThingType.objects.create(name='Electronics')
|
||||||
|
|
||||||
|
def test_thing_type_str_returns_name(self):
|
||||||
|
"""ThingType __str__ should return the name."""
|
||||||
|
self.assertEqual(str(self.thing_type), 'Electronics')
|
||||||
|
|
||||||
|
def test_thing_type_creation(self):
|
||||||
|
"""ThingType should be created with correct attributes."""
|
||||||
|
self.assertEqual(self.thing_type.name, 'Electronics')
|
||||||
|
|
||||||
|
def test_thing_type_hierarchy(self):
|
||||||
|
"""ThingType should support parent-child relationships."""
|
||||||
|
child = ThingType.objects.create(
|
||||||
|
name='Resistors',
|
||||||
|
parent=self.thing_type
|
||||||
|
)
|
||||||
|
self.assertEqual(child.parent, self.thing_type)
|
||||||
|
self.assertIn(child, self.thing_type.children.all())
|
||||||
|
|
||||||
|
def test_thing_type_is_leaf_node(self):
|
||||||
|
"""ThingType without children should be a leaf node."""
|
||||||
|
self.assertTrue(self.thing_type.is_leaf_node())
|
||||||
|
|
||||||
|
def test_thing_type_is_not_leaf_with_children(self):
|
||||||
|
"""ThingType with children should not be a leaf node."""
|
||||||
|
ThingType.objects.create(name='Capacitors', parent=self.thing_type)
|
||||||
|
self.assertFalse(self.thing_type.is_leaf_node())
|
||||||
|
|
||||||
|
def test_thing_type_ancestors(self):
|
||||||
|
"""ThingType should return correct ancestors."""
|
||||||
|
child = ThingType.objects.create(
|
||||||
|
name='Resistors',
|
||||||
|
parent=self.thing_type
|
||||||
|
)
|
||||||
|
grandchild = ThingType.objects.create(
|
||||||
|
name='10k Resistors',
|
||||||
|
parent=child
|
||||||
|
)
|
||||||
|
ancestors = list(grandchild.get_ancestors())
|
||||||
|
self.assertEqual(ancestors, [self.thing_type, child])
|
||||||
|
|
||||||
|
def test_thing_type_descendants(self):
|
||||||
|
"""ThingType should return correct descendants."""
|
||||||
|
child = ThingType.objects.create(
|
||||||
|
name='Resistors',
|
||||||
|
parent=self.thing_type
|
||||||
|
)
|
||||||
|
grandchild = ThingType.objects.create(
|
||||||
|
name='10k Resistors',
|
||||||
|
parent=child
|
||||||
|
)
|
||||||
|
descendants = list(self.thing_type.get_descendants())
|
||||||
|
self.assertEqual(descendants, [child, grandchild])
|
||||||
|
|
||||||
|
def test_thing_type_level(self):
|
||||||
|
"""ThingType should have correct level in hierarchy."""
|
||||||
|
self.assertEqual(self.thing_type.level, 0)
|
||||||
|
child = ThingType.objects.create(
|
||||||
|
name='Resistors',
|
||||||
|
parent=self.thing_type
|
||||||
|
)
|
||||||
|
self.assertEqual(child.level, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class ThingModelTests(TestCase):
|
||||||
|
"""Tests for the Thing 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
|
||||||
|
)
|
||||||
|
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_str_returns_name(self):
|
||||||
|
"""Thing __str__ should return the name."""
|
||||||
|
self.assertEqual(str(self.thing), 'Arduino Uno')
|
||||||
|
|
||||||
|
def test_thing_creation(self):
|
||||||
|
"""Thing should be created with correct attributes."""
|
||||||
|
self.assertEqual(self.thing.name, 'Arduino Uno')
|
||||||
|
self.assertEqual(self.thing.thing_type, self.thing_type)
|
||||||
|
self.assertEqual(self.thing.box, self.box)
|
||||||
|
|
||||||
|
def test_thing_optional_description(self):
|
||||||
|
"""Thing description should be optional."""
|
||||||
|
self.assertEqual(self.thing.description, '')
|
||||||
|
self.thing.description = 'A microcontroller board'
|
||||||
|
self.thing.save()
|
||||||
|
self.thing.refresh_from_db()
|
||||||
|
self.assertEqual(self.thing.description, 'A microcontroller board')
|
||||||
|
|
||||||
|
def test_thing_optional_picture(self):
|
||||||
|
"""Thing picture should be optional."""
|
||||||
|
self.assertEqual(self.thing.picture.name, '')
|
||||||
|
|
||||||
|
def test_thing_with_picture(self):
|
||||||
|
"""Thing should accept an image upload."""
|
||||||
|
# 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='Test Item',
|
||||||
|
thing_type=self.thing_type,
|
||||||
|
box=self.box,
|
||||||
|
picture=image
|
||||||
|
)
|
||||||
|
self.assertTrue(thing.picture.name.startswith('things/'))
|
||||||
|
# Clean up
|
||||||
|
thing.picture.delete()
|
||||||
|
|
||||||
|
def test_thing_ordering(self):
|
||||||
|
"""Things should be ordered by name."""
|
||||||
|
Thing.objects.create(
|
||||||
|
name='Zeta Item',
|
||||||
|
thing_type=self.thing_type,
|
||||||
|
box=self.box
|
||||||
|
)
|
||||||
|
Thing.objects.create(
|
||||||
|
name='Alpha Item',
|
||||||
|
thing_type=self.thing_type,
|
||||||
|
box=self.box
|
||||||
|
)
|
||||||
|
things = list(Thing.objects.values_list('name', flat=True))
|
||||||
|
self.assertEqual(things, ['Alpha Item', 'Arduino Uno', 'Zeta Item'])
|
||||||
|
|
||||||
|
def test_thing_type_relationship(self):
|
||||||
|
"""Thing should be accessible from ThingType via related_name."""
|
||||||
|
self.assertIn(self.thing, self.thing_type.things.all())
|
||||||
|
|
||||||
|
def test_thing_box_relationship(self):
|
||||||
|
"""Thing should be accessible from Box via related_name."""
|
||||||
|
self.assertIn(self.thing, self.box.things.all())
|
||||||
|
|
||||||
|
def test_thing_type_protect_on_delete(self):
|
||||||
|
"""Deleting a ThingType with things should raise IntegrityError."""
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
self.thing_type.delete()
|
||||||
|
|
||||||
|
def test_box_protect_on_delete_with_things(self):
|
||||||
|
"""Deleting a Box with things should raise IntegrityError."""
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
self.box.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class ThingTypeAdminTests(TestCase):
|
||||||
|
"""Tests for the ThingType admin configuration."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.site = AdminSite()
|
||||||
|
self.admin = ThingTypeAdmin(ThingType, self.site)
|
||||||
|
|
||||||
|
def test_search_fields(self):
|
||||||
|
"""ThingTypeAdmin should search by name."""
|
||||||
|
self.assertEqual(self.admin.search_fields, ('name',))
|
||||||
|
|
||||||
|
|
||||||
|
class ThingAdminTests(TestCase):
|
||||||
|
"""Tests for the Thing admin configuration."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.site = AdminSite()
|
||||||
|
self.admin = ThingAdmin(Thing, self.site)
|
||||||
|
|
||||||
|
def test_list_display(self):
|
||||||
|
"""ThingAdmin should display correct fields."""
|
||||||
|
self.assertEqual(
|
||||||
|
self.admin.list_display,
|
||||||
|
('name', 'thing_type', 'box')
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_list_filter(self):
|
||||||
|
"""ThingAdmin should filter by thing_type and box."""
|
||||||
|
self.assertEqual(self.admin.list_filter, ('thing_type', 'box'))
|
||||||
|
|
||||||
|
def test_search_fields(self):
|
||||||
|
"""ThingAdmin should search by name and description."""
|
||||||
|
self.assertEqual(self.admin.search_fields, ('name', 'description'))
|
||||||
|
|||||||
Binary file not shown.
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'django_mptt_admin',
|
||||||
'boxes',
|
'boxes',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user