diff --git a/argocd/deployment.yaml b/argocd/deployment.yaml
index cd4cb0c..9a232fb 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.050
+ image: git.baumann.gr/adebaumann/labhelper:0.051
imagePullPolicy: Always
ports:
- containerPort: 8000
diff --git a/boxes/forms.py b/boxes/forms.py
index a9cde86..765e71b 100644
--- a/boxes/forms.py
+++ b/boxes/forms.py
@@ -4,15 +4,14 @@ from .models import Box, BoxType, Thing, ThingFile, ThingLink
class ThingForm(forms.ModelForm):
- """Form for adding a Thing."""
+ """Form for adding/editing a Thing."""
class Meta:
model = Thing
- fields = ('name', 'description', 'picture', 'tags')
+ fields = ('name', 'description', 'picture')
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
- 'tags': forms.CheckboxSelectMultiple(attrs={'class': 'tags-checkboxes'}),
}
diff --git a/boxes/templates/boxes/edit_thing.html b/boxes/templates/boxes/edit_thing.html
new file mode 100644
index 0000000..659216b
--- /dev/null
+++ b/boxes/templates/boxes/edit_thing.html
@@ -0,0 +1,324 @@
+{% extends "base.html" %}
+{% load thumbnail %}
+{% load dict_extras %}
+
+{% block title %}Edit {{ thing.name }} - LabHelper{% endblock %}
+
+{% block page_header %}
+
+{% endblock %}
+
+{% block content %}
+
-
- Add Attachments
-
-
-
-
-
{% endblock %}
{% block extra_css %}
@@ -352,15 +200,3 @@
}
{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
\ No newline at end of file
diff --git a/boxes/tests.py b/boxes/tests.py
index bc9db0d..8730eec 100644
--- a/boxes/tests.py
+++ b/boxes/tests.py
@@ -441,9 +441,16 @@ class ThingDetailViewTests(AuthTestCase):
self.assertContains(response, 'Arduino Uno')
def test_thing_detail_shows_tags_section(self):
- """Thing detail page should show tags section."""
+ """Thing detail page should show tags section when tags exist."""
+ facet = Facet.objects.create(
+ name='Electronics',
+ color='#FF5733',
+ cardinality=Facet.Cardinality.MULTIPLE
+ )
+ tag = Tag.objects.create(facet=facet, name='Arduino')
+ self.thing.tags.add(tag)
response = self.client.get(f'/thing/{self.thing.id}/')
- self.assertContains(response, 'Tags')
+ self.assertContains(response, 'Electronics')
def test_thing_detail_shows_description(self):
"""Thing detail page should show thing description."""
@@ -472,25 +479,11 @@ class ThingDetailViewTests(AuthTestCase):
response = self.client.get(f'/thing/{self.thing.id}/')
self.assertRedirects(response, f'/login/?next=/thing/{self.thing.id}/')
- def test_thing_detail_move_to_box(self):
- """Thing can be moved to another box via POST."""
- new_box = Box.objects.create(id='BOX002', box_type=self.box_type)
- response = self.client.post(
- f'/thing/{self.thing.id}/',
- {'action': 'move', 'new_box': 'BOX002'}
- )
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
- self.thing.refresh_from_db()
- self.assertEqual(self.thing.box, new_box)
-
- def test_thing_detail_move_shows_all_boxes(self):
- """Thing detail page should show all available boxes in dropdown."""
- Box.objects.create(id='BOX002', box_type=self.box_type)
- Box.objects.create(id='BOX003', box_type=self.box_type)
+ def test_thing_detail_has_edit_button(self):
+ """Thing detail page should have an edit button."""
response = self.client.get(f'/thing/{self.thing.id}/')
- self.assertContains(response, 'BOX001')
- self.assertContains(response, 'BOX002')
- self.assertContains(response, 'BOX003')
+ self.assertContains(response, 'Edit')
+ self.assertContains(response, f'/thing/{self.thing.id}/edit/')
class SearchViewTests(AuthTestCase):
@@ -1024,14 +1017,14 @@ class ThingPictureUploadTests(AuthTestCase):
b'\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82'
)
- def test_thing_detail_shows_add_picture_button(self):
- """Thing detail page should show 'Add picture' button when thing has no picture."""
- response = self.client.get(f'/thing/{self.thing.id}/')
+ def test_edit_thing_shows_add_picture_button(self):
+ """Edit thing page should show 'Add picture' button when thing has no picture."""
+ response = self.client.get(f'/thing/{self.thing.id}/edit/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Add picture')
- def test_thing_detail_shows_change_picture_button(self):
- """Thing detail page should show 'Change picture' button when thing has a picture."""
+ def test_edit_thing_shows_change_picture_button(self):
+ """Edit thing page should show 'Change picture' button when thing has a picture."""
image = SimpleUploadedFile(
name='test.png',
content=self.image_data,
@@ -1039,14 +1032,14 @@ class ThingPictureUploadTests(AuthTestCase):
)
self.thing.picture = image
self.thing.save()
- response = self.client.get(f'/thing/{self.thing.id}/')
+ response = self.client.get(f'/thing/{self.thing.id}/edit/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Change picture')
# Clean up
self.thing.picture.delete(save=False)
- def test_thing_detail_shows_remove_button(self):
- """Thing detail page should show 'Remove' button when thing has a picture."""
+ def test_edit_thing_shows_remove_button(self):
+ """Edit thing page should show 'Remove' button when thing has a picture."""
image = SimpleUploadedFile(
name='test.png',
content=self.image_data,
@@ -1054,14 +1047,14 @@ class ThingPictureUploadTests(AuthTestCase):
)
self.thing.picture = image
self.thing.save()
- response = self.client.get(f'/thing/{self.thing.id}/')
+ response = self.client.get(f'/thing/{self.thing.id}/edit/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Remove')
# Clean up
self.thing.picture.delete(save=False)
def test_delete_picture_removes_picture(self):
- """Deleting a picture should remove it from the thing."""
+ """Deleting a picture should remove it from thing."""
image = SimpleUploadedFile(
name='test.png',
content=self.image_data,
@@ -1070,19 +1063,28 @@ class ThingPictureUploadTests(AuthTestCase):
self.thing.picture = image
self.thing.save()
- response = self.client.post(f'/thing/{self.thing.id}/', {
+ response = self.client.post(f'/thing/{self.thing.id}/edit/', {
'action': 'delete_picture'
})
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.thing.refresh_from_db()
self.assertFalse(self.thing.picture.name)
def test_delete_picture_on_thing_without_picture(self):
"""Deleting a picture from a thing without a picture should succeed."""
- response = self.client.post(f'/thing/{self.thing.id}/', {
+ response = self.client.post(f'/thing/{self.thing.id}/edit/', {
'action': 'delete_picture'
})
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
+ self.thing.refresh_from_db()
+ self.assertFalse(self.thing.picture.name)
+
+ def test_delete_picture_on_thing_without_picture(self):
+ """Deleting a picture from a thing without a picture should succeed."""
+ response = self.client.post(f'/thing/{self.thing.id}/edit/', {
+ 'action': 'delete_picture'
+ })
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.thing.refresh_from_db()
self.assertFalse(self.thing.picture.name)
@@ -1257,24 +1259,24 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
content_type='application/pdf'
)
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{'action': 'add_file', 'title': 'Datasheet', 'file': uploaded_file}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.assertEqual(self.thing.files.count(), 1)
self.assertEqual(self.thing.files.first().title, 'Datasheet')
def test_add_link_to_thing(self):
"""Adding a link should create ThingLink."""
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{
'action': 'add_link',
'title': 'Manufacturer',
'url': 'https://www.arduino.cc'
}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.assertEqual(self.thing.links.count(), 1)
self.assertEqual(self.thing.links.first().title, 'Manufacturer')
self.assertEqual(self.thing.links.first().url, 'https://www.arduino.cc')
@@ -1288,10 +1290,10 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
)
file_id = thing_file.id
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{'action': 'delete_file', 'file_id': str(file_id)}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.assertFalse(ThingFile.objects.filter(id=file_id).exists())
def test_delete_link_from_thing(self):
@@ -1303,10 +1305,10 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
)
link_id = thing_link.id
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{'action': 'delete_link', 'link_id': str(link_id)}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.assertFalse(ThingLink.objects.filter(id=link_id).exists())
def test_cannot_delete_file_from_other_thing(self):
@@ -1322,10 +1324,10 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
)
file_id = thing_file.id
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{'action': 'delete_file', 'file_id': str(file_id)}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.assertTrue(ThingFile.objects.filter(id=file_id).exists())
def test_cannot_delete_link_from_other_thing(self):
@@ -1341,10 +1343,10 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
)
link_id = thing_link.id
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{'action': 'delete_link', 'link_id': str(link_id)}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.assertTrue(ThingLink.objects.filter(id=link_id).exists())
def test_thing_detail_shows_files_section(self):
@@ -1371,9 +1373,9 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
self.assertContains(response, 'Links')
self.assertContains(response, 'Documentation')
- def test_thing_detail_shows_upload_forms(self):
- """Thing detail page should show upload forms."""
- response = self.client.get(f'/thing/{self.thing.id}/')
+ def test_edit_thing_shows_upload_forms(self):
+ """Edit thing page should show upload forms."""
+ response = self.client.get(f'/thing/{self.thing.id}/edit/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Upload File')
self.assertContains(response, 'Add Link')
@@ -1630,29 +1632,29 @@ class ThingTagTests(AuthTestCase):
self.assertNotIn(self.tag, self.thing.tags.all())
def test_thing_detail_add_tag(self):
- """Thing detail page can add a tag via POST."""
+ """Edit thing page can add a tag via POST."""
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{'action': 'add_tag', 'tag_id': str(self.tag.id)}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.thing.refresh_from_db()
self.assertIn(self.tag, self.thing.tags.all())
def test_thing_detail_remove_tag(self):
- """Thing detail page can remove a tag via POST."""
+ """Edit thing page can remove a tag via POST."""
self.thing.tags.add(self.tag)
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{'action': 'remove_tag', 'tag_id': str(self.tag.id)}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.thing.refresh_from_db()
self.assertNotIn(self.tag, self.thing.tags.all())
def test_thing_detail_shows_available_tags(self):
- """Thing detail page should show available tags to add."""
- response = self.client.get(f'/thing/{self.thing.id}/')
+ """Edit thing page should show available tags to add."""
+ response = self.client.get(f'/thing/{self.thing.id}/edit/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Electronics')
@@ -1674,10 +1676,10 @@ class ThingTagTests(AuthTestCase):
self.thing.tags.add(tag_low)
response = self.client.post(
- f'/thing/{self.thing.id}/',
+ f'/thing/{self.thing.id}/edit/',
{'action': 'add_tag', 'tag_id': str(tag_high.id)}
)
- self.assertRedirects(response, f'/thing/{self.thing.id}/')
+ self.assertRedirects(response, f'/thing/{self.thing.id}/edit/')
self.thing.refresh_from_db()
self.assertNotIn(tag_low, self.thing.tags.all())
self.assertIn(tag_high, self.thing.tags.all())
diff --git a/boxes/views.py b/boxes/views.py
index 89a5976..f239dbd 100644
--- a/boxes/views.py
+++ b/boxes/views.py
@@ -10,6 +10,7 @@ from .forms import (
BoxForm,
BoxTypeForm,
ThingFileForm,
+ ThingForm,
ThingFormSet,
ThingLinkForm,
ThingPictureForm,
@@ -64,7 +65,17 @@ def box_detail(request, box_id):
@login_required
def thing_detail(request, thing_id):
- """Display details of a thing."""
+ """Display details of a thing (read-only)."""
+ thing = get_object_or_404(
+ Thing.objects.select_related('box', 'box__box_type').prefetch_related('files', 'links', 'tags'),
+ pk=thing_id
+ )
+ return render(request, 'boxes/thing_detail.html', {'thing': thing})
+
+
+@login_required
+def edit_thing(request, thing_id):
+ """Edit a thing's details."""
thing = get_object_or_404(
Thing.objects.select_related('box', 'box__box_type').prefetch_related('files', 'links', 'tags'),
pk=thing_id
@@ -79,26 +90,32 @@ def thing_detail(request, thing_id):
if request.method == 'POST':
action = request.POST.get('action')
- if action == 'move':
+ if action == 'save_details':
+ form = ThingForm(request.POST, request.FILES, instance=thing)
+ if form.is_valid():
+ form.save()
+ return redirect('thing_detail', thing_id=thing.id)
+
+ elif action == 'move':
new_box_id = request.POST.get('new_box')
if new_box_id:
new_box = get_object_or_404(Box, pk=new_box_id)
thing.box = new_box
thing.save()
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
elif action == 'upload_picture':
picture_form = ThingPictureForm(request.POST, request.FILES, instance=thing)
if picture_form.is_valid():
picture_form.save()
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
elif action == 'delete_picture':
if thing.picture:
thing.picture.delete()
thing.picture = None
thing.save()
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
elif action == 'add_file':
file_form = ThingFileForm(request.POST, request.FILES)
@@ -106,7 +123,7 @@ def thing_detail(request, thing_id):
thing_file = file_form.save(commit=False)
thing_file.thing = thing
thing_file.save()
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
elif action == 'add_link':
link_form = ThingLinkForm(request.POST)
@@ -114,7 +131,7 @@ def thing_detail(request, thing_id):
thing_link = link_form.save(commit=False)
thing_link.thing = thing
thing_link.save()
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
elif action == 'delete_file':
file_id = request.POST.get('file_id')
@@ -125,7 +142,7 @@ def thing_detail(request, thing_id):
thing_file.delete()
except ThingFile.DoesNotExist:
pass
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
elif action == 'delete_link':
link_id = request.POST.get('link_id')
@@ -135,7 +152,7 @@ def thing_detail(request, thing_id):
thing_link.delete()
except ThingLink.DoesNotExist:
pass
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
elif action == 'add_tag':
tag_id = request.POST.get('tag_id')
@@ -149,7 +166,7 @@ def thing_detail(request, thing_id):
thing.tags.add(tag)
except Tag.DoesNotExist:
pass
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
elif action == 'remove_tag':
tag_id = request.POST.get('tag_id')
@@ -159,15 +176,18 @@ def thing_detail(request, thing_id):
thing.tags.remove(tag)
except Tag.DoesNotExist:
pass
- return redirect('thing_detail', thing_id=thing.id)
+ return redirect('edit_thing', thing_id=thing.id)
- return render(request, 'boxes/thing_detail.html', {
+ thing_form = ThingForm(instance=thing)
+
+ return render(request, 'boxes/edit_thing.html', {
'thing': thing,
'boxes': boxes,
'facets': facets,
'picture_form': picture_form,
'file_form': file_form,
'link_form': link_form,
+ 'thing_form': thing_form,
})
diff --git a/labhelper/urls.py b/labhelper/urls.py
index 09838e3..29c0c3c 100644
--- a/labhelper/urls.py
+++ b/labhelper/urls.py
@@ -30,6 +30,7 @@ from boxes.views import (
delete_box_type,
edit_box,
edit_box_type,
+ edit_thing,
index,
search,
search_api,
@@ -49,6 +50,7 @@ urlpatterns = [
path('box/
/delete/', delete_box, name='delete_box'),
path('box//', box_detail, name='box_detail'),
path('thing//', thing_detail, name='thing_detail'),
+ path('thing//edit/', edit_thing, name='edit_thing'),
path('box//add/', add_things, name='add_things'),
path('search/', search, name='search'),
path('search/api/', search_api, name='search_api'),