diff --git a/argocd/deployment.yaml b/argocd/deployment.yaml index 210dee2..1f74c74 100644 --- a/argocd/deployment.yaml +++ b/argocd/deployment.yaml @@ -27,7 +27,7 @@ spec: mountPath: /data containers: - name: web - image: git.baumann.gr/adebaumann/labhelper:0.023 + image: git.baumann.gr/adebaumann/labhelper:0.024 imagePullPolicy: Always ports: - containerPort: 8000 diff --git a/boxes/forms.py b/boxes/forms.py new file mode 100644 index 0000000..e0945d0 --- /dev/null +++ b/boxes/forms.py @@ -0,0 +1,24 @@ +from django import forms + +from .models import Thing + + +class ThingForm(forms.ModelForm): + """Form for adding a Thing.""" + + class Meta: + model = Thing + fields = ('name', 'thing_type', 'description', 'picture') + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control'}), + 'thing_type': forms.Select(attrs={'class': 'form-control'}), + 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}), + } + + +ThingFormSet = forms.modelformset_factory( + Thing, + form=ThingForm, + extra=1, + can_delete=False +) diff --git a/boxes/templates/boxes/add_things.html b/boxes/templates/boxes/add_things.html new file mode 100644 index 0000000..733d9f0 --- /dev/null +++ b/boxes/templates/boxes/add_things.html @@ -0,0 +1,215 @@ + + + + + + Add Things to Box {{ box.id }} - LabHelper + + + + ← Home + +

Add Things to Box {{ box.id }}

+ +
+

+ Box: {{ box.id }} ({{ box.box_type.name }}) +

+ + {% if formset.non_form_errors %} +
+ {% for form_errors in formset.non_form_errors %} + {% for field, errors in form_errors.items %} + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} + {% endfor %} + {% endfor %} +
    + {% endif %} + + {% if formset.total_form_count %} +
    + {% csrf_token %} + + + + + + + + + {{ formset.management_form }} + {% for form in formset %} + + + + + + + + {% endfor %} + + + +
    NameTypeDescriptionPicture
    + {{ form.id }} + + {{ form.name }} + {% for error in form.name.errors %} +
    + {% for e in error %} +
  • {{ e }}
  • + {% endfor %} +
    + {% endfor %} + +
    + {{ form.thing_type }} + {% for error in form.thing_type.errors %} +
    + {% for e in error %} +
  • {{ e }}
  • + {% endfor %} +
    + {% endfor %} + +
    + {{ form.description }} + {% for error in form.description.errors %} +
    + {% for e in error %} +
  • {{ e }}
  • + {% endfor %} +
    + {% endfor %} +
    + {{ form.picture }} + {% for error in form.picture.errors %} +
    + {% for e in error %} +
  • {{ e }}
  • + {% endfor %} +
    + {% endfor %} +
    + +
    +
    + {% endif %} +
    + + diff --git a/boxes/templates/boxes/box_detail.html b/boxes/templates/boxes/box_detail.html index 68d3a87..548b913 100644 --- a/boxes/templates/boxes/box_detail.html +++ b/boxes/templates/boxes/box_detail.html @@ -79,6 +79,8 @@
    Type: {{ box.box_type.name }} ({{ box.box_type.width }} x {{ box.box_type.height }} x {{ box.box_type.length }} mm) +

    + + Add Things
    {% if things %} diff --git a/boxes/tests.py b/boxes/tests.py index 2fdb7d3..6f865ef 100644 --- a/boxes/tests.py +++ b/boxes/tests.py @@ -663,3 +663,126 @@ class SearchApiTests(TestCase): results = response.json()['results'] self.assertEqual(results[0]['type'], 'Electronics') self.assertEqual(results[0]['box'], 'BOX001') + + +class AddThingsViewTests(TestCase): + """Tests for add things 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_add_things_get_request(self): + """Add things page should return 200 for GET request.""" + response = self.client.get(f'/box/{self.box.id}/add/') + self.assertEqual(response.status_code, 200) + + def test_add_things_shows_box_id(self): + """Add things page should show box ID.""" + response = self.client.get(f'/box/{self.box.id}/add/') + self.assertContains(response, 'BOX001') + self.assertContains(response, 'Standard Box') + + def test_add_things_shows_form(self): + """Add things page should show form.""" + response = self.client.get(f'/box/{self.box.id}/add/') + self.assertContains(response, 'Save Things') + self.assertContains(response, 'Name') + self.assertContains(response, 'Type') + self.assertContains(response, 'Description') + self.assertContains(response, 'Picture') + + def test_add_things_post_valid(self): + """Adding valid things should redirect to box detail.""" + response = self.client.post(f'/box/{self.box.id}/add/', { + 'form-TOTAL_FORMS': '3', + 'form-0-name': 'Arduino Uno', + 'form-0-thing_type': self.thing_type.id, + 'form-0-description': 'A microcontroller', + 'form-1-name': 'LED Strip', + 'form-1-thing_type': self.thing_type.id, + 'form-1-description': 'Lighting component', + }) + self.assertRedirects(response, f'/box/{self.box.id}/') + self.assertEqual(Thing.objects.count(), 2) + + def test_add_things_post_required_name(self): + """Adding things without name should show error.""" + response = self.client.post(f'/box/{self.box.id}/add/', { + 'form-TOTAL_FORMS': '2', + 'form-0-thing_type': self.thing_type.id, + 'form-1-thing_type': self.thing_type.id, + }) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'This field is required') + + def test_add_things_post_partial_valid_invalid(self): + """Partial submission: one valid, one missing name.""" + response = self.client.post(f'/box/{self.box.id}/add/', { + 'form-TOTAL_FORMS': '2', + 'form-0-name': 'Arduino Uno', + 'form-0-thing_type': self.thing_type.id, + 'form-1-thing_type': self.thing_type.id, + }) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'This field is required') + self.assertEqual(Thing.objects.count(), 1) + + def test_add_things_creates_thing_types(self): + """Can create new thing types while adding things.""" + new_type = ThingType.objects.create(name='Components') + response = self.client.post(f'/box/{self.box.id}/add/', { + 'form-TOTAL_FORMS': '2', + 'form-0-name': 'Resistor', + 'form-0-thing_type': new_type.id, + 'form-1-name': 'Capacitor', + 'form-1-thing_type': new_type.id, + }) + self.assertRedirects(response, f'/box/{self.box.id}/') + self.assertEqual(Thing.objects.count(), 2) + self.assertEqual(Thing.objects.filter(thing_type=new_type).count(), 2) + + def test_add_things_empty_all(self): + """Submitting empty forms should not create anything.""" + response = self.client.post(f'/box/{self.box.id}/add/', { + 'form-TOTAL_FORMS': '2', + 'form-0-name': '', + 'form-0-thing_type': self.thing_type.id, + 'form-1-name': '', + 'form-1-thing_type': self.thing_type.id, + }) + self.assertRedirects(response, f'/box/{self.box.id}/') + self.assertEqual(Thing.objects.count(), 0) + + def test_add_things_box_not_exists(self): + """Adding things to non-existent box should return 404.""" + response = self.client.get('/box/INVALID/add/') + self.assertEqual(response.status_code, 404) + + def test_add_things_populates_box(self): + """Created things should be assigned to the correct box.""" + self.thing_type_2 = ThingType.objects.create(name='Mechanical') + response = self.client.post(f'/box/{self.box.id}/add/', { + 'form-TOTAL_FORMS': '3', + 'form-0-name': 'Bolt', + 'form-0-thing_type': self.thing_type_2.id, + 'form-1-name': 'Nut', + 'form-1-thing_type': self.thing_type_2.id, + 'form-2-name': 'Washer', + 'form-2-thing_type': self.thing_type_2.id, + }) + self.assertRedirects(response, f'/box/{self.box.id}/') + things = Thing.objects.all() + for thing in things: + self.assertEqual(thing.box, self.box) diff --git a/boxes/views.py b/boxes/views.py index 2ae3b86..2c8db81 100644 --- a/boxes/views.py +++ b/boxes/views.py @@ -1,6 +1,7 @@ from django.http import HttpResponse, JsonResponse -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, redirect, render +from .forms import ThingFormSet from .models import Box, Thing @@ -55,3 +56,34 @@ def search_api(request): for thing in things ] return JsonResponse({'results': results}) + + +def add_things(request, box_id): + """Add multiple things to a box at once.""" + box = get_object_or_404(Box, pk=box_id) + + if request.method == 'POST': + formset = ThingFormSet(request.POST) + + if formset.is_valid(): + things = formset.save(commit=False) + created_count = 0 + for thing in things: + if thing.name or thing.thing_type or thing.description or thing.picture: + thing.box = box + thing.save() + created_count += 1 + print(f'DEBUG: created_count={created_count}') + if created_count > 0: + print(f'DEBUG: Redirecting to box_detail with box_id={box_id}') + return redirect('box_detail', box_id=box_id) + else: + print('DEBUG: No valid data submitted') + + else: + formset = ThingFormSet() + + return render(request, 'boxes/add_things.html', { + 'box': box, + 'formset': formset, + }) diff --git a/labhelper/urls.py b/labhelper/urls.py index 4637a65..03bad63 100644 --- a/labhelper/urls.py +++ b/labhelper/urls.py @@ -19,12 +19,13 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path -from boxes.views import box_detail, index, search, search_api, thing_detail +from boxes.views import add_things, box_detail, index, search, search_api, thing_detail urlpatterns = [ path('', index, name='index'), path('box//', box_detail, name='box_detail'), path('thing//', thing_detail, name='thing_detail'), + path('box//add/', add_things, name='add_things'), path('search/', search, name='search'), path('search/api/', search_api, name='search_api'), path('admin/', admin.site.urls),